react-native-notify-kit 10.7.2 → 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 +944 -93
- package/android/src/test/java/app/notifee/core/ForegroundServiceTest.java +4281 -57
- package/android/src/test/java/app/notifee/core/NotificationManagerActionRoutingTest.java +37 -0
- package/dist/fcm/parseFcmPayload.d.ts +1 -0
- package/dist/fcm/parseFcmPayload.js.map +1 -1
- package/dist/fcm/reconstructNotification.js +96 -8
- package/dist/fcm/reconstructNotification.js.map +1 -1
- package/dist/types/Notification.d.ts +25 -1
- package/dist/types/Notification.js +10 -0
- package/dist/types/Notification.js.map +1 -1
- package/dist/validators/validateAndroidPressAction.js +3 -0
- package/dist/validators/validateAndroidPressAction.js.map +1 -1
- package/dist/validators/validateIOSNotification.js +5 -2
- package/dist/validators/validateIOSNotification.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/ios/NotifeeCore/NotifeeCore.m +36 -3
- package/ios/NotifeeCore/NotifeeCoreExtensionHelper.m +82 -3
- package/ios/NotifeeCore/NotifeeCoreFcmIdentity.h +91 -0
- package/package.json +1 -1
- package/server/README.md +5 -3
- package/server/dist/buildPayload.d.ts.map +1 -1
- package/server/dist/buildPayload.js +2 -0
- package/server/dist/buildPayload.js.map +1 -1
- package/server/dist/ios.d.ts.map +1 -1
- package/server/dist/ios.js +8 -1
- package/server/dist/ios.js.map +1 -1
- package/server/dist/serialize.d.ts +2 -0
- package/server/dist/serialize.d.ts.map +1 -1
- package/server/dist/serialize.js +6 -0
- package/server/dist/serialize.js.map +1 -1
- package/server/dist/types.d.ts +1 -1
- package/server/dist/types.d.ts.map +1 -1
- package/server/dist/validation.d.ts.map +1 -1
- package/server/dist/validation.js +160 -6
- package/server/dist/validation.js.map +1 -1
- package/server/src/buildPayload.ts +2 -0
- package/server/src/ios.ts +8 -1
- package/server/src/serialize.ts +8 -0
- package/server/src/types.ts +1 -0
- package/server/src/validation.ts +187 -6
- package/src/fcm/parseFcmPayload.ts +1 -0
- package/src/fcm/reconstructNotification.ts +105 -10
- package/src/internal/fcmContract.d.ts +57 -12
- package/src/types/Notification.ts +27 -0
- package/src/validators/validateAndroidPressAction.ts +4 -0
- package/src/validators/validateIOSNotification.ts +7 -2
- package/src/version.ts +1 -1
|
@@ -18,6 +18,7 @@ package app.notifee.core;
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
import android.annotation.SuppressLint;
|
|
21
|
+
import android.app.ForegroundServiceStartNotAllowedException;
|
|
21
22
|
import android.app.Notification;
|
|
22
23
|
import android.app.NotificationChannel;
|
|
23
24
|
import android.app.NotificationManager;
|
|
@@ -29,7 +30,9 @@ import android.content.pm.PackageManager;
|
|
|
29
30
|
import android.content.pm.ServiceInfo;
|
|
30
31
|
import android.os.Build;
|
|
31
32
|
import android.os.Bundle;
|
|
33
|
+
import android.os.Handler;
|
|
32
34
|
import android.os.IBinder;
|
|
35
|
+
import android.os.Looper;
|
|
33
36
|
import android.os.Trace;
|
|
34
37
|
import androidx.annotation.Nullable;
|
|
35
38
|
import androidx.annotation.RequiresApi;
|
|
@@ -40,6 +43,13 @@ import app.notifee.core.event.NotificationEvent;
|
|
|
40
43
|
import app.notifee.core.interfaces.MethodCallResult;
|
|
41
44
|
import app.notifee.core.model.NotificationModel;
|
|
42
45
|
import app.notifee.core.utility.ParcelableCompatReader;
|
|
46
|
+
import java.util.ArrayList;
|
|
47
|
+
import java.util.HashMap;
|
|
48
|
+
import java.util.HashSet;
|
|
49
|
+
import java.util.List;
|
|
50
|
+
import java.util.Map;
|
|
51
|
+
import java.util.Set;
|
|
52
|
+
import java.util.UUID;
|
|
43
53
|
|
|
44
54
|
public class ForegroundService extends Service {
|
|
45
55
|
private static final String TAG = "ForegroundService";
|
|
@@ -49,9 +59,189 @@ public class ForegroundService extends Service {
|
|
|
49
59
|
"app.notifee.core.ForegroundService.STOP";
|
|
50
60
|
|
|
51
61
|
private static final Object sLock = new Object();
|
|
62
|
+
// Serializes client START acceptance with instance-targeted STOP. Notification cache fields
|
|
63
|
+
// are not lifecycle authority. Lock order is sLifecycleLock, then sLock.
|
|
64
|
+
private static final Object sLifecycleLock = new Object();
|
|
65
|
+
// Only START senders take this lock (the notification pipeline runs on its executor). Service
|
|
66
|
+
// lifecycle callbacks must never wait for a sender's Binder reply before promoting themselves.
|
|
67
|
+
private static final Object sStartDispatchLock = new Object();
|
|
68
|
+
private static final Map<String, StartRequest> sPendingStarts = new HashMap<>();
|
|
69
|
+
private static final String START_REQUEST_ID = "notifee.foregroundServiceStartRequestId";
|
|
70
|
+
private static long sRunGeneration;
|
|
71
|
+
private static ForegroundService sInstance;
|
|
72
|
+
private static Object sStopRequest;
|
|
73
|
+
private static ForegroundService sStateOwner;
|
|
74
|
+
private int mLastStartId;
|
|
75
|
+
private boolean mStopInitiated;
|
|
76
|
+
private boolean mStartCommandFailed;
|
|
77
|
+
private boolean mForegroundActive;
|
|
78
|
+
private boolean mStartCommandSatisfied;
|
|
79
|
+
private long mRunGeneration;
|
|
80
|
+
// The admission fence may advance before posting. Cached state belongs to its last successful
|
|
81
|
+
// generation until the replacement platform call returns normally.
|
|
82
|
+
private long mStateGeneration;
|
|
83
|
+
private int mRunFirstStartId;
|
|
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;
|
|
52
196
|
private static final String DEFENSIVE_CHANNEL_ID = "notifee_fg_default";
|
|
53
197
|
private static final int DEFENSIVE_NOTIFICATION_ID = Integer.MAX_VALUE - 1;
|
|
54
198
|
|
|
199
|
+
private static final class StartRequest {
|
|
200
|
+
final long generation;
|
|
201
|
+
final Object precedingStop;
|
|
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;
|
|
206
|
+
// Null until delivery. An unfulfilled delivered request belongs to that physical instance.
|
|
207
|
+
ForegroundService instance;
|
|
208
|
+
|
|
209
|
+
StartRequest(long generation, Object precedingStop) {
|
|
210
|
+
this.generation = generation;
|
|
211
|
+
this.precedingStop = precedingStop;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
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
|
+
|
|
55
245
|
/**
|
|
56
246
|
* Tracks whether startForeground() has been called on THIS service instance. This is an instance
|
|
57
247
|
* field (not static) because the static fields below track cross-invocation notification state
|
|
@@ -73,6 +263,66 @@ public class ForegroundService extends Service {
|
|
|
73
263
|
private static Notification mCurrentNotification = null;
|
|
74
264
|
private static int mCurrentHashCode = 0;
|
|
75
265
|
|
|
266
|
+
@Override
|
|
267
|
+
public void onCreate() {
|
|
268
|
+
super.onCreate();
|
|
269
|
+
synchronized (sLifecycleLock) {
|
|
270
|
+
if (sInstance != null) {
|
|
271
|
+
sInstance.mDeniedStartRecovery = null;
|
|
272
|
+
}
|
|
273
|
+
sInstance = this;
|
|
274
|
+
mRunGeneration = sRunGeneration;
|
|
275
|
+
synchronized (sLock) {
|
|
276
|
+
sStateOwner = this;
|
|
277
|
+
clearCurrentState();
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
@Override
|
|
283
|
+
public void onDestroy() {
|
|
284
|
+
synchronized (sLifecycleLock) {
|
|
285
|
+
mDeniedStartRecovery = null;
|
|
286
|
+
sPendingStarts.values().removeIf(request -> request.instance == this);
|
|
287
|
+
if (sInstance == this) {
|
|
288
|
+
sInstance = null;
|
|
289
|
+
if (sPendingStarts.isEmpty()) {
|
|
290
|
+
sStopRequest = null;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
clearOwnedState();
|
|
294
|
+
mForegroundActive = false;
|
|
295
|
+
synchronized (sLock) {
|
|
296
|
+
if (sStateOwner == this) {
|
|
297
|
+
sStateOwner = null;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
super.onDestroy();
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Caller holds sLock. Ownership is checked by clearOwnedState before any terminal cleanup.
|
|
305
|
+
private static void clearCurrentState() {
|
|
306
|
+
mCurrentNotificationId = null;
|
|
307
|
+
mCurrentForegroundServiceType = -1;
|
|
308
|
+
mCurrentNotificationBundle = null;
|
|
309
|
+
mCurrentNotification = null;
|
|
310
|
+
mCurrentHashCode = 0;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
private void clearOwnedState() {
|
|
314
|
+
synchronized (sLock) {
|
|
315
|
+
if (sStateOwner == this) {
|
|
316
|
+
mDeniedStartRecovery = null;
|
|
317
|
+
clearCurrentState();
|
|
318
|
+
mRunnerToken = null;
|
|
319
|
+
if (mForegroundEpoch != null) {
|
|
320
|
+
mForegroundEpoch.retired = true;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
76
326
|
/**
|
|
77
327
|
* Submits the latest foreground service notification again if the given notification ID matches
|
|
78
328
|
* the active foreground service. On Android 14+, users can dismiss ongoing foreground service
|
|
@@ -82,16 +332,25 @@ public class ForegroundService extends Service {
|
|
|
82
332
|
*/
|
|
83
333
|
@SuppressLint("MissingPermission")
|
|
84
334
|
static boolean repostIfActive(String notificationId) {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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
|
+
}
|
|
90
351
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
.notify(mCurrentHashCode, mCurrentNotification);
|
|
94
|
-
return true;
|
|
352
|
+
} finally {
|
|
353
|
+
publishTimeoutEventsIfUnlocked();
|
|
95
354
|
}
|
|
96
355
|
}
|
|
97
356
|
|
|
@@ -130,55 +389,405 @@ public class ForegroundService extends Service {
|
|
|
130
389
|
intent.putExtra("notification", notification);
|
|
131
390
|
intent.putExtra("notificationBundle", notificationBundle);
|
|
132
391
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
392
|
+
synchronized (sStartDispatchLock) {
|
|
393
|
+
// A retry/redelivery from a previous process must not alias a new pending request.
|
|
394
|
+
String requestId = UUID.randomUUID().toString();
|
|
395
|
+
intent.putExtra(START_REQUEST_ID, requestId);
|
|
396
|
+
StartRequest request;
|
|
397
|
+
synchronized (sLifecycleLock) {
|
|
398
|
+
request = new StartRequest(sRunGeneration, sStopRequest);
|
|
399
|
+
sPendingStarts.put(requestId, request);
|
|
400
|
+
}
|
|
401
|
+
ComponentName component;
|
|
402
|
+
try {
|
|
403
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
404
|
+
component = context.startForegroundService(intent);
|
|
405
|
+
} else {
|
|
406
|
+
component = context.startService(intent);
|
|
407
|
+
}
|
|
408
|
+
} catch (RuntimeException e) {
|
|
409
|
+
synchronized (sLifecycleLock) {
|
|
410
|
+
// Only a definite platform rejection releases an undelivered obligation. An unrelated
|
|
411
|
+
// exception can be a lost Binder reply: UNKNOWN must remain held until actual delivery.
|
|
412
|
+
if (!request.accepted
|
|
413
|
+
&& (e instanceof SecurityException
|
|
414
|
+
|| (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
|
|
415
|
+
&& e instanceof ForegroundServiceStartNotAllowedException))) {
|
|
416
|
+
sPendingStarts.remove(requestId);
|
|
417
|
+
}
|
|
418
|
+
scheduleStop();
|
|
419
|
+
}
|
|
420
|
+
throw e;
|
|
421
|
+
}
|
|
422
|
+
synchronized (sLifecycleLock) {
|
|
423
|
+
if (component != null) {
|
|
424
|
+
acceptStart(request);
|
|
425
|
+
} else if (!request.accepted) {
|
|
426
|
+
sPendingStarts.remove(requestId);
|
|
427
|
+
}
|
|
428
|
+
scheduleStop();
|
|
429
|
+
}
|
|
138
430
|
}
|
|
139
431
|
}
|
|
140
432
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
433
|
+
// Caller holds sLifecycleLock. Delivery itself can prove acceptance before Context returns.
|
|
434
|
+
// A late reply never cancels a STOP ordered after this START was reserved/delivered.
|
|
435
|
+
private static void acceptStart(StartRequest request) {
|
|
436
|
+
if (request.promotionDenied) {
|
|
145
437
|
return;
|
|
146
438
|
}
|
|
439
|
+
if (!request.accepted) {
|
|
440
|
+
request.accepted = true;
|
|
441
|
+
if (sInstance != null) {
|
|
442
|
+
sInstance.mDeniedStartRecovery = null;
|
|
443
|
+
}
|
|
444
|
+
if (sStopRequest == request.precedingStop) {
|
|
445
|
+
sStopRequest = null;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
147
449
|
|
|
148
|
-
|
|
149
|
-
|
|
450
|
+
static void stop() {
|
|
451
|
+
synchronized (sLifecycleLock) {
|
|
452
|
+
if (sInstance == null && sPendingStarts.isEmpty()) {
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
if (sInstance != null) {
|
|
456
|
+
sInstance.mDeniedStartRecovery = null;
|
|
457
|
+
}
|
|
458
|
+
sStopRequest = new Object();
|
|
459
|
+
sRunGeneration++;
|
|
460
|
+
scheduleStop();
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Caller holds sLifecycleLock. No START/STOP intent is dispatched here, so a stale reference
|
|
465
|
+
// cannot cold-start a service. onCreate alone is UNKNOWN: wait for a delivered command.
|
|
466
|
+
private static void scheduleStop() {
|
|
467
|
+
ForegroundService instance = sInstance;
|
|
468
|
+
Object request = sStopRequest;
|
|
469
|
+
if (instance != null && request != null) {
|
|
470
|
+
new Handler(Looper.getMainLooper()).post(() -> instance.stopIfRequested(request));
|
|
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
|
+
}
|
|
150
477
|
|
|
478
|
+
private void stopDeniedStartIfCurrent(DeniedStartRecovery recovery) {
|
|
151
479
|
try {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
private void stopIfRequested(Object request) {
|
|
547
|
+
try {
|
|
548
|
+
stopIfRequestedLocked(request);
|
|
549
|
+
} finally {
|
|
550
|
+
publishTimeoutEventsIfUnlocked();
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
private void stopIfRequestedLocked(Object request) {
|
|
555
|
+
synchronized (sLifecycleLock) {
|
|
556
|
+
if (sInstance != this
|
|
557
|
+
|| sStopRequest != request
|
|
558
|
+
|| !sPendingStarts.isEmpty()
|
|
559
|
+
|| mLastStartId == 0
|
|
560
|
+
|| mStopInitiated
|
|
561
|
+
|| mStartCommandFailed) {
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
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
|
+
}
|
|
581
|
+
}
|
|
164
582
|
}
|
|
165
583
|
}
|
|
166
584
|
|
|
167
|
-
@SuppressLint({"ForegroundServiceType", "MissingPermission"})
|
|
168
585
|
@Override
|
|
169
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) {
|
|
595
|
+
synchronized (sLifecycleLock) {
|
|
596
|
+
// Any later delivery supersedes the recovery startId, including terminal commands.
|
|
597
|
+
mDeniedStartRecovery = null;
|
|
598
|
+
mLastStartId = startId;
|
|
599
|
+
boolean terminal =
|
|
600
|
+
intent == null || STOP_FOREGROUND_SERVICE_ACTION.equals(intent.getAction());
|
|
601
|
+
if (terminal) {
|
|
602
|
+
sRunGeneration++;
|
|
603
|
+
}
|
|
604
|
+
if (terminal && !sPendingStarts.isEmpty()) {
|
|
605
|
+
// The earlier accepted START must promote with its real notification/type first.
|
|
606
|
+
sStopRequest = new Object();
|
|
607
|
+
return START_STICKY_COMPATIBILITY;
|
|
608
|
+
}
|
|
609
|
+
if (!terminal) {
|
|
610
|
+
mStopInitiated = false;
|
|
611
|
+
}
|
|
612
|
+
String requestId = intent == null ? null : intent.getStringExtra(START_REQUEST_ID);
|
|
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;
|
|
621
|
+
boolean superseded = false;
|
|
622
|
+
if (!terminal) {
|
|
623
|
+
long generation = request == null ? sRunGeneration : request.generation;
|
|
624
|
+
if (request != null) {
|
|
625
|
+
request.instance = this;
|
|
626
|
+
acceptStart(request);
|
|
627
|
+
}
|
|
628
|
+
superseded = mRunFirstStartId != 0 && generation < mRunGeneration;
|
|
629
|
+
if (!superseded && mRunGeneration != generation) {
|
|
630
|
+
// Reserve the new session without retiring the previous submitted state or runner.
|
|
631
|
+
// A failed promotion must preserve them, while older deliveries remain fenced.
|
|
632
|
+
mRunGeneration = generation;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
mStartCommandSatisfied = mForegroundActive;
|
|
636
|
+
try {
|
|
637
|
+
int result;
|
|
638
|
+
if (superseded) {
|
|
639
|
+
// A lost-reply START can arrive after a newer run. Preserve the new cache/runner. If
|
|
640
|
+
// that run already ended, still perform the old request's real promotion before teardown.
|
|
641
|
+
result = mForegroundActive ? START_NOT_STICKY : fulfillSupersededStart(intent);
|
|
642
|
+
} else {
|
|
643
|
+
result =
|
|
644
|
+
handleStartCommand(intent, flags, startId, frameworkRedelivery, requestId, request);
|
|
645
|
+
}
|
|
646
|
+
mStartCommandFailed = false;
|
|
647
|
+
if (mStartCommandSatisfied) {
|
|
648
|
+
if (!terminal && !superseded) {
|
|
649
|
+
recordSatisfiedStart(startId);
|
|
650
|
+
}
|
|
651
|
+
settleDeliveredStarts();
|
|
652
|
+
}
|
|
653
|
+
if (superseded
|
|
654
|
+
&& mStartCommandSatisfied
|
|
655
|
+
&& !mForegroundActive
|
|
656
|
+
&& mRunnerToken == null
|
|
657
|
+
&& sPendingStarts.isEmpty()
|
|
658
|
+
&& sStopRequest == null) {
|
|
659
|
+
sStopRequest = new Object();
|
|
660
|
+
}
|
|
661
|
+
if (!terminal) {
|
|
662
|
+
scheduleStop();
|
|
663
|
+
}
|
|
664
|
+
return result;
|
|
665
|
+
} catch (RuntimeException e) {
|
|
666
|
+
mStartCommandFailed = !mStartCommandSatisfied;
|
|
667
|
+
if (mStartCommandSatisfied) {
|
|
668
|
+
if (!terminal && !superseded) {
|
|
669
|
+
recordSatisfiedStart(startId);
|
|
670
|
+
}
|
|
671
|
+
settleDeliveredStarts();
|
|
672
|
+
}
|
|
673
|
+
throw e;
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
}
|
|
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
|
+
|
|
730
|
+
// A real promotion (or an already foreground instance) satisfies the physical instance's
|
|
731
|
+
// delivered obligations, including a prior ambiguous delivery. Undelivered STARTs remain held.
|
|
732
|
+
// This does not turn a posting/type exception into success: onStartCommand still rethrows it.
|
|
733
|
+
private void settleDeliveredStarts() {
|
|
734
|
+
mDeniedStartRecovery = null;
|
|
735
|
+
sPendingStarts.values().removeIf(request -> request.instance == this);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
@SuppressLint({"ForegroundServiceType", "MissingPermission"})
|
|
739
|
+
private int fulfillSupersededStart(Intent intent) {
|
|
740
|
+
Bundle extras = intent.getExtras();
|
|
741
|
+
if (extras == null) {
|
|
742
|
+
return START_NOT_STICKY;
|
|
743
|
+
}
|
|
744
|
+
Notification notification =
|
|
745
|
+
ParcelableCompatReader.getParcelable(extras, "notification", Notification.class);
|
|
746
|
+
Bundle bundle = extras.getBundle("notificationBundle");
|
|
747
|
+
if (notification == null || bundle == null) {
|
|
748
|
+
return START_NOT_STICKY;
|
|
749
|
+
}
|
|
750
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
751
|
+
int type = NotificationModel.fromBundle(bundle).getAndroid().getForegroundServiceType();
|
|
752
|
+
if (type == ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST) {
|
|
753
|
+
type = resolveManifestServiceType();
|
|
754
|
+
}
|
|
755
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE
|
|
756
|
+
&& type == ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE) {
|
|
757
|
+
ensureStartForegroundContractSatisfied(false);
|
|
758
|
+
return START_NOT_STICKY;
|
|
759
|
+
}
|
|
760
|
+
startForeground(extras.getInt("hashCode"), notification, type);
|
|
761
|
+
} else {
|
|
762
|
+
startForeground(extras.getInt("hashCode"), notification);
|
|
763
|
+
}
|
|
764
|
+
mStartForegroundCalled = true;
|
|
765
|
+
mForegroundActive = true;
|
|
766
|
+
mStartCommandSatisfied = true;
|
|
767
|
+
stopForegroundCompat();
|
|
768
|
+
return START_NOT_STICKY;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
@SuppressLint({"ForegroundServiceType", "MissingPermission"})
|
|
772
|
+
private int handleStartCommand(
|
|
773
|
+
Intent intent,
|
|
774
|
+
int flags,
|
|
775
|
+
int startId,
|
|
776
|
+
boolean frameworkRedelivery,
|
|
777
|
+
String requestId,
|
|
778
|
+
StartRequest request) {
|
|
170
779
|
Trace.beginSection("notifee:ForegroundService.onStartCommand");
|
|
171
780
|
try {
|
|
172
781
|
// Check if action is to stop the foreground service
|
|
173
782
|
if (intent == null || STOP_FOREGROUND_SERVICE_ACTION.equals(intent.getAction())) {
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
783
|
+
StopAuthority authority = captureStopAuthority();
|
|
784
|
+
ensureStartForegroundContractSatisfied(true);
|
|
785
|
+
if (ownsStopAuthority(authority)
|
|
786
|
+
&& stopSelfResult(authority.startId)
|
|
787
|
+
&& ownsStopAuthority(authority)) {
|
|
788
|
+
mStopInitiated = true;
|
|
789
|
+
mForegroundActive = false;
|
|
790
|
+
clearOwnedState();
|
|
182
791
|
}
|
|
183
792
|
return Service.START_STICKY_COMPATIBILITY;
|
|
184
793
|
}
|
|
@@ -198,9 +807,14 @@ public class ForegroundService extends Service {
|
|
|
198
807
|
|
|
199
808
|
Object pendingEvent = null;
|
|
200
809
|
boolean noneEarlyReturn = false;
|
|
810
|
+
RuntimeException deniedInitialPromotion = null;
|
|
811
|
+
int deniedForegroundServiceType = -1;
|
|
812
|
+
boolean deniedTrackedStart = false;
|
|
201
813
|
|
|
202
814
|
synchronized (sLock) {
|
|
203
|
-
if (mCurrentNotificationId == null
|
|
815
|
+
if (mCurrentNotificationId == null
|
|
816
|
+
|| mStateGeneration != mRunGeneration
|
|
817
|
+
|| (mForegroundEpoch != null && mForegroundEpoch.retiring)) {
|
|
204
818
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
205
819
|
int foregroundServiceType =
|
|
206
820
|
notificationModel.getAndroid().getForegroundServiceType();
|
|
@@ -217,14 +831,39 @@ public class ForegroundService extends Service {
|
|
|
217
831
|
// Binder IPC (PackageManager + startForeground) and must not hold sLock.
|
|
218
832
|
noneEarlyReturn = true;
|
|
219
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);
|
|
220
840
|
Trace.beginSection("notifee:startForeground");
|
|
221
841
|
try {
|
|
222
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;
|
|
223
858
|
} finally {
|
|
224
859
|
Trace.endSection();
|
|
225
860
|
}
|
|
226
|
-
|
|
227
|
-
|
|
861
|
+
if (deniedInitialPromotion == null) {
|
|
862
|
+
mStartForegroundCalled = true;
|
|
863
|
+
mForegroundActive = true;
|
|
864
|
+
mStartCommandSatisfied = true;
|
|
865
|
+
mCurrentForegroundServiceType = foregroundServiceType;
|
|
866
|
+
}
|
|
228
867
|
}
|
|
229
868
|
} else {
|
|
230
869
|
Trace.beginSection("notifee:startForeground");
|
|
@@ -234,23 +873,51 @@ public class ForegroundService extends Service {
|
|
|
234
873
|
Trace.endSection();
|
|
235
874
|
}
|
|
236
875
|
mStartForegroundCalled = true;
|
|
876
|
+
mForegroundActive = true;
|
|
877
|
+
mStartCommandSatisfied = true;
|
|
237
878
|
}
|
|
238
879
|
|
|
239
|
-
if (!noneEarlyReturn) {
|
|
880
|
+
if (!noneEarlyReturn && deniedInitialPromotion == null) {
|
|
881
|
+
commitForegroundEpoch();
|
|
240
882
|
mCurrentNotificationId = notificationId;
|
|
241
883
|
mCurrentNotificationBundle = bundle;
|
|
242
884
|
mCurrentNotification = notification;
|
|
243
885
|
mCurrentHashCode = hashCode;
|
|
886
|
+
mStateGeneration = mRunGeneration;
|
|
887
|
+
mRunFirstStartId = mLastStartId;
|
|
888
|
+
final Object runnerToken = new Object();
|
|
889
|
+
final long runnerGeneration = mRunGeneration;
|
|
890
|
+
mRunnerToken = runnerToken;
|
|
244
891
|
// On headless task complete
|
|
245
892
|
final MethodCallResult<Void> methodCallResult =
|
|
246
893
|
(e, aVoid) -> {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
894
|
+
Runnable complete =
|
|
895
|
+
() -> {
|
|
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
|
+
}
|
|
912
|
+
}
|
|
913
|
+
} finally {
|
|
914
|
+
publishTimeoutEventsIfUnlocked();
|
|
915
|
+
}
|
|
916
|
+
};
|
|
917
|
+
if (Looper.myLooper() == Looper.getMainLooper()) {
|
|
918
|
+
complete.run();
|
|
919
|
+
} else {
|
|
920
|
+
new Handler(Looper.getMainLooper()).post(complete);
|
|
254
921
|
}
|
|
255
922
|
};
|
|
256
923
|
|
|
@@ -258,6 +925,8 @@ public class ForegroundService extends Service {
|
|
|
258
925
|
}
|
|
259
926
|
} else {
|
|
260
927
|
if (mCurrentNotificationId.equals(notificationId)) {
|
|
928
|
+
ForegroundEpoch updateEpoch = mForegroundEpoch;
|
|
929
|
+
long updateGeneration = sRunGeneration;
|
|
261
930
|
// Before API 31, updating the service's registered notification requires
|
|
262
931
|
// startForeground(). API 31+ processes a same-ID notify() against the foreground
|
|
263
932
|
// service record. Keep that path for an unchanged type: repeating
|
|
@@ -291,11 +960,27 @@ public class ForegroundService extends Service {
|
|
|
291
960
|
} finally {
|
|
292
961
|
Trace.endSection();
|
|
293
962
|
}
|
|
294
|
-
mStartForegroundCalled = true;
|
|
295
963
|
} else {
|
|
296
964
|
NotificationManagerCompat.from(ContextHolder.getApplicationContext())
|
|
297
965
|
.notify(mCurrentHashCode, notification);
|
|
298
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
|
+
}
|
|
299
984
|
// The platform call returned normally, so cache the latest submitted state for
|
|
300
985
|
// re-post and timeout. A normal return does not acknowledge display.
|
|
301
986
|
mCurrentForegroundServiceType = foregroundServiceType;
|
|
@@ -309,10 +994,32 @@ public class ForegroundService extends Service {
|
|
|
309
994
|
}
|
|
310
995
|
}
|
|
311
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
|
+
|
|
312
1019
|
// Handle NONE early return outside the lock — the helper performs Binder IPC
|
|
313
1020
|
// (PackageManager query + startForeground) that must not hold sLock.
|
|
314
1021
|
if (noneEarlyReturn) {
|
|
315
|
-
ensureStartForegroundContractSatisfied();
|
|
1022
|
+
ensureStartForegroundContractSatisfied(false);
|
|
316
1023
|
return START_NOT_STICKY;
|
|
317
1024
|
}
|
|
318
1025
|
|
|
@@ -341,7 +1048,11 @@ public class ForegroundService extends Service {
|
|
|
341
1048
|
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
|
342
1049
|
@Override
|
|
343
1050
|
public void onTimeout(int startId) {
|
|
344
|
-
|
|
1051
|
+
try {
|
|
1052
|
+
handleTimeout(startId, -1, true);
|
|
1053
|
+
} finally {
|
|
1054
|
+
publishTimeoutEventsIfUnlocked();
|
|
1055
|
+
}
|
|
345
1056
|
}
|
|
346
1057
|
|
|
347
1058
|
/**
|
|
@@ -351,7 +1062,11 @@ public class ForegroundService extends Service {
|
|
|
351
1062
|
@RequiresApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
|
|
352
1063
|
@Override
|
|
353
1064
|
public void onTimeout(int startId, int fgsType) {
|
|
354
|
-
|
|
1065
|
+
try {
|
|
1066
|
+
handleTimeout(startId, fgsType, false);
|
|
1067
|
+
} finally {
|
|
1068
|
+
publishTimeoutEventsIfUnlocked();
|
|
1069
|
+
}
|
|
355
1070
|
}
|
|
356
1071
|
|
|
357
1072
|
@RequiresApi(Build.VERSION_CODES.Q)
|
|
@@ -402,6 +1117,15 @@ public class ForegroundService extends Service {
|
|
|
402
1117
|
|
|
403
1118
|
@SuppressWarnings("deprecation")
|
|
404
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() {
|
|
405
1129
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
406
1130
|
stopForeground(STOP_FOREGROUND_REMOVE);
|
|
407
1131
|
} else {
|
|
@@ -420,7 +1144,7 @@ public class ForegroundService extends Service {
|
|
|
420
1144
|
* on API 29-33 and to {@code FOREGROUND_SERVICE_TYPE_NONE} on API 34+. This method is an instance
|
|
421
1145
|
* method that uses {@code this} as context (always valid inside a running Service) and returns
|
|
422
1146
|
* the raw declared value without any API-level mapping, which is what the proactive manifest
|
|
423
|
-
* check in {@link #ensureStartForegroundContractSatisfied()} needs.
|
|
1147
|
+
* check in {@link #ensureStartForegroundContractSatisfied(boolean)} needs.
|
|
424
1148
|
*
|
|
425
1149
|
* @return the raw {@code foregroundServiceType} bitmask from the manifest, or 0 if the service is
|
|
426
1150
|
* not declared or has no explicit type
|
|
@@ -485,10 +1209,12 @@ public class ForegroundService extends Service {
|
|
|
485
1209
|
* @throws RuntimeException if the manifest is missing required {@code foregroundServiceType}
|
|
486
1210
|
* declarations on API 34+, or if the defensive {@code startForeground()} call fails for a
|
|
487
1211
|
* non-platform-permission reason. {@link SecurityException} is handled as a best-effort stop
|
|
488
|
-
* because API 34+ can deny while-in-use types when the app is backgrounded.
|
|
1212
|
+
* because API 34+ can deny while-in-use types when the app is backgrounded. A terminal
|
|
1213
|
+
* STOP/null also logs and tolerates the API 31+ promotion denial specifically; that policy
|
|
1214
|
+
* does not apply to NONE or to an earlier unfulfilled accepted START.
|
|
489
1215
|
*/
|
|
490
1216
|
@SuppressLint({"ForegroundServiceType", "MissingPermission"})
|
|
491
|
-
private void ensureStartForegroundContractSatisfied() {
|
|
1217
|
+
private void ensureStartForegroundContractSatisfied(boolean terminalStop) {
|
|
492
1218
|
if (mStartForegroundCalled) {
|
|
493
1219
|
return;
|
|
494
1220
|
}
|
|
@@ -516,66 +1242,191 @@ public class ForegroundService extends Service {
|
|
|
516
1242
|
}
|
|
517
1243
|
}
|
|
518
1244
|
|
|
1245
|
+
ensureDefensiveChannel();
|
|
1246
|
+
Notification placeholder =
|
|
1247
|
+
new NotificationCompat.Builder(this, DEFENSIVE_CHANNEL_ID)
|
|
1248
|
+
.setSmallIcon(android.R.drawable.ic_dialog_info)
|
|
1249
|
+
.build();
|
|
1250
|
+
// Resolve the safe declared type before entering the promotion-only exception boundary.
|
|
1251
|
+
int defensiveTypes =
|
|
1252
|
+
Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE
|
|
1253
|
+
? selectDefensiveForegroundServiceType(declaredTypes)
|
|
1254
|
+
: 0;
|
|
519
1255
|
try {
|
|
520
|
-
ensureDefensiveChannel();
|
|
521
|
-
Notification placeholder =
|
|
522
|
-
new NotificationCompat.Builder(this, DEFENSIVE_CHANNEL_ID)
|
|
523
|
-
.setSmallIcon(android.R.drawable.ic_dialog_info)
|
|
524
|
-
.build();
|
|
525
1256
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
|
526
|
-
// The STOP/null defensive placeholder does not represent the real workload. Prefer a
|
|
527
|
-
// declared type without while-in-use runtime restrictions before falling back to the
|
|
528
|
-
// manifest-declared mask.
|
|
529
|
-
int defensiveTypes = selectDefensiveForegroundServiceType(declaredTypes);
|
|
530
1257
|
startForeground(DEFENSIVE_NOTIFICATION_ID, placeholder, defensiveTypes);
|
|
531
1258
|
} else {
|
|
532
1259
|
startForeground(DEFENSIVE_NOTIFICATION_ID, placeholder);
|
|
533
1260
|
}
|
|
534
|
-
mStartForegroundCalled = true;
|
|
535
|
-
stopForegroundCompat();
|
|
536
1261
|
} catch (SecurityException e) {
|
|
537
1262
|
Logger.w(
|
|
538
1263
|
TAG,
|
|
539
1264
|
"Defensive startForeground() was denied by Android while stopping the service; "
|
|
540
1265
|
+ "continuing with stopSelf() to avoid a fatal app crash.",
|
|
541
1266
|
e);
|
|
1267
|
+
return;
|
|
542
1268
|
} catch (Exception e) {
|
|
1269
|
+
// NONE and unrelated IllegalStateException/type failures remain observable.
|
|
1270
|
+
if (terminalStop
|
|
1271
|
+
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
|
|
1272
|
+
&& e instanceof ForegroundServiceStartNotAllowedException) {
|
|
1273
|
+
Logger.w(
|
|
1274
|
+
TAG,
|
|
1275
|
+
"Defensive startForeground() was not allowed by Android while stopping; "
|
|
1276
|
+
+ "continuing with stopSelf().",
|
|
1277
|
+
e);
|
|
1278
|
+
return;
|
|
1279
|
+
}
|
|
543
1280
|
String msg =
|
|
544
1281
|
"react-native-notify-kit: defensive startForeground() failed. "
|
|
545
1282
|
+ "This indicates an inconsistent service state and the process cannot recover.";
|
|
546
1283
|
Logger.e(TAG, msg, e);
|
|
547
1284
|
throw new RuntimeException(msg, e);
|
|
548
1285
|
}
|
|
1286
|
+
mStartForegroundCalled = true;
|
|
1287
|
+
mForegroundActive = true;
|
|
1288
|
+
mStartCommandSatisfied = true;
|
|
1289
|
+
stopForegroundCompat();
|
|
549
1290
|
}
|
|
550
1291
|
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
+ fgsType
|
|
558
|
-
+ "). 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
|
+
}
|
|
559
1298
|
|
|
560
|
-
|
|
1299
|
+
private void retireTimeoutEpoch(ForegroundEpoch epoch) {
|
|
561
1300
|
synchronized (sLock) {
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
mCurrentNotification = null;
|
|
567
|
-
mCurrentHashCode = 0;
|
|
1301
|
+
if (ownsTimeoutEpoch(epoch)) {
|
|
1302
|
+
mForegroundActive = false;
|
|
1303
|
+
clearOwnedState();
|
|
1304
|
+
}
|
|
568
1305
|
}
|
|
1306
|
+
}
|
|
569
1307
|
|
|
570
|
-
|
|
571
|
-
|
|
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;
|
|
1319
|
+
synchronized (sLock) {
|
|
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
|
+
}
|
|
572
1347
|
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
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
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
|
|
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);
|
|
579
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;
|
|
580
1431
|
}
|
|
581
1432
|
}
|