react-native-notify-kit 10.7.2 → 10.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/src/main/java/app/notifee/core/ForegroundService.java +363 -66
- package/android/src/test/java/app/notifee/core/ForegroundServiceTest.java +1194 -32
- package/android/src/test/java/app/notifee/core/NotificationManagerActionRoutingTest.java +37 -0
- package/dist/fcm/parseFcmPayload.d.ts +1 -0
- package/dist/fcm/parseFcmPayload.js.map +1 -1
- package/dist/fcm/reconstructNotification.js +96 -8
- package/dist/fcm/reconstructNotification.js.map +1 -1
- package/dist/validators/validateAndroidPressAction.js +3 -0
- package/dist/validators/validateAndroidPressAction.js.map +1 -1
- package/dist/validators/validateIOSNotification.js +5 -2
- package/dist/validators/validateIOSNotification.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/ios/NotifeeCore/NotifeeCore.m +36 -3
- package/ios/NotifeeCore/NotifeeCoreExtensionHelper.m +82 -3
- package/ios/NotifeeCore/NotifeeCoreFcmIdentity.h +91 -0
- package/package.json +1 -1
- package/server/README.md +5 -3
- package/server/dist/buildPayload.d.ts.map +1 -1
- package/server/dist/buildPayload.js +2 -0
- package/server/dist/buildPayload.js.map +1 -1
- package/server/dist/ios.d.ts.map +1 -1
- package/server/dist/ios.js +8 -1
- package/server/dist/ios.js.map +1 -1
- package/server/dist/serialize.d.ts +2 -0
- package/server/dist/serialize.d.ts.map +1 -1
- package/server/dist/serialize.js +6 -0
- package/server/dist/serialize.js.map +1 -1
- package/server/dist/types.d.ts +1 -1
- package/server/dist/types.d.ts.map +1 -1
- package/server/dist/validation.d.ts.map +1 -1
- package/server/dist/validation.js +160 -6
- package/server/dist/validation.js.map +1 -1
- package/server/src/buildPayload.ts +2 -0
- package/server/src/ios.ts +8 -1
- package/server/src/serialize.ts +8 -0
- package/server/src/types.ts +1 -0
- package/server/src/validation.ts +187 -6
- package/src/fcm/parseFcmPayload.ts +1 -0
- package/src/fcm/reconstructNotification.ts +105 -10
- package/src/internal/fcmContract.d.ts +57 -12
- package/src/validators/validateAndroidPressAction.ts +4 -0
- package/src/validators/validateIOSNotification.ts +7 -2
- package/src/version.ts +1 -1
|
@@ -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,9 @@ 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.HashMap;
|
|
47
|
+
import java.util.Map;
|
|
48
|
+
import java.util.UUID;
|
|
43
49
|
|
|
44
50
|
public class ForegroundService extends Service {
|
|
45
51
|
private static final String TAG = "ForegroundService";
|
|
@@ -49,9 +55,45 @@ public class ForegroundService extends Service {
|
|
|
49
55
|
"app.notifee.core.ForegroundService.STOP";
|
|
50
56
|
|
|
51
57
|
private static final Object sLock = new Object();
|
|
58
|
+
// Serializes client START acceptance with instance-targeted STOP. Notification cache fields
|
|
59
|
+
// are not lifecycle authority. Lock order is sLifecycleLock, then sLock.
|
|
60
|
+
private static final Object sLifecycleLock = new Object();
|
|
61
|
+
// Only START senders take this lock (the notification pipeline runs on its executor). Service
|
|
62
|
+
// lifecycle callbacks must never wait for a sender's Binder reply before promoting themselves.
|
|
63
|
+
private static final Object sStartDispatchLock = new Object();
|
|
64
|
+
private static final Map<String, StartRequest> sPendingStarts = new HashMap<>();
|
|
65
|
+
private static final String START_REQUEST_ID = "notifee.foregroundServiceStartRequestId";
|
|
66
|
+
private static long sRunGeneration;
|
|
67
|
+
private static ForegroundService sInstance;
|
|
68
|
+
private static Object sStopRequest;
|
|
69
|
+
private static ForegroundService sStateOwner;
|
|
70
|
+
private int mLastStartId;
|
|
71
|
+
private boolean mStopInitiated;
|
|
72
|
+
private boolean mStartCommandFailed;
|
|
73
|
+
private boolean mForegroundActive;
|
|
74
|
+
private boolean mStartCommandSatisfied;
|
|
75
|
+
private long mRunGeneration;
|
|
76
|
+
// The admission fence may advance before posting. Cached state belongs to its last successful
|
|
77
|
+
// generation until the replacement platform call returns normally.
|
|
78
|
+
private long mStateGeneration;
|
|
79
|
+
private int mRunFirstStartId;
|
|
80
|
+
private Object mRunnerToken;
|
|
52
81
|
private static final String DEFENSIVE_CHANNEL_ID = "notifee_fg_default";
|
|
53
82
|
private static final int DEFENSIVE_NOTIFICATION_ID = Integer.MAX_VALUE - 1;
|
|
54
83
|
|
|
84
|
+
private static final class StartRequest {
|
|
85
|
+
final long generation;
|
|
86
|
+
final Object precedingStop;
|
|
87
|
+
boolean accepted;
|
|
88
|
+
// Null until delivery. An unfulfilled delivered request belongs to that physical instance.
|
|
89
|
+
ForegroundService instance;
|
|
90
|
+
|
|
91
|
+
StartRequest(long generation, Object precedingStop) {
|
|
92
|
+
this.generation = generation;
|
|
93
|
+
this.precedingStop = precedingStop;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
55
97
|
/**
|
|
56
98
|
* Tracks whether startForeground() has been called on THIS service instance. This is an instance
|
|
57
99
|
* field (not static) because the static fields below track cross-invocation notification state
|
|
@@ -73,6 +115,58 @@ public class ForegroundService extends Service {
|
|
|
73
115
|
private static Notification mCurrentNotification = null;
|
|
74
116
|
private static int mCurrentHashCode = 0;
|
|
75
117
|
|
|
118
|
+
@Override
|
|
119
|
+
public void onCreate() {
|
|
120
|
+
super.onCreate();
|
|
121
|
+
synchronized (sLifecycleLock) {
|
|
122
|
+
sInstance = this;
|
|
123
|
+
mRunGeneration = sRunGeneration;
|
|
124
|
+
synchronized (sLock) {
|
|
125
|
+
sStateOwner = this;
|
|
126
|
+
clearCurrentState();
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
@Override
|
|
132
|
+
public void onDestroy() {
|
|
133
|
+
synchronized (sLifecycleLock) {
|
|
134
|
+
sPendingStarts.values().removeIf(request -> request.instance == this);
|
|
135
|
+
if (sInstance == this) {
|
|
136
|
+
sInstance = null;
|
|
137
|
+
if (sPendingStarts.isEmpty()) {
|
|
138
|
+
sStopRequest = null;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
clearOwnedState();
|
|
142
|
+
mForegroundActive = false;
|
|
143
|
+
synchronized (sLock) {
|
|
144
|
+
if (sStateOwner == this) {
|
|
145
|
+
sStateOwner = null;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
super.onDestroy();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Caller holds sLock. Ownership is checked by clearOwnedState before any terminal cleanup.
|
|
153
|
+
private static void clearCurrentState() {
|
|
154
|
+
mCurrentNotificationId = null;
|
|
155
|
+
mCurrentForegroundServiceType = -1;
|
|
156
|
+
mCurrentNotificationBundle = null;
|
|
157
|
+
mCurrentNotification = null;
|
|
158
|
+
mCurrentHashCode = 0;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private void clearOwnedState() {
|
|
162
|
+
synchronized (sLock) {
|
|
163
|
+
if (sStateOwner == this) {
|
|
164
|
+
clearCurrentState();
|
|
165
|
+
mRunnerToken = null;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
76
170
|
/**
|
|
77
171
|
* Submits the latest foreground service notification again if the given notification ID matches
|
|
78
172
|
* the active foreground service. On Android 14+, users can dismiss ongoing foreground service
|
|
@@ -130,55 +224,219 @@ public class ForegroundService extends Service {
|
|
|
130
224
|
intent.putExtra("notification", notification);
|
|
131
225
|
intent.putExtra("notificationBundle", notificationBundle);
|
|
132
226
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
227
|
+
synchronized (sStartDispatchLock) {
|
|
228
|
+
// A retry/redelivery from a previous process must not alias a new pending request.
|
|
229
|
+
String requestId = UUID.randomUUID().toString();
|
|
230
|
+
intent.putExtra(START_REQUEST_ID, requestId);
|
|
231
|
+
StartRequest request;
|
|
232
|
+
synchronized (sLifecycleLock) {
|
|
233
|
+
request = new StartRequest(sRunGeneration, sStopRequest);
|
|
234
|
+
sPendingStarts.put(requestId, request);
|
|
235
|
+
}
|
|
236
|
+
ComponentName component;
|
|
237
|
+
try {
|
|
238
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
239
|
+
component = context.startForegroundService(intent);
|
|
240
|
+
} else {
|
|
241
|
+
component = context.startService(intent);
|
|
242
|
+
}
|
|
243
|
+
} catch (RuntimeException e) {
|
|
244
|
+
synchronized (sLifecycleLock) {
|
|
245
|
+
// Only a definite platform rejection releases an undelivered obligation. An unrelated
|
|
246
|
+
// exception can be a lost Binder reply: UNKNOWN must remain held until actual delivery.
|
|
247
|
+
if (!request.accepted
|
|
248
|
+
&& (e instanceof SecurityException
|
|
249
|
+
|| (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
|
|
250
|
+
&& e instanceof ForegroundServiceStartNotAllowedException))) {
|
|
251
|
+
sPendingStarts.remove(requestId);
|
|
252
|
+
}
|
|
253
|
+
scheduleStop();
|
|
254
|
+
}
|
|
255
|
+
throw e;
|
|
256
|
+
}
|
|
257
|
+
synchronized (sLifecycleLock) {
|
|
258
|
+
if (component != null) {
|
|
259
|
+
acceptStart(request);
|
|
260
|
+
} else if (!request.accepted) {
|
|
261
|
+
sPendingStarts.remove(requestId);
|
|
262
|
+
}
|
|
263
|
+
scheduleStop();
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Caller holds sLifecycleLock. Delivery itself can prove acceptance before Context returns.
|
|
269
|
+
// A late reply never cancels a STOP ordered after this START was reserved/delivered.
|
|
270
|
+
private static void acceptStart(StartRequest request) {
|
|
271
|
+
if (!request.accepted) {
|
|
272
|
+
request.accepted = true;
|
|
273
|
+
if (sStopRequest == request.precedingStop) {
|
|
274
|
+
sStopRequest = null;
|
|
275
|
+
}
|
|
138
276
|
}
|
|
139
277
|
}
|
|
140
278
|
|
|
141
279
|
static void stop() {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
280
|
+
synchronized (sLifecycleLock) {
|
|
281
|
+
if (sInstance == null && sPendingStarts.isEmpty()) {
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
sStopRequest = new Object();
|
|
285
|
+
sRunGeneration++;
|
|
286
|
+
scheduleStop();
|
|
146
287
|
}
|
|
288
|
+
}
|
|
147
289
|
|
|
148
|
-
|
|
149
|
-
|
|
290
|
+
// Caller holds sLifecycleLock. No START/STOP intent is dispatched here, so a stale reference
|
|
291
|
+
// cannot cold-start a service. onCreate alone is UNKNOWN: wait for a delivered command.
|
|
292
|
+
private static void scheduleStop() {
|
|
293
|
+
ForegroundService instance = sInstance;
|
|
294
|
+
Object request = sStopRequest;
|
|
295
|
+
if (instance != null && request != null) {
|
|
296
|
+
new Handler(Looper.getMainLooper()).post(() -> instance.stopIfRequested(request));
|
|
297
|
+
}
|
|
298
|
+
}
|
|
150
299
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
300
|
+
private void stopIfRequested(Object request) {
|
|
301
|
+
synchronized (sLifecycleLock) {
|
|
302
|
+
if (sInstance != this
|
|
303
|
+
|| sStopRequest != request
|
|
304
|
+
|| !sPendingStarts.isEmpty()
|
|
305
|
+
|| mLastStartId == 0
|
|
306
|
+
|| mStopInitiated
|
|
307
|
+
|| mStartCommandFailed) {
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
// The framework token and startId also protect against a start we have not yet received.
|
|
311
|
+
if (stopSelfResult(mLastStartId)) {
|
|
312
|
+
mStopInitiated = true;
|
|
313
|
+
sStopRequest = null;
|
|
314
|
+
mForegroundActive = false;
|
|
315
|
+
clearOwnedState();
|
|
316
|
+
}
|
|
164
317
|
}
|
|
165
318
|
}
|
|
166
319
|
|
|
167
|
-
@SuppressLint({"ForegroundServiceType", "MissingPermission"})
|
|
168
320
|
@Override
|
|
169
321
|
public int onStartCommand(Intent intent, int flags, int startId) {
|
|
322
|
+
synchronized (sLifecycleLock) {
|
|
323
|
+
mLastStartId = startId;
|
|
324
|
+
boolean terminal =
|
|
325
|
+
intent == null || STOP_FOREGROUND_SERVICE_ACTION.equals(intent.getAction());
|
|
326
|
+
if (terminal) {
|
|
327
|
+
sRunGeneration++;
|
|
328
|
+
}
|
|
329
|
+
if (terminal && !sPendingStarts.isEmpty()) {
|
|
330
|
+
// The earlier accepted START must promote with its real notification/type first.
|
|
331
|
+
sStopRequest = new Object();
|
|
332
|
+
return START_STICKY_COMPATIBILITY;
|
|
333
|
+
}
|
|
334
|
+
if (!terminal) {
|
|
335
|
+
mStopInitiated = false;
|
|
336
|
+
}
|
|
337
|
+
String requestId = intent == null ? null : intent.getStringExtra(START_REQUEST_ID);
|
|
338
|
+
StartRequest request = sPendingStarts.get(requestId);
|
|
339
|
+
boolean superseded = false;
|
|
340
|
+
if (!terminal) {
|
|
341
|
+
long generation = request == null ? sRunGeneration : request.generation;
|
|
342
|
+
if (request != null) {
|
|
343
|
+
request.instance = this;
|
|
344
|
+
acceptStart(request);
|
|
345
|
+
}
|
|
346
|
+
superseded = mRunFirstStartId != 0 && generation < mRunGeneration;
|
|
347
|
+
if (!superseded && mRunGeneration != generation) {
|
|
348
|
+
// Reserve the new session without retiring the previous submitted state or runner.
|
|
349
|
+
// A failed promotion must preserve them, while older deliveries remain fenced.
|
|
350
|
+
mRunGeneration = generation;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
mStartCommandSatisfied = mForegroundActive;
|
|
354
|
+
try {
|
|
355
|
+
int result;
|
|
356
|
+
if (superseded) {
|
|
357
|
+
// A lost-reply START can arrive after a newer run. Preserve the new cache/runner. If
|
|
358
|
+
// that run already ended, still perform the old request's real promotion before teardown.
|
|
359
|
+
result = mForegroundActive ? START_NOT_STICKY : fulfillSupersededStart(intent);
|
|
360
|
+
} else {
|
|
361
|
+
result = handleStartCommand(intent, flags, startId);
|
|
362
|
+
}
|
|
363
|
+
mStartCommandFailed = false;
|
|
364
|
+
if (mStartCommandSatisfied) {
|
|
365
|
+
settleDeliveredStarts();
|
|
366
|
+
}
|
|
367
|
+
if (superseded
|
|
368
|
+
&& mStartCommandSatisfied
|
|
369
|
+
&& !mForegroundActive
|
|
370
|
+
&& mRunnerToken == null
|
|
371
|
+
&& sPendingStarts.isEmpty()
|
|
372
|
+
&& sStopRequest == null) {
|
|
373
|
+
sStopRequest = new Object();
|
|
374
|
+
}
|
|
375
|
+
if (!terminal) {
|
|
376
|
+
scheduleStop();
|
|
377
|
+
}
|
|
378
|
+
return result;
|
|
379
|
+
} catch (RuntimeException e) {
|
|
380
|
+
mStartCommandFailed = !mStartCommandSatisfied;
|
|
381
|
+
if (mStartCommandSatisfied) {
|
|
382
|
+
settleDeliveredStarts();
|
|
383
|
+
}
|
|
384
|
+
throw e;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// A real promotion (or an already foreground instance) satisfies the physical instance's
|
|
390
|
+
// delivered obligations, including a prior ambiguous delivery. Undelivered STARTs remain held.
|
|
391
|
+
// This does not turn a posting/type exception into success: onStartCommand still rethrows it.
|
|
392
|
+
private void settleDeliveredStarts() {
|
|
393
|
+
sPendingStarts.values().removeIf(request -> request.instance == this);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
@SuppressLint({"ForegroundServiceType", "MissingPermission"})
|
|
397
|
+
private int fulfillSupersededStart(Intent intent) {
|
|
398
|
+
Bundle extras = intent.getExtras();
|
|
399
|
+
if (extras == null) {
|
|
400
|
+
return START_NOT_STICKY;
|
|
401
|
+
}
|
|
402
|
+
Notification notification =
|
|
403
|
+
ParcelableCompatReader.getParcelable(extras, "notification", Notification.class);
|
|
404
|
+
Bundle bundle = extras.getBundle("notificationBundle");
|
|
405
|
+
if (notification == null || bundle == null) {
|
|
406
|
+
return START_NOT_STICKY;
|
|
407
|
+
}
|
|
408
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
409
|
+
int type = NotificationModel.fromBundle(bundle).getAndroid().getForegroundServiceType();
|
|
410
|
+
if (type == ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST) {
|
|
411
|
+
type = resolveManifestServiceType();
|
|
412
|
+
}
|
|
413
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE
|
|
414
|
+
&& type == ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE) {
|
|
415
|
+
ensureStartForegroundContractSatisfied(false);
|
|
416
|
+
return START_NOT_STICKY;
|
|
417
|
+
}
|
|
418
|
+
startForeground(extras.getInt("hashCode"), notification, type);
|
|
419
|
+
} else {
|
|
420
|
+
startForeground(extras.getInt("hashCode"), notification);
|
|
421
|
+
}
|
|
422
|
+
mStartForegroundCalled = true;
|
|
423
|
+
mForegroundActive = true;
|
|
424
|
+
mStartCommandSatisfied = true;
|
|
425
|
+
stopForegroundCompat();
|
|
426
|
+
return START_NOT_STICKY;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
@SuppressLint({"ForegroundServiceType", "MissingPermission"})
|
|
430
|
+
private int handleStartCommand(Intent intent, int flags, int startId) {
|
|
170
431
|
Trace.beginSection("notifee:ForegroundService.onStartCommand");
|
|
171
432
|
try {
|
|
172
433
|
// Check if action is to stop the foreground service
|
|
173
434
|
if (intent == null || STOP_FOREGROUND_SERVICE_ACTION.equals(intent.getAction())) {
|
|
174
|
-
ensureStartForegroundContractSatisfied();
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
mCurrentNotificationBundle = null;
|
|
180
|
-
mCurrentNotification = null;
|
|
181
|
-
mCurrentHashCode = 0;
|
|
435
|
+
ensureStartForegroundContractSatisfied(true);
|
|
436
|
+
if (stopSelfResult(startId)) {
|
|
437
|
+
mStopInitiated = true;
|
|
438
|
+
mForegroundActive = false;
|
|
439
|
+
clearOwnedState();
|
|
182
440
|
}
|
|
183
441
|
return Service.START_STICKY_COMPATIBILITY;
|
|
184
442
|
}
|
|
@@ -200,7 +458,7 @@ public class ForegroundService extends Service {
|
|
|
200
458
|
boolean noneEarlyReturn = false;
|
|
201
459
|
|
|
202
460
|
synchronized (sLock) {
|
|
203
|
-
if (mCurrentNotificationId == null) {
|
|
461
|
+
if (mCurrentNotificationId == null || mStateGeneration != mRunGeneration) {
|
|
204
462
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
205
463
|
int foregroundServiceType =
|
|
206
464
|
notificationModel.getAndroid().getForegroundServiceType();
|
|
@@ -224,6 +482,8 @@ public class ForegroundService extends Service {
|
|
|
224
482
|
Trace.endSection();
|
|
225
483
|
}
|
|
226
484
|
mStartForegroundCalled = true;
|
|
485
|
+
mForegroundActive = true;
|
|
486
|
+
mStartCommandSatisfied = true;
|
|
227
487
|
mCurrentForegroundServiceType = foregroundServiceType;
|
|
228
488
|
}
|
|
229
489
|
} else {
|
|
@@ -234,6 +494,8 @@ public class ForegroundService extends Service {
|
|
|
234
494
|
Trace.endSection();
|
|
235
495
|
}
|
|
236
496
|
mStartForegroundCalled = true;
|
|
497
|
+
mForegroundActive = true;
|
|
498
|
+
mStartCommandSatisfied = true;
|
|
237
499
|
}
|
|
238
500
|
|
|
239
501
|
if (!noneEarlyReturn) {
|
|
@@ -241,16 +503,29 @@ public class ForegroundService extends Service {
|
|
|
241
503
|
mCurrentNotificationBundle = bundle;
|
|
242
504
|
mCurrentNotification = notification;
|
|
243
505
|
mCurrentHashCode = hashCode;
|
|
506
|
+
mStateGeneration = mRunGeneration;
|
|
507
|
+
mRunFirstStartId = mLastStartId;
|
|
508
|
+
final Object runnerToken = new Object();
|
|
509
|
+
final long runnerGeneration = mRunGeneration;
|
|
510
|
+
mRunnerToken = runnerToken;
|
|
244
511
|
// On headless task complete
|
|
245
512
|
final MethodCallResult<Void> methodCallResult =
|
|
246
513
|
(e, aVoid) -> {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
514
|
+
Runnable complete =
|
|
515
|
+
() -> {
|
|
516
|
+
synchronized (sLifecycleLock) {
|
|
517
|
+
if (sInstance == this
|
|
518
|
+
&& mRunnerToken == runnerToken
|
|
519
|
+
&& sRunGeneration == runnerGeneration) {
|
|
520
|
+
stopForegroundCompat();
|
|
521
|
+
clearOwnedState();
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
};
|
|
525
|
+
if (Looper.myLooper() == Looper.getMainLooper()) {
|
|
526
|
+
complete.run();
|
|
527
|
+
} else {
|
|
528
|
+
new Handler(Looper.getMainLooper()).post(complete);
|
|
254
529
|
}
|
|
255
530
|
};
|
|
256
531
|
|
|
@@ -292,6 +567,8 @@ public class ForegroundService extends Service {
|
|
|
292
567
|
Trace.endSection();
|
|
293
568
|
}
|
|
294
569
|
mStartForegroundCalled = true;
|
|
570
|
+
mForegroundActive = true;
|
|
571
|
+
mStartCommandSatisfied = true;
|
|
295
572
|
} else {
|
|
296
573
|
NotificationManagerCompat.from(ContextHolder.getApplicationContext())
|
|
297
574
|
.notify(mCurrentHashCode, notification);
|
|
@@ -312,7 +589,7 @@ public class ForegroundService extends Service {
|
|
|
312
589
|
// Handle NONE early return outside the lock — the helper performs Binder IPC
|
|
313
590
|
// (PackageManager query + startForeground) that must not hold sLock.
|
|
314
591
|
if (noneEarlyReturn) {
|
|
315
|
-
ensureStartForegroundContractSatisfied();
|
|
592
|
+
ensureStartForegroundContractSatisfied(false);
|
|
316
593
|
return START_NOT_STICKY;
|
|
317
594
|
}
|
|
318
595
|
|
|
@@ -407,6 +684,7 @@ public class ForegroundService extends Service {
|
|
|
407
684
|
} else {
|
|
408
685
|
stopForeground(true);
|
|
409
686
|
}
|
|
687
|
+
mForegroundActive = false;
|
|
410
688
|
}
|
|
411
689
|
|
|
412
690
|
/**
|
|
@@ -420,7 +698,7 @@ public class ForegroundService extends Service {
|
|
|
420
698
|
* on API 29-33 and to {@code FOREGROUND_SERVICE_TYPE_NONE} on API 34+. This method is an instance
|
|
421
699
|
* method that uses {@code this} as context (always valid inside a running Service) and returns
|
|
422
700
|
* the raw declared value without any API-level mapping, which is what the proactive manifest
|
|
423
|
-
* check in {@link #ensureStartForegroundContractSatisfied()} needs.
|
|
701
|
+
* check in {@link #ensureStartForegroundContractSatisfied(boolean)} needs.
|
|
424
702
|
*
|
|
425
703
|
* @return the raw {@code foregroundServiceType} bitmask from the manifest, or 0 if the service is
|
|
426
704
|
* not declared or has no explicit type
|
|
@@ -485,10 +763,12 @@ public class ForegroundService extends Service {
|
|
|
485
763
|
* @throws RuntimeException if the manifest is missing required {@code foregroundServiceType}
|
|
486
764
|
* declarations on API 34+, or if the defensive {@code startForeground()} call fails for a
|
|
487
765
|
* 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.
|
|
766
|
+
* because API 34+ can deny while-in-use types when the app is backgrounded. A terminal
|
|
767
|
+
* STOP/null also logs and tolerates the API 31+ promotion denial specifically; that policy
|
|
768
|
+
* does not apply to NONE or to an earlier unfulfilled accepted START.
|
|
489
769
|
*/
|
|
490
770
|
@SuppressLint({"ForegroundServiceType", "MissingPermission"})
|
|
491
|
-
private void ensureStartForegroundContractSatisfied() {
|
|
771
|
+
private void ensureStartForegroundContractSatisfied(boolean terminalStop) {
|
|
492
772
|
if (mStartForegroundCalled) {
|
|
493
773
|
return;
|
|
494
774
|
}
|
|
@@ -516,36 +796,51 @@ public class ForegroundService extends Service {
|
|
|
516
796
|
}
|
|
517
797
|
}
|
|
518
798
|
|
|
799
|
+
ensureDefensiveChannel();
|
|
800
|
+
Notification placeholder =
|
|
801
|
+
new NotificationCompat.Builder(this, DEFENSIVE_CHANNEL_ID)
|
|
802
|
+
.setSmallIcon(android.R.drawable.ic_dialog_info)
|
|
803
|
+
.build();
|
|
804
|
+
// Resolve the safe declared type before entering the promotion-only exception boundary.
|
|
805
|
+
int defensiveTypes =
|
|
806
|
+
Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE
|
|
807
|
+
? selectDefensiveForegroundServiceType(declaredTypes)
|
|
808
|
+
: 0;
|
|
519
809
|
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
810
|
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
811
|
startForeground(DEFENSIVE_NOTIFICATION_ID, placeholder, defensiveTypes);
|
|
531
812
|
} else {
|
|
532
813
|
startForeground(DEFENSIVE_NOTIFICATION_ID, placeholder);
|
|
533
814
|
}
|
|
534
|
-
mStartForegroundCalled = true;
|
|
535
|
-
stopForegroundCompat();
|
|
536
815
|
} catch (SecurityException e) {
|
|
537
816
|
Logger.w(
|
|
538
817
|
TAG,
|
|
539
818
|
"Defensive startForeground() was denied by Android while stopping the service; "
|
|
540
819
|
+ "continuing with stopSelf() to avoid a fatal app crash.",
|
|
541
820
|
e);
|
|
821
|
+
return;
|
|
542
822
|
} catch (Exception e) {
|
|
823
|
+
// NONE and unrelated IllegalStateException/type failures remain observable.
|
|
824
|
+
if (terminalStop
|
|
825
|
+
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
|
|
826
|
+
&& e instanceof ForegroundServiceStartNotAllowedException) {
|
|
827
|
+
Logger.w(
|
|
828
|
+
TAG,
|
|
829
|
+
"Defensive startForeground() was not allowed by Android while stopping; "
|
|
830
|
+
+ "continuing with stopSelf().",
|
|
831
|
+
e);
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
543
834
|
String msg =
|
|
544
835
|
"react-native-notify-kit: defensive startForeground() failed. "
|
|
545
836
|
+ "This indicates an inconsistent service state and the process cannot recover.";
|
|
546
837
|
Logger.e(TAG, msg, e);
|
|
547
838
|
throw new RuntimeException(msg, e);
|
|
548
839
|
}
|
|
840
|
+
mStartForegroundCalled = true;
|
|
841
|
+
mForegroundActive = true;
|
|
842
|
+
mStartCommandSatisfied = true;
|
|
843
|
+
stopForegroundCompat();
|
|
549
844
|
}
|
|
550
845
|
|
|
551
846
|
private void handleTimeout(int startId, int fgsType) {
|
|
@@ -558,18 +853,20 @@ public class ForegroundService extends Service {
|
|
|
558
853
|
+ "). Stopping service.");
|
|
559
854
|
|
|
560
855
|
Bundle notifBundle;
|
|
561
|
-
synchronized (
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
856
|
+
synchronized (sLifecycleLock) {
|
|
857
|
+
// shortService can report the first START of this run after same-ID updates. Reject only
|
|
858
|
+
// callbacks from an earlier run, not earlier STARTs within the current run.
|
|
859
|
+
if (sInstance != this || (mRunFirstStartId != 0 && startId < mRunFirstStartId)) {
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
synchronized (sLock) {
|
|
863
|
+
notifBundle = sStateOwner == this ? mCurrentNotificationBundle : null;
|
|
864
|
+
clearOwnedState();
|
|
865
|
+
}
|
|
866
|
+
stopForegroundCompat();
|
|
867
|
+
stopSelf(startId);
|
|
568
868
|
}
|
|
569
869
|
|
|
570
|
-
stopForegroundCompat();
|
|
571
|
-
stopSelf(startId);
|
|
572
|
-
|
|
573
870
|
if (notifBundle != null) {
|
|
574
871
|
NotificationModel model = NotificationModel.fromBundle(notifBundle);
|
|
575
872
|
Bundle extras = new Bundle();
|