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
|
@@ -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
|
+
}
|