react-native-notify-kit 10.8.0 → 10.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/android/src/main/java/app/notifee/core/ForegroundService.java +614 -60
- package/android/src/test/java/app/notifee/core/ForegroundServiceTest.java +3080 -18
- package/dist/types/Notification.d.ts +25 -1
- package/dist/types/Notification.js +10 -0
- package/dist/types/Notification.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
- package/src/types/Notification.ts +27 -0
- package/src/version.ts +1 -1
|
@@ -43,8 +43,12 @@ import app.notifee.core.event.NotificationEvent;
|
|
|
43
43
|
import app.notifee.core.interfaces.MethodCallResult;
|
|
44
44
|
import app.notifee.core.model.NotificationModel;
|
|
45
45
|
import app.notifee.core.utility.ParcelableCompatReader;
|
|
46
|
+
import java.util.ArrayList;
|
|
46
47
|
import java.util.HashMap;
|
|
48
|
+
import java.util.HashSet;
|
|
49
|
+
import java.util.List;
|
|
47
50
|
import java.util.Map;
|
|
51
|
+
import java.util.Set;
|
|
48
52
|
import java.util.UUID;
|
|
49
53
|
|
|
50
54
|
public class ForegroundService extends Service {
|
|
@@ -78,6 +82,117 @@ public class ForegroundService extends Service {
|
|
|
78
82
|
private long mStateGeneration;
|
|
79
83
|
private int mRunFirstStartId;
|
|
80
84
|
private Object mRunnerToken;
|
|
85
|
+
private ForegroundEpoch mForegroundEpoch;
|
|
86
|
+
// Previously admitted metadata can collide with a successor even across run generations.
|
|
87
|
+
// These IDs only disqualify stop authority; they never establish Android epoch provenance.
|
|
88
|
+
private final Set<Integer> mTimeoutStartIds = new HashSet<>();
|
|
89
|
+
// A reentrant platform callback may still inherit an outer lifecycle monitor. Drain these
|
|
90
|
+
// publications synchronously at that call's unlocked boundary, without scheduling or waiting.
|
|
91
|
+
private static final ThreadLocal<List<Runnable>> sTimeoutPublications = new ThreadLocal<>();
|
|
92
|
+
|
|
93
|
+
private static void publishTimeoutEventsIfUnlocked() {
|
|
94
|
+
if (Thread.holdsLock(sLifecycleLock) || Thread.holdsLock(sLock)) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
List<Runnable> publications = sTimeoutPublications.get();
|
|
98
|
+
if (publications != null) {
|
|
99
|
+
sTimeoutPublications.remove();
|
|
100
|
+
for (Runnable publication : publications) {
|
|
101
|
+
publication.run();
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Local publication identity, never provenance supplied by Android's timeout callback.
|
|
107
|
+
// Guarded by sLifecycleLock then sLock. Retiring consumes timeout authority even on failure.
|
|
108
|
+
private static final class ForegroundEpoch {
|
|
109
|
+
final long runGeneration;
|
|
110
|
+
final boolean ambiguousPromotion;
|
|
111
|
+
final Set<Integer> satisfiedStartIds = new HashSet<>();
|
|
112
|
+
int lastSatisfiedStartId;
|
|
113
|
+
boolean retiring;
|
|
114
|
+
boolean retired;
|
|
115
|
+
|
|
116
|
+
ForegroundEpoch(long runGeneration, boolean ambiguousPromotion, int startId) {
|
|
117
|
+
this.runGeneration = runGeneration;
|
|
118
|
+
this.ambiguousPromotion = ambiguousPromotion;
|
|
119
|
+
satisfiedStartIds.add(startId);
|
|
120
|
+
lastSatisfiedStartId = startId;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Caller holds both locks, after the actual promotion returned normally.
|
|
125
|
+
private void commitForegroundEpoch() {
|
|
126
|
+
ForegroundEpoch previous = mForegroundEpoch;
|
|
127
|
+
mForegroundEpoch =
|
|
128
|
+
new ForegroundEpoch(
|
|
129
|
+
mRunGeneration,
|
|
130
|
+
previous != null && previous.runGeneration == mRunGeneration,
|
|
131
|
+
mLastStartId);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Snapshot before platform IPC; a reply cannot acquire authority over a reentrant successor.
|
|
135
|
+
private static final class StopAuthority {
|
|
136
|
+
final ForegroundService owner;
|
|
137
|
+
final ForegroundEpoch epoch;
|
|
138
|
+
final long generation;
|
|
139
|
+
final long runGeneration;
|
|
140
|
+
final int startId;
|
|
141
|
+
final Object request;
|
|
142
|
+
|
|
143
|
+
StopAuthority(ForegroundService service) {
|
|
144
|
+
owner = sStateOwner;
|
|
145
|
+
epoch = service.mForegroundEpoch;
|
|
146
|
+
generation = sRunGeneration;
|
|
147
|
+
runGeneration = service.mRunGeneration;
|
|
148
|
+
startId = service.mLastStartId;
|
|
149
|
+
request = sStopRequest;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Caller holds sLifecycleLock. Follow the lifecycle -> state lock order for both snapshots.
|
|
154
|
+
private StopAuthority captureStopAuthority() {
|
|
155
|
+
synchronized (sLock) {
|
|
156
|
+
return new StopAuthority(this);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
private boolean ownsStopAuthority(StopAuthority authority) {
|
|
161
|
+
synchronized (sLock) {
|
|
162
|
+
return sInstance == this
|
|
163
|
+
&& sStateOwner == authority.owner
|
|
164
|
+
&& mForegroundEpoch == authority.epoch
|
|
165
|
+
&& sRunGeneration == authority.generation
|
|
166
|
+
&& mRunGeneration == authority.runGeneration
|
|
167
|
+
&& mLastStartId == authority.startId
|
|
168
|
+
&& sStopRequest == authority.request
|
|
169
|
+
&& sPendingStarts.isEmpty();
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// A delivered command can be physically satisfied by the existing foreground publication,
|
|
174
|
+
// even if its notification update throws. A future/unfulfilled delivery gains no authority.
|
|
175
|
+
private void recordSatisfiedStart(int startId) {
|
|
176
|
+
synchronized (sLock) {
|
|
177
|
+
ForegroundEpoch epoch = mForegroundEpoch;
|
|
178
|
+
if (sInstance == this
|
|
179
|
+
&& sStateOwner == this
|
|
180
|
+
&& epoch != null
|
|
181
|
+
&& !epoch.retiring
|
|
182
|
+
&& !epoch.retired
|
|
183
|
+
&& mForegroundActive
|
|
184
|
+
&& mStartCommandSatisfied
|
|
185
|
+
&& mLastStartId == startId
|
|
186
|
+
&& epoch.runGeneration == mRunGeneration) {
|
|
187
|
+
epoch.satisfiedStartIds.add(startId);
|
|
188
|
+
epoch.lastSatisfiedStartId = startId;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// A denied initial promotion is not a client STOP. Its recovery cannot acquire authority
|
|
194
|
+
// over a later accepted/promoted run, and is consumed by one eligible stop attempt.
|
|
195
|
+
private DeniedStartRecovery mDeniedStartRecovery;
|
|
81
196
|
private static final String DEFENSIVE_CHANNEL_ID = "notifee_fg_default";
|
|
82
197
|
private static final int DEFENSIVE_NOTIFICATION_ID = Integer.MAX_VALUE - 1;
|
|
83
198
|
|
|
@@ -85,6 +200,9 @@ public class ForegroundService extends Service {
|
|
|
85
200
|
final long generation;
|
|
86
201
|
final Object precedingStop;
|
|
87
202
|
boolean accepted;
|
|
203
|
+
// Dispatch acceptance is distinct from the promotion outcome and the physical instance's
|
|
204
|
+
// obligation. A denied delivered request is terminal even while its sender's reply is pending.
|
|
205
|
+
boolean promotionDenied;
|
|
88
206
|
// Null until delivery. An unfulfilled delivered request belongs to that physical instance.
|
|
89
207
|
ForegroundService instance;
|
|
90
208
|
|
|
@@ -94,6 +212,36 @@ public class ForegroundService extends Service {
|
|
|
94
212
|
}
|
|
95
213
|
}
|
|
96
214
|
|
|
215
|
+
private static final class DeniedStartRecovery {
|
|
216
|
+
final long generation;
|
|
217
|
+
final long runGeneration;
|
|
218
|
+
final int startId;
|
|
219
|
+
final int flags;
|
|
220
|
+
final RuntimeException denial;
|
|
221
|
+
final String requestId;
|
|
222
|
+
final String notificationId;
|
|
223
|
+
final int foregroundServiceType;
|
|
224
|
+
|
|
225
|
+
DeniedStartRecovery(
|
|
226
|
+
long generation,
|
|
227
|
+
long runGeneration,
|
|
228
|
+
int startId,
|
|
229
|
+
int flags,
|
|
230
|
+
RuntimeException denial,
|
|
231
|
+
String requestId,
|
|
232
|
+
String notificationId,
|
|
233
|
+
int foregroundServiceType) {
|
|
234
|
+
this.generation = generation;
|
|
235
|
+
this.runGeneration = runGeneration;
|
|
236
|
+
this.startId = startId;
|
|
237
|
+
this.flags = flags;
|
|
238
|
+
this.denial = denial;
|
|
239
|
+
this.requestId = requestId;
|
|
240
|
+
this.notificationId = notificationId;
|
|
241
|
+
this.foregroundServiceType = foregroundServiceType;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
97
245
|
/**
|
|
98
246
|
* Tracks whether startForeground() has been called on THIS service instance. This is an instance
|
|
99
247
|
* field (not static) because the static fields below track cross-invocation notification state
|
|
@@ -119,6 +267,9 @@ public class ForegroundService extends Service {
|
|
|
119
267
|
public void onCreate() {
|
|
120
268
|
super.onCreate();
|
|
121
269
|
synchronized (sLifecycleLock) {
|
|
270
|
+
if (sInstance != null) {
|
|
271
|
+
sInstance.mDeniedStartRecovery = null;
|
|
272
|
+
}
|
|
122
273
|
sInstance = this;
|
|
123
274
|
mRunGeneration = sRunGeneration;
|
|
124
275
|
synchronized (sLock) {
|
|
@@ -131,6 +282,7 @@ public class ForegroundService extends Service {
|
|
|
131
282
|
@Override
|
|
132
283
|
public void onDestroy() {
|
|
133
284
|
synchronized (sLifecycleLock) {
|
|
285
|
+
mDeniedStartRecovery = null;
|
|
134
286
|
sPendingStarts.values().removeIf(request -> request.instance == this);
|
|
135
287
|
if (sInstance == this) {
|
|
136
288
|
sInstance = null;
|
|
@@ -161,8 +313,12 @@ public class ForegroundService extends Service {
|
|
|
161
313
|
private void clearOwnedState() {
|
|
162
314
|
synchronized (sLock) {
|
|
163
315
|
if (sStateOwner == this) {
|
|
316
|
+
mDeniedStartRecovery = null;
|
|
164
317
|
clearCurrentState();
|
|
165
318
|
mRunnerToken = null;
|
|
319
|
+
if (mForegroundEpoch != null) {
|
|
320
|
+
mForegroundEpoch.retired = true;
|
|
321
|
+
}
|
|
166
322
|
}
|
|
167
323
|
}
|
|
168
324
|
}
|
|
@@ -176,16 +332,25 @@ public class ForegroundService extends Service {
|
|
|
176
332
|
*/
|
|
177
333
|
@SuppressLint("MissingPermission")
|
|
178
334
|
static boolean repostIfActive(String notificationId) {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
335
|
+
try {
|
|
336
|
+
synchronized (sLifecycleLock) {
|
|
337
|
+
synchronized (sLock) {
|
|
338
|
+
if (mCurrentNotificationId == null
|
|
339
|
+
|| (sStateOwner != null
|
|
340
|
+
&& sStateOwner.mForegroundEpoch != null
|
|
341
|
+
&& sStateOwner.mForegroundEpoch.retiring)
|
|
342
|
+
|| !mCurrentNotificationId.equals(notificationId)
|
|
343
|
+
|| mCurrentNotification == null) {
|
|
344
|
+
return false;
|
|
345
|
+
}
|
|
346
|
+
Logger.w(TAG, "Submitting dismissed foreground service notification for re-post");
|
|
347
|
+
NotificationManagerCompat.from(ContextHolder.getApplicationContext())
|
|
348
|
+
.notify(mCurrentHashCode, mCurrentNotification);
|
|
349
|
+
return true;
|
|
350
|
+
}
|
|
184
351
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
.notify(mCurrentHashCode, mCurrentNotification);
|
|
188
|
-
return true;
|
|
352
|
+
} finally {
|
|
353
|
+
publishTimeoutEventsIfUnlocked();
|
|
189
354
|
}
|
|
190
355
|
}
|
|
191
356
|
|
|
@@ -268,8 +433,14 @@ public class ForegroundService extends Service {
|
|
|
268
433
|
// Caller holds sLifecycleLock. Delivery itself can prove acceptance before Context returns.
|
|
269
434
|
// A late reply never cancels a STOP ordered after this START was reserved/delivered.
|
|
270
435
|
private static void acceptStart(StartRequest request) {
|
|
436
|
+
if (request.promotionDenied) {
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
271
439
|
if (!request.accepted) {
|
|
272
440
|
request.accepted = true;
|
|
441
|
+
if (sInstance != null) {
|
|
442
|
+
sInstance.mDeniedStartRecovery = null;
|
|
443
|
+
}
|
|
273
444
|
if (sStopRequest == request.precedingStop) {
|
|
274
445
|
sStopRequest = null;
|
|
275
446
|
}
|
|
@@ -281,6 +452,9 @@ public class ForegroundService extends Service {
|
|
|
281
452
|
if (sInstance == null && sPendingStarts.isEmpty()) {
|
|
282
453
|
return;
|
|
283
454
|
}
|
|
455
|
+
if (sInstance != null) {
|
|
456
|
+
sInstance.mDeniedStartRecovery = null;
|
|
457
|
+
}
|
|
284
458
|
sStopRequest = new Object();
|
|
285
459
|
sRunGeneration++;
|
|
286
460
|
scheduleStop();
|
|
@@ -295,9 +469,89 @@ public class ForegroundService extends Service {
|
|
|
295
469
|
if (instance != null && request != null) {
|
|
296
470
|
new Handler(Looper.getMainLooper()).post(() -> instance.stopIfRequested(request));
|
|
297
471
|
}
|
|
472
|
+
if (instance != null && instance.mDeniedStartRecovery != null && sPendingStarts.isEmpty()) {
|
|
473
|
+
DeniedStartRecovery recovery = instance.mDeniedStartRecovery;
|
|
474
|
+
new Handler(Looper.getMainLooper()).post(() -> instance.stopDeniedStartIfCurrent(recovery));
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
private void stopDeniedStartIfCurrent(DeniedStartRecovery recovery) {
|
|
479
|
+
try {
|
|
480
|
+
stopDeniedStartIfCurrentLocked(recovery);
|
|
481
|
+
} finally {
|
|
482
|
+
publishTimeoutEventsIfUnlocked();
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
private void stopDeniedStartIfCurrentLocked(DeniedStartRecovery recovery) {
|
|
487
|
+
synchronized (sLifecycleLock) {
|
|
488
|
+
if (mDeniedStartRecovery != recovery) {
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
synchronized (sLock) {
|
|
492
|
+
if (!ownsUnpromotedState()
|
|
493
|
+
|| sRunGeneration != recovery.generation
|
|
494
|
+
|| mRunGeneration != recovery.runGeneration
|
|
495
|
+
|| mLastStartId != recovery.startId
|
|
496
|
+
|| mStopInitiated) {
|
|
497
|
+
mDeniedStartRecovery = null;
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
String stopOutcome = "deferred";
|
|
502
|
+
try {
|
|
503
|
+
if (sPendingStarts.isEmpty()) {
|
|
504
|
+
StopAuthority authority = captureStopAuthority();
|
|
505
|
+
// Consume before Binder IPC: refusal or failure grants no automatic retry authority.
|
|
506
|
+
mDeniedStartRecovery = null;
|
|
507
|
+
stopOutcome = "failed";
|
|
508
|
+
boolean stopped = stopSelfResult(recovery.startId);
|
|
509
|
+
stopOutcome = stopped ? "accepted" : "refused";
|
|
510
|
+
synchronized (sLock) {
|
|
511
|
+
if (stopped && ownsStopAuthority(authority) && ownsUnpromotedState()) {
|
|
512
|
+
mStopInitiated = true;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
} finally {
|
|
517
|
+
// Preserve the promotion diagnostic even if the separate stop operation throws.
|
|
518
|
+
Logger.w(
|
|
519
|
+
TAG,
|
|
520
|
+
(recovery.requestId == null
|
|
521
|
+
? "Framework redelivered START promotion denied (startId="
|
|
522
|
+
: "Tracked START PROMOTION_DENIED (cause=UNKNOWN, requestId="
|
|
523
|
+
+ recovery.requestId
|
|
524
|
+
+ ", notificationId="
|
|
525
|
+
+ recovery.notificationId
|
|
526
|
+
+ ", api="
|
|
527
|
+
+ Build.VERSION.SDK_INT
|
|
528
|
+
+ ", type="
|
|
529
|
+
+ recovery.foregroundServiceType
|
|
530
|
+
+ ", generation="
|
|
531
|
+
+ recovery.generation
|
|
532
|
+
+ ", runGeneration="
|
|
533
|
+
+ recovery.runGeneration
|
|
534
|
+
+ ", startId=")
|
|
535
|
+
+ recovery.startId
|
|
536
|
+
+ ", flags="
|
|
537
|
+
+ recovery.flags
|
|
538
|
+
+ ", stop="
|
|
539
|
+
+ stopOutcome
|
|
540
|
+
+ ").",
|
|
541
|
+
recovery.denial);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
298
544
|
}
|
|
299
545
|
|
|
300
546
|
private void stopIfRequested(Object request) {
|
|
547
|
+
try {
|
|
548
|
+
stopIfRequestedLocked(request);
|
|
549
|
+
} finally {
|
|
550
|
+
publishTimeoutEventsIfUnlocked();
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
private void stopIfRequestedLocked(Object request) {
|
|
301
555
|
synchronized (sLifecycleLock) {
|
|
302
556
|
if (sInstance != this
|
|
303
557
|
|| sStopRequest != request
|
|
@@ -307,19 +561,40 @@ public class ForegroundService extends Service {
|
|
|
307
561
|
|| mStartCommandFailed) {
|
|
308
562
|
return;
|
|
309
563
|
}
|
|
310
|
-
//
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
564
|
+
// Consume this request before IPC so a reentrant START cannot schedule it against its
|
|
565
|
+
// successor. Refusal/failure restores it only while the original admission still owns it.
|
|
566
|
+
sStopRequest = null;
|
|
567
|
+
StopAuthority authority = captureStopAuthority();
|
|
568
|
+
boolean stopped = false;
|
|
569
|
+
try {
|
|
570
|
+
// The framework token and startId also protect an accepted, not yet delivered START.
|
|
571
|
+
stopped = stopSelfResult(authority.startId);
|
|
572
|
+
if (stopped && ownsStopAuthority(authority)) {
|
|
573
|
+
mStopInitiated = true;
|
|
574
|
+
mForegroundActive = false;
|
|
575
|
+
clearOwnedState();
|
|
576
|
+
}
|
|
577
|
+
} finally {
|
|
578
|
+
if (!stopped && ownsStopAuthority(authority)) {
|
|
579
|
+
sStopRequest = request;
|
|
580
|
+
}
|
|
316
581
|
}
|
|
317
582
|
}
|
|
318
583
|
}
|
|
319
584
|
|
|
320
585
|
@Override
|
|
321
586
|
public int onStartCommand(Intent intent, int flags, int startId) {
|
|
587
|
+
try {
|
|
588
|
+
return onStartCommandLocked(intent, flags, startId);
|
|
589
|
+
} finally {
|
|
590
|
+
publishTimeoutEventsIfUnlocked();
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
private int onStartCommandLocked(Intent intent, int flags, int startId) {
|
|
322
595
|
synchronized (sLifecycleLock) {
|
|
596
|
+
// Any later delivery supersedes the recovery startId, including terminal commands.
|
|
597
|
+
mDeniedStartRecovery = null;
|
|
323
598
|
mLastStartId = startId;
|
|
324
599
|
boolean terminal =
|
|
325
600
|
intent == null || STOP_FOREGROUND_SERVICE_ACTION.equals(intent.getAction());
|
|
@@ -336,6 +611,13 @@ public class ForegroundService extends Service {
|
|
|
336
611
|
}
|
|
337
612
|
String requestId = intent == null ? null : intent.getStringExtra(START_REQUEST_ID);
|
|
338
613
|
StartRequest request = sPendingStarts.get(requestId);
|
|
614
|
+
boolean frameworkRedelivery =
|
|
615
|
+
intent != null
|
|
616
|
+
&& START_FOREGROUND_SERVICE_ACTION.equals(intent.getAction())
|
|
617
|
+
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
|
|
618
|
+
&& (flags & (START_FLAG_RETRY | START_FLAG_REDELIVERY)) != 0
|
|
619
|
+
&& isValidStartRequestId(requestId)
|
|
620
|
+
&& request == null;
|
|
339
621
|
boolean superseded = false;
|
|
340
622
|
if (!terminal) {
|
|
341
623
|
long generation = request == null ? sRunGeneration : request.generation;
|
|
@@ -358,10 +640,14 @@ public class ForegroundService extends Service {
|
|
|
358
640
|
// that run already ended, still perform the old request's real promotion before teardown.
|
|
359
641
|
result = mForegroundActive ? START_NOT_STICKY : fulfillSupersededStart(intent);
|
|
360
642
|
} else {
|
|
361
|
-
result =
|
|
643
|
+
result =
|
|
644
|
+
handleStartCommand(intent, flags, startId, frameworkRedelivery, requestId, request);
|
|
362
645
|
}
|
|
363
646
|
mStartCommandFailed = false;
|
|
364
647
|
if (mStartCommandSatisfied) {
|
|
648
|
+
if (!terminal && !superseded) {
|
|
649
|
+
recordSatisfiedStart(startId);
|
|
650
|
+
}
|
|
365
651
|
settleDeliveredStarts();
|
|
366
652
|
}
|
|
367
653
|
if (superseded
|
|
@@ -379,6 +665,9 @@ public class ForegroundService extends Service {
|
|
|
379
665
|
} catch (RuntimeException e) {
|
|
380
666
|
mStartCommandFailed = !mStartCommandSatisfied;
|
|
381
667
|
if (mStartCommandSatisfied) {
|
|
668
|
+
if (!terminal && !superseded) {
|
|
669
|
+
recordSatisfiedStart(startId);
|
|
670
|
+
}
|
|
382
671
|
settleDeliveredStarts();
|
|
383
672
|
}
|
|
384
673
|
throw e;
|
|
@@ -386,10 +675,63 @@ public class ForegroundService extends Service {
|
|
|
386
675
|
}
|
|
387
676
|
}
|
|
388
677
|
|
|
678
|
+
private static boolean isValidStartRequestId(@Nullable String requestId) {
|
|
679
|
+
if (requestId == null || requestId.length() != 36) {
|
|
680
|
+
return false;
|
|
681
|
+
}
|
|
682
|
+
try {
|
|
683
|
+
return UUID.fromString(requestId).toString().equalsIgnoreCase(requestId);
|
|
684
|
+
} catch (IllegalArgumentException e) {
|
|
685
|
+
return false;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// Caller holds sLifecycleLock and sLock. Recovery cannot retire an acquired run or another owner.
|
|
690
|
+
private boolean ownsUnpromotedState() {
|
|
691
|
+
return sInstance == this
|
|
692
|
+
&& sStateOwner == this
|
|
693
|
+
&& !mStartForegroundCalled
|
|
694
|
+
&& !mForegroundActive
|
|
695
|
+
&& !mStartCommandSatisfied
|
|
696
|
+
&& mRunnerToken == null
|
|
697
|
+
&& mRunFirstStartId == 0
|
|
698
|
+
&& mCurrentNotificationId == null
|
|
699
|
+
&& mCurrentForegroundServiceType == -1
|
|
700
|
+
&& mCurrentNotificationBundle == null
|
|
701
|
+
&& mCurrentNotification == null
|
|
702
|
+
&& mCurrentHashCode == 0;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// Caller holds both lifecycle/state locks. Revalidate the original admission after the platform
|
|
706
|
+
// attempt: a synchronous authority change must not authorize a stale request's recovery.
|
|
707
|
+
private boolean canRecoverTrackedDenial(
|
|
708
|
+
Intent intent,
|
|
709
|
+
String requestId,
|
|
710
|
+
StartRequest request,
|
|
711
|
+
long generation,
|
|
712
|
+
long runGeneration,
|
|
713
|
+
int startId) {
|
|
714
|
+
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM
|
|
715
|
+
&& START_FOREGROUND_SERVICE_ACTION.equals(intent.getAction())
|
|
716
|
+
&& isValidStartRequestId(requestId)
|
|
717
|
+
&& request != null
|
|
718
|
+
&& sPendingStarts.get(requestId) == request
|
|
719
|
+
&& request.instance == this
|
|
720
|
+
&& request.accepted
|
|
721
|
+
&& !request.promotionDenied
|
|
722
|
+
&& request.generation == runGeneration
|
|
723
|
+
&& sRunGeneration == generation
|
|
724
|
+
&& mRunGeneration == runGeneration
|
|
725
|
+
&& mLastStartId == startId
|
|
726
|
+
&& !mStopInitiated
|
|
727
|
+
&& ownsUnpromotedState();
|
|
728
|
+
}
|
|
729
|
+
|
|
389
730
|
// A real promotion (or an already foreground instance) satisfies the physical instance's
|
|
390
731
|
// delivered obligations, including a prior ambiguous delivery. Undelivered STARTs remain held.
|
|
391
732
|
// This does not turn a posting/type exception into success: onStartCommand still rethrows it.
|
|
392
733
|
private void settleDeliveredStarts() {
|
|
734
|
+
mDeniedStartRecovery = null;
|
|
393
735
|
sPendingStarts.values().removeIf(request -> request.instance == this);
|
|
394
736
|
}
|
|
395
737
|
|
|
@@ -427,13 +769,22 @@ public class ForegroundService extends Service {
|
|
|
427
769
|
}
|
|
428
770
|
|
|
429
771
|
@SuppressLint({"ForegroundServiceType", "MissingPermission"})
|
|
430
|
-
private int handleStartCommand(
|
|
772
|
+
private int handleStartCommand(
|
|
773
|
+
Intent intent,
|
|
774
|
+
int flags,
|
|
775
|
+
int startId,
|
|
776
|
+
boolean frameworkRedelivery,
|
|
777
|
+
String requestId,
|
|
778
|
+
StartRequest request) {
|
|
431
779
|
Trace.beginSection("notifee:ForegroundService.onStartCommand");
|
|
432
780
|
try {
|
|
433
781
|
// Check if action is to stop the foreground service
|
|
434
782
|
if (intent == null || STOP_FOREGROUND_SERVICE_ACTION.equals(intent.getAction())) {
|
|
783
|
+
StopAuthority authority = captureStopAuthority();
|
|
435
784
|
ensureStartForegroundContractSatisfied(true);
|
|
436
|
-
if (
|
|
785
|
+
if (ownsStopAuthority(authority)
|
|
786
|
+
&& stopSelfResult(authority.startId)
|
|
787
|
+
&& ownsStopAuthority(authority)) {
|
|
437
788
|
mStopInitiated = true;
|
|
438
789
|
mForegroundActive = false;
|
|
439
790
|
clearOwnedState();
|
|
@@ -456,9 +807,14 @@ public class ForegroundService extends Service {
|
|
|
456
807
|
|
|
457
808
|
Object pendingEvent = null;
|
|
458
809
|
boolean noneEarlyReturn = false;
|
|
810
|
+
RuntimeException deniedInitialPromotion = null;
|
|
811
|
+
int deniedForegroundServiceType = -1;
|
|
812
|
+
boolean deniedTrackedStart = false;
|
|
459
813
|
|
|
460
814
|
synchronized (sLock) {
|
|
461
|
-
if (mCurrentNotificationId == null
|
|
815
|
+
if (mCurrentNotificationId == null
|
|
816
|
+
|| mStateGeneration != mRunGeneration
|
|
817
|
+
|| (mForegroundEpoch != null && mForegroundEpoch.retiring)) {
|
|
462
818
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
463
819
|
int foregroundServiceType =
|
|
464
820
|
notificationModel.getAndroid().getForegroundServiceType();
|
|
@@ -475,16 +831,39 @@ public class ForegroundService extends Service {
|
|
|
475
831
|
// Binder IPC (PackageManager + startForeground) and must not hold sLock.
|
|
476
832
|
noneEarlyReturn = true;
|
|
477
833
|
} else {
|
|
834
|
+
boolean recoverInitialDenial = frameworkRedelivery && ownsUnpromotedState();
|
|
835
|
+
long generation = sRunGeneration;
|
|
836
|
+
long runGeneration = mRunGeneration;
|
|
837
|
+
boolean recoverTrackedDenial =
|
|
838
|
+
canRecoverTrackedDenial(
|
|
839
|
+
intent, requestId, request, generation, runGeneration, startId);
|
|
478
840
|
Trace.beginSection("notifee:startForeground");
|
|
479
841
|
try {
|
|
480
842
|
startForeground(hashCode, notification, foregroundServiceType);
|
|
843
|
+
} catch (RuntimeException e) {
|
|
844
|
+
// Only the real initial promotion is exempt. Type resolution, NONE, posting,
|
|
845
|
+
// updates and teardown retain their existing failure boundaries.
|
|
846
|
+
boolean trackedDenial =
|
|
847
|
+
recoverTrackedDenial
|
|
848
|
+
&& canRecoverTrackedDenial(
|
|
849
|
+
intent, requestId, request, generation, runGeneration, startId);
|
|
850
|
+
if (!(trackedDenial || (recoverInitialDenial && ownsUnpromotedState()))
|
|
851
|
+
|| Build.VERSION.SDK_INT < Build.VERSION_CODES.S
|
|
852
|
+
|| e.getClass() != ForegroundServiceStartNotAllowedException.class) {
|
|
853
|
+
throw e;
|
|
854
|
+
}
|
|
855
|
+
deniedInitialPromotion = e;
|
|
856
|
+
deniedForegroundServiceType = foregroundServiceType;
|
|
857
|
+
deniedTrackedStart = trackedDenial;
|
|
481
858
|
} finally {
|
|
482
859
|
Trace.endSection();
|
|
483
860
|
}
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
861
|
+
if (deniedInitialPromotion == null) {
|
|
862
|
+
mStartForegroundCalled = true;
|
|
863
|
+
mForegroundActive = true;
|
|
864
|
+
mStartCommandSatisfied = true;
|
|
865
|
+
mCurrentForegroundServiceType = foregroundServiceType;
|
|
866
|
+
}
|
|
488
867
|
}
|
|
489
868
|
} else {
|
|
490
869
|
Trace.beginSection("notifee:startForeground");
|
|
@@ -498,7 +877,8 @@ public class ForegroundService extends Service {
|
|
|
498
877
|
mStartCommandSatisfied = true;
|
|
499
878
|
}
|
|
500
879
|
|
|
501
|
-
if (!noneEarlyReturn) {
|
|
880
|
+
if (!noneEarlyReturn && deniedInitialPromotion == null) {
|
|
881
|
+
commitForegroundEpoch();
|
|
502
882
|
mCurrentNotificationId = notificationId;
|
|
503
883
|
mCurrentNotificationBundle = bundle;
|
|
504
884
|
mCurrentNotification = notification;
|
|
@@ -513,13 +893,25 @@ public class ForegroundService extends Service {
|
|
|
513
893
|
(e, aVoid) -> {
|
|
514
894
|
Runnable complete =
|
|
515
895
|
() -> {
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
896
|
+
try {
|
|
897
|
+
synchronized (sLifecycleLock) {
|
|
898
|
+
if (sInstance == this
|
|
899
|
+
&& mRunnerToken == runnerToken
|
|
900
|
+
&& sRunGeneration == runnerGeneration
|
|
901
|
+
&& mForegroundEpoch != null
|
|
902
|
+
&& !mForegroundEpoch.retiring
|
|
903
|
+
&& !mForegroundEpoch.retired) {
|
|
904
|
+
ForegroundEpoch epoch = mForegroundEpoch;
|
|
905
|
+
stopForegroundCompat();
|
|
906
|
+
if (sInstance == this
|
|
907
|
+
&& mForegroundEpoch == epoch
|
|
908
|
+
&& mRunnerToken == runnerToken) {
|
|
909
|
+
clearOwnedState();
|
|
910
|
+
}
|
|
911
|
+
}
|
|
522
912
|
}
|
|
913
|
+
} finally {
|
|
914
|
+
publishTimeoutEventsIfUnlocked();
|
|
523
915
|
}
|
|
524
916
|
};
|
|
525
917
|
if (Looper.myLooper() == Looper.getMainLooper()) {
|
|
@@ -533,6 +925,8 @@ public class ForegroundService extends Service {
|
|
|
533
925
|
}
|
|
534
926
|
} else {
|
|
535
927
|
if (mCurrentNotificationId.equals(notificationId)) {
|
|
928
|
+
ForegroundEpoch updateEpoch = mForegroundEpoch;
|
|
929
|
+
long updateGeneration = sRunGeneration;
|
|
536
930
|
// Before API 31, updating the service's registered notification requires
|
|
537
931
|
// startForeground(). API 31+ processes a same-ID notify() against the foreground
|
|
538
932
|
// service record. Keep that path for an unchanged type: repeating
|
|
@@ -566,13 +960,27 @@ public class ForegroundService extends Service {
|
|
|
566
960
|
} finally {
|
|
567
961
|
Trace.endSection();
|
|
568
962
|
}
|
|
569
|
-
mStartForegroundCalled = true;
|
|
570
|
-
mForegroundActive = true;
|
|
571
|
-
mStartCommandSatisfied = true;
|
|
572
963
|
} else {
|
|
573
964
|
NotificationManagerCompat.from(ContextHolder.getApplicationContext())
|
|
574
965
|
.notify(mCurrentHashCode, notification);
|
|
575
966
|
}
|
|
967
|
+
// A timeout or successor may have retired/replaced the publication during IPC.
|
|
968
|
+
// A normal return cannot reacquire its cache, runner or promotion authority.
|
|
969
|
+
if (!ownsTimeoutEpoch(updateEpoch)
|
|
970
|
+
|| updateEpoch == null
|
|
971
|
+
|| updateEpoch.retiring
|
|
972
|
+
|| updateEpoch.retired
|
|
973
|
+
|| mLastStartId != startId
|
|
974
|
+
|| mRunGeneration != updateEpoch.runGeneration
|
|
975
|
+
|| sRunGeneration != updateGeneration) {
|
|
976
|
+
return START_NOT_STICKY;
|
|
977
|
+
}
|
|
978
|
+
if (shouldStartForeground) {
|
|
979
|
+
mStartForegroundCalled = true;
|
|
980
|
+
mForegroundActive = true;
|
|
981
|
+
mStartCommandSatisfied = true;
|
|
982
|
+
commitForegroundEpoch();
|
|
983
|
+
}
|
|
576
984
|
// The platform call returned normally, so cache the latest submitted state for
|
|
577
985
|
// re-post and timeout. A normal return does not acknowledge display.
|
|
578
986
|
mCurrentForegroundServiceType = foregroundServiceType;
|
|
@@ -586,6 +994,28 @@ public class ForegroundService extends Service {
|
|
|
586
994
|
}
|
|
587
995
|
}
|
|
588
996
|
|
|
997
|
+
if (deniedInitialPromotion != null) {
|
|
998
|
+
// Release sLock before stop Binder IPC. Terminalize only the exact denied request;
|
|
999
|
+
// this is not a successful promotion and cannot fulfill any other delivered START.
|
|
1000
|
+
if (deniedTrackedStart) {
|
|
1001
|
+
request.promotionDenied = true;
|
|
1002
|
+
sPendingStarts.remove(requestId, request);
|
|
1003
|
+
}
|
|
1004
|
+
DeniedStartRecovery recovery =
|
|
1005
|
+
new DeniedStartRecovery(
|
|
1006
|
+
sRunGeneration,
|
|
1007
|
+
mRunGeneration,
|
|
1008
|
+
startId,
|
|
1009
|
+
flags,
|
|
1010
|
+
deniedInitialPromotion,
|
|
1011
|
+
deniedTrackedStart ? requestId : null,
|
|
1012
|
+
notificationId,
|
|
1013
|
+
deniedForegroundServiceType);
|
|
1014
|
+
mDeniedStartRecovery = recovery;
|
|
1015
|
+
stopDeniedStartIfCurrent(recovery);
|
|
1016
|
+
return START_NOT_STICKY;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
589
1019
|
// Handle NONE early return outside the lock — the helper performs Binder IPC
|
|
590
1020
|
// (PackageManager query + startForeground) that must not hold sLock.
|
|
591
1021
|
if (noneEarlyReturn) {
|
|
@@ -618,7 +1048,11 @@ public class ForegroundService extends Service {
|
|
|
618
1048
|
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
|
619
1049
|
@Override
|
|
620
1050
|
public void onTimeout(int startId) {
|
|
621
|
-
|
|
1051
|
+
try {
|
|
1052
|
+
handleTimeout(startId, -1, true);
|
|
1053
|
+
} finally {
|
|
1054
|
+
publishTimeoutEventsIfUnlocked();
|
|
1055
|
+
}
|
|
622
1056
|
}
|
|
623
1057
|
|
|
624
1058
|
/**
|
|
@@ -628,7 +1062,11 @@ public class ForegroundService extends Service {
|
|
|
628
1062
|
@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
|
|
629
1063
|
@Override
|
|
630
1064
|
public void onTimeout(int startId, int fgsType) {
|
|
631
|
-
|
|
1065
|
+
try {
|
|
1066
|
+
handleTimeout(startId, fgsType, false);
|
|
1067
|
+
} finally {
|
|
1068
|
+
publishTimeoutEventsIfUnlocked();
|
|
1069
|
+
}
|
|
632
1070
|
}
|
|
633
1071
|
|
|
634
1072
|
@RequiresApi(Build.VERSION_CODES.Q)
|
|
@@ -679,12 +1117,20 @@ public class ForegroundService extends Service {
|
|
|
679
1117
|
|
|
680
1118
|
@SuppressWarnings("deprecation")
|
|
681
1119
|
private void stopForegroundCompat() {
|
|
1120
|
+
ForegroundEpoch epoch = mForegroundEpoch;
|
|
1121
|
+
removeForegroundNotification();
|
|
1122
|
+
if (sInstance == this && sStateOwner == this && mForegroundEpoch == epoch) {
|
|
1123
|
+
mForegroundActive = false;
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
@SuppressWarnings("deprecation")
|
|
1128
|
+
private void removeForegroundNotification() {
|
|
682
1129
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
683
1130
|
stopForeground(STOP_FOREGROUND_REMOVE);
|
|
684
1131
|
} else {
|
|
685
1132
|
stopForeground(true);
|
|
686
1133
|
}
|
|
687
|
-
mForegroundActive = false;
|
|
688
1134
|
}
|
|
689
1135
|
|
|
690
1136
|
/**
|
|
@@ -843,36 +1289,144 @@ public class ForegroundService extends Service {
|
|
|
843
1289
|
stopForegroundCompat();
|
|
844
1290
|
}
|
|
845
1291
|
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
+ fgsType
|
|
853
|
-
+ "). Stopping service.");
|
|
1292
|
+
// Caller holds sLifecycleLock. Platform calls may synchronously reenter lifecycle callbacks.
|
|
1293
|
+
private boolean ownsTimeoutEpoch(ForegroundEpoch epoch) {
|
|
1294
|
+
synchronized (sLock) {
|
|
1295
|
+
return sInstance == this && sStateOwner == this && mForegroundEpoch == epoch;
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
854
1298
|
|
|
855
|
-
|
|
856
|
-
synchronized (
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
return;
|
|
1299
|
+
private void retireTimeoutEpoch(ForegroundEpoch epoch) {
|
|
1300
|
+
synchronized (sLock) {
|
|
1301
|
+
if (ownsTimeoutEpoch(epoch)) {
|
|
1302
|
+
mForegroundActive = false;
|
|
1303
|
+
clearOwnedState();
|
|
861
1304
|
}
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
|
1309
|
+
private void handleTimeout(int startId, int fgsType, boolean shortServiceCallback) {
|
|
1310
|
+
Bundle notificationBundle;
|
|
1311
|
+
ForegroundEpoch epoch;
|
|
1312
|
+
RuntimeException primary = null;
|
|
1313
|
+
String demotionOutcome = "failed";
|
|
1314
|
+
String stopOutcome = "deferred";
|
|
1315
|
+
synchronized (sLifecycleLock) {
|
|
1316
|
+
final boolean stopAuthorized;
|
|
1317
|
+
final int stopId;
|
|
1318
|
+
final Object stopRequest = sStopRequest;
|
|
862
1319
|
synchronized (sLock) {
|
|
863
|
-
|
|
864
|
-
|
|
1320
|
+
epoch = mForegroundEpoch;
|
|
1321
|
+
int callbackType =
|
|
1322
|
+
shortServiceCallback ? ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE : fgsType;
|
|
1323
|
+
if (sInstance != this
|
|
1324
|
+
|| sStateOwner != this
|
|
1325
|
+
|| epoch == null
|
|
1326
|
+
|| epoch.retiring
|
|
1327
|
+
|| epoch.retired
|
|
1328
|
+
|| !mForegroundActive
|
|
1329
|
+
|| (mRunFirstStartId != 0 && startId < mRunFirstStartId)
|
|
1330
|
+
|| callbackType <= 0
|
|
1331
|
+
|| mCurrentForegroundServiceType <= 0
|
|
1332
|
+
|| (callbackType & mCurrentForegroundServiceType) == 0) {
|
|
1333
|
+
return;
|
|
1334
|
+
}
|
|
1335
|
+
// Matching IDs are library authority only. A repeated platform promotion within the
|
|
1336
|
+
// same run can expose coincident metadata, so it never upgrades ambiguous provenance.
|
|
1337
|
+
stopAuthorized =
|
|
1338
|
+
!epoch.ambiguousPromotion
|
|
1339
|
+
&& !mTimeoutStartIds.contains(startId)
|
|
1340
|
+
&& epoch.satisfiedStartIds.contains(startId);
|
|
1341
|
+
stopId = epoch.lastSatisfiedStartId;
|
|
1342
|
+
notificationBundle =
|
|
1343
|
+
mCurrentNotificationBundle == null ? null : new Bundle(mCurrentNotificationBundle);
|
|
1344
|
+
mTimeoutStartIds.add(startId);
|
|
1345
|
+
epoch.retiring = true;
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
// Nothing that posts an event, converts a model or retires owned evidence precedes this.
|
|
1349
|
+
try {
|
|
1350
|
+
removeForegroundNotification();
|
|
1351
|
+
demotionOutcome = "returned"; // Normal Binder return is not an AMS acknowledgement.
|
|
1352
|
+
retireTimeoutEpoch(epoch);
|
|
1353
|
+
} catch (RuntimeException failure) {
|
|
1354
|
+
primary = failure;
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
if (stopAuthorized
|
|
1358
|
+
&& ownsTimeoutEpoch(epoch)
|
|
1359
|
+
&& epoch.runGeneration == mRunGeneration
|
|
1360
|
+
&& epoch.runGeneration == sRunGeneration
|
|
1361
|
+
&& sStopRequest == stopRequest
|
|
1362
|
+
&& sPendingStarts.isEmpty()
|
|
1363
|
+
&& mLastStartId == stopId
|
|
1364
|
+
&& stopId != 0
|
|
1365
|
+
&& !mStartCommandFailed
|
|
1366
|
+
&& !mStopInitiated) {
|
|
1367
|
+
try {
|
|
1368
|
+
stopOutcome = "failed";
|
|
1369
|
+
StopAuthority authority = captureStopAuthority();
|
|
1370
|
+
boolean stopped = stopSelfResult(stopId);
|
|
1371
|
+
stopOutcome = stopped ? "accepted" : "refused";
|
|
1372
|
+
if (stopped && ownsTimeoutEpoch(epoch) && ownsStopAuthority(authority)) {
|
|
1373
|
+
mStopInitiated = true;
|
|
1374
|
+
retireTimeoutEpoch(epoch);
|
|
1375
|
+
}
|
|
1376
|
+
} catch (RuntimeException failure) {
|
|
1377
|
+
if (primary == null) primary = failure;
|
|
1378
|
+
else if (primary != failure) primary.addSuppressed(failure);
|
|
1379
|
+
}
|
|
865
1380
|
}
|
|
866
|
-
stopForegroundCompat();
|
|
867
|
-
stopSelf(startId);
|
|
868
1381
|
}
|
|
869
1382
|
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
1383
|
+
// Consume publication authority once, including when a reentrant caller still owns locks.
|
|
1384
|
+
final RuntimeException timeoutFailure = primary;
|
|
1385
|
+
final String diagnostic =
|
|
1386
|
+
"Foreground timeout (startId="
|
|
1387
|
+
+ startId
|
|
1388
|
+
+ ", type="
|
|
1389
|
+
+ fgsType
|
|
1390
|
+
+ ", runGeneration="
|
|
1391
|
+
+ epoch.runGeneration
|
|
1392
|
+
+ ", demotion="
|
|
1393
|
+
+ demotionOutcome
|
|
1394
|
+
+ ", stop="
|
|
1395
|
+
+ stopOutcome
|
|
1396
|
+
+ ").";
|
|
1397
|
+
List<Runnable> publications = sTimeoutPublications.get();
|
|
1398
|
+
if (publications == null) {
|
|
1399
|
+
publications = new ArrayList<>();
|
|
1400
|
+
sTimeoutPublications.set(publications);
|
|
876
1401
|
}
|
|
1402
|
+
publications.add(
|
|
1403
|
+
() -> {
|
|
1404
|
+
if (notificationBundle != null) {
|
|
1405
|
+
try {
|
|
1406
|
+
NotificationModel model = NotificationModel.fromBundle(notificationBundle);
|
|
1407
|
+
Bundle extras = new Bundle();
|
|
1408
|
+
extras.putInt("startId", startId);
|
|
1409
|
+
extras.putInt("fgsType", fgsType);
|
|
1410
|
+
EventBus.post(
|
|
1411
|
+
new NotificationEvent(NotificationEvent.TYPE_FG_TIMEOUT, model, extras));
|
|
1412
|
+
} catch (RuntimeException failure) {
|
|
1413
|
+
if (timeoutFailure != null && timeoutFailure != failure)
|
|
1414
|
+
timeoutFailure.addSuppressed(failure);
|
|
1415
|
+
Logger.w(TAG, "Timeout event publication failed", failure);
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
// Preserve the existing error LogEvent, but only after the essential teardown and
|
|
1419
|
+
// timeout publication. Its failure cannot replace a primary platform exception.
|
|
1420
|
+
try {
|
|
1421
|
+
if (timeoutFailure == null) Logger.e(TAG, diagnostic);
|
|
1422
|
+
else Logger.e(TAG, diagnostic, timeoutFailure);
|
|
1423
|
+
} catch (RuntimeException failure) {
|
|
1424
|
+
if (timeoutFailure != null && timeoutFailure != failure)
|
|
1425
|
+
timeoutFailure.addSuppressed(failure);
|
|
1426
|
+
Logger.w(TAG, "Timeout diagnostic publication failed", failure);
|
|
1427
|
+
}
|
|
1428
|
+
});
|
|
1429
|
+
publishTimeoutEventsIfUnlocked();
|
|
1430
|
+
if (primary != null) throw primary;
|
|
877
1431
|
}
|
|
878
1432
|
}
|