react-native-notify-kit 10.8.1 → 10.9.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 +35 -18
- package/android/build.gradle +50 -4
- package/android/src/main/AndroidManifest.xml +15 -0
- package/android/src/main/java/app/notifee/core/EventBus.java +4 -0
- package/android/src/main/java/app/notifee/core/ForegroundService.java +395 -5
- package/android/src/main/java/app/notifee/core/ForegroundServiceRunLimitReceiver.java +15 -0
- package/android/src/main/java/app/notifee/core/ForegroundServiceRunLimitScheduler.java +461 -0
- package/android/src/main/java/app/notifee/core/FullScreenNotificationActivity.java +76 -0
- package/android/src/main/java/app/notifee/core/InitialNotificationLedger.java +262 -0
- package/android/src/main/java/app/notifee/core/Notifee.java +142 -20
- package/android/src/main/java/app/notifee/core/NotificationManager.java +50 -6
- package/android/src/main/java/app/notifee/core/NotificationPendingIntent.java +14 -0
- package/android/src/main/java/app/notifee/core/NotificationReceiverHandler.java +11 -2
- package/android/src/main/java/app/notifee/core/event/InitialNotificationEvent.java +15 -0
- package/android/src/main/java/app/notifee/core/event/NotificationEvent.java +6 -0
- package/android/src/main/java/app/notifee/core/model/NotificationAndroidModel.java +29 -0
- package/android/src/main/kotlin/io/invertase/notifee/NotifeeEventSubscriber.kt +3 -0
- package/android/src/test/java/app/notifee/core/ForegroundServiceRunLimitTest.java +2357 -0
- package/android/src/test/java/app/notifee/core/ForegroundServiceTest.java +23 -13
- package/android/src/test/java/app/notifee/core/NotifeeGetInitialNotificationTest.java +3805 -0
- package/android/src/test/java/app/notifee/core/ShadowServiceCompatibilityTest.java +170 -0
- package/android/src/test/resources/robolectric.properties +4 -0
- package/cli/dist/cli.js +0 -1
- package/cli/dist/commands/initNse.js +1 -2
- package/cli/dist/lib/detectProject.js +1 -2
- package/cli/dist/lib/initNseCore.js +0 -1
- package/cli/dist/lib/logger.js +0 -1
- package/cli/dist/lib/patchPodfile.js +0 -1
- package/cli/dist/lib/patchXcodeProject.js +22 -3
- package/cli/dist/lib/writeTemplates.js +0 -1
- package/cli/dist/lib/xcode.js +6 -0
- package/cli/dist/templates/NotificationService.swift.tmpl +74 -6
- package/cli/package.json +1 -0
- package/cli/vendor/xcode/AUTHORS +6 -0
- package/cli/vendor/xcode/LICENSE +202 -0
- package/cli/vendor/xcode/NOTICE +5 -0
- package/cli/vendor/xcode/README.md +32 -0
- package/cli/vendor/xcode/UPSTREAM-SHA256.json +12 -0
- package/cli/vendor/xcode/index.js +18 -0
- package/cli/vendor/xcode/lib/parseJob.js +32 -0
- package/cli/vendor/xcode/lib/parser/pbxproj.js +1904 -0
- package/cli/vendor/xcode/lib/parser/pbxproj.pegjs +280 -0
- package/cli/vendor/xcode/lib/pbxFile.js +233 -0
- package/cli/vendor/xcode/lib/pbxProject.js +2205 -0
- package/cli/vendor/xcode/lib/pbxWriter.js +309 -0
- package/dist/NotifeeNativeModule.web.d.ts +2 -2
- package/dist/types/Module.d.ts +10 -3
- package/dist/types/Notification.d.ts +12 -1
- package/dist/types/Notification.js +3 -0
- package/dist/types/Notification.js.map +1 -1
- package/dist/types/NotificationAndroid.d.ts +9 -0
- package/dist/types/NotificationAndroid.js.map +1 -1
- package/dist/validators/validateAndroidNotification.js +26 -1
- package/dist/validators/validateAndroidNotification.js.map +1 -1
- package/dist/validators/validateAndroidStyle.js +6 -0
- package/dist/validators/validateAndroidStyle.js.map +1 -1
- package/dist/validators/validateIOSAttachment.js +3 -1
- package/dist/validators/validateIOSAttachment.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +8 -5
- package/src/NotifeeNativeModule.web.ts +2 -2
- package/src/types/Module.ts +10 -3
- package/src/types/Notification.ts +13 -0
- package/src/types/NotificationAndroid.ts +10 -0
- package/src/validators/validateAndroidNotification.ts +31 -1
- package/src/validators/validateAndroidStyle.ts +10 -2
- package/src/validators/validateIOSAttachment.ts +5 -1
- package/src/version.ts +1 -1
- package/cli/dist/cli.bundle.js +0 -20834
- package/cli/dist/cli.d.ts +0 -2
- package/cli/dist/cli.js.map +0 -1
- package/cli/dist/commands/initNse.d.ts +0 -12
- package/cli/dist/commands/initNse.js.map +0 -1
- package/cli/dist/lib/detectProject.d.ts +0 -12
- package/cli/dist/lib/detectProject.js.map +0 -1
- package/cli/dist/lib/initNseCore.d.ts +0 -8
- package/cli/dist/lib/initNseCore.js.map +0 -1
- package/cli/dist/lib/logger.d.ts +0 -4
- package/cli/dist/lib/logger.js.map +0 -1
- package/cli/dist/lib/patchPodfile.d.ts +0 -23
- package/cli/dist/lib/patchPodfile.js.map +0 -1
- package/cli/dist/lib/patchXcodeProject.d.ts +0 -41
- package/cli/dist/lib/patchXcodeProject.js.map +0 -1
- package/cli/dist/lib/writeTemplates.d.ts +0 -11
- package/cli/dist/lib/writeTemplates.js.map +0 -1
- package/cli/dist/templates/templates/Info.plist.tmpl +0 -29
- package/cli/dist/templates/templates/NotificationService.swift.tmpl +0 -100
- package/cli/dist/templates/templates/NotifyKitNSE.entitlements.tmpl +0 -6
|
@@ -33,6 +33,7 @@ import android.os.Bundle;
|
|
|
33
33
|
import android.os.Handler;
|
|
34
34
|
import android.os.IBinder;
|
|
35
35
|
import android.os.Looper;
|
|
36
|
+
import android.os.SystemClock;
|
|
36
37
|
import android.os.Trace;
|
|
37
38
|
import androidx.annotation.Nullable;
|
|
38
39
|
import androidx.annotation.RequiresApi;
|
|
@@ -68,6 +69,7 @@ public class ForegroundService extends Service {
|
|
|
68
69
|
private static final Map<String, StartRequest> sPendingStarts = new HashMap<>();
|
|
69
70
|
private static final String START_REQUEST_ID = "notifee.foregroundServiceStartRequestId";
|
|
70
71
|
private static long sRunGeneration;
|
|
72
|
+
private static long sStartAdmissionRevision;
|
|
71
73
|
private static ForegroundService sInstance;
|
|
72
74
|
private static Object sStopRequest;
|
|
73
75
|
private static ForegroundService sStateOwner;
|
|
@@ -83,6 +85,9 @@ public class ForegroundService extends Service {
|
|
|
83
85
|
private int mRunFirstStartId;
|
|
84
86
|
private Object mRunnerToken;
|
|
85
87
|
private ForegroundEpoch mForegroundEpoch;
|
|
88
|
+
private ForegroundServiceRunLimitScheduler.Ticket mAlarmPoc;
|
|
89
|
+
private long mAlarmPocAttemptedRun = Long.MIN_VALUE;
|
|
90
|
+
private String mAlarmPocInstanceId;
|
|
86
91
|
// Previously admitted metadata can collide with a successor even across run generations.
|
|
87
92
|
// These IDs only disqualify stop authority; they never establish Android epoch provenance.
|
|
88
93
|
private final Set<Integer> mTimeoutStartIds = new HashSet<>();
|
|
@@ -107,10 +112,14 @@ public class ForegroundService extends Service {
|
|
|
107
112
|
// Guarded by sLifecycleLock then sLock. Retiring consumes timeout authority even on failure.
|
|
108
113
|
private static final class ForegroundEpoch {
|
|
109
114
|
final long runGeneration;
|
|
115
|
+
final long promotedElapsed = SystemClock.elapsedRealtime();
|
|
110
116
|
final boolean ambiguousPromotion;
|
|
111
117
|
final Set<Integer> satisfiedStartIds = new HashSet<>();
|
|
112
118
|
int lastSatisfiedStartId;
|
|
113
119
|
boolean retiring;
|
|
120
|
+
// A consumed voluntary alarm must not suppress a later genuine framework timeout
|
|
121
|
+
// when both native teardown attempts failed and the FGS is still active.
|
|
122
|
+
boolean voluntaryRetirement;
|
|
114
123
|
boolean retired;
|
|
115
124
|
|
|
116
125
|
ForegroundEpoch(long runGeneration, boolean ambiguousPromotion, int startId) {
|
|
@@ -122,13 +131,30 @@ public class ForegroundService extends Service {
|
|
|
122
131
|
}
|
|
123
132
|
|
|
124
133
|
// Caller holds both locks, after the actual promotion returned normally.
|
|
125
|
-
private
|
|
134
|
+
private boolean commitForegroundEpoch() {
|
|
126
135
|
ForegroundEpoch previous = mForegroundEpoch;
|
|
136
|
+
long generation = sRunGeneration;
|
|
137
|
+
long runGeneration = mRunGeneration;
|
|
138
|
+
int startId = mLastStartId;
|
|
139
|
+
if (mAlarmPoc != null) {
|
|
140
|
+
ForegroundServiceRunLimitScheduler.marker(
|
|
141
|
+
"ARM_RETIRED", mAlarmPoc, "promotion epoch replaced; no automatic rearm");
|
|
142
|
+
}
|
|
143
|
+
invalidateAlarmPoc();
|
|
144
|
+
// Cancellation crosses IPC/storage. Neither this commit nor its caller may overwrite
|
|
145
|
+
// a successor admitted while the old ticket was being cancelled.
|
|
146
|
+
if (sInstance != this
|
|
147
|
+
|| sStateOwner != this
|
|
148
|
+
|| mForegroundEpoch != previous
|
|
149
|
+
|| sRunGeneration != generation
|
|
150
|
+
|| mRunGeneration != runGeneration
|
|
151
|
+
|| mLastStartId != startId) return false;
|
|
127
152
|
mForegroundEpoch =
|
|
128
153
|
new ForegroundEpoch(
|
|
129
154
|
mRunGeneration,
|
|
130
155
|
previous != null && previous.runGeneration == mRunGeneration,
|
|
131
156
|
mLastStartId);
|
|
157
|
+
return true;
|
|
132
158
|
}
|
|
133
159
|
|
|
134
160
|
// Snapshot before platform IPC; a reply cannot acquire authority over a reentrant successor.
|
|
@@ -203,6 +229,8 @@ public class ForegroundService extends Service {
|
|
|
203
229
|
// Dispatch acceptance is distinct from the promotion outcome and the physical instance's
|
|
204
230
|
// obligation. A denied delivered request is terminal even while its sender's reply is pending.
|
|
205
231
|
boolean promotionDenied;
|
|
232
|
+
ForegroundServiceRunLimitScheduler.Request alarmPoc;
|
|
233
|
+
ForegroundEpoch alarmPocEpoch;
|
|
206
234
|
// Null until delivery. An unfulfilled delivered request belongs to that physical instance.
|
|
207
235
|
ForegroundService instance;
|
|
208
236
|
|
|
@@ -269,6 +297,7 @@ public class ForegroundService extends Service {
|
|
|
269
297
|
synchronized (sLifecycleLock) {
|
|
270
298
|
if (sInstance != null) {
|
|
271
299
|
sInstance.mDeniedStartRecovery = null;
|
|
300
|
+
sInstance.invalidateAlarmPoc();
|
|
272
301
|
}
|
|
273
302
|
sInstance = this;
|
|
274
303
|
mRunGeneration = sRunGeneration;
|
|
@@ -282,6 +311,7 @@ public class ForegroundService extends Service {
|
|
|
282
311
|
@Override
|
|
283
312
|
public void onDestroy() {
|
|
284
313
|
synchronized (sLifecycleLock) {
|
|
314
|
+
invalidateAlarmPoc();
|
|
285
315
|
mDeniedStartRecovery = null;
|
|
286
316
|
sPendingStarts.values().removeIf(request -> request.instance == this);
|
|
287
317
|
if (sInstance == this) {
|
|
@@ -311,8 +341,10 @@ public class ForegroundService extends Service {
|
|
|
311
341
|
}
|
|
312
342
|
|
|
313
343
|
private void clearOwnedState() {
|
|
344
|
+
ForegroundServiceRunLimitScheduler.Ticket ticket = null;
|
|
314
345
|
synchronized (sLock) {
|
|
315
346
|
if (sStateOwner == this) {
|
|
347
|
+
ticket = revokeAlarmPoc();
|
|
316
348
|
mDeniedStartRecovery = null;
|
|
317
349
|
clearCurrentState();
|
|
318
350
|
mRunnerToken = null;
|
|
@@ -321,6 +353,8 @@ public class ForegroundService extends Service {
|
|
|
321
353
|
}
|
|
322
354
|
}
|
|
323
355
|
}
|
|
356
|
+
// Finish A's local retirement before cancellation can synchronously admit a successor.
|
|
357
|
+
if (ticket != null) ForegroundServiceRunLimitScheduler.cancel(ticket);
|
|
324
358
|
}
|
|
325
359
|
|
|
326
360
|
/**
|
|
@@ -354,7 +388,46 @@ public class ForegroundService extends Service {
|
|
|
354
388
|
}
|
|
355
389
|
}
|
|
356
390
|
|
|
391
|
+
// Internal smoke entrypoint, deliberately absent from Notifee/React Native public APIs.
|
|
392
|
+
static void startAlarmPoc(
|
|
393
|
+
int hashCode,
|
|
394
|
+
Notification notification,
|
|
395
|
+
Bundle notificationBundle,
|
|
396
|
+
String run,
|
|
397
|
+
long requestedDurationMs) {
|
|
398
|
+
start(
|
|
399
|
+
hashCode,
|
|
400
|
+
notification,
|
|
401
|
+
notificationBundle,
|
|
402
|
+
new ForegroundServiceRunLimitScheduler.Request(
|
|
403
|
+
ContextHolder.getApplicationContext(), run, requestedDurationMs));
|
|
404
|
+
}
|
|
405
|
+
|
|
357
406
|
static void start(int hashCode, Notification notification, Bundle notificationBundle) {
|
|
407
|
+
app.notifee.core.model.NotificationAndroidModel android =
|
|
408
|
+
NotificationModel.fromBundle(notificationBundle).getAndroid();
|
|
409
|
+
Long duration = android.getForegroundServiceBestEffortStopAfterMs();
|
|
410
|
+
ForegroundServiceRunLimitScheduler.Request request = null;
|
|
411
|
+
if (duration != null) {
|
|
412
|
+
// The model rejects this too; keep the platform guard explicit at API-specific calls.
|
|
413
|
+
if (Build.VERSION.SDK_INT < 35) {
|
|
414
|
+
throw new IllegalArgumentException(
|
|
415
|
+
"notification.android.foregroundServiceBestEffortStopAfterMs requires API 35+");
|
|
416
|
+
}
|
|
417
|
+
int type = android.getForegroundServiceType();
|
|
418
|
+
if (type == ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST) type = resolveManifestServiceType();
|
|
419
|
+
request =
|
|
420
|
+
ForegroundServiceRunLimitScheduler.publicRequest(
|
|
421
|
+
ContextHolder.getApplicationContext(), duration, type);
|
|
422
|
+
}
|
|
423
|
+
start(hashCode, notification, notificationBundle, request);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
private static void start(
|
|
427
|
+
int hashCode,
|
|
428
|
+
Notification notification,
|
|
429
|
+
Bundle notificationBundle,
|
|
430
|
+
ForegroundServiceRunLimitScheduler.Request alarmPoc) {
|
|
358
431
|
Context context = ContextHolder.getApplicationContext();
|
|
359
432
|
if (context == null) {
|
|
360
433
|
Logger.e(TAG, "Application context is null; cannot start ForegroundService.");
|
|
@@ -395,8 +468,35 @@ public class ForegroundService extends Service {
|
|
|
395
468
|
intent.putExtra(START_REQUEST_ID, requestId);
|
|
396
469
|
StartRequest request;
|
|
397
470
|
synchronized (sLifecycleLock) {
|
|
471
|
+
if (alarmPoc != null
|
|
472
|
+
&& (!sPendingStarts.isEmpty()
|
|
473
|
+
|| (sInstance != null
|
|
474
|
+
&& (sInstance.mForegroundActive || sInstance.mRunnerToken != null)))) {
|
|
475
|
+
if (!alarmPoc.publicOption)
|
|
476
|
+
throw new IllegalStateException("Diagnostic arming requires a new quiescent run");
|
|
477
|
+
// Repeating the same option on an owned same-ID update is accepted without rearming.
|
|
478
|
+
// Enabling/changing it during an existing run or unresolved START is rejected
|
|
479
|
+
// pre-dispatch.
|
|
480
|
+
synchronized (sLock) {
|
|
481
|
+
ForegroundService owner = sInstance;
|
|
482
|
+
String id = NotificationModel.fromBundle(notificationBundle).getId();
|
|
483
|
+
if (!sPendingStarts.isEmpty()
|
|
484
|
+
|| owner == null
|
|
485
|
+
|| owner.mAlarmPoc == null
|
|
486
|
+
|| !owner.ownsAlarmPoc(owner.mAlarmPoc)
|
|
487
|
+
|| !id.equals(mCurrentNotificationId)
|
|
488
|
+
|| alarmPoc.duration != owner.mAlarmPoc.duration) {
|
|
489
|
+
throw new IllegalArgumentException(
|
|
490
|
+
"notification.android.foregroundServiceBestEffortStopAfterMs cannot be enabled or"
|
|
491
|
+
+ " changed during an active/pending run; stop and start a new run");
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
alarmPoc = null;
|
|
495
|
+
}
|
|
398
496
|
request = new StartRequest(sRunGeneration, sStopRequest);
|
|
497
|
+
request.alarmPoc = alarmPoc;
|
|
399
498
|
sPendingStarts.put(requestId, request);
|
|
499
|
+
sStartAdmissionRevision++;
|
|
400
500
|
}
|
|
401
501
|
ComponentName component;
|
|
402
502
|
try {
|
|
@@ -455,8 +555,11 @@ public class ForegroundService extends Service {
|
|
|
455
555
|
if (sInstance != null) {
|
|
456
556
|
sInstance.mDeniedStartRecovery = null;
|
|
457
557
|
}
|
|
558
|
+
// Order STOP before cancellation IPC/storage: a reentrant later START must observe
|
|
559
|
+
// and be able to supersede this request, rather than inherit an outer stale STOP.
|
|
458
560
|
sStopRequest = new Object();
|
|
459
561
|
sRunGeneration++;
|
|
562
|
+
if (sInstance != null) sInstance.invalidateAlarmPoc();
|
|
460
563
|
scheduleStop();
|
|
461
564
|
}
|
|
462
565
|
}
|
|
@@ -599,7 +702,22 @@ public class ForegroundService extends Service {
|
|
|
599
702
|
boolean terminal =
|
|
600
703
|
intent == null || STOP_FOREGROUND_SERVICE_ACTION.equals(intent.getAction());
|
|
601
704
|
if (terminal) {
|
|
602
|
-
sRunGeneration
|
|
705
|
+
long generation = ++sRunGeneration;
|
|
706
|
+
long admission = sStartAdmissionRevision;
|
|
707
|
+
long runGeneration = mRunGeneration;
|
|
708
|
+
ForegroundEpoch epoch = mForegroundEpoch;
|
|
709
|
+
invalidateAlarmPoc();
|
|
710
|
+
// This terminal delivery predates every START accepted during cancellation, including
|
|
711
|
+
// an undelivered obligation whose startId/epoch cannot yet reveal the new admission.
|
|
712
|
+
if (sInstance != this
|
|
713
|
+
|| sStateOwner != this
|
|
714
|
+
|| sRunGeneration != generation
|
|
715
|
+
|| sStartAdmissionRevision != admission
|
|
716
|
+
|| mRunGeneration != runGeneration
|
|
717
|
+
|| mForegroundEpoch != epoch
|
|
718
|
+
|| mLastStartId != startId) {
|
|
719
|
+
return START_STICKY_COMPATIBILITY;
|
|
720
|
+
}
|
|
603
721
|
}
|
|
604
722
|
if (terminal && !sPendingStarts.isEmpty()) {
|
|
605
723
|
// The earlier accepted START must promote with its real notification/type first.
|
|
@@ -629,6 +747,7 @@ public class ForegroundService extends Service {
|
|
|
629
747
|
if (!superseded && mRunGeneration != generation) {
|
|
630
748
|
// Reserve the new session without retiring the previous submitted state or runner.
|
|
631
749
|
// A failed promotion must preserve them, while older deliveries remain fenced.
|
|
750
|
+
invalidateAlarmPoc();
|
|
632
751
|
mRunGeneration = generation;
|
|
633
752
|
}
|
|
634
753
|
}
|
|
@@ -649,6 +768,7 @@ public class ForegroundService extends Service {
|
|
|
649
768
|
recordSatisfiedStart(startId);
|
|
650
769
|
}
|
|
651
770
|
settleDeliveredStarts();
|
|
771
|
+
if (!terminal && !superseded) maybeArmAlarmPoc(request);
|
|
652
772
|
}
|
|
653
773
|
if (superseded
|
|
654
774
|
&& mStartCommandSatisfied
|
|
@@ -878,7 +998,9 @@ public class ForegroundService extends Service {
|
|
|
878
998
|
}
|
|
879
999
|
|
|
880
1000
|
if (!noneEarlyReturn && deniedInitialPromotion == null) {
|
|
881
|
-
commitForegroundEpoch();
|
|
1001
|
+
if (!commitForegroundEpoch()) return START_NOT_STICKY;
|
|
1002
|
+
if (request != null && request.alarmPoc != null)
|
|
1003
|
+
request.alarmPocEpoch = mForegroundEpoch;
|
|
882
1004
|
mCurrentNotificationId = notificationId;
|
|
883
1005
|
mCurrentNotificationBundle = bundle;
|
|
884
1006
|
mCurrentNotification = notification;
|
|
@@ -979,7 +1101,7 @@ public class ForegroundService extends Service {
|
|
|
979
1101
|
mStartForegroundCalled = true;
|
|
980
1102
|
mForegroundActive = true;
|
|
981
1103
|
mStartCommandSatisfied = true;
|
|
982
|
-
commitForegroundEpoch();
|
|
1104
|
+
if (!commitForegroundEpoch()) return START_NOT_STICKY;
|
|
983
1105
|
}
|
|
984
1106
|
// The platform call returned normally, so cache the latest submitted state for
|
|
985
1107
|
// re-post and timeout. A normal return does not acknowledge display.
|
|
@@ -1309,6 +1431,7 @@ public class ForegroundService extends Service {
|
|
|
1309
1431
|
private void handleTimeout(int startId, int fgsType, boolean shortServiceCallback) {
|
|
1310
1432
|
Bundle notificationBundle;
|
|
1311
1433
|
ForegroundEpoch epoch;
|
|
1434
|
+
ForegroundServiceRunLimitScheduler.Ticket timeoutAlarm;
|
|
1312
1435
|
RuntimeException primary = null;
|
|
1313
1436
|
String demotionOutcome = "failed";
|
|
1314
1437
|
String stopOutcome = "deferred";
|
|
@@ -1323,7 +1446,7 @@ public class ForegroundService extends Service {
|
|
|
1323
1446
|
if (sInstance != this
|
|
1324
1447
|
|| sStateOwner != this
|
|
1325
1448
|
|| epoch == null
|
|
1326
|
-
|| epoch.retiring
|
|
1449
|
+
|| (epoch.retiring && !epoch.voluntaryRetirement)
|
|
1327
1450
|
|| epoch.retired
|
|
1328
1451
|
|| !mForegroundActive
|
|
1329
1452
|
|| (mRunFirstStartId != 0 && startId < mRunFirstStartId)
|
|
@@ -1343,6 +1466,8 @@ public class ForegroundService extends Service {
|
|
|
1343
1466
|
mCurrentNotificationBundle == null ? null : new Bundle(mCurrentNotificationBundle);
|
|
1344
1467
|
mTimeoutStartIds.add(startId);
|
|
1345
1468
|
epoch.retiring = true;
|
|
1469
|
+
epoch.voluntaryRetirement = false;
|
|
1470
|
+
timeoutAlarm = revokeAlarmPoc();
|
|
1346
1471
|
}
|
|
1347
1472
|
|
|
1348
1473
|
// Nothing that posts an event, converts a model or retires owned evidence precedes this.
|
|
@@ -1380,6 +1505,8 @@ public class ForegroundService extends Service {
|
|
|
1380
1505
|
}
|
|
1381
1506
|
}
|
|
1382
1507
|
|
|
1508
|
+
if (timeoutAlarm != null) ForegroundServiceRunLimitScheduler.cancel(timeoutAlarm);
|
|
1509
|
+
|
|
1383
1510
|
// Consume publication authority once, including when a reentrant caller still owns locks.
|
|
1384
1511
|
final RuntimeException timeoutFailure = primary;
|
|
1385
1512
|
final String diagnostic =
|
|
@@ -1429,4 +1556,267 @@ public class ForegroundService extends Service {
|
|
|
1429
1556
|
publishTimeoutEventsIfUnlocked();
|
|
1430
1557
|
if (primary != null) throw primary;
|
|
1431
1558
|
}
|
|
1559
|
+
|
|
1560
|
+
// All diagnostic authority is subordinate to the existing lifecycle/epoch registry.
|
|
1561
|
+
// Caller holds sLifecycleLock; nested state checks always take sLock second.
|
|
1562
|
+
private void invalidateAlarmPoc() {
|
|
1563
|
+
ForegroundServiceRunLimitScheduler.Ticket ticket = revokeAlarmPoc();
|
|
1564
|
+
if (ticket != null) ForegroundServiceRunLimitScheduler.cancel(ticket);
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
private ForegroundServiceRunLimitScheduler.Ticket revokeAlarmPoc() {
|
|
1568
|
+
ForegroundServiceRunLimitScheduler.Ticket ticket = mAlarmPoc;
|
|
1569
|
+
mAlarmPoc = null;
|
|
1570
|
+
if (ticket != null) ticket.valid = false;
|
|
1571
|
+
return ticket;
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
private void maybeArmAlarmPoc(StartRequest request) {
|
|
1575
|
+
if (request == null || request.alarmPoc == null || Build.VERSION.SDK_INT < 35) return;
|
|
1576
|
+
synchronized (sLock) {
|
|
1577
|
+
ForegroundEpoch epoch = mForegroundEpoch;
|
|
1578
|
+
if ((!request.alarmPoc.publicOption && mAlarmPocAttemptedRun == mRunGeneration)
|
|
1579
|
+
|| request.alarmPocEpoch != epoch) {
|
|
1580
|
+
ForegroundServiceRunLimitScheduler.marker(
|
|
1581
|
+
"ARM_REJECTED", null, "no new diagnostic promotion/run");
|
|
1582
|
+
return;
|
|
1583
|
+
}
|
|
1584
|
+
mAlarmPocAttemptedRun = mRunGeneration;
|
|
1585
|
+
if (sInstance != this
|
|
1586
|
+
|| sStateOwner != this
|
|
1587
|
+
|| epoch == null
|
|
1588
|
+
|| epoch.retiring
|
|
1589
|
+
|| epoch.retired
|
|
1590
|
+
|| !mForegroundActive
|
|
1591
|
+
|| !mStartCommandSatisfied
|
|
1592
|
+
|| epoch.runGeneration != mRunGeneration
|
|
1593
|
+
|| sRunGeneration != mRunGeneration
|
|
1594
|
+
|| epoch.lastSatisfiedStartId != mLastStartId
|
|
1595
|
+
|| mStopInitiated
|
|
1596
|
+
|| mCurrentForegroundServiceType != ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC) {
|
|
1597
|
+
ForegroundServiceRunLimitScheduler.marker(
|
|
1598
|
+
"ARM_REJECTED", null, "requires current single dataSync epoch");
|
|
1599
|
+
return;
|
|
1600
|
+
}
|
|
1601
|
+
ForegroundServiceRunLimitScheduler.Ticket ticket = null;
|
|
1602
|
+
try {
|
|
1603
|
+
// Promotion has already returned; no ledger/GC I/O is added to default START latency.
|
|
1604
|
+
if (request.alarmPoc.publicOption) ForegroundServiceRunLimitScheduler.collectOrphans(this);
|
|
1605
|
+
ticket =
|
|
1606
|
+
new ForegroundServiceRunLimitScheduler.Ticket(
|
|
1607
|
+
this,
|
|
1608
|
+
epoch,
|
|
1609
|
+
sRunGeneration,
|
|
1610
|
+
mRunGeneration,
|
|
1611
|
+
request.alarmPoc,
|
|
1612
|
+
epoch.promotedElapsed);
|
|
1613
|
+
// Ticket construction reads boot identity through a platform API. Revalidate after it.
|
|
1614
|
+
if (sInstance != this
|
|
1615
|
+
|| sStateOwner != this
|
|
1616
|
+
|| mForegroundEpoch != epoch
|
|
1617
|
+
|| epoch.retiring
|
|
1618
|
+
|| epoch.retired
|
|
1619
|
+
|| sRunGeneration != ticket.generation
|
|
1620
|
+
|| mRunGeneration != ticket.runGeneration) {
|
|
1621
|
+
ForegroundServiceRunLimitScheduler.cancel(ticket);
|
|
1622
|
+
return;
|
|
1623
|
+
}
|
|
1624
|
+
mAlarmPoc = ticket;
|
|
1625
|
+
if (!ForegroundServiceRunLimitScheduler.arm(ticket) || !ownsAlarmPoc(ticket)) {
|
|
1626
|
+
if (mAlarmPoc == ticket) mAlarmPoc = null;
|
|
1627
|
+
ForegroundServiceRunLimitScheduler.cancel(ticket);
|
|
1628
|
+
}
|
|
1629
|
+
} catch (org.json.JSONException | RuntimeException failure) {
|
|
1630
|
+
// Construction/storage may have admitted B reentrantly. Cleanup belongs only to A.
|
|
1631
|
+
if (ticket != null) {
|
|
1632
|
+
if (mAlarmPoc == ticket) mAlarmPoc = null;
|
|
1633
|
+
ForegroundServiceRunLimitScheduler.cancel(ticket);
|
|
1634
|
+
}
|
|
1635
|
+
ForegroundServiceRunLimitScheduler.marker("ARM_FAILED", ticket, failure.toString());
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
String alarmPocInstanceId() {
|
|
1641
|
+
if (mAlarmPocInstanceId == null) mAlarmPocInstanceId = UUID.randomUUID().toString();
|
|
1642
|
+
return mAlarmPocInstanceId;
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
private boolean ownsAlarmPoc(ForegroundServiceRunLimitScheduler.Ticket ticket) {
|
|
1646
|
+
return Build.VERSION.SDK_INT >= 35
|
|
1647
|
+
&& sInstance == this
|
|
1648
|
+
&& sStateOwner == this
|
|
1649
|
+
&& ticket.owner == this
|
|
1650
|
+
&& mAlarmPoc == ticket
|
|
1651
|
+
&& ticket.valid
|
|
1652
|
+
&& ticket.epoch == mForegroundEpoch
|
|
1653
|
+
&& sRunGeneration == ticket.generation
|
|
1654
|
+
&& mRunGeneration == ticket.runGeneration
|
|
1655
|
+
&& mForegroundEpoch != null
|
|
1656
|
+
&& mForegroundEpoch.runGeneration == ticket.runGeneration
|
|
1657
|
+
&& !mForegroundEpoch.retiring
|
|
1658
|
+
&& !mForegroundEpoch.retired
|
|
1659
|
+
&& mForegroundActive
|
|
1660
|
+
&& mCurrentForegroundServiceType == ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
|
|
1661
|
+
&& mStartCommandSatisfied
|
|
1662
|
+
&& mLastStartId != 0
|
|
1663
|
+
&& mLastStartId == mForegroundEpoch.lastSatisfiedStartId
|
|
1664
|
+
&& !mStartCommandFailed
|
|
1665
|
+
&& !mStopInitiated;
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
static void receiveAlarmPoc(Intent intent) {
|
|
1669
|
+
try {
|
|
1670
|
+
synchronized (sLifecycleLock) {
|
|
1671
|
+
ForegroundService owner = sInstance;
|
|
1672
|
+
if (owner == null) {
|
|
1673
|
+
ForegroundServiceRunLimitScheduler.marker(
|
|
1674
|
+
"NO_MATCHING_LIVE_INSTANCE", null, "cold delivery");
|
|
1675
|
+
return;
|
|
1676
|
+
}
|
|
1677
|
+
owner.expireAlarmPoc(intent);
|
|
1678
|
+
}
|
|
1679
|
+
} finally {
|
|
1680
|
+
publishTimeoutEventsIfUnlocked();
|
|
1681
|
+
}
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
private void expireAlarmPoc(Intent intent) {
|
|
1685
|
+
final ForegroundServiceRunLimitScheduler.Ticket ticket;
|
|
1686
|
+
final ForegroundEpoch epoch;
|
|
1687
|
+
final StopAuthority stop;
|
|
1688
|
+
final Bundle notificationBundle;
|
|
1689
|
+
final long deliveredElapsed;
|
|
1690
|
+
String demotionOutcome = "failed";
|
|
1691
|
+
String stopOutcome = "deferred";
|
|
1692
|
+
synchronized (sLock) {
|
|
1693
|
+
ticket = mAlarmPoc;
|
|
1694
|
+
if (ticket == null
|
|
1695
|
+
|| !ownsAlarmPoc(ticket)
|
|
1696
|
+
|| !ForegroundServiceRunLimitScheduler.matches(ticket, intent)) {
|
|
1697
|
+
ForegroundServiceRunLimitScheduler.marker(
|
|
1698
|
+
"ALARM_STALE", null, "no matching live authority");
|
|
1699
|
+
return;
|
|
1700
|
+
}
|
|
1701
|
+
ForegroundServiceRunLimitScheduler.marker("ALARM_DELIVERED", ticket, "native receiver");
|
|
1702
|
+
if (!ForegroundServiceRunLimitScheduler.ledgerMatches(ticket)) {
|
|
1703
|
+
if (mAlarmPoc == ticket) invalidateAlarmPoc();
|
|
1704
|
+
ForegroundServiceRunLimitScheduler.marker(
|
|
1705
|
+
"LEDGER_INVALID", ticket, "no teardown authority");
|
|
1706
|
+
return;
|
|
1707
|
+
}
|
|
1708
|
+
long now = SystemClock.elapsedRealtime();
|
|
1709
|
+
if (now < ticket.deadline) {
|
|
1710
|
+
ForegroundServiceRunLimitScheduler.marker("ALARM_EARLY", ticket, "no demotion");
|
|
1711
|
+
return;
|
|
1712
|
+
}
|
|
1713
|
+
if (now > ticket.deadline) {
|
|
1714
|
+
ForegroundServiceRunLimitScheduler.marker(
|
|
1715
|
+
"ALARM_LATE", ticket, "delayMs=" + (now - ticket.deadline));
|
|
1716
|
+
}
|
|
1717
|
+
// Boot/ledger validation crosses platform APIs. It cannot transfer the old alarm's
|
|
1718
|
+
// authority to a successor that appeared while those calls were in progress.
|
|
1719
|
+
if (!ownsAlarmPoc(ticket)) {
|
|
1720
|
+
ForegroundServiceRunLimitScheduler.marker(
|
|
1721
|
+
"ALARM_STALE", ticket, "owner changed during validation");
|
|
1722
|
+
return;
|
|
1723
|
+
}
|
|
1724
|
+
epoch = mForegroundEpoch;
|
|
1725
|
+
stop = captureStopAuthority();
|
|
1726
|
+
deliveredElapsed = now;
|
|
1727
|
+
notificationBundle =
|
|
1728
|
+
mCurrentNotificationBundle == null ? null : new Bundle(mCurrentNotificationBundle);
|
|
1729
|
+
mAlarmPoc = null;
|
|
1730
|
+
ticket.valid = false;
|
|
1731
|
+
epoch.retiring = true;
|
|
1732
|
+
epoch.voluntaryRetirement = true;
|
|
1733
|
+
}
|
|
1734
|
+
try {
|
|
1735
|
+
ForegroundServiceRunLimitScheduler.marker(
|
|
1736
|
+
"DEMOTION_ATTEMPTED",
|
|
1737
|
+
ticket,
|
|
1738
|
+
"REMOVE notificationId="
|
|
1739
|
+
+ mCurrentNotificationId
|
|
1740
|
+
+ " startId="
|
|
1741
|
+
+ stop.startId
|
|
1742
|
+
+ " satisfiedStartId="
|
|
1743
|
+
+ epoch.lastSatisfiedStartId
|
|
1744
|
+
+ " pendingStarts="
|
|
1745
|
+
+ sPendingStarts.size());
|
|
1746
|
+
try {
|
|
1747
|
+
removeForegroundNotification();
|
|
1748
|
+
demotionOutcome = "returned";
|
|
1749
|
+
ForegroundServiceRunLimitScheduler.marker(
|
|
1750
|
+
"DEMOTION_RETURNED", ticket, "not an AMS acknowledgement");
|
|
1751
|
+
retireTimeoutEpoch(epoch);
|
|
1752
|
+
} catch (RuntimeException failure) {
|
|
1753
|
+
ForegroundServiceRunLimitScheduler.marker("DEMOTION_FAILED", ticket, failure.toString());
|
|
1754
|
+
}
|
|
1755
|
+
if (ownsTimeoutEpoch(epoch)
|
|
1756
|
+
&& epoch.runGeneration == mRunGeneration
|
|
1757
|
+
&& epoch.runGeneration == sRunGeneration
|
|
1758
|
+
&& ownsStopAuthority(stop)
|
|
1759
|
+
&& mLastStartId == epoch.lastSatisfiedStartId
|
|
1760
|
+
&& mLastStartId != 0
|
|
1761
|
+
&& !mStartCommandFailed
|
|
1762
|
+
&& !mStopInitiated) {
|
|
1763
|
+
try {
|
|
1764
|
+
stopOutcome = "failed";
|
|
1765
|
+
boolean stopped = stopSelfResult(stop.startId);
|
|
1766
|
+
stopOutcome = stopped ? "accepted" : "refused";
|
|
1767
|
+
ForegroundServiceRunLimitScheduler.marker(
|
|
1768
|
+
stopped ? "STOP_ACCEPTED" : "STOP_REFUSED", ticket, "startId=" + stop.startId);
|
|
1769
|
+
if (stopped && ownsTimeoutEpoch(epoch) && ownsStopAuthority(stop)) {
|
|
1770
|
+
mStopInitiated = true;
|
|
1771
|
+
retireTimeoutEpoch(epoch);
|
|
1772
|
+
}
|
|
1773
|
+
} catch (RuntimeException failure) {
|
|
1774
|
+
ForegroundServiceRunLimitScheduler.marker("STOP_FAILED", ticket, failure.toString());
|
|
1775
|
+
}
|
|
1776
|
+
} else {
|
|
1777
|
+
ForegroundServiceRunLimitScheduler.marker(
|
|
1778
|
+
"STOP_DEFERRED", ticket, "pending/unknown START or ownership changed");
|
|
1779
|
+
}
|
|
1780
|
+
ForegroundServiceRunLimitScheduler.marker(
|
|
1781
|
+
"NATIVE_FINAL_STATE",
|
|
1782
|
+
ticket,
|
|
1783
|
+
"sameEpoch="
|
|
1784
|
+
+ ownsTimeoutEpoch(epoch)
|
|
1785
|
+
+ " foregroundActive="
|
|
1786
|
+
+ mForegroundActive
|
|
1787
|
+
+ " stopInitiated="
|
|
1788
|
+
+ mStopInitiated
|
|
1789
|
+
+ " pendingStarts="
|
|
1790
|
+
+ sPendingStarts.size());
|
|
1791
|
+
} finally {
|
|
1792
|
+
ForegroundServiceRunLimitScheduler.cancel(ticket);
|
|
1793
|
+
}
|
|
1794
|
+
if (ticket.publicOption && notificationBundle != null) {
|
|
1795
|
+
Bundle metadata = new Bundle();
|
|
1796
|
+
metadata.putLong("requestedDurationMs", ticket.duration);
|
|
1797
|
+
metadata.putLong("deadlineElapsed", ticket.deadline);
|
|
1798
|
+
metadata.putLong("deliveredElapsed", deliveredElapsed);
|
|
1799
|
+
metadata.putString("demotion", demotionOutcome);
|
|
1800
|
+
metadata.putString("conditionalStop", stopOutcome);
|
|
1801
|
+
List<Runnable> publications = sTimeoutPublications.get();
|
|
1802
|
+
if (publications == null) {
|
|
1803
|
+
publications = new ArrayList<>();
|
|
1804
|
+
sTimeoutPublications.set(publications);
|
|
1805
|
+
}
|
|
1806
|
+
publications.add(
|
|
1807
|
+
() -> {
|
|
1808
|
+
try {
|
|
1809
|
+
Bundle extras = new Bundle();
|
|
1810
|
+
extras.putBundle("foregroundServiceRunLimit", metadata);
|
|
1811
|
+
EventBus.post(
|
|
1812
|
+
new NotificationEvent(
|
|
1813
|
+
NotificationEvent.TYPE_FG_RUN_LIMIT_REACHED,
|
|
1814
|
+
NotificationModel.fromBundle(notificationBundle),
|
|
1815
|
+
extras));
|
|
1816
|
+
} catch (RuntimeException failure) {
|
|
1817
|
+
ForegroundServiceRunLimitScheduler.marker("EVENT_FAILED", ticket, failure.toString());
|
|
1818
|
+
}
|
|
1819
|
+
});
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1432
1822
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
package app.notifee.core;
|
|
2
|
+
|
|
3
|
+
import android.content.BroadcastReceiver;
|
|
4
|
+
import android.content.Context;
|
|
5
|
+
import android.content.Intent;
|
|
6
|
+
|
|
7
|
+
/** Internal explicit receiver. Does not start a service or require React. */
|
|
8
|
+
public final class ForegroundServiceRunLimitReceiver extends BroadcastReceiver {
|
|
9
|
+
@Override
|
|
10
|
+
public void onReceive(Context context, Intent intent) {
|
|
11
|
+
if (intent == null || !ForegroundServiceRunLimitScheduler.ACTION.equals(intent.getAction()))
|
|
12
|
+
return;
|
|
13
|
+
ForegroundService.receiveAlarmPoc(intent);
|
|
14
|
+
}
|
|
15
|
+
}
|