react-native-notify-kit 10.7.1 → 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 +408 -103
- package/android/src/test/java/app/notifee/core/ForegroundServiceTest.java +1761 -44
- 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
|
|
@@ -63,6 +105,8 @@ public class ForegroundService extends Service {
|
|
|
63
105
|
*/
|
|
64
106
|
private volatile boolean mStartForegroundCalled = false;
|
|
65
107
|
|
|
108
|
+
// While active, mCurrent* tracks the latest state submitted by a platform call that returned
|
|
109
|
+
// normally. Android does not acknowledge that the notification was displayed.
|
|
66
110
|
public static String mCurrentNotificationId = null;
|
|
67
111
|
|
|
68
112
|
public static int mCurrentForegroundServiceType = -1;
|
|
@@ -71,13 +115,64 @@ public class ForegroundService extends Service {
|
|
|
71
115
|
private static Notification mCurrentNotification = null;
|
|
72
116
|
private static int mCurrentHashCode = 0;
|
|
73
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
|
+
|
|
74
170
|
/**
|
|
75
|
-
*
|
|
76
|
-
* foreground service. On Android 14+, users can dismiss ongoing foreground service
|
|
77
|
-
* for most service types
|
|
78
|
-
* running service.
|
|
171
|
+
* Submits the latest foreground service notification again if the given notification ID matches
|
|
172
|
+
* the active foreground service. On Android 14+, users can dismiss ongoing foreground service
|
|
173
|
+
* notifications for most service types.
|
|
79
174
|
*
|
|
80
|
-
* @return true if
|
|
175
|
+
* @return true if a re-post was submitted without a synchronous exception, false otherwise
|
|
81
176
|
*/
|
|
82
177
|
@SuppressLint("MissingPermission")
|
|
83
178
|
static boolean repostIfActive(String notificationId) {
|
|
@@ -87,7 +182,7 @@ public class ForegroundService extends Service {
|
|
|
87
182
|
|| mCurrentNotification == null) {
|
|
88
183
|
return false;
|
|
89
184
|
}
|
|
90
|
-
Logger.w(TAG, "
|
|
185
|
+
Logger.w(TAG, "Submitting dismissed foreground service notification for re-post");
|
|
91
186
|
NotificationManagerCompat.from(ContextHolder.getApplicationContext())
|
|
92
187
|
.notify(mCurrentHashCode, mCurrentNotification);
|
|
93
188
|
return true;
|
|
@@ -129,55 +224,219 @@ public class ForegroundService extends Service {
|
|
|
129
224
|
intent.putExtra("notification", notification);
|
|
130
225
|
intent.putExtra("notificationBundle", notificationBundle);
|
|
131
226
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
+
}
|
|
137
276
|
}
|
|
138
277
|
}
|
|
139
278
|
|
|
140
279
|
static void stop() {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
280
|
+
synchronized (sLifecycleLock) {
|
|
281
|
+
if (sInstance == null && sPendingStarts.isEmpty()) {
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
sStopRequest = new Object();
|
|
285
|
+
sRunGeneration++;
|
|
286
|
+
scheduleStop();
|
|
145
287
|
}
|
|
288
|
+
}
|
|
146
289
|
|
|
147
|
-
|
|
148
|
-
|
|
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
|
+
}
|
|
149
299
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
+
}
|
|
163
317
|
}
|
|
164
318
|
}
|
|
165
319
|
|
|
166
|
-
@SuppressLint({"ForegroundServiceType", "MissingPermission"})
|
|
167
320
|
@Override
|
|
168
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) {
|
|
169
431
|
Trace.beginSection("notifee:ForegroundService.onStartCommand");
|
|
170
432
|
try {
|
|
171
433
|
// Check if action is to stop the foreground service
|
|
172
434
|
if (intent == null || STOP_FOREGROUND_SERVICE_ACTION.equals(intent.getAction())) {
|
|
173
|
-
ensureStartForegroundContractSatisfied();
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
mCurrentNotificationBundle = null;
|
|
179
|
-
mCurrentNotification = null;
|
|
180
|
-
mCurrentHashCode = 0;
|
|
435
|
+
ensureStartForegroundContractSatisfied(true);
|
|
436
|
+
if (stopSelfResult(startId)) {
|
|
437
|
+
mStopInitiated = true;
|
|
438
|
+
mForegroundActive = false;
|
|
439
|
+
clearOwnedState();
|
|
181
440
|
}
|
|
182
441
|
return Service.START_STICKY_COMPATIBILITY;
|
|
183
442
|
}
|
|
@@ -193,17 +452,13 @@ public class ForegroundService extends Service {
|
|
|
193
452
|
|
|
194
453
|
if (notification != null && bundle != null) {
|
|
195
454
|
NotificationModel notificationModel = NotificationModel.fromBundle(bundle);
|
|
455
|
+
String notificationId = notificationModel.getId();
|
|
196
456
|
|
|
197
457
|
Object pendingEvent = null;
|
|
198
458
|
boolean noneEarlyReturn = false;
|
|
199
459
|
|
|
200
460
|
synchronized (sLock) {
|
|
201
|
-
if (mCurrentNotificationId == null) {
|
|
202
|
-
mCurrentNotificationId = notificationModel.getId();
|
|
203
|
-
mCurrentNotificationBundle = bundle;
|
|
204
|
-
mCurrentNotification = notification;
|
|
205
|
-
mCurrentHashCode = hashCode;
|
|
206
|
-
|
|
461
|
+
if (mCurrentNotificationId == null || mStateGeneration != mRunGeneration) {
|
|
207
462
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
208
463
|
int foregroundServiceType =
|
|
209
464
|
notificationModel.getAndroid().getForegroundServiceType();
|
|
@@ -216,12 +471,6 @@ public class ForegroundService extends Service {
|
|
|
216
471
|
TAG,
|
|
217
472
|
"Resolved foreground service type is NONE on API 34+; aborting"
|
|
218
473
|
+ " startForeground to avoid InvalidForegroundServiceTypeException.");
|
|
219
|
-
// Reset stale state while still holding the lock.
|
|
220
|
-
mCurrentNotificationId = null;
|
|
221
|
-
mCurrentForegroundServiceType = -1;
|
|
222
|
-
mCurrentNotificationBundle = null;
|
|
223
|
-
mCurrentNotification = null;
|
|
224
|
-
mCurrentHashCode = 0;
|
|
225
474
|
// Set flag to call the helper AFTER releasing sLock — it performs
|
|
226
475
|
// Binder IPC (PackageManager + startForeground) and must not hold sLock.
|
|
227
476
|
noneEarlyReturn = true;
|
|
@@ -233,6 +482,8 @@ public class ForegroundService extends Service {
|
|
|
233
482
|
Trace.endSection();
|
|
234
483
|
}
|
|
235
484
|
mStartForegroundCalled = true;
|
|
485
|
+
mForegroundActive = true;
|
|
486
|
+
mStartCommandSatisfied = true;
|
|
236
487
|
mCurrentForegroundServiceType = foregroundServiceType;
|
|
237
488
|
}
|
|
238
489
|
} else {
|
|
@@ -243,56 +494,90 @@ public class ForegroundService extends Service {
|
|
|
243
494
|
Trace.endSection();
|
|
244
495
|
}
|
|
245
496
|
mStartForegroundCalled = true;
|
|
497
|
+
mForegroundActive = true;
|
|
498
|
+
mStartCommandSatisfied = true;
|
|
246
499
|
}
|
|
247
500
|
|
|
248
501
|
if (!noneEarlyReturn) {
|
|
502
|
+
mCurrentNotificationId = notificationId;
|
|
503
|
+
mCurrentNotificationBundle = bundle;
|
|
504
|
+
mCurrentNotification = notification;
|
|
505
|
+
mCurrentHashCode = hashCode;
|
|
506
|
+
mStateGeneration = mRunGeneration;
|
|
507
|
+
mRunFirstStartId = mLastStartId;
|
|
508
|
+
final Object runnerToken = new Object();
|
|
509
|
+
final long runnerGeneration = mRunGeneration;
|
|
510
|
+
mRunnerToken = runnerToken;
|
|
249
511
|
// On headless task complete
|
|
250
512
|
final MethodCallResult<Void> methodCallResult =
|
|
251
513
|
(e, aVoid) -> {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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);
|
|
259
529
|
}
|
|
260
530
|
};
|
|
261
531
|
|
|
262
532
|
pendingEvent = new ForegroundServiceEvent(notificationModel, methodCallResult);
|
|
263
533
|
}
|
|
264
534
|
} else {
|
|
265
|
-
if (mCurrentNotificationId.equals(
|
|
266
|
-
|
|
267
|
-
//
|
|
535
|
+
if (mCurrentNotificationId.equals(notificationId)) {
|
|
536
|
+
// Before API 31, updating the service's registered notification requires
|
|
537
|
+
// startForeground(). API 31+ processes a same-ID notify() against the foreground
|
|
538
|
+
// service record. Keep that path for an unchanged type: repeating
|
|
539
|
+
// startForeground() can extend a shortService timeout on API 34+.
|
|
540
|
+
boolean shouldStartForeground = Build.VERSION.SDK_INT < Build.VERSION_CODES.S;
|
|
541
|
+
int foregroundServiceType = mCurrentForegroundServiceType;
|
|
268
542
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
269
|
-
int
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
foregroundServiceType = resolveManifestServiceType();
|
|
543
|
+
int requestedType = notificationModel.getAndroid().getForegroundServiceType();
|
|
544
|
+
if (requestedType == ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST) {
|
|
545
|
+
requestedType = resolveManifestServiceType();
|
|
273
546
|
}
|
|
274
547
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE
|
|
275
|
-
&&
|
|
548
|
+
&& requestedType == ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE) {
|
|
276
549
|
Logger.e(
|
|
277
550
|
TAG,
|
|
278
551
|
"Resolved foreground service type is NONE on API 34+; skipping type"
|
|
279
552
|
+ " change.");
|
|
280
|
-
} else if (
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
startForeground(hashCode, notification, foregroundServiceType);
|
|
284
|
-
} finally {
|
|
285
|
-
Trace.endSection();
|
|
286
|
-
}
|
|
287
|
-
mStartForegroundCalled = true;
|
|
288
|
-
mCurrentForegroundServiceType = foregroundServiceType;
|
|
289
|
-
shouldPostNotificationAgain = false;
|
|
553
|
+
} else if (requestedType != mCurrentForegroundServiceType) {
|
|
554
|
+
foregroundServiceType = requestedType;
|
|
555
|
+
shouldStartForeground = true;
|
|
290
556
|
}
|
|
291
557
|
}
|
|
292
|
-
if (
|
|
558
|
+
if (shouldStartForeground) {
|
|
559
|
+
Trace.beginSection("notifee:startForeground");
|
|
560
|
+
try {
|
|
561
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
562
|
+
startForeground(mCurrentHashCode, notification, foregroundServiceType);
|
|
563
|
+
} else {
|
|
564
|
+
startForeground(mCurrentHashCode, notification);
|
|
565
|
+
}
|
|
566
|
+
} finally {
|
|
567
|
+
Trace.endSection();
|
|
568
|
+
}
|
|
569
|
+
mStartForegroundCalled = true;
|
|
570
|
+
mForegroundActive = true;
|
|
571
|
+
mStartCommandSatisfied = true;
|
|
572
|
+
} else {
|
|
293
573
|
NotificationManagerCompat.from(ContextHolder.getApplicationContext())
|
|
294
|
-
.notify(
|
|
574
|
+
.notify(mCurrentHashCode, notification);
|
|
295
575
|
}
|
|
576
|
+
// The platform call returned normally, so cache the latest submitted state for
|
|
577
|
+
// re-post and timeout. A normal return does not acknowledge display.
|
|
578
|
+
mCurrentForegroundServiceType = foregroundServiceType;
|
|
579
|
+
mCurrentNotification = notification;
|
|
580
|
+
mCurrentNotificationBundle = bundle;
|
|
296
581
|
} else {
|
|
297
582
|
pendingEvent =
|
|
298
583
|
new NotificationEvent(
|
|
@@ -304,7 +589,7 @@ public class ForegroundService extends Service {
|
|
|
304
589
|
// Handle NONE early return outside the lock — the helper performs Binder IPC
|
|
305
590
|
// (PackageManager query + startForeground) that must not hold sLock.
|
|
306
591
|
if (noneEarlyReturn) {
|
|
307
|
-
ensureStartForegroundContractSatisfied();
|
|
592
|
+
ensureStartForegroundContractSatisfied(false);
|
|
308
593
|
return START_NOT_STICKY;
|
|
309
594
|
}
|
|
310
595
|
|
|
@@ -399,6 +684,7 @@ public class ForegroundService extends Service {
|
|
|
399
684
|
} else {
|
|
400
685
|
stopForeground(true);
|
|
401
686
|
}
|
|
687
|
+
mForegroundActive = false;
|
|
402
688
|
}
|
|
403
689
|
|
|
404
690
|
/**
|
|
@@ -412,7 +698,7 @@ public class ForegroundService extends Service {
|
|
|
412
698
|
* on API 29-33 and to {@code FOREGROUND_SERVICE_TYPE_NONE} on API 34+. This method is an instance
|
|
413
699
|
* method that uses {@code this} as context (always valid inside a running Service) and returns
|
|
414
700
|
* the raw declared value without any API-level mapping, which is what the proactive manifest
|
|
415
|
-
* check in {@link #ensureStartForegroundContractSatisfied()} needs.
|
|
701
|
+
* check in {@link #ensureStartForegroundContractSatisfied(boolean)} needs.
|
|
416
702
|
*
|
|
417
703
|
* @return the raw {@code foregroundServiceType} bitmask from the manifest, or 0 if the service is
|
|
418
704
|
* not declared or has no explicit type
|
|
@@ -477,10 +763,12 @@ public class ForegroundService extends Service {
|
|
|
477
763
|
* @throws RuntimeException if the manifest is missing required {@code foregroundServiceType}
|
|
478
764
|
* declarations on API 34+, or if the defensive {@code startForeground()} call fails for a
|
|
479
765
|
* non-platform-permission reason. {@link SecurityException} is handled as a best-effort stop
|
|
480
|
-
* 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.
|
|
481
769
|
*/
|
|
482
770
|
@SuppressLint({"ForegroundServiceType", "MissingPermission"})
|
|
483
|
-
private void ensureStartForegroundContractSatisfied() {
|
|
771
|
+
private void ensureStartForegroundContractSatisfied(boolean terminalStop) {
|
|
484
772
|
if (mStartForegroundCalled) {
|
|
485
773
|
return;
|
|
486
774
|
}
|
|
@@ -508,36 +796,51 @@ public class ForegroundService extends Service {
|
|
|
508
796
|
}
|
|
509
797
|
}
|
|
510
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;
|
|
511
809
|
try {
|
|
512
|
-
ensureDefensiveChannel();
|
|
513
|
-
Notification placeholder =
|
|
514
|
-
new NotificationCompat.Builder(this, DEFENSIVE_CHANNEL_ID)
|
|
515
|
-
.setSmallIcon(android.R.drawable.ic_dialog_info)
|
|
516
|
-
.build();
|
|
517
810
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
|
518
|
-
// The STOP/null defensive placeholder does not represent the real workload. Prefer a
|
|
519
|
-
// declared type without while-in-use runtime restrictions before falling back to the
|
|
520
|
-
// manifest-declared mask.
|
|
521
|
-
int defensiveTypes = selectDefensiveForegroundServiceType(declaredTypes);
|
|
522
811
|
startForeground(DEFENSIVE_NOTIFICATION_ID, placeholder, defensiveTypes);
|
|
523
812
|
} else {
|
|
524
813
|
startForeground(DEFENSIVE_NOTIFICATION_ID, placeholder);
|
|
525
814
|
}
|
|
526
|
-
mStartForegroundCalled = true;
|
|
527
|
-
stopForegroundCompat();
|
|
528
815
|
} catch (SecurityException e) {
|
|
529
816
|
Logger.w(
|
|
530
817
|
TAG,
|
|
531
818
|
"Defensive startForeground() was denied by Android while stopping the service; "
|
|
532
819
|
+ "continuing with stopSelf() to avoid a fatal app crash.",
|
|
533
820
|
e);
|
|
821
|
+
return;
|
|
534
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
|
+
}
|
|
535
834
|
String msg =
|
|
536
835
|
"react-native-notify-kit: defensive startForeground() failed. "
|
|
537
836
|
+ "This indicates an inconsistent service state and the process cannot recover.";
|
|
538
837
|
Logger.e(TAG, msg, e);
|
|
539
838
|
throw new RuntimeException(msg, e);
|
|
540
839
|
}
|
|
840
|
+
mStartForegroundCalled = true;
|
|
841
|
+
mForegroundActive = true;
|
|
842
|
+
mStartCommandSatisfied = true;
|
|
843
|
+
stopForegroundCompat();
|
|
541
844
|
}
|
|
542
845
|
|
|
543
846
|
private void handleTimeout(int startId, int fgsType) {
|
|
@@ -550,18 +853,20 @@ public class ForegroundService extends Service {
|
|
|
550
853
|
+ "). Stopping service.");
|
|
551
854
|
|
|
552
855
|
Bundle notifBundle;
|
|
553
|
-
synchronized (
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
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);
|
|
560
868
|
}
|
|
561
869
|
|
|
562
|
-
stopForegroundCompat();
|
|
563
|
-
stopSelf(startId);
|
|
564
|
-
|
|
565
870
|
if (notifBundle != null) {
|
|
566
871
|
NotificationModel model = NotificationModel.fromBundle(notifBundle);
|
|
567
872
|
Bundle extras = new Bundle();
|