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
|
@@ -33,6 +33,7 @@ import android.os.Bundle;
|
|
|
33
33
|
import android.os.Handler;
|
|
34
34
|
import android.os.IBinder;
|
|
35
35
|
import android.os.Looper;
|
|
36
|
+
import android.os.SystemClock;
|
|
36
37
|
import android.os.Trace;
|
|
37
38
|
import androidx.annotation.Nullable;
|
|
38
39
|
import androidx.annotation.RequiresApi;
|
|
@@ -43,8 +44,12 @@ import app.notifee.core.event.NotificationEvent;
|
|
|
43
44
|
import app.notifee.core.interfaces.MethodCallResult;
|
|
44
45
|
import app.notifee.core.model.NotificationModel;
|
|
45
46
|
import app.notifee.core.utility.ParcelableCompatReader;
|
|
47
|
+
import java.util.ArrayList;
|
|
46
48
|
import java.util.HashMap;
|
|
49
|
+
import java.util.HashSet;
|
|
50
|
+
import java.util.List;
|
|
47
51
|
import java.util.Map;
|
|
52
|
+
import java.util.Set;
|
|
48
53
|
import java.util.UUID;
|
|
49
54
|
|
|
50
55
|
public class ForegroundService extends Service {
|
|
@@ -64,6 +69,7 @@ public class ForegroundService extends Service {
|
|
|
64
69
|
private static final Map<String, StartRequest> sPendingStarts = new HashMap<>();
|
|
65
70
|
private static final String START_REQUEST_ID = "notifee.foregroundServiceStartRequestId";
|
|
66
71
|
private static long sRunGeneration;
|
|
72
|
+
private static long sStartAdmissionRevision;
|
|
67
73
|
private static ForegroundService sInstance;
|
|
68
74
|
private static Object sStopRequest;
|
|
69
75
|
private static ForegroundService sStateOwner;
|
|
@@ -78,6 +84,141 @@ public class ForegroundService extends Service {
|
|
|
78
84
|
private long mStateGeneration;
|
|
79
85
|
private int mRunFirstStartId;
|
|
80
86
|
private Object mRunnerToken;
|
|
87
|
+
private ForegroundEpoch mForegroundEpoch;
|
|
88
|
+
private ForegroundServiceRunLimitScheduler.Ticket mAlarmPoc;
|
|
89
|
+
private long mAlarmPocAttemptedRun = Long.MIN_VALUE;
|
|
90
|
+
private String mAlarmPocInstanceId;
|
|
91
|
+
// Previously admitted metadata can collide with a successor even across run generations.
|
|
92
|
+
// These IDs only disqualify stop authority; they never establish Android epoch provenance.
|
|
93
|
+
private final Set<Integer> mTimeoutStartIds = new HashSet<>();
|
|
94
|
+
// A reentrant platform callback may still inherit an outer lifecycle monitor. Drain these
|
|
95
|
+
// publications synchronously at that call's unlocked boundary, without scheduling or waiting.
|
|
96
|
+
private static final ThreadLocal<List<Runnable>> sTimeoutPublications = new ThreadLocal<>();
|
|
97
|
+
|
|
98
|
+
private static void publishTimeoutEventsIfUnlocked() {
|
|
99
|
+
if (Thread.holdsLock(sLifecycleLock) || Thread.holdsLock(sLock)) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
List<Runnable> publications = sTimeoutPublications.get();
|
|
103
|
+
if (publications != null) {
|
|
104
|
+
sTimeoutPublications.remove();
|
|
105
|
+
for (Runnable publication : publications) {
|
|
106
|
+
publication.run();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Local publication identity, never provenance supplied by Android's timeout callback.
|
|
112
|
+
// Guarded by sLifecycleLock then sLock. Retiring consumes timeout authority even on failure.
|
|
113
|
+
private static final class ForegroundEpoch {
|
|
114
|
+
final long runGeneration;
|
|
115
|
+
final long promotedElapsed = SystemClock.elapsedRealtime();
|
|
116
|
+
final boolean ambiguousPromotion;
|
|
117
|
+
final Set<Integer> satisfiedStartIds = new HashSet<>();
|
|
118
|
+
int lastSatisfiedStartId;
|
|
119
|
+
boolean retiring;
|
|
120
|
+
// A consumed voluntary alarm must not suppress a later genuine framework timeout
|
|
121
|
+
// when both native teardown attempts failed and the FGS is still active.
|
|
122
|
+
boolean voluntaryRetirement;
|
|
123
|
+
boolean retired;
|
|
124
|
+
|
|
125
|
+
ForegroundEpoch(long runGeneration, boolean ambiguousPromotion, int startId) {
|
|
126
|
+
this.runGeneration = runGeneration;
|
|
127
|
+
this.ambiguousPromotion = ambiguousPromotion;
|
|
128
|
+
satisfiedStartIds.add(startId);
|
|
129
|
+
lastSatisfiedStartId = startId;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Caller holds both locks, after the actual promotion returned normally.
|
|
134
|
+
private boolean commitForegroundEpoch() {
|
|
135
|
+
ForegroundEpoch previous = mForegroundEpoch;
|
|
136
|
+
long generation = sRunGeneration;
|
|
137
|
+
long runGeneration = mRunGeneration;
|
|
138
|
+
int startId = mLastStartId;
|
|
139
|
+
if (mAlarmPoc != null) {
|
|
140
|
+
ForegroundServiceRunLimitScheduler.marker(
|
|
141
|
+
"ARM_RETIRED", mAlarmPoc, "promotion epoch replaced; no automatic rearm");
|
|
142
|
+
}
|
|
143
|
+
invalidateAlarmPoc();
|
|
144
|
+
// Cancellation crosses IPC/storage. Neither this commit nor its caller may overwrite
|
|
145
|
+
// a successor admitted while the old ticket was being cancelled.
|
|
146
|
+
if (sInstance != this
|
|
147
|
+
|| sStateOwner != this
|
|
148
|
+
|| mForegroundEpoch != previous
|
|
149
|
+
|| sRunGeneration != generation
|
|
150
|
+
|| mRunGeneration != runGeneration
|
|
151
|
+
|| mLastStartId != startId) return false;
|
|
152
|
+
mForegroundEpoch =
|
|
153
|
+
new ForegroundEpoch(
|
|
154
|
+
mRunGeneration,
|
|
155
|
+
previous != null && previous.runGeneration == mRunGeneration,
|
|
156
|
+
mLastStartId);
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Snapshot before platform IPC; a reply cannot acquire authority over a reentrant successor.
|
|
161
|
+
private static final class StopAuthority {
|
|
162
|
+
final ForegroundService owner;
|
|
163
|
+
final ForegroundEpoch epoch;
|
|
164
|
+
final long generation;
|
|
165
|
+
final long runGeneration;
|
|
166
|
+
final int startId;
|
|
167
|
+
final Object request;
|
|
168
|
+
|
|
169
|
+
StopAuthority(ForegroundService service) {
|
|
170
|
+
owner = sStateOwner;
|
|
171
|
+
epoch = service.mForegroundEpoch;
|
|
172
|
+
generation = sRunGeneration;
|
|
173
|
+
runGeneration = service.mRunGeneration;
|
|
174
|
+
startId = service.mLastStartId;
|
|
175
|
+
request = sStopRequest;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Caller holds sLifecycleLock. Follow the lifecycle -> state lock order for both snapshots.
|
|
180
|
+
private StopAuthority captureStopAuthority() {
|
|
181
|
+
synchronized (sLock) {
|
|
182
|
+
return new StopAuthority(this);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private boolean ownsStopAuthority(StopAuthority authority) {
|
|
187
|
+
synchronized (sLock) {
|
|
188
|
+
return sInstance == this
|
|
189
|
+
&& sStateOwner == authority.owner
|
|
190
|
+
&& mForegroundEpoch == authority.epoch
|
|
191
|
+
&& sRunGeneration == authority.generation
|
|
192
|
+
&& mRunGeneration == authority.runGeneration
|
|
193
|
+
&& mLastStartId == authority.startId
|
|
194
|
+
&& sStopRequest == authority.request
|
|
195
|
+
&& sPendingStarts.isEmpty();
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// A delivered command can be physically satisfied by the existing foreground publication,
|
|
200
|
+
// even if its notification update throws. A future/unfulfilled delivery gains no authority.
|
|
201
|
+
private void recordSatisfiedStart(int startId) {
|
|
202
|
+
synchronized (sLock) {
|
|
203
|
+
ForegroundEpoch epoch = mForegroundEpoch;
|
|
204
|
+
if (sInstance == this
|
|
205
|
+
&& sStateOwner == this
|
|
206
|
+
&& epoch != null
|
|
207
|
+
&& !epoch.retiring
|
|
208
|
+
&& !epoch.retired
|
|
209
|
+
&& mForegroundActive
|
|
210
|
+
&& mStartCommandSatisfied
|
|
211
|
+
&& mLastStartId == startId
|
|
212
|
+
&& epoch.runGeneration == mRunGeneration) {
|
|
213
|
+
epoch.satisfiedStartIds.add(startId);
|
|
214
|
+
epoch.lastSatisfiedStartId = startId;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// A denied initial promotion is not a client STOP. Its recovery cannot acquire authority
|
|
220
|
+
// over a later accepted/promoted run, and is consumed by one eligible stop attempt.
|
|
221
|
+
private DeniedStartRecovery mDeniedStartRecovery;
|
|
81
222
|
private static final String DEFENSIVE_CHANNEL_ID = "notifee_fg_default";
|
|
82
223
|
private static final int DEFENSIVE_NOTIFICATION_ID = Integer.MAX_VALUE - 1;
|
|
83
224
|
|
|
@@ -85,6 +226,11 @@ public class ForegroundService extends Service {
|
|
|
85
226
|
final long generation;
|
|
86
227
|
final Object precedingStop;
|
|
87
228
|
boolean accepted;
|
|
229
|
+
// Dispatch acceptance is distinct from the promotion outcome and the physical instance's
|
|
230
|
+
// obligation. A denied delivered request is terminal even while its sender's reply is pending.
|
|
231
|
+
boolean promotionDenied;
|
|
232
|
+
ForegroundServiceRunLimitScheduler.Request alarmPoc;
|
|
233
|
+
ForegroundEpoch alarmPocEpoch;
|
|
88
234
|
// Null until delivery. An unfulfilled delivered request belongs to that physical instance.
|
|
89
235
|
ForegroundService instance;
|
|
90
236
|
|
|
@@ -94,6 +240,36 @@ public class ForegroundService extends Service {
|
|
|
94
240
|
}
|
|
95
241
|
}
|
|
96
242
|
|
|
243
|
+
private static final class DeniedStartRecovery {
|
|
244
|
+
final long generation;
|
|
245
|
+
final long runGeneration;
|
|
246
|
+
final int startId;
|
|
247
|
+
final int flags;
|
|
248
|
+
final RuntimeException denial;
|
|
249
|
+
final String requestId;
|
|
250
|
+
final String notificationId;
|
|
251
|
+
final int foregroundServiceType;
|
|
252
|
+
|
|
253
|
+
DeniedStartRecovery(
|
|
254
|
+
long generation,
|
|
255
|
+
long runGeneration,
|
|
256
|
+
int startId,
|
|
257
|
+
int flags,
|
|
258
|
+
RuntimeException denial,
|
|
259
|
+
String requestId,
|
|
260
|
+
String notificationId,
|
|
261
|
+
int foregroundServiceType) {
|
|
262
|
+
this.generation = generation;
|
|
263
|
+
this.runGeneration = runGeneration;
|
|
264
|
+
this.startId = startId;
|
|
265
|
+
this.flags = flags;
|
|
266
|
+
this.denial = denial;
|
|
267
|
+
this.requestId = requestId;
|
|
268
|
+
this.notificationId = notificationId;
|
|
269
|
+
this.foregroundServiceType = foregroundServiceType;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
97
273
|
/**
|
|
98
274
|
* Tracks whether startForeground() has been called on THIS service instance. This is an instance
|
|
99
275
|
* field (not static) because the static fields below track cross-invocation notification state
|
|
@@ -119,6 +295,10 @@ public class ForegroundService extends Service {
|
|
|
119
295
|
public void onCreate() {
|
|
120
296
|
super.onCreate();
|
|
121
297
|
synchronized (sLifecycleLock) {
|
|
298
|
+
if (sInstance != null) {
|
|
299
|
+
sInstance.mDeniedStartRecovery = null;
|
|
300
|
+
sInstance.invalidateAlarmPoc();
|
|
301
|
+
}
|
|
122
302
|
sInstance = this;
|
|
123
303
|
mRunGeneration = sRunGeneration;
|
|
124
304
|
synchronized (sLock) {
|
|
@@ -131,6 +311,8 @@ public class ForegroundService extends Service {
|
|
|
131
311
|
@Override
|
|
132
312
|
public void onDestroy() {
|
|
133
313
|
synchronized (sLifecycleLock) {
|
|
314
|
+
invalidateAlarmPoc();
|
|
315
|
+
mDeniedStartRecovery = null;
|
|
134
316
|
sPendingStarts.values().removeIf(request -> request.instance == this);
|
|
135
317
|
if (sInstance == this) {
|
|
136
318
|
sInstance = null;
|
|
@@ -159,12 +341,20 @@ public class ForegroundService extends Service {
|
|
|
159
341
|
}
|
|
160
342
|
|
|
161
343
|
private void clearOwnedState() {
|
|
344
|
+
ForegroundServiceRunLimitScheduler.Ticket ticket = null;
|
|
162
345
|
synchronized (sLock) {
|
|
163
346
|
if (sStateOwner == this) {
|
|
347
|
+
ticket = revokeAlarmPoc();
|
|
348
|
+
mDeniedStartRecovery = null;
|
|
164
349
|
clearCurrentState();
|
|
165
350
|
mRunnerToken = null;
|
|
351
|
+
if (mForegroundEpoch != null) {
|
|
352
|
+
mForegroundEpoch.retired = true;
|
|
353
|
+
}
|
|
166
354
|
}
|
|
167
355
|
}
|
|
356
|
+
// Finish A's local retirement before cancellation can synchronously admit a successor.
|
|
357
|
+
if (ticket != null) ForegroundServiceRunLimitScheduler.cancel(ticket);
|
|
168
358
|
}
|
|
169
359
|
|
|
170
360
|
/**
|
|
@@ -176,20 +366,68 @@ public class ForegroundService extends Service {
|
|
|
176
366
|
*/
|
|
177
367
|
@SuppressLint("MissingPermission")
|
|
178
368
|
static boolean repostIfActive(String notificationId) {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
369
|
+
try {
|
|
370
|
+
synchronized (sLifecycleLock) {
|
|
371
|
+
synchronized (sLock) {
|
|
372
|
+
if (mCurrentNotificationId == null
|
|
373
|
+
|| (sStateOwner != null
|
|
374
|
+
&& sStateOwner.mForegroundEpoch != null
|
|
375
|
+
&& sStateOwner.mForegroundEpoch.retiring)
|
|
376
|
+
|| !mCurrentNotificationId.equals(notificationId)
|
|
377
|
+
|| mCurrentNotification == null) {
|
|
378
|
+
return false;
|
|
379
|
+
}
|
|
380
|
+
Logger.w(TAG, "Submitting dismissed foreground service notification for re-post");
|
|
381
|
+
NotificationManagerCompat.from(ContextHolder.getApplicationContext())
|
|
382
|
+
.notify(mCurrentHashCode, mCurrentNotification);
|
|
383
|
+
return true;
|
|
384
|
+
}
|
|
184
385
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
.notify(mCurrentHashCode, mCurrentNotification);
|
|
188
|
-
return true;
|
|
386
|
+
} finally {
|
|
387
|
+
publishTimeoutEventsIfUnlocked();
|
|
189
388
|
}
|
|
190
389
|
}
|
|
191
390
|
|
|
391
|
+
// Internal smoke entrypoint, deliberately absent from Notifee/React Native public APIs.
|
|
392
|
+
static void startAlarmPoc(
|
|
393
|
+
int hashCode,
|
|
394
|
+
Notification notification,
|
|
395
|
+
Bundle notificationBundle,
|
|
396
|
+
String run,
|
|
397
|
+
long requestedDurationMs) {
|
|
398
|
+
start(
|
|
399
|
+
hashCode,
|
|
400
|
+
notification,
|
|
401
|
+
notificationBundle,
|
|
402
|
+
new ForegroundServiceRunLimitScheduler.Request(
|
|
403
|
+
ContextHolder.getApplicationContext(), run, requestedDurationMs));
|
|
404
|
+
}
|
|
405
|
+
|
|
192
406
|
static void start(int hashCode, Notification notification, Bundle notificationBundle) {
|
|
407
|
+
app.notifee.core.model.NotificationAndroidModel android =
|
|
408
|
+
NotificationModel.fromBundle(notificationBundle).getAndroid();
|
|
409
|
+
Long duration = android.getForegroundServiceBestEffortStopAfterMs();
|
|
410
|
+
ForegroundServiceRunLimitScheduler.Request request = null;
|
|
411
|
+
if (duration != null) {
|
|
412
|
+
// The model rejects this too; keep the platform guard explicit at API-specific calls.
|
|
413
|
+
if (Build.VERSION.SDK_INT < 35) {
|
|
414
|
+
throw new IllegalArgumentException(
|
|
415
|
+
"notification.android.foregroundServiceBestEffortStopAfterMs requires API 35+");
|
|
416
|
+
}
|
|
417
|
+
int type = android.getForegroundServiceType();
|
|
418
|
+
if (type == ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST) type = resolveManifestServiceType();
|
|
419
|
+
request =
|
|
420
|
+
ForegroundServiceRunLimitScheduler.publicRequest(
|
|
421
|
+
ContextHolder.getApplicationContext(), duration, type);
|
|
422
|
+
}
|
|
423
|
+
start(hashCode, notification, notificationBundle, request);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
private static void start(
|
|
427
|
+
int hashCode,
|
|
428
|
+
Notification notification,
|
|
429
|
+
Bundle notificationBundle,
|
|
430
|
+
ForegroundServiceRunLimitScheduler.Request alarmPoc) {
|
|
193
431
|
Context context = ContextHolder.getApplicationContext();
|
|
194
432
|
if (context == null) {
|
|
195
433
|
Logger.e(TAG, "Application context is null; cannot start ForegroundService.");
|
|
@@ -230,8 +468,35 @@ public class ForegroundService extends Service {
|
|
|
230
468
|
intent.putExtra(START_REQUEST_ID, requestId);
|
|
231
469
|
StartRequest request;
|
|
232
470
|
synchronized (sLifecycleLock) {
|
|
471
|
+
if (alarmPoc != null
|
|
472
|
+
&& (!sPendingStarts.isEmpty()
|
|
473
|
+
|| (sInstance != null
|
|
474
|
+
&& (sInstance.mForegroundActive || sInstance.mRunnerToken != null)))) {
|
|
475
|
+
if (!alarmPoc.publicOption)
|
|
476
|
+
throw new IllegalStateException("Diagnostic arming requires a new quiescent run");
|
|
477
|
+
// Repeating the same option on an owned same-ID update is accepted without rearming.
|
|
478
|
+
// Enabling/changing it during an existing run or unresolved START is rejected
|
|
479
|
+
// pre-dispatch.
|
|
480
|
+
synchronized (sLock) {
|
|
481
|
+
ForegroundService owner = sInstance;
|
|
482
|
+
String id = NotificationModel.fromBundle(notificationBundle).getId();
|
|
483
|
+
if (!sPendingStarts.isEmpty()
|
|
484
|
+
|| owner == null
|
|
485
|
+
|| owner.mAlarmPoc == null
|
|
486
|
+
|| !owner.ownsAlarmPoc(owner.mAlarmPoc)
|
|
487
|
+
|| !id.equals(mCurrentNotificationId)
|
|
488
|
+
|| alarmPoc.duration != owner.mAlarmPoc.duration) {
|
|
489
|
+
throw new IllegalArgumentException(
|
|
490
|
+
"notification.android.foregroundServiceBestEffortStopAfterMs cannot be enabled or"
|
|
491
|
+
+ " changed during an active/pending run; stop and start a new run");
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
alarmPoc = null;
|
|
495
|
+
}
|
|
233
496
|
request = new StartRequest(sRunGeneration, sStopRequest);
|
|
497
|
+
request.alarmPoc = alarmPoc;
|
|
234
498
|
sPendingStarts.put(requestId, request);
|
|
499
|
+
sStartAdmissionRevision++;
|
|
235
500
|
}
|
|
236
501
|
ComponentName component;
|
|
237
502
|
try {
|
|
@@ -268,8 +533,14 @@ public class ForegroundService extends Service {
|
|
|
268
533
|
// Caller holds sLifecycleLock. Delivery itself can prove acceptance before Context returns.
|
|
269
534
|
// A late reply never cancels a STOP ordered after this START was reserved/delivered.
|
|
270
535
|
private static void acceptStart(StartRequest request) {
|
|
536
|
+
if (request.promotionDenied) {
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
271
539
|
if (!request.accepted) {
|
|
272
540
|
request.accepted = true;
|
|
541
|
+
if (sInstance != null) {
|
|
542
|
+
sInstance.mDeniedStartRecovery = null;
|
|
543
|
+
}
|
|
273
544
|
if (sStopRequest == request.precedingStop) {
|
|
274
545
|
sStopRequest = null;
|
|
275
546
|
}
|
|
@@ -281,8 +552,14 @@ public class ForegroundService extends Service {
|
|
|
281
552
|
if (sInstance == null && sPendingStarts.isEmpty()) {
|
|
282
553
|
return;
|
|
283
554
|
}
|
|
555
|
+
if (sInstance != null) {
|
|
556
|
+
sInstance.mDeniedStartRecovery = null;
|
|
557
|
+
}
|
|
558
|
+
// Order STOP before cancellation IPC/storage: a reentrant later START must observe
|
|
559
|
+
// and be able to supersede this request, rather than inherit an outer stale STOP.
|
|
284
560
|
sStopRequest = new Object();
|
|
285
561
|
sRunGeneration++;
|
|
562
|
+
if (sInstance != null) sInstance.invalidateAlarmPoc();
|
|
286
563
|
scheduleStop();
|
|
287
564
|
}
|
|
288
565
|
}
|
|
@@ -295,9 +572,89 @@ public class ForegroundService extends Service {
|
|
|
295
572
|
if (instance != null && request != null) {
|
|
296
573
|
new Handler(Looper.getMainLooper()).post(() -> instance.stopIfRequested(request));
|
|
297
574
|
}
|
|
575
|
+
if (instance != null && instance.mDeniedStartRecovery != null && sPendingStarts.isEmpty()) {
|
|
576
|
+
DeniedStartRecovery recovery = instance.mDeniedStartRecovery;
|
|
577
|
+
new Handler(Looper.getMainLooper()).post(() -> instance.stopDeniedStartIfCurrent(recovery));
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
private void stopDeniedStartIfCurrent(DeniedStartRecovery recovery) {
|
|
582
|
+
try {
|
|
583
|
+
stopDeniedStartIfCurrentLocked(recovery);
|
|
584
|
+
} finally {
|
|
585
|
+
publishTimeoutEventsIfUnlocked();
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
private void stopDeniedStartIfCurrentLocked(DeniedStartRecovery recovery) {
|
|
590
|
+
synchronized (sLifecycleLock) {
|
|
591
|
+
if (mDeniedStartRecovery != recovery) {
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
synchronized (sLock) {
|
|
595
|
+
if (!ownsUnpromotedState()
|
|
596
|
+
|| sRunGeneration != recovery.generation
|
|
597
|
+
|| mRunGeneration != recovery.runGeneration
|
|
598
|
+
|| mLastStartId != recovery.startId
|
|
599
|
+
|| mStopInitiated) {
|
|
600
|
+
mDeniedStartRecovery = null;
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
String stopOutcome = "deferred";
|
|
605
|
+
try {
|
|
606
|
+
if (sPendingStarts.isEmpty()) {
|
|
607
|
+
StopAuthority authority = captureStopAuthority();
|
|
608
|
+
// Consume before Binder IPC: refusal or failure grants no automatic retry authority.
|
|
609
|
+
mDeniedStartRecovery = null;
|
|
610
|
+
stopOutcome = "failed";
|
|
611
|
+
boolean stopped = stopSelfResult(recovery.startId);
|
|
612
|
+
stopOutcome = stopped ? "accepted" : "refused";
|
|
613
|
+
synchronized (sLock) {
|
|
614
|
+
if (stopped && ownsStopAuthority(authority) && ownsUnpromotedState()) {
|
|
615
|
+
mStopInitiated = true;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
} finally {
|
|
620
|
+
// Preserve the promotion diagnostic even if the separate stop operation throws.
|
|
621
|
+
Logger.w(
|
|
622
|
+
TAG,
|
|
623
|
+
(recovery.requestId == null
|
|
624
|
+
? "Framework redelivered START promotion denied (startId="
|
|
625
|
+
: "Tracked START PROMOTION_DENIED (cause=UNKNOWN, requestId="
|
|
626
|
+
+ recovery.requestId
|
|
627
|
+
+ ", notificationId="
|
|
628
|
+
+ recovery.notificationId
|
|
629
|
+
+ ", api="
|
|
630
|
+
+ Build.VERSION.SDK_INT
|
|
631
|
+
+ ", type="
|
|
632
|
+
+ recovery.foregroundServiceType
|
|
633
|
+
+ ", generation="
|
|
634
|
+
+ recovery.generation
|
|
635
|
+
+ ", runGeneration="
|
|
636
|
+
+ recovery.runGeneration
|
|
637
|
+
+ ", startId=")
|
|
638
|
+
+ recovery.startId
|
|
639
|
+
+ ", flags="
|
|
640
|
+
+ recovery.flags
|
|
641
|
+
+ ", stop="
|
|
642
|
+
+ stopOutcome
|
|
643
|
+
+ ").",
|
|
644
|
+
recovery.denial);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
298
647
|
}
|
|
299
648
|
|
|
300
649
|
private void stopIfRequested(Object request) {
|
|
650
|
+
try {
|
|
651
|
+
stopIfRequestedLocked(request);
|
|
652
|
+
} finally {
|
|
653
|
+
publishTimeoutEventsIfUnlocked();
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
private void stopIfRequestedLocked(Object request) {
|
|
301
658
|
synchronized (sLifecycleLock) {
|
|
302
659
|
if (sInstance != this
|
|
303
660
|
|| sStopRequest != request
|
|
@@ -307,24 +664,60 @@ public class ForegroundService extends Service {
|
|
|
307
664
|
|| mStartCommandFailed) {
|
|
308
665
|
return;
|
|
309
666
|
}
|
|
310
|
-
//
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
667
|
+
// Consume this request before IPC so a reentrant START cannot schedule it against its
|
|
668
|
+
// successor. Refusal/failure restores it only while the original admission still owns it.
|
|
669
|
+
sStopRequest = null;
|
|
670
|
+
StopAuthority authority = captureStopAuthority();
|
|
671
|
+
boolean stopped = false;
|
|
672
|
+
try {
|
|
673
|
+
// The framework token and startId also protect an accepted, not yet delivered START.
|
|
674
|
+
stopped = stopSelfResult(authority.startId);
|
|
675
|
+
if (stopped && ownsStopAuthority(authority)) {
|
|
676
|
+
mStopInitiated = true;
|
|
677
|
+
mForegroundActive = false;
|
|
678
|
+
clearOwnedState();
|
|
679
|
+
}
|
|
680
|
+
} finally {
|
|
681
|
+
if (!stopped && ownsStopAuthority(authority)) {
|
|
682
|
+
sStopRequest = request;
|
|
683
|
+
}
|
|
316
684
|
}
|
|
317
685
|
}
|
|
318
686
|
}
|
|
319
687
|
|
|
320
688
|
@Override
|
|
321
689
|
public int onStartCommand(Intent intent, int flags, int startId) {
|
|
690
|
+
try {
|
|
691
|
+
return onStartCommandLocked(intent, flags, startId);
|
|
692
|
+
} finally {
|
|
693
|
+
publishTimeoutEventsIfUnlocked();
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
private int onStartCommandLocked(Intent intent, int flags, int startId) {
|
|
322
698
|
synchronized (sLifecycleLock) {
|
|
699
|
+
// Any later delivery supersedes the recovery startId, including terminal commands.
|
|
700
|
+
mDeniedStartRecovery = null;
|
|
323
701
|
mLastStartId = startId;
|
|
324
702
|
boolean terminal =
|
|
325
703
|
intent == null || STOP_FOREGROUND_SERVICE_ACTION.equals(intent.getAction());
|
|
326
704
|
if (terminal) {
|
|
327
|
-
sRunGeneration
|
|
705
|
+
long generation = ++sRunGeneration;
|
|
706
|
+
long admission = sStartAdmissionRevision;
|
|
707
|
+
long runGeneration = mRunGeneration;
|
|
708
|
+
ForegroundEpoch epoch = mForegroundEpoch;
|
|
709
|
+
invalidateAlarmPoc();
|
|
710
|
+
// This terminal delivery predates every START accepted during cancellation, including
|
|
711
|
+
// an undelivered obligation whose startId/epoch cannot yet reveal the new admission.
|
|
712
|
+
if (sInstance != this
|
|
713
|
+
|| sStateOwner != this
|
|
714
|
+
|| sRunGeneration != generation
|
|
715
|
+
|| sStartAdmissionRevision != admission
|
|
716
|
+
|| mRunGeneration != runGeneration
|
|
717
|
+
|| mForegroundEpoch != epoch
|
|
718
|
+
|| mLastStartId != startId) {
|
|
719
|
+
return START_STICKY_COMPATIBILITY;
|
|
720
|
+
}
|
|
328
721
|
}
|
|
329
722
|
if (terminal && !sPendingStarts.isEmpty()) {
|
|
330
723
|
// The earlier accepted START must promote with its real notification/type first.
|
|
@@ -336,6 +729,13 @@ public class ForegroundService extends Service {
|
|
|
336
729
|
}
|
|
337
730
|
String requestId = intent == null ? null : intent.getStringExtra(START_REQUEST_ID);
|
|
338
731
|
StartRequest request = sPendingStarts.get(requestId);
|
|
732
|
+
boolean frameworkRedelivery =
|
|
733
|
+
intent != null
|
|
734
|
+
&& START_FOREGROUND_SERVICE_ACTION.equals(intent.getAction())
|
|
735
|
+
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
|
|
736
|
+
&& (flags & (START_FLAG_RETRY | START_FLAG_REDELIVERY)) != 0
|
|
737
|
+
&& isValidStartRequestId(requestId)
|
|
738
|
+
&& request == null;
|
|
339
739
|
boolean superseded = false;
|
|
340
740
|
if (!terminal) {
|
|
341
741
|
long generation = request == null ? sRunGeneration : request.generation;
|
|
@@ -347,6 +747,7 @@ public class ForegroundService extends Service {
|
|
|
347
747
|
if (!superseded && mRunGeneration != generation) {
|
|
348
748
|
// Reserve the new session without retiring the previous submitted state or runner.
|
|
349
749
|
// A failed promotion must preserve them, while older deliveries remain fenced.
|
|
750
|
+
invalidateAlarmPoc();
|
|
350
751
|
mRunGeneration = generation;
|
|
351
752
|
}
|
|
352
753
|
}
|
|
@@ -358,11 +759,16 @@ public class ForegroundService extends Service {
|
|
|
358
759
|
// that run already ended, still perform the old request's real promotion before teardown.
|
|
359
760
|
result = mForegroundActive ? START_NOT_STICKY : fulfillSupersededStart(intent);
|
|
360
761
|
} else {
|
|
361
|
-
result =
|
|
762
|
+
result =
|
|
763
|
+
handleStartCommand(intent, flags, startId, frameworkRedelivery, requestId, request);
|
|
362
764
|
}
|
|
363
765
|
mStartCommandFailed = false;
|
|
364
766
|
if (mStartCommandSatisfied) {
|
|
767
|
+
if (!terminal && !superseded) {
|
|
768
|
+
recordSatisfiedStart(startId);
|
|
769
|
+
}
|
|
365
770
|
settleDeliveredStarts();
|
|
771
|
+
if (!terminal && !superseded) maybeArmAlarmPoc(request);
|
|
366
772
|
}
|
|
367
773
|
if (superseded
|
|
368
774
|
&& mStartCommandSatisfied
|
|
@@ -379,6 +785,9 @@ public class ForegroundService extends Service {
|
|
|
379
785
|
} catch (RuntimeException e) {
|
|
380
786
|
mStartCommandFailed = !mStartCommandSatisfied;
|
|
381
787
|
if (mStartCommandSatisfied) {
|
|
788
|
+
if (!terminal && !superseded) {
|
|
789
|
+
recordSatisfiedStart(startId);
|
|
790
|
+
}
|
|
382
791
|
settleDeliveredStarts();
|
|
383
792
|
}
|
|
384
793
|
throw e;
|
|
@@ -386,10 +795,63 @@ public class ForegroundService extends Service {
|
|
|
386
795
|
}
|
|
387
796
|
}
|
|
388
797
|
|
|
798
|
+
private static boolean isValidStartRequestId(@Nullable String requestId) {
|
|
799
|
+
if (requestId == null || requestId.length() != 36) {
|
|
800
|
+
return false;
|
|
801
|
+
}
|
|
802
|
+
try {
|
|
803
|
+
return UUID.fromString(requestId).toString().equalsIgnoreCase(requestId);
|
|
804
|
+
} catch (IllegalArgumentException e) {
|
|
805
|
+
return false;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
// Caller holds sLifecycleLock and sLock. Recovery cannot retire an acquired run or another owner.
|
|
810
|
+
private boolean ownsUnpromotedState() {
|
|
811
|
+
return sInstance == this
|
|
812
|
+
&& sStateOwner == this
|
|
813
|
+
&& !mStartForegroundCalled
|
|
814
|
+
&& !mForegroundActive
|
|
815
|
+
&& !mStartCommandSatisfied
|
|
816
|
+
&& mRunnerToken == null
|
|
817
|
+
&& mRunFirstStartId == 0
|
|
818
|
+
&& mCurrentNotificationId == null
|
|
819
|
+
&& mCurrentForegroundServiceType == -1
|
|
820
|
+
&& mCurrentNotificationBundle == null
|
|
821
|
+
&& mCurrentNotification == null
|
|
822
|
+
&& mCurrentHashCode == 0;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// Caller holds both lifecycle/state locks. Revalidate the original admission after the platform
|
|
826
|
+
// attempt: a synchronous authority change must not authorize a stale request's recovery.
|
|
827
|
+
private boolean canRecoverTrackedDenial(
|
|
828
|
+
Intent intent,
|
|
829
|
+
String requestId,
|
|
830
|
+
StartRequest request,
|
|
831
|
+
long generation,
|
|
832
|
+
long runGeneration,
|
|
833
|
+
int startId) {
|
|
834
|
+
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM
|
|
835
|
+
&& START_FOREGROUND_SERVICE_ACTION.equals(intent.getAction())
|
|
836
|
+
&& isValidStartRequestId(requestId)
|
|
837
|
+
&& request != null
|
|
838
|
+
&& sPendingStarts.get(requestId) == request
|
|
839
|
+
&& request.instance == this
|
|
840
|
+
&& request.accepted
|
|
841
|
+
&& !request.promotionDenied
|
|
842
|
+
&& request.generation == runGeneration
|
|
843
|
+
&& sRunGeneration == generation
|
|
844
|
+
&& mRunGeneration == runGeneration
|
|
845
|
+
&& mLastStartId == startId
|
|
846
|
+
&& !mStopInitiated
|
|
847
|
+
&& ownsUnpromotedState();
|
|
848
|
+
}
|
|
849
|
+
|
|
389
850
|
// A real promotion (or an already foreground instance) satisfies the physical instance's
|
|
390
851
|
// delivered obligations, including a prior ambiguous delivery. Undelivered STARTs remain held.
|
|
391
852
|
// This does not turn a posting/type exception into success: onStartCommand still rethrows it.
|
|
392
853
|
private void settleDeliveredStarts() {
|
|
854
|
+
mDeniedStartRecovery = null;
|
|
393
855
|
sPendingStarts.values().removeIf(request -> request.instance == this);
|
|
394
856
|
}
|
|
395
857
|
|
|
@@ -427,13 +889,22 @@ public class ForegroundService extends Service {
|
|
|
427
889
|
}
|
|
428
890
|
|
|
429
891
|
@SuppressLint({"ForegroundServiceType", "MissingPermission"})
|
|
430
|
-
private int handleStartCommand(
|
|
892
|
+
private int handleStartCommand(
|
|
893
|
+
Intent intent,
|
|
894
|
+
int flags,
|
|
895
|
+
int startId,
|
|
896
|
+
boolean frameworkRedelivery,
|
|
897
|
+
String requestId,
|
|
898
|
+
StartRequest request) {
|
|
431
899
|
Trace.beginSection("notifee:ForegroundService.onStartCommand");
|
|
432
900
|
try {
|
|
433
901
|
// Check if action is to stop the foreground service
|
|
434
902
|
if (intent == null || STOP_FOREGROUND_SERVICE_ACTION.equals(intent.getAction())) {
|
|
903
|
+
StopAuthority authority = captureStopAuthority();
|
|
435
904
|
ensureStartForegroundContractSatisfied(true);
|
|
436
|
-
if (
|
|
905
|
+
if (ownsStopAuthority(authority)
|
|
906
|
+
&& stopSelfResult(authority.startId)
|
|
907
|
+
&& ownsStopAuthority(authority)) {
|
|
437
908
|
mStopInitiated = true;
|
|
438
909
|
mForegroundActive = false;
|
|
439
910
|
clearOwnedState();
|
|
@@ -456,9 +927,14 @@ public class ForegroundService extends Service {
|
|
|
456
927
|
|
|
457
928
|
Object pendingEvent = null;
|
|
458
929
|
boolean noneEarlyReturn = false;
|
|
930
|
+
RuntimeException deniedInitialPromotion = null;
|
|
931
|
+
int deniedForegroundServiceType = -1;
|
|
932
|
+
boolean deniedTrackedStart = false;
|
|
459
933
|
|
|
460
934
|
synchronized (sLock) {
|
|
461
|
-
if (mCurrentNotificationId == null
|
|
935
|
+
if (mCurrentNotificationId == null
|
|
936
|
+
|| mStateGeneration != mRunGeneration
|
|
937
|
+
|| (mForegroundEpoch != null && mForegroundEpoch.retiring)) {
|
|
462
938
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
463
939
|
int foregroundServiceType =
|
|
464
940
|
notificationModel.getAndroid().getForegroundServiceType();
|
|
@@ -475,16 +951,39 @@ public class ForegroundService extends Service {
|
|
|
475
951
|
// Binder IPC (PackageManager + startForeground) and must not hold sLock.
|
|
476
952
|
noneEarlyReturn = true;
|
|
477
953
|
} else {
|
|
954
|
+
boolean recoverInitialDenial = frameworkRedelivery && ownsUnpromotedState();
|
|
955
|
+
long generation = sRunGeneration;
|
|
956
|
+
long runGeneration = mRunGeneration;
|
|
957
|
+
boolean recoverTrackedDenial =
|
|
958
|
+
canRecoverTrackedDenial(
|
|
959
|
+
intent, requestId, request, generation, runGeneration, startId);
|
|
478
960
|
Trace.beginSection("notifee:startForeground");
|
|
479
961
|
try {
|
|
480
962
|
startForeground(hashCode, notification, foregroundServiceType);
|
|
963
|
+
} catch (RuntimeException e) {
|
|
964
|
+
// Only the real initial promotion is exempt. Type resolution, NONE, posting,
|
|
965
|
+
// updates and teardown retain their existing failure boundaries.
|
|
966
|
+
boolean trackedDenial =
|
|
967
|
+
recoverTrackedDenial
|
|
968
|
+
&& canRecoverTrackedDenial(
|
|
969
|
+
intent, requestId, request, generation, runGeneration, startId);
|
|
970
|
+
if (!(trackedDenial || (recoverInitialDenial && ownsUnpromotedState()))
|
|
971
|
+
|| Build.VERSION.SDK_INT < Build.VERSION_CODES.S
|
|
972
|
+
|| e.getClass() != ForegroundServiceStartNotAllowedException.class) {
|
|
973
|
+
throw e;
|
|
974
|
+
}
|
|
975
|
+
deniedInitialPromotion = e;
|
|
976
|
+
deniedForegroundServiceType = foregroundServiceType;
|
|
977
|
+
deniedTrackedStart = trackedDenial;
|
|
481
978
|
} finally {
|
|
482
979
|
Trace.endSection();
|
|
483
980
|
}
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
981
|
+
if (deniedInitialPromotion == null) {
|
|
982
|
+
mStartForegroundCalled = true;
|
|
983
|
+
mForegroundActive = true;
|
|
984
|
+
mStartCommandSatisfied = true;
|
|
985
|
+
mCurrentForegroundServiceType = foregroundServiceType;
|
|
986
|
+
}
|
|
488
987
|
}
|
|
489
988
|
} else {
|
|
490
989
|
Trace.beginSection("notifee:startForeground");
|
|
@@ -498,7 +997,10 @@ public class ForegroundService extends Service {
|
|
|
498
997
|
mStartCommandSatisfied = true;
|
|
499
998
|
}
|
|
500
999
|
|
|
501
|
-
if (!noneEarlyReturn) {
|
|
1000
|
+
if (!noneEarlyReturn && deniedInitialPromotion == null) {
|
|
1001
|
+
if (!commitForegroundEpoch()) return START_NOT_STICKY;
|
|
1002
|
+
if (request != null && request.alarmPoc != null)
|
|
1003
|
+
request.alarmPocEpoch = mForegroundEpoch;
|
|
502
1004
|
mCurrentNotificationId = notificationId;
|
|
503
1005
|
mCurrentNotificationBundle = bundle;
|
|
504
1006
|
mCurrentNotification = notification;
|
|
@@ -513,13 +1015,25 @@ public class ForegroundService extends Service {
|
|
|
513
1015
|
(e, aVoid) -> {
|
|
514
1016
|
Runnable complete =
|
|
515
1017
|
() -> {
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
1018
|
+
try {
|
|
1019
|
+
synchronized (sLifecycleLock) {
|
|
1020
|
+
if (sInstance == this
|
|
1021
|
+
&& mRunnerToken == runnerToken
|
|
1022
|
+
&& sRunGeneration == runnerGeneration
|
|
1023
|
+
&& mForegroundEpoch != null
|
|
1024
|
+
&& !mForegroundEpoch.retiring
|
|
1025
|
+
&& !mForegroundEpoch.retired) {
|
|
1026
|
+
ForegroundEpoch epoch = mForegroundEpoch;
|
|
1027
|
+
stopForegroundCompat();
|
|
1028
|
+
if (sInstance == this
|
|
1029
|
+
&& mForegroundEpoch == epoch
|
|
1030
|
+
&& mRunnerToken == runnerToken) {
|
|
1031
|
+
clearOwnedState();
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
522
1034
|
}
|
|
1035
|
+
} finally {
|
|
1036
|
+
publishTimeoutEventsIfUnlocked();
|
|
523
1037
|
}
|
|
524
1038
|
};
|
|
525
1039
|
if (Looper.myLooper() == Looper.getMainLooper()) {
|
|
@@ -533,6 +1047,8 @@ public class ForegroundService extends Service {
|
|
|
533
1047
|
}
|
|
534
1048
|
} else {
|
|
535
1049
|
if (mCurrentNotificationId.equals(notificationId)) {
|
|
1050
|
+
ForegroundEpoch updateEpoch = mForegroundEpoch;
|
|
1051
|
+
long updateGeneration = sRunGeneration;
|
|
536
1052
|
// Before API 31, updating the service's registered notification requires
|
|
537
1053
|
// startForeground(). API 31+ processes a same-ID notify() against the foreground
|
|
538
1054
|
// service record. Keep that path for an unchanged type: repeating
|
|
@@ -566,13 +1082,27 @@ public class ForegroundService extends Service {
|
|
|
566
1082
|
} finally {
|
|
567
1083
|
Trace.endSection();
|
|
568
1084
|
}
|
|
569
|
-
mStartForegroundCalled = true;
|
|
570
|
-
mForegroundActive = true;
|
|
571
|
-
mStartCommandSatisfied = true;
|
|
572
1085
|
} else {
|
|
573
1086
|
NotificationManagerCompat.from(ContextHolder.getApplicationContext())
|
|
574
1087
|
.notify(mCurrentHashCode, notification);
|
|
575
1088
|
}
|
|
1089
|
+
// A timeout or successor may have retired/replaced the publication during IPC.
|
|
1090
|
+
// A normal return cannot reacquire its cache, runner or promotion authority.
|
|
1091
|
+
if (!ownsTimeoutEpoch(updateEpoch)
|
|
1092
|
+
|| updateEpoch == null
|
|
1093
|
+
|| updateEpoch.retiring
|
|
1094
|
+
|| updateEpoch.retired
|
|
1095
|
+
|| mLastStartId != startId
|
|
1096
|
+
|| mRunGeneration != updateEpoch.runGeneration
|
|
1097
|
+
|| sRunGeneration != updateGeneration) {
|
|
1098
|
+
return START_NOT_STICKY;
|
|
1099
|
+
}
|
|
1100
|
+
if (shouldStartForeground) {
|
|
1101
|
+
mStartForegroundCalled = true;
|
|
1102
|
+
mForegroundActive = true;
|
|
1103
|
+
mStartCommandSatisfied = true;
|
|
1104
|
+
if (!commitForegroundEpoch()) return START_NOT_STICKY;
|
|
1105
|
+
}
|
|
576
1106
|
// The platform call returned normally, so cache the latest submitted state for
|
|
577
1107
|
// re-post and timeout. A normal return does not acknowledge display.
|
|
578
1108
|
mCurrentForegroundServiceType = foregroundServiceType;
|
|
@@ -586,6 +1116,28 @@ public class ForegroundService extends Service {
|
|
|
586
1116
|
}
|
|
587
1117
|
}
|
|
588
1118
|
|
|
1119
|
+
if (deniedInitialPromotion != null) {
|
|
1120
|
+
// Release sLock before stop Binder IPC. Terminalize only the exact denied request;
|
|
1121
|
+
// this is not a successful promotion and cannot fulfill any other delivered START.
|
|
1122
|
+
if (deniedTrackedStart) {
|
|
1123
|
+
request.promotionDenied = true;
|
|
1124
|
+
sPendingStarts.remove(requestId, request);
|
|
1125
|
+
}
|
|
1126
|
+
DeniedStartRecovery recovery =
|
|
1127
|
+
new DeniedStartRecovery(
|
|
1128
|
+
sRunGeneration,
|
|
1129
|
+
mRunGeneration,
|
|
1130
|
+
startId,
|
|
1131
|
+
flags,
|
|
1132
|
+
deniedInitialPromotion,
|
|
1133
|
+
deniedTrackedStart ? requestId : null,
|
|
1134
|
+
notificationId,
|
|
1135
|
+
deniedForegroundServiceType);
|
|
1136
|
+
mDeniedStartRecovery = recovery;
|
|
1137
|
+
stopDeniedStartIfCurrent(recovery);
|
|
1138
|
+
return START_NOT_STICKY;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
589
1141
|
// Handle NONE early return outside the lock — the helper performs Binder IPC
|
|
590
1142
|
// (PackageManager query + startForeground) that must not hold sLock.
|
|
591
1143
|
if (noneEarlyReturn) {
|
|
@@ -618,7 +1170,11 @@ public class ForegroundService extends Service {
|
|
|
618
1170
|
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
|
619
1171
|
@Override
|
|
620
1172
|
public void onTimeout(int startId) {
|
|
621
|
-
|
|
1173
|
+
try {
|
|
1174
|
+
handleTimeout(startId, -1, true);
|
|
1175
|
+
} finally {
|
|
1176
|
+
publishTimeoutEventsIfUnlocked();
|
|
1177
|
+
}
|
|
622
1178
|
}
|
|
623
1179
|
|
|
624
1180
|
/**
|
|
@@ -628,7 +1184,11 @@ public class ForegroundService extends Service {
|
|
|
628
1184
|
@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
|
|
629
1185
|
@Override
|
|
630
1186
|
public void onTimeout(int startId, int fgsType) {
|
|
631
|
-
|
|
1187
|
+
try {
|
|
1188
|
+
handleTimeout(startId, fgsType, false);
|
|
1189
|
+
} finally {
|
|
1190
|
+
publishTimeoutEventsIfUnlocked();
|
|
1191
|
+
}
|
|
632
1192
|
}
|
|
633
1193
|
|
|
634
1194
|
@RequiresApi(Build.VERSION_CODES.Q)
|
|
@@ -679,12 +1239,20 @@ public class ForegroundService extends Service {
|
|
|
679
1239
|
|
|
680
1240
|
@SuppressWarnings("deprecation")
|
|
681
1241
|
private void stopForegroundCompat() {
|
|
1242
|
+
ForegroundEpoch epoch = mForegroundEpoch;
|
|
1243
|
+
removeForegroundNotification();
|
|
1244
|
+
if (sInstance == this && sStateOwner == this && mForegroundEpoch == epoch) {
|
|
1245
|
+
mForegroundActive = false;
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
@SuppressWarnings("deprecation")
|
|
1250
|
+
private void removeForegroundNotification() {
|
|
682
1251
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
683
1252
|
stopForeground(STOP_FOREGROUND_REMOVE);
|
|
684
1253
|
} else {
|
|
685
1254
|
stopForeground(true);
|
|
686
1255
|
}
|
|
687
|
-
mForegroundActive = false;
|
|
688
1256
|
}
|
|
689
1257
|
|
|
690
1258
|
/**
|
|
@@ -843,36 +1411,412 @@ public class ForegroundService extends Service {
|
|
|
843
1411
|
stopForegroundCompat();
|
|
844
1412
|
}
|
|
845
1413
|
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
1414
|
+
// Caller holds sLifecycleLock. Platform calls may synchronously reenter lifecycle callbacks.
|
|
1415
|
+
private boolean ownsTimeoutEpoch(ForegroundEpoch epoch) {
|
|
1416
|
+
synchronized (sLock) {
|
|
1417
|
+
return sInstance == this && sStateOwner == this && mForegroundEpoch == epoch;
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
private void retireTimeoutEpoch(ForegroundEpoch epoch) {
|
|
1422
|
+
synchronized (sLock) {
|
|
1423
|
+
if (ownsTimeoutEpoch(epoch)) {
|
|
1424
|
+
mForegroundActive = false;
|
|
1425
|
+
clearOwnedState();
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
|
1431
|
+
private void handleTimeout(int startId, int fgsType, boolean shortServiceCallback) {
|
|
1432
|
+
Bundle notificationBundle;
|
|
1433
|
+
ForegroundEpoch epoch;
|
|
1434
|
+
ForegroundServiceRunLimitScheduler.Ticket timeoutAlarm;
|
|
1435
|
+
RuntimeException primary = null;
|
|
1436
|
+
String demotionOutcome = "failed";
|
|
1437
|
+
String stopOutcome = "deferred";
|
|
1438
|
+
synchronized (sLifecycleLock) {
|
|
1439
|
+
final boolean stopAuthorized;
|
|
1440
|
+
final int stopId;
|
|
1441
|
+
final Object stopRequest = sStopRequest;
|
|
1442
|
+
synchronized (sLock) {
|
|
1443
|
+
epoch = mForegroundEpoch;
|
|
1444
|
+
int callbackType =
|
|
1445
|
+
shortServiceCallback ? ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE : fgsType;
|
|
1446
|
+
if (sInstance != this
|
|
1447
|
+
|| sStateOwner != this
|
|
1448
|
+
|| epoch == null
|
|
1449
|
+
|| (epoch.retiring && !epoch.voluntaryRetirement)
|
|
1450
|
+
|| epoch.retired
|
|
1451
|
+
|| !mForegroundActive
|
|
1452
|
+
|| (mRunFirstStartId != 0 && startId < mRunFirstStartId)
|
|
1453
|
+
|| callbackType <= 0
|
|
1454
|
+
|| mCurrentForegroundServiceType <= 0
|
|
1455
|
+
|| (callbackType & mCurrentForegroundServiceType) == 0) {
|
|
1456
|
+
return;
|
|
1457
|
+
}
|
|
1458
|
+
// Matching IDs are library authority only. A repeated platform promotion within the
|
|
1459
|
+
// same run can expose coincident metadata, so it never upgrades ambiguous provenance.
|
|
1460
|
+
stopAuthorized =
|
|
1461
|
+
!epoch.ambiguousPromotion
|
|
1462
|
+
&& !mTimeoutStartIds.contains(startId)
|
|
1463
|
+
&& epoch.satisfiedStartIds.contains(startId);
|
|
1464
|
+
stopId = epoch.lastSatisfiedStartId;
|
|
1465
|
+
notificationBundle =
|
|
1466
|
+
mCurrentNotificationBundle == null ? null : new Bundle(mCurrentNotificationBundle);
|
|
1467
|
+
mTimeoutStartIds.add(startId);
|
|
1468
|
+
epoch.retiring = true;
|
|
1469
|
+
epoch.voluntaryRetirement = false;
|
|
1470
|
+
timeoutAlarm = revokeAlarmPoc();
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
// Nothing that posts an event, converts a model or retires owned evidence precedes this.
|
|
1474
|
+
try {
|
|
1475
|
+
removeForegroundNotification();
|
|
1476
|
+
demotionOutcome = "returned"; // Normal Binder return is not an AMS acknowledgement.
|
|
1477
|
+
retireTimeoutEpoch(epoch);
|
|
1478
|
+
} catch (RuntimeException failure) {
|
|
1479
|
+
primary = failure;
|
|
1480
|
+
}
|
|
1481
|
+
|
|
1482
|
+
if (stopAuthorized
|
|
1483
|
+
&& ownsTimeoutEpoch(epoch)
|
|
1484
|
+
&& epoch.runGeneration == mRunGeneration
|
|
1485
|
+
&& epoch.runGeneration == sRunGeneration
|
|
1486
|
+
&& sStopRequest == stopRequest
|
|
1487
|
+
&& sPendingStarts.isEmpty()
|
|
1488
|
+
&& mLastStartId == stopId
|
|
1489
|
+
&& stopId != 0
|
|
1490
|
+
&& !mStartCommandFailed
|
|
1491
|
+
&& !mStopInitiated) {
|
|
1492
|
+
try {
|
|
1493
|
+
stopOutcome = "failed";
|
|
1494
|
+
StopAuthority authority = captureStopAuthority();
|
|
1495
|
+
boolean stopped = stopSelfResult(stopId);
|
|
1496
|
+
stopOutcome = stopped ? "accepted" : "refused";
|
|
1497
|
+
if (stopped && ownsTimeoutEpoch(epoch) && ownsStopAuthority(authority)) {
|
|
1498
|
+
mStopInitiated = true;
|
|
1499
|
+
retireTimeoutEpoch(epoch);
|
|
1500
|
+
}
|
|
1501
|
+
} catch (RuntimeException failure) {
|
|
1502
|
+
if (primary == null) primary = failure;
|
|
1503
|
+
else if (primary != failure) primary.addSuppressed(failure);
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
if (timeoutAlarm != null) ForegroundServiceRunLimitScheduler.cancel(timeoutAlarm);
|
|
1509
|
+
|
|
1510
|
+
// Consume publication authority once, including when a reentrant caller still owns locks.
|
|
1511
|
+
final RuntimeException timeoutFailure = primary;
|
|
1512
|
+
final String diagnostic =
|
|
1513
|
+
"Foreground timeout (startId="
|
|
850
1514
|
+ startId
|
|
851
1515
|
+ ", type="
|
|
852
1516
|
+ fgsType
|
|
853
|
-
+ "
|
|
1517
|
+
+ ", runGeneration="
|
|
1518
|
+
+ epoch.runGeneration
|
|
1519
|
+
+ ", demotion="
|
|
1520
|
+
+ demotionOutcome
|
|
1521
|
+
+ ", stop="
|
|
1522
|
+
+ stopOutcome
|
|
1523
|
+
+ ").";
|
|
1524
|
+
List<Runnable> publications = sTimeoutPublications.get();
|
|
1525
|
+
if (publications == null) {
|
|
1526
|
+
publications = new ArrayList<>();
|
|
1527
|
+
sTimeoutPublications.set(publications);
|
|
1528
|
+
}
|
|
1529
|
+
publications.add(
|
|
1530
|
+
() -> {
|
|
1531
|
+
if (notificationBundle != null) {
|
|
1532
|
+
try {
|
|
1533
|
+
NotificationModel model = NotificationModel.fromBundle(notificationBundle);
|
|
1534
|
+
Bundle extras = new Bundle();
|
|
1535
|
+
extras.putInt("startId", startId);
|
|
1536
|
+
extras.putInt("fgsType", fgsType);
|
|
1537
|
+
EventBus.post(
|
|
1538
|
+
new NotificationEvent(NotificationEvent.TYPE_FG_TIMEOUT, model, extras));
|
|
1539
|
+
} catch (RuntimeException failure) {
|
|
1540
|
+
if (timeoutFailure != null && timeoutFailure != failure)
|
|
1541
|
+
timeoutFailure.addSuppressed(failure);
|
|
1542
|
+
Logger.w(TAG, "Timeout event publication failed", failure);
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
// Preserve the existing error LogEvent, but only after the essential teardown and
|
|
1546
|
+
// timeout publication. Its failure cannot replace a primary platform exception.
|
|
1547
|
+
try {
|
|
1548
|
+
if (timeoutFailure == null) Logger.e(TAG, diagnostic);
|
|
1549
|
+
else Logger.e(TAG, diagnostic, timeoutFailure);
|
|
1550
|
+
} catch (RuntimeException failure) {
|
|
1551
|
+
if (timeoutFailure != null && timeoutFailure != failure)
|
|
1552
|
+
timeoutFailure.addSuppressed(failure);
|
|
1553
|
+
Logger.w(TAG, "Timeout diagnostic publication failed", failure);
|
|
1554
|
+
}
|
|
1555
|
+
});
|
|
1556
|
+
publishTimeoutEventsIfUnlocked();
|
|
1557
|
+
if (primary != null) throw primary;
|
|
1558
|
+
}
|
|
854
1559
|
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
1560
|
+
// All diagnostic authority is subordinate to the existing lifecycle/epoch registry.
|
|
1561
|
+
// Caller holds sLifecycleLock; nested state checks always take sLock second.
|
|
1562
|
+
private void invalidateAlarmPoc() {
|
|
1563
|
+
ForegroundServiceRunLimitScheduler.Ticket ticket = revokeAlarmPoc();
|
|
1564
|
+
if (ticket != null) ForegroundServiceRunLimitScheduler.cancel(ticket);
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
private ForegroundServiceRunLimitScheduler.Ticket revokeAlarmPoc() {
|
|
1568
|
+
ForegroundServiceRunLimitScheduler.Ticket ticket = mAlarmPoc;
|
|
1569
|
+
mAlarmPoc = null;
|
|
1570
|
+
if (ticket != null) ticket.valid = false;
|
|
1571
|
+
return ticket;
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
private void maybeArmAlarmPoc(StartRequest request) {
|
|
1575
|
+
if (request == null || request.alarmPoc == null || Build.VERSION.SDK_INT < 35) return;
|
|
1576
|
+
synchronized (sLock) {
|
|
1577
|
+
ForegroundEpoch epoch = mForegroundEpoch;
|
|
1578
|
+
if ((!request.alarmPoc.publicOption && mAlarmPocAttemptedRun == mRunGeneration)
|
|
1579
|
+
|| request.alarmPocEpoch != epoch) {
|
|
1580
|
+
ForegroundServiceRunLimitScheduler.marker(
|
|
1581
|
+
"ARM_REJECTED", null, "no new diagnostic promotion/run");
|
|
860
1582
|
return;
|
|
861
1583
|
}
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
1584
|
+
mAlarmPocAttemptedRun = mRunGeneration;
|
|
1585
|
+
if (sInstance != this
|
|
1586
|
+
|| sStateOwner != this
|
|
1587
|
+
|| epoch == null
|
|
1588
|
+
|| epoch.retiring
|
|
1589
|
+
|| epoch.retired
|
|
1590
|
+
|| !mForegroundActive
|
|
1591
|
+
|| !mStartCommandSatisfied
|
|
1592
|
+
|| epoch.runGeneration != mRunGeneration
|
|
1593
|
+
|| sRunGeneration != mRunGeneration
|
|
1594
|
+
|| epoch.lastSatisfiedStartId != mLastStartId
|
|
1595
|
+
|| mStopInitiated
|
|
1596
|
+
|| mCurrentForegroundServiceType != ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC) {
|
|
1597
|
+
ForegroundServiceRunLimitScheduler.marker(
|
|
1598
|
+
"ARM_REJECTED", null, "requires current single dataSync epoch");
|
|
1599
|
+
return;
|
|
1600
|
+
}
|
|
1601
|
+
ForegroundServiceRunLimitScheduler.Ticket ticket = null;
|
|
1602
|
+
try {
|
|
1603
|
+
// Promotion has already returned; no ledger/GC I/O is added to default START latency.
|
|
1604
|
+
if (request.alarmPoc.publicOption) ForegroundServiceRunLimitScheduler.collectOrphans(this);
|
|
1605
|
+
ticket =
|
|
1606
|
+
new ForegroundServiceRunLimitScheduler.Ticket(
|
|
1607
|
+
this,
|
|
1608
|
+
epoch,
|
|
1609
|
+
sRunGeneration,
|
|
1610
|
+
mRunGeneration,
|
|
1611
|
+
request.alarmPoc,
|
|
1612
|
+
epoch.promotedElapsed);
|
|
1613
|
+
// Ticket construction reads boot identity through a platform API. Revalidate after it.
|
|
1614
|
+
if (sInstance != this
|
|
1615
|
+
|| sStateOwner != this
|
|
1616
|
+
|| mForegroundEpoch != epoch
|
|
1617
|
+
|| epoch.retiring
|
|
1618
|
+
|| epoch.retired
|
|
1619
|
+
|| sRunGeneration != ticket.generation
|
|
1620
|
+
|| mRunGeneration != ticket.runGeneration) {
|
|
1621
|
+
ForegroundServiceRunLimitScheduler.cancel(ticket);
|
|
1622
|
+
return;
|
|
1623
|
+
}
|
|
1624
|
+
mAlarmPoc = ticket;
|
|
1625
|
+
if (!ForegroundServiceRunLimitScheduler.arm(ticket) || !ownsAlarmPoc(ticket)) {
|
|
1626
|
+
if (mAlarmPoc == ticket) mAlarmPoc = null;
|
|
1627
|
+
ForegroundServiceRunLimitScheduler.cancel(ticket);
|
|
1628
|
+
}
|
|
1629
|
+
} catch (org.json.JSONException | RuntimeException failure) {
|
|
1630
|
+
// Construction/storage may have admitted B reentrantly. Cleanup belongs only to A.
|
|
1631
|
+
if (ticket != null) {
|
|
1632
|
+
if (mAlarmPoc == ticket) mAlarmPoc = null;
|
|
1633
|
+
ForegroundServiceRunLimitScheduler.cancel(ticket);
|
|
1634
|
+
}
|
|
1635
|
+
ForegroundServiceRunLimitScheduler.marker("ARM_FAILED", ticket, failure.toString());
|
|
865
1636
|
}
|
|
866
|
-
stopForegroundCompat();
|
|
867
|
-
stopSelf(startId);
|
|
868
1637
|
}
|
|
1638
|
+
}
|
|
869
1639
|
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
1640
|
+
String alarmPocInstanceId() {
|
|
1641
|
+
if (mAlarmPocInstanceId == null) mAlarmPocInstanceId = UUID.randomUUID().toString();
|
|
1642
|
+
return mAlarmPocInstanceId;
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
private boolean ownsAlarmPoc(ForegroundServiceRunLimitScheduler.Ticket ticket) {
|
|
1646
|
+
return Build.VERSION.SDK_INT >= 35
|
|
1647
|
+
&& sInstance == this
|
|
1648
|
+
&& sStateOwner == this
|
|
1649
|
+
&& ticket.owner == this
|
|
1650
|
+
&& mAlarmPoc == ticket
|
|
1651
|
+
&& ticket.valid
|
|
1652
|
+
&& ticket.epoch == mForegroundEpoch
|
|
1653
|
+
&& sRunGeneration == ticket.generation
|
|
1654
|
+
&& mRunGeneration == ticket.runGeneration
|
|
1655
|
+
&& mForegroundEpoch != null
|
|
1656
|
+
&& mForegroundEpoch.runGeneration == ticket.runGeneration
|
|
1657
|
+
&& !mForegroundEpoch.retiring
|
|
1658
|
+
&& !mForegroundEpoch.retired
|
|
1659
|
+
&& mForegroundActive
|
|
1660
|
+
&& mCurrentForegroundServiceType == ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
|
1661
|
+
&& mStartCommandSatisfied
|
|
1662
|
+
&& mLastStartId != 0
|
|
1663
|
+
&& mLastStartId == mForegroundEpoch.lastSatisfiedStartId
|
|
1664
|
+
&& !mStartCommandFailed
|
|
1665
|
+
&& !mStopInitiated;
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
static void receiveAlarmPoc(Intent intent) {
|
|
1669
|
+
try {
|
|
1670
|
+
synchronized (sLifecycleLock) {
|
|
1671
|
+
ForegroundService owner = sInstance;
|
|
1672
|
+
if (owner == null) {
|
|
1673
|
+
ForegroundServiceRunLimitScheduler.marker(
|
|
1674
|
+
"NO_MATCHING_LIVE_INSTANCE", null, "cold delivery");
|
|
1675
|
+
return;
|
|
1676
|
+
}
|
|
1677
|
+
owner.expireAlarmPoc(intent);
|
|
1678
|
+
}
|
|
1679
|
+
} finally {
|
|
1680
|
+
publishTimeoutEventsIfUnlocked();
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
private void expireAlarmPoc(Intent intent) {
|
|
1685
|
+
final ForegroundServiceRunLimitScheduler.Ticket ticket;
|
|
1686
|
+
final ForegroundEpoch epoch;
|
|
1687
|
+
final StopAuthority stop;
|
|
1688
|
+
final Bundle notificationBundle;
|
|
1689
|
+
final long deliveredElapsed;
|
|
1690
|
+
String demotionOutcome = "failed";
|
|
1691
|
+
String stopOutcome = "deferred";
|
|
1692
|
+
synchronized (sLock) {
|
|
1693
|
+
ticket = mAlarmPoc;
|
|
1694
|
+
if (ticket == null
|
|
1695
|
+
|| !ownsAlarmPoc(ticket)
|
|
1696
|
+
|| !ForegroundServiceRunLimitScheduler.matches(ticket, intent)) {
|
|
1697
|
+
ForegroundServiceRunLimitScheduler.marker(
|
|
1698
|
+
"ALARM_STALE", null, "no matching live authority");
|
|
1699
|
+
return;
|
|
1700
|
+
}
|
|
1701
|
+
ForegroundServiceRunLimitScheduler.marker("ALARM_DELIVERED", ticket, "native receiver");
|
|
1702
|
+
if (!ForegroundServiceRunLimitScheduler.ledgerMatches(ticket)) {
|
|
1703
|
+
if (mAlarmPoc == ticket) invalidateAlarmPoc();
|
|
1704
|
+
ForegroundServiceRunLimitScheduler.marker(
|
|
1705
|
+
"LEDGER_INVALID", ticket, "no teardown authority");
|
|
1706
|
+
return;
|
|
1707
|
+
}
|
|
1708
|
+
long now = SystemClock.elapsedRealtime();
|
|
1709
|
+
if (now < ticket.deadline) {
|
|
1710
|
+
ForegroundServiceRunLimitScheduler.marker("ALARM_EARLY", ticket, "no demotion");
|
|
1711
|
+
return;
|
|
1712
|
+
}
|
|
1713
|
+
if (now > ticket.deadline) {
|
|
1714
|
+
ForegroundServiceRunLimitScheduler.marker(
|
|
1715
|
+
"ALARM_LATE", ticket, "delayMs=" + (now - ticket.deadline));
|
|
1716
|
+
}
|
|
1717
|
+
// Boot/ledger validation crosses platform APIs. It cannot transfer the old alarm's
|
|
1718
|
+
// authority to a successor that appeared while those calls were in progress.
|
|
1719
|
+
if (!ownsAlarmPoc(ticket)) {
|
|
1720
|
+
ForegroundServiceRunLimitScheduler.marker(
|
|
1721
|
+
"ALARM_STALE", ticket, "owner changed during validation");
|
|
1722
|
+
return;
|
|
1723
|
+
}
|
|
1724
|
+
epoch = mForegroundEpoch;
|
|
1725
|
+
stop = captureStopAuthority();
|
|
1726
|
+
deliveredElapsed = now;
|
|
1727
|
+
notificationBundle =
|
|
1728
|
+
mCurrentNotificationBundle == null ? null : new Bundle(mCurrentNotificationBundle);
|
|
1729
|
+
mAlarmPoc = null;
|
|
1730
|
+
ticket.valid = false;
|
|
1731
|
+
epoch.retiring = true;
|
|
1732
|
+
epoch.voluntaryRetirement = true;
|
|
1733
|
+
}
|
|
1734
|
+
try {
|
|
1735
|
+
ForegroundServiceRunLimitScheduler.marker(
|
|
1736
|
+
"DEMOTION_ATTEMPTED",
|
|
1737
|
+
ticket,
|
|
1738
|
+
"REMOVE notificationId="
|
|
1739
|
+
+ mCurrentNotificationId
|
|
1740
|
+
+ " startId="
|
|
1741
|
+
+ stop.startId
|
|
1742
|
+
+ " satisfiedStartId="
|
|
1743
|
+
+ epoch.lastSatisfiedStartId
|
|
1744
|
+
+ " pendingStarts="
|
|
1745
|
+
+ sPendingStarts.size());
|
|
1746
|
+
try {
|
|
1747
|
+
removeForegroundNotification();
|
|
1748
|
+
demotionOutcome = "returned";
|
|
1749
|
+
ForegroundServiceRunLimitScheduler.marker(
|
|
1750
|
+
"DEMOTION_RETURNED", ticket, "not an AMS acknowledgement");
|
|
1751
|
+
retireTimeoutEpoch(epoch);
|
|
1752
|
+
} catch (RuntimeException failure) {
|
|
1753
|
+
ForegroundServiceRunLimitScheduler.marker("DEMOTION_FAILED", ticket, failure.toString());
|
|
1754
|
+
}
|
|
1755
|
+
if (ownsTimeoutEpoch(epoch)
|
|
1756
|
+
&& epoch.runGeneration == mRunGeneration
|
|
1757
|
+
&& epoch.runGeneration == sRunGeneration
|
|
1758
|
+
&& ownsStopAuthority(stop)
|
|
1759
|
+
&& mLastStartId == epoch.lastSatisfiedStartId
|
|
1760
|
+
&& mLastStartId != 0
|
|
1761
|
+
&& !mStartCommandFailed
|
|
1762
|
+
&& !mStopInitiated) {
|
|
1763
|
+
try {
|
|
1764
|
+
stopOutcome = "failed";
|
|
1765
|
+
boolean stopped = stopSelfResult(stop.startId);
|
|
1766
|
+
stopOutcome = stopped ? "accepted" : "refused";
|
|
1767
|
+
ForegroundServiceRunLimitScheduler.marker(
|
|
1768
|
+
stopped ? "STOP_ACCEPTED" : "STOP_REFUSED", ticket, "startId=" + stop.startId);
|
|
1769
|
+
if (stopped && ownsTimeoutEpoch(epoch) && ownsStopAuthority(stop)) {
|
|
1770
|
+
mStopInitiated = true;
|
|
1771
|
+
retireTimeoutEpoch(epoch);
|
|
1772
|
+
}
|
|
1773
|
+
} catch (RuntimeException failure) {
|
|
1774
|
+
ForegroundServiceRunLimitScheduler.marker("STOP_FAILED", ticket, failure.toString());
|
|
1775
|
+
}
|
|
1776
|
+
} else {
|
|
1777
|
+
ForegroundServiceRunLimitScheduler.marker(
|
|
1778
|
+
"STOP_DEFERRED", ticket, "pending/unknown START or ownership changed");
|
|
1779
|
+
}
|
|
1780
|
+
ForegroundServiceRunLimitScheduler.marker(
|
|
1781
|
+
"NATIVE_FINAL_STATE",
|
|
1782
|
+
ticket,
|
|
1783
|
+
"sameEpoch="
|
|
1784
|
+
+ ownsTimeoutEpoch(epoch)
|
|
1785
|
+
+ " foregroundActive="
|
|
1786
|
+
+ mForegroundActive
|
|
1787
|
+
+ " stopInitiated="
|
|
1788
|
+
+ mStopInitiated
|
|
1789
|
+
+ " pendingStarts="
|
|
1790
|
+
+ sPendingStarts.size());
|
|
1791
|
+
} finally {
|
|
1792
|
+
ForegroundServiceRunLimitScheduler.cancel(ticket);
|
|
1793
|
+
}
|
|
1794
|
+
if (ticket.publicOption && notificationBundle != null) {
|
|
1795
|
+
Bundle metadata = new Bundle();
|
|
1796
|
+
metadata.putLong("requestedDurationMs", ticket.duration);
|
|
1797
|
+
metadata.putLong("deadlineElapsed", ticket.deadline);
|
|
1798
|
+
metadata.putLong("deliveredElapsed", deliveredElapsed);
|
|
1799
|
+
metadata.putString("demotion", demotionOutcome);
|
|
1800
|
+
metadata.putString("conditionalStop", stopOutcome);
|
|
1801
|
+
List<Runnable> publications = sTimeoutPublications.get();
|
|
1802
|
+
if (publications == null) {
|
|
1803
|
+
publications = new ArrayList<>();
|
|
1804
|
+
sTimeoutPublications.set(publications);
|
|
1805
|
+
}
|
|
1806
|
+
publications.add(
|
|
1807
|
+
() -> {
|
|
1808
|
+
try {
|
|
1809
|
+
Bundle extras = new Bundle();
|
|
1810
|
+
extras.putBundle("foregroundServiceRunLimit", metadata);
|
|
1811
|
+
EventBus.post(
|
|
1812
|
+
new NotificationEvent(
|
|
1813
|
+
NotificationEvent.TYPE_FG_RUN_LIMIT_REACHED,
|
|
1814
|
+
NotificationModel.fromBundle(notificationBundle),
|
|
1815
|
+
extras));
|
|
1816
|
+
} catch (RuntimeException failure) {
|
|
1817
|
+
ForegroundServiceRunLimitScheduler.marker("EVENT_FAILED", ticket, failure.toString());
|
|
1818
|
+
}
|
|
1819
|
+
});
|
|
876
1820
|
}
|
|
877
1821
|
}
|
|
878
1822
|
}
|