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.
Files changed (89) hide show
  1. package/README.md +35 -18
  2. package/android/build.gradle +50 -4
  3. package/android/src/main/AndroidManifest.xml +15 -0
  4. package/android/src/main/java/app/notifee/core/EventBus.java +4 -0
  5. package/android/src/main/java/app/notifee/core/ForegroundService.java +395 -5
  6. package/android/src/main/java/app/notifee/core/ForegroundServiceRunLimitReceiver.java +15 -0
  7. package/android/src/main/java/app/notifee/core/ForegroundServiceRunLimitScheduler.java +461 -0
  8. package/android/src/main/java/app/notifee/core/FullScreenNotificationActivity.java +76 -0
  9. package/android/src/main/java/app/notifee/core/InitialNotificationLedger.java +262 -0
  10. package/android/src/main/java/app/notifee/core/Notifee.java +142 -20
  11. package/android/src/main/java/app/notifee/core/NotificationManager.java +50 -6
  12. package/android/src/main/java/app/notifee/core/NotificationPendingIntent.java +14 -0
  13. package/android/src/main/java/app/notifee/core/NotificationReceiverHandler.java +11 -2
  14. package/android/src/main/java/app/notifee/core/event/InitialNotificationEvent.java +15 -0
  15. package/android/src/main/java/app/notifee/core/event/NotificationEvent.java +6 -0
  16. package/android/src/main/java/app/notifee/core/model/NotificationAndroidModel.java +29 -0
  17. package/android/src/main/kotlin/io/invertase/notifee/NotifeeEventSubscriber.kt +3 -0
  18. package/android/src/test/java/app/notifee/core/ForegroundServiceRunLimitTest.java +2357 -0
  19. package/android/src/test/java/app/notifee/core/ForegroundServiceTest.java +23 -13
  20. package/android/src/test/java/app/notifee/core/NotifeeGetInitialNotificationTest.java +3805 -0
  21. package/android/src/test/java/app/notifee/core/ShadowServiceCompatibilityTest.java +170 -0
  22. package/android/src/test/resources/robolectric.properties +4 -0
  23. package/cli/dist/cli.js +0 -1
  24. package/cli/dist/commands/initNse.js +1 -2
  25. package/cli/dist/lib/detectProject.js +1 -2
  26. package/cli/dist/lib/initNseCore.js +0 -1
  27. package/cli/dist/lib/logger.js +0 -1
  28. package/cli/dist/lib/patchPodfile.js +0 -1
  29. package/cli/dist/lib/patchXcodeProject.js +22 -3
  30. package/cli/dist/lib/writeTemplates.js +0 -1
  31. package/cli/dist/lib/xcode.js +6 -0
  32. package/cli/dist/templates/NotificationService.swift.tmpl +74 -6
  33. package/cli/package.json +1 -0
  34. package/cli/vendor/xcode/AUTHORS +6 -0
  35. package/cli/vendor/xcode/LICENSE +202 -0
  36. package/cli/vendor/xcode/NOTICE +5 -0
  37. package/cli/vendor/xcode/README.md +32 -0
  38. package/cli/vendor/xcode/UPSTREAM-SHA256.json +12 -0
  39. package/cli/vendor/xcode/index.js +18 -0
  40. package/cli/vendor/xcode/lib/parseJob.js +32 -0
  41. package/cli/vendor/xcode/lib/parser/pbxproj.js +1904 -0
  42. package/cli/vendor/xcode/lib/parser/pbxproj.pegjs +280 -0
  43. package/cli/vendor/xcode/lib/pbxFile.js +233 -0
  44. package/cli/vendor/xcode/lib/pbxProject.js +2205 -0
  45. package/cli/vendor/xcode/lib/pbxWriter.js +309 -0
  46. package/dist/NotifeeNativeModule.web.d.ts +2 -2
  47. package/dist/types/Module.d.ts +10 -3
  48. package/dist/types/Notification.d.ts +12 -1
  49. package/dist/types/Notification.js +3 -0
  50. package/dist/types/Notification.js.map +1 -1
  51. package/dist/types/NotificationAndroid.d.ts +9 -0
  52. package/dist/types/NotificationAndroid.js.map +1 -1
  53. package/dist/validators/validateAndroidNotification.js +26 -1
  54. package/dist/validators/validateAndroidNotification.js.map +1 -1
  55. package/dist/validators/validateAndroidStyle.js +6 -0
  56. package/dist/validators/validateAndroidStyle.js.map +1 -1
  57. package/dist/validators/validateIOSAttachment.js +3 -1
  58. package/dist/validators/validateIOSAttachment.js.map +1 -1
  59. package/dist/version.d.ts +1 -1
  60. package/dist/version.js +1 -1
  61. package/package.json +8 -5
  62. package/src/NotifeeNativeModule.web.ts +2 -2
  63. package/src/types/Module.ts +10 -3
  64. package/src/types/Notification.ts +13 -0
  65. package/src/types/NotificationAndroid.ts +10 -0
  66. package/src/validators/validateAndroidNotification.ts +31 -1
  67. package/src/validators/validateAndroidStyle.ts +10 -2
  68. package/src/validators/validateIOSAttachment.ts +5 -1
  69. package/src/version.ts +1 -1
  70. package/cli/dist/cli.bundle.js +0 -20834
  71. package/cli/dist/cli.d.ts +0 -2
  72. package/cli/dist/cli.js.map +0 -1
  73. package/cli/dist/commands/initNse.d.ts +0 -12
  74. package/cli/dist/commands/initNse.js.map +0 -1
  75. package/cli/dist/lib/detectProject.d.ts +0 -12
  76. package/cli/dist/lib/detectProject.js.map +0 -1
  77. package/cli/dist/lib/initNseCore.d.ts +0 -8
  78. package/cli/dist/lib/initNseCore.js.map +0 -1
  79. package/cli/dist/lib/logger.d.ts +0 -4
  80. package/cli/dist/lib/logger.js.map +0 -1
  81. package/cli/dist/lib/patchPodfile.d.ts +0 -23
  82. package/cli/dist/lib/patchPodfile.js.map +0 -1
  83. package/cli/dist/lib/patchXcodeProject.d.ts +0 -41
  84. package/cli/dist/lib/patchXcodeProject.js.map +0 -1
  85. package/cli/dist/lib/writeTemplates.d.ts +0 -11
  86. package/cli/dist/lib/writeTemplates.js.map +0 -1
  87. package/cli/dist/templates/templates/Info.plist.tmpl +0 -29
  88. package/cli/dist/templates/templates/NotificationService.swift.tmpl +0 -100
  89. package/cli/dist/templates/templates/NotifyKitNSE.entitlements.tmpl +0 -6
@@ -0,0 +1,461 @@
1
+ package app.notifee.core;
2
+
3
+ import android.app.AlarmManager;
4
+ import android.app.Application;
5
+ import android.app.PendingIntent;
6
+ import android.content.ComponentName;
7
+ import android.content.Context;
8
+ import android.content.Intent;
9
+ import android.content.SharedPreferences;
10
+ import android.content.pm.ActivityInfo;
11
+ import android.content.pm.ApplicationInfo;
12
+ import android.content.pm.PackageManager;
13
+ import android.content.pm.ServiceInfo;
14
+ import android.net.Uri;
15
+ import android.os.Build;
16
+ import android.os.SystemClock;
17
+ import android.provider.Settings;
18
+ import android.util.Log;
19
+ import java.util.HashSet;
20
+ import java.util.Set;
21
+ import java.util.UUID;
22
+ import org.json.JSONException;
23
+ import org.json.JSONObject;
24
+
25
+ /** Internal opt-in run limit. Persistence never grants lifecycle authority. */
26
+ final class ForegroundServiceRunLimitScheduler {
27
+ static final String ACTION = "app.notifee.core.FGS_RUN_LIMIT_EXPIRE";
28
+ static final String PROCESS = UUID.randomUUID().toString();
29
+ static final String PREFS = "notifee-fgs-run-limit";
30
+ static final int SCHEMA = 1;
31
+ // Index only new tickets. Never enumerate/adopt legacy or unindexed storage.
32
+ static final int GC_SLOTS = 32;
33
+ static final int GC_BATCH = 8;
34
+ private static final Object GC_LOCK = new Object();
35
+ private static final boolean[] RESERVED = new boolean[GC_SLOTS];
36
+ private static final Set<String> LIVE_KEYS = new HashSet<>();
37
+ private static int gcCursor;
38
+ private static boolean gcRunning;
39
+
40
+ static final class Request {
41
+ final String run;
42
+ final long duration;
43
+ final boolean publicOption;
44
+
45
+ private Request(long duration) {
46
+ this.run = UUID.randomUUID().toString();
47
+ this.duration = duration;
48
+ this.publicOption = true;
49
+ }
50
+
51
+ Request(Context context, String run, long duration) {
52
+ if (run == null || !run.matches("[a-zA-Z0-9_-]{1,80}") || duration < 1 || duration > 600000) {
53
+ throw new IllegalArgumentException(
54
+ "Explicit diagnostic run and duration (1..600000ms) required");
55
+ }
56
+ if (!available(context)) throw new IllegalStateException("Smoke debug receiver unavailable");
57
+ this.run = run;
58
+ this.duration = duration;
59
+ this.publicOption = false;
60
+ // Diagnostic fixture retains its reviewed admission-time sweep.
61
+ collectOrphans(context);
62
+ }
63
+ }
64
+
65
+ static Request publicRequest(Context context, long duration, int effectiveType) {
66
+ if (effectiveType != ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
67
+ || !available(context, false)) {
68
+ throw new IllegalArgumentException(
69
+ "notification.android.foregroundServiceBestEffortStopAfterMs requires single dataSync and"
70
+ + " an internal receiver in the app FGS process (API 35+)");
71
+ }
72
+ return new Request(duration);
73
+ }
74
+
75
+ static boolean available(Context context) {
76
+ return available(context, true);
77
+ }
78
+
79
+ private static boolean available(Context context, boolean diagnostic) {
80
+ if (context == null
81
+ || Build.VERSION.SDK_INT < 35
82
+ || (diagnostic
83
+ && (context.getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) == 0))
84
+ return false;
85
+ try {
86
+ ActivityInfo receiver =
87
+ context
88
+ .getPackageManager()
89
+ .getReceiverInfo(
90
+ new ComponentName(context, ForegroundServiceRunLimitReceiver.class), 0);
91
+ ServiceInfo service =
92
+ context
93
+ .getPackageManager()
94
+ .getServiceInfo(new ComponentName(context, ForegroundService.class), 0);
95
+ return (service.getForegroundServiceType() & ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC)
96
+ != 0
97
+ && receiver.enabled
98
+ && !receiver.exported
99
+ && context.getPackageName().equals(receiver.packageName)
100
+ && java.util.Objects.equals(receiver.processName, service.processName)
101
+ && (diagnostic || context.getApplicationInfo().processName.equals(service.processName));
102
+ } catch (PackageManager.NameNotFoundException | RuntimeException failure) {
103
+ return false;
104
+ }
105
+ }
106
+
107
+ static int boot(Context context) {
108
+ return Settings.Global.getInt(context.getContentResolver(), Settings.Global.BOOT_COUNT, -1);
109
+ }
110
+
111
+ static final class Ticket {
112
+ final ForegroundService owner;
113
+ final Object epoch;
114
+ final long generation;
115
+ final long runGeneration;
116
+ final long deadline;
117
+ final long duration;
118
+ final boolean publicOption;
119
+ final int boot;
120
+ final String token = UUID.randomUUID().toString();
121
+ // Physical ownership: even a delayed/reentrant editor for A cannot address B's keys.
122
+ final String recordKey = "ticket/" + PROCESS + "/" + token;
123
+ final String stateKey = recordKey + "/state";
124
+ final String wire;
125
+ final Intent intent;
126
+ PendingIntent pendingIntent;
127
+ boolean valid = true;
128
+ boolean scheduled;
129
+
130
+ @androidx.annotation.RequiresApi(35)
131
+ Ticket(
132
+ ForegroundService owner,
133
+ Object epoch,
134
+ long generation,
135
+ long runGeneration,
136
+ Request request,
137
+ long promotedElapsed)
138
+ throws JSONException {
139
+ this.owner = owner;
140
+ this.epoch = epoch;
141
+ this.generation = generation;
142
+ this.runGeneration = runGeneration;
143
+ duration = request.duration;
144
+ publicOption = request.publicOption;
145
+ deadline = Math.addExact(promotedElapsed, request.duration);
146
+ boot = boot(owner);
147
+ if (boot < 0) throw new IllegalStateException("Boot identity unavailable");
148
+ wire =
149
+ new JSONObject()
150
+ .put("schema", SCHEMA)
151
+ .put("boot", boot)
152
+ .put("process", PROCESS)
153
+ .put("instance", owner.alarmPocInstanceId())
154
+ .put("run", request.run)
155
+ .put("epoch", UUID.randomUUID().toString())
156
+ .put("generation", generation)
157
+ .put("runGeneration", runGeneration)
158
+ .put("token", token)
159
+ .put("firstSuccessfulPromotionElapsed", promotedElapsed)
160
+ .put("deadlineElapsed", deadline)
161
+ .put("type", ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC)
162
+ .toString();
163
+ intent =
164
+ new Intent(owner, ForegroundServiceRunLimitReceiver.class)
165
+ .setAction(ACTION)
166
+ // Intent identity includes data; notification hashes and extras are never identity.
167
+ .setData(Uri.parse("notifykit-fgs-run-limit://expiry/" + PROCESS + "/" + token))
168
+ .putExtra("schema", SCHEMA)
169
+ .putExtra("token", token)
170
+ .putExtra("record", wire);
171
+ }
172
+ }
173
+
174
+ private static SharedPreferences ledger(Context context) {
175
+ return context.getSharedPreferences(PREFS, Context.MODE_PRIVATE);
176
+ }
177
+
178
+ private static void write(Ticket ticket, String state, int slot) {
179
+ SharedPreferences.Editor editor = ledger(ticket.owner).edit();
180
+ if ("ARMING".equals(state)) editor.putString("gc/slot/" + slot, ticket.recordKey);
181
+ if (!editor
182
+ .putString(ticket.recordKey, ticket.wire)
183
+ .putString(ticket.stateKey, state)
184
+ .commit()) {
185
+ throw new IllegalStateException("Ledger commit refused");
186
+ }
187
+ }
188
+
189
+ static boolean arm(Ticket ticket) {
190
+ String phase = "STORAGE_FAILED";
191
+ int slot = -1;
192
+ try {
193
+ synchronized (GC_LOCK) {
194
+ LIVE_KEYS.add(ticket.recordKey);
195
+ }
196
+ slot = reserveEmptySlot(ticket.owner);
197
+ write(ticket, "ARMING", slot);
198
+ if (!ticket.valid) return false;
199
+ phase = "SCHEDULE_FAILED";
200
+ ticket.pendingIntent =
201
+ PendingIntent.getBroadcast(ticket.owner, 0, ticket.intent, PendingIntent.FLAG_IMMUTABLE);
202
+ ticket
203
+ .owner
204
+ .getSystemService(AlarmManager.class)
205
+ .setAndAllowWhileIdle(
206
+ AlarmManager.ELAPSED_REALTIME_WAKEUP, ticket.deadline, ticket.pendingIntent);
207
+ if (!ticket.valid) return false;
208
+ phase = "STORAGE_FAILED";
209
+ write(ticket, "ARMED", slot);
210
+ if (!ticket.valid) return false;
211
+ ticket.scheduled = true;
212
+ marker("ALARM_ARMED", ticket, "requestedDurationIsVoluntary=true");
213
+ return true;
214
+ } catch (RuntimeException failure) {
215
+ marker(phase, ticket, failure.toString());
216
+ return false;
217
+ } finally {
218
+ if (slot >= 0) releaseSlot(slot);
219
+ if (!ticket.scheduled) cancel(ticket);
220
+ }
221
+ }
222
+
223
+ static boolean matches(Ticket ticket, Intent intent) {
224
+ try {
225
+ return ticket.valid
226
+ && ticket.scheduled
227
+ && boot(ticket.owner) == ticket.boot
228
+ && intent != null
229
+ && ticket.intent.filterEquals(intent)
230
+ && intent.getIntExtra("schema", -1) == SCHEMA
231
+ && ticket.token.equals(intent.getStringExtra("token"))
232
+ && ticket.wire.equals(intent.getStringExtra("record"));
233
+ } catch (RuntimeException malformed) {
234
+ return false;
235
+ }
236
+ }
237
+
238
+ static boolean ledgerMatches(Ticket ticket) {
239
+ try {
240
+ SharedPreferences prefs = ledger(ticket.owner);
241
+ return ticket.wire.equals(prefs.getString(ticket.recordKey, null))
242
+ && "ARMED".equals(prefs.getString(ticket.stateKey, null));
243
+ } catch (RuntimeException failure) {
244
+ marker("STORAGE_FAILED", ticket, failure.toString());
245
+ return false;
246
+ }
247
+ }
248
+
249
+ // Caller has already revoked the live authority. Cancellation cannot revoke an in-flight send.
250
+ static void cancel(Ticket ticket) {
251
+ ticket.valid = false;
252
+ synchronized (GC_LOCK) {
253
+ LIVE_KEYS.remove(ticket.recordKey);
254
+ }
255
+ if (ticket.pendingIntent != null) {
256
+ try {
257
+ ticket.owner.getSystemService(AlarmManager.class).cancel(ticket.pendingIntent);
258
+ } catch (RuntimeException failure) {
259
+ marker("CANCEL_FAILED", ticket, failure.toString());
260
+ }
261
+ try {
262
+ ticket.pendingIntent.cancel();
263
+ } catch (RuntimeException failure) {
264
+ marker("CANCEL_FAILED", ticket, failure.toString());
265
+ }
266
+ }
267
+ try {
268
+ SharedPreferences prefs = ledger(ticket.owner);
269
+ // The comparison rejects corrupt records; it is not a CAS. Safety comes from the
270
+ // immutable ticket-specific keys at commit, including reentrancy inside remove/commit.
271
+ // Unindexed legacy/global orphans stay inert: never adopt or bulk-delete them.
272
+ if (ticket.wire.equals(prefs.getString(ticket.recordKey, null))
273
+ && !prefs.edit().remove(ticket.recordKey).remove(ticket.stateKey).commit()) {
274
+ marker("STORAGE_FAILED", ticket, "cancellation commit refused");
275
+ }
276
+ } catch (RuntimeException failure) {
277
+ marker("STORAGE_FAILED", ticket, failure.toString());
278
+ }
279
+ }
280
+
281
+ private static boolean reserveSlot(int slot) {
282
+ synchronized (GC_LOCK) {
283
+ if (RESERVED[slot]) return false;
284
+ RESERVED[slot] = true;
285
+ return true;
286
+ }
287
+ }
288
+
289
+ private static void releaseSlot(int slot) {
290
+ synchronized (GC_LOCK) {
291
+ RESERVED[slot] = false;
292
+ }
293
+ }
294
+
295
+ private static boolean liveKey(String key) {
296
+ synchronized (GC_LOCK) {
297
+ return LIVE_KEYS.contains(key);
298
+ }
299
+ }
300
+
301
+ private static final String UUID_PATTERN = "[0-9a-f]{8}(-[0-9a-f]{4}){3}-[0-9a-f]{12}";
302
+
303
+ private static boolean nonnegativeInteger(Object value) {
304
+ return (value instanceof Integer || value instanceof Long) && ((Number) value).longValue() >= 0;
305
+ }
306
+
307
+ private static boolean ticketKey(String key) {
308
+ return key != null && key.matches("ticket/" + UUID_PATTERN + "/" + UUID_PATTERN);
309
+ }
310
+
311
+ private static int reserveEmptySlot(Context context) {
312
+ SharedPreferences prefs = ledger(context);
313
+ for (int slot = 0; slot < GC_SLOTS; slot++) {
314
+ if (!reserveSlot(slot)) continue;
315
+ boolean claimed = false;
316
+ try {
317
+ String key = prefs.getString("gc/slot/" + slot, null);
318
+ if (key == null
319
+ || (ticketKey(key)
320
+ && !liveKey(key)
321
+ && !prefs.contains(key)
322
+ && !prefs.contains(key + "/state"))) {
323
+ claimed = true;
324
+ return slot;
325
+ }
326
+ } finally {
327
+ if (!claimed) releaseSlot(slot);
328
+ }
329
+ }
330
+ marker("GC_CAPACITY", null, "32 slots occupied/ambiguous; diagnostic protection not armed");
331
+ throw new IllegalStateException("Diagnostic ledger index full; preserving existing records");
332
+ }
333
+
334
+ @androidx.annotation.RequiresApi(35)
335
+ private static boolean orphanRecord(String key, String wire, String state) {
336
+ if (!ticketKey(key)
337
+ || wire == null
338
+ || wire.length() > 2048
339
+ || !("ARMING".equals(state) || "ARMED".equals(state))) return false;
340
+ try {
341
+ JSONObject record = new JSONObject(wire);
342
+ String process = record.getString("process");
343
+ String token = record.getString("token");
344
+ return Integer.valueOf(SCHEMA).equals(record.get("schema"))
345
+ && record.get("boot") instanceof Integer
346
+ && record.getInt("boot") >= 0
347
+ && key.equals("ticket/" + process + "/" + token)
348
+ && record.getString("instance").matches(UUID_PATTERN)
349
+ && record.getString("epoch").matches(UUID_PATTERN)
350
+ && record.getString("run").matches("[a-zA-Z0-9_-]{1,80}")
351
+ && nonnegativeInteger(record.get("generation"))
352
+ && nonnegativeInteger(record.get("runGeneration"))
353
+ && nonnegativeInteger(record.get("firstSuccessfulPromotionElapsed"))
354
+ && nonnegativeInteger(record.get("deadlineElapsed"))
355
+ && record.getLong("deadlineElapsed") > record.getLong("firstSuccessfulPromotionElapsed")
356
+ && record.getLong("deadlineElapsed") - record.getLong("firstSuccessfulPromotionElapsed")
357
+ <= 18000000
358
+ && Integer.valueOf(ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC)
359
+ .equals(record.get("type"))
360
+ // Another incarnation can never own a ticket here. For this incarnation, only
361
+ // absence from the native arm registry proves non-ownership; age is irrelevant.
362
+ && (!PROCESS.equals(process) || !liveKey(key));
363
+ } catch (JSONException | RuntimeException ambiguous) {
364
+ return false;
365
+ }
366
+ }
367
+
368
+ static void collectOrphans(Context context) {
369
+ // A different configured Service process may still be alive. Its PROCESS UUID is not
370
+ // proof of a previous incarnation. Admission runs outside lifecycle locks, so verify
371
+ // the manifest process before touching storage. Unknown process identity keeps data.
372
+ try {
373
+ if (Build.VERSION.SDK_INT < 35
374
+ || !Application.getProcessName()
375
+ .equals(
376
+ context
377
+ .getPackageManager()
378
+ .getServiceInfo(new ComponentName(context, ForegroundService.class), 0)
379
+ .processName)) {
380
+ marker("GC_RESULT", null, "examined=0 skipped=service_process_mismatch");
381
+ return;
382
+ }
383
+ } catch (PackageManager.NameNotFoundException | RuntimeException unknown) {
384
+ marker("GC_RESULT", null, "examined=0 skipped=service_process_unknown");
385
+ return;
386
+ }
387
+ int first;
388
+ synchronized (GC_LOCK) {
389
+ if (gcRunning) return;
390
+ gcRunning = true;
391
+ first = gcCursor;
392
+ gcCursor = (gcCursor + GC_BATCH) % GC_SLOTS;
393
+ }
394
+ int examined = 0, removed = 0, retained = 0, failures = 0;
395
+ try {
396
+ SharedPreferences prefs = ledger(context);
397
+ for (int offset = 0; offset < GC_BATCH; offset++) {
398
+ int slot = (first + offset) % GC_SLOTS;
399
+ examined++;
400
+ if (!reserveSlot(slot)) {
401
+ retained++;
402
+ continue;
403
+ }
404
+ try {
405
+ String key = prefs.getString("gc/slot/" + slot, null);
406
+ if (key == null) continue;
407
+ if (!ticketKey(key) || liveKey(key)) {
408
+ retained++;
409
+ continue;
410
+ }
411
+ String wire = prefs.getString(key, null);
412
+ String state = prefs.getString(key + "/state", null);
413
+ if (wire == null && state == null) continue;
414
+ if (!orphanRecord(key, wire, state) || liveKey(key)) {
415
+ retained++;
416
+ continue;
417
+ }
418
+ // This slot is reserved across all external calls. Neither its immutable victim
419
+ // address nor its index can be reassigned by a reentrant/new arm. Never remove
420
+ // the reusable index slot here: an old editor must only contain victim keys.
421
+ if (prefs.edit().remove(key).remove(key + "/state").commit()) removed++;
422
+ else failures++;
423
+ } catch (RuntimeException failure) {
424
+ failures++;
425
+ } finally {
426
+ releaseSlot(slot);
427
+ }
428
+ }
429
+ } catch (RuntimeException failure) {
430
+ failures++;
431
+ } finally {
432
+ synchronized (GC_LOCK) {
433
+ gcRunning = false;
434
+ }
435
+ marker(
436
+ "GC_RESULT",
437
+ null,
438
+ "examined="
439
+ + examined
440
+ + " removed="
441
+ + removed
442
+ + " retained="
443
+ + retained
444
+ + " failures="
445
+ + failures
446
+ + " unindexed=unknown");
447
+ }
448
+ }
449
+
450
+ static void marker(String event, Ticket ticket, String detail) {
451
+ // android.util.Log only: no EventBus, ReactContext, JS event or headless task.
452
+ Log.i(
453
+ "ForegroundServiceRunLimit",
454
+ event
455
+ + " elapsed="
456
+ + SystemClock.elapsedRealtime()
457
+ + (ticket == null ? "" : " record=" + ticket.wire)
458
+ + " detail="
459
+ + detail);
460
+ }
461
+ }
@@ -0,0 +1,76 @@
1
+ package app.notifee.core;
2
+
3
+ /*
4
+ * Copyright (c) 2016-present Invertase Limited & Contributors
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this library except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ *
18
+ */
19
+
20
+ import android.app.Activity;
21
+ import android.content.Intent;
22
+ import android.os.Bundle;
23
+ import android.view.WindowManager;
24
+
25
+ /** Captures a full-screen launch before starting its consumer, without displaying a window. */
26
+ public class FullScreenNotificationActivity extends Activity {
27
+ static final String ACTION = "app.notifee.core.FULL_SCREEN_LAUNCH";
28
+ static final String LAUNCH_INTENT = "app.notifee.core.FULL_SCREEN_CONSUMER";
29
+
30
+ @Override
31
+ protected void onCreate(Bundle state) {
32
+ // API 26 uses window flags; newer versions also use the manifest's screen/keyguard flags.
33
+ getWindow()
34
+ .addFlags(
35
+ WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
36
+ | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON);
37
+ super.onCreate(state);
38
+ launchConsumer(getIntent());
39
+ }
40
+
41
+ @Override
42
+ protected void onNewIntent(Intent intent) {
43
+ super.onNewIntent(intent);
44
+ launchConsumer(intent);
45
+ }
46
+
47
+ private void launchConsumer(Intent intent) {
48
+ try {
49
+ if (intent == null
50
+ || !ACTION.equals(intent.getAction())
51
+ || !Notifee.isValidFullScreenLaunch(intent)) return;
52
+ Intent destination = intent.getParcelableExtra(LAUNCH_INTENT);
53
+ if (destination == null
54
+ || destination.getComponent() == null
55
+ || !getPackageName().equals(destination.getComponent().getPackageName())) return;
56
+ // An invalid/disabled destination must not retire a previous unread launch.
57
+ if (getPackageManager().resolveActivity(destination, 0) == null) {
58
+ return;
59
+ }
60
+ // The carrier owns the payload. The nested Intent contains only the original destination
61
+ // and flags, so the private routing extra never reaches the consumer or the public API.
62
+ Intent consumer = new Intent(destination);
63
+ consumer.replaceExtras(new Bundle(intent.getExtras()));
64
+ consumer.removeExtra(LAUNCH_INTENT);
65
+ if (ContextHolder.getApplicationContext() == null) {
66
+ ContextHolder.setApplicationContext(getApplicationContext());
67
+ }
68
+ Notifee.recordFullScreenLaunch(consumer);
69
+ startActivity(consumer);
70
+ } catch (Exception e) {
71
+ Logger.e("FullScreenNotificationActivity", "Failed to launch full-screen consumer", e);
72
+ } finally {
73
+ finish();
74
+ }
75
+ }
76
+ }