react-native-notify-kit 9.2.1 → 9.4.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/README.md +85 -7
- package/android/build.gradle +6 -5
- package/android/proguard-rules.pro +1 -0
- package/android/src/main/baseline-prof.txt +382 -0
- package/android/src/main/java/app/notifee/core/ForegroundService.java +269 -80
- package/android/src/main/java/app/notifee/core/InitProvider.java +48 -5
- package/android/src/main/java/app/notifee/core/Logger.java +5 -0
- package/android/src/main/java/app/notifee/core/NotificationManager.java +217 -150
- package/android/src/main/java/app/notifee/core/NotificationPendingIntent.java +10 -0
- package/android/src/main/java/app/notifee/core/WarmupHelper.java +84 -0
- package/android/src/main/java/app/notifee/core/model/NotificationAndroidModel.java +17 -1
- package/android/src/main/kotlin/io/invertase/notifee/NotifeeApiModule.kt +30 -0
- package/android/src/test/java/app/notifee/core/ForegroundServiceTest.java +182 -0
- package/android/src/test/java/app/notifee/core/NotificationPendingIntentTest.java +108 -0
- package/dist/NotifeeApiModule.d.ts +1 -0
- package/dist/NotifeeApiModule.js +6 -0
- package/dist/NotifeeApiModule.js.map +1 -1
- package/dist/specs/NativeNotifeeModule.d.ts +1 -0
- package/dist/specs/NativeNotifeeModule.js.map +1 -1
- package/dist/types/Module.d.ts +19 -0
- package/dist/types/NotificationAndroid.d.ts +66 -4
- package/dist/types/NotificationAndroid.js +43 -0
- package/dist/types/NotificationAndroid.js.map +1 -1
- package/dist/utils/validate.d.ts +1 -0
- package/dist/utils/validate.js +10 -4
- package/dist/utils/validate.js.map +1 -1
- package/dist/validators/validateAndroidChannel.js +3 -3
- package/dist/validators/validateAndroidChannel.js.map +1 -1
- package/dist/validators/validateAndroidNotification.js +58 -14
- package/dist/validators/validateAndroidNotification.js.map +1 -1
- package/dist/validators/validateIOSPermissions.js +1 -1
- package/dist/validators/validateIOSPermissions.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/ios/NotifeeCore/NotifeeCore+NSNotificationCenter.m +5 -1
- package/ios/NotifeeCore/NotifeeCore+UNUserNotificationCenter.m +15 -15
- package/ios/NotifeeCore/NotifeeCore.m +41 -14
- package/ios/NotifeeCore/NotifeeCoreDelegateHolder.m +32 -18
- package/ios/NotifeeCore/NotifeeCoreExtensionHelper.m +7 -2
- package/ios/RNNotifee/NotifeeApiModule.mm +33 -11
- package/package.json +1 -1
- package/src/NotifeeApiModule.ts +7 -0
- package/src/specs/NativeNotifeeModule.ts +1 -0
- package/src/types/Module.ts +20 -0
- package/src/types/NotificationAndroid.ts +70 -4
- package/src/utils/validate.ts +12 -4
- package/src/validators/validateAndroidChannel.ts +3 -3
- package/src/validators/validateAndroidNotification.ts +64 -11
- package/src/validators/validateIOSPermissions.ts +1 -1
- package/src/version.ts +1 -1
|
@@ -31,6 +31,7 @@ import android.graphics.Bitmap;
|
|
|
31
31
|
import android.net.Uri;
|
|
32
32
|
import android.os.Build;
|
|
33
33
|
import android.os.Bundle;
|
|
34
|
+
import android.os.Trace;
|
|
34
35
|
import android.service.notification.StatusBarNotification;
|
|
35
36
|
import androidx.annotation.NonNull;
|
|
36
37
|
import androidx.concurrent.futures.CallbackToFutureAdapter;
|
|
@@ -103,156 +104,204 @@ class NotificationManager {
|
|
|
103
104
|
*/
|
|
104
105
|
Callable<NotificationCompat.Builder> builderCallable =
|
|
105
106
|
() -> {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
notificationModel.toBundle()));
|
|
118
|
-
int targetSdkVersion =
|
|
119
|
-
ContextHolder.getApplicationContext().getApplicationInfo().targetSdkVersion;
|
|
120
|
-
if (targetSdkVersion >= Build.VERSION_CODES.S
|
|
121
|
-
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
122
|
-
builder.setContentIntent(
|
|
123
|
-
NotificationPendingIntent.createIntent(
|
|
124
|
-
notificationModel.getHashCode(),
|
|
125
|
-
androidModel.getPressAction(),
|
|
126
|
-
TYPE_PRESS,
|
|
127
|
-
new String[] {"notification", "pressAction"},
|
|
128
|
-
notificationModel.toBundle(),
|
|
129
|
-
androidModel.getPressAction()));
|
|
130
|
-
} else {
|
|
131
|
-
builder.setContentIntent(
|
|
107
|
+
Trace.beginSection("notifee:buildNotification");
|
|
108
|
+
try {
|
|
109
|
+
Boolean hasCustomSound = false;
|
|
110
|
+
NotificationCompat.Builder builder =
|
|
111
|
+
new NotificationCompat.Builder(
|
|
112
|
+
getApplicationContext(), androidModel.getChannelId());
|
|
113
|
+
|
|
114
|
+
// must always keep at top
|
|
115
|
+
builder.setExtras(notificationModel.getData());
|
|
116
|
+
|
|
117
|
+
builder.setDeleteIntent(
|
|
132
118
|
ReceiverService.createIntent(
|
|
133
|
-
ReceiverService.
|
|
134
|
-
new String[] {"notification"
|
|
135
|
-
notificationModel.toBundle()
|
|
136
|
-
|
|
137
|
-
|
|
119
|
+
ReceiverService.DELETE_INTENT,
|
|
120
|
+
new String[] {"notification"},
|
|
121
|
+
notificationModel.toBundle()));
|
|
122
|
+
// Resolve the effective pressAction bundle for the content intent.
|
|
123
|
+
// Three cases:
|
|
124
|
+
// 1. pressAction is null (absent from bundle, e.g. trigger rehydrated from Room DB
|
|
125
|
+
// after app kill): synthesize default { id:'default', launchActivity:'default' }
|
|
126
|
+
// so tapping the notification opens the app (defense-in-depth for paths that
|
|
127
|
+
// bypass the TS validator).
|
|
128
|
+
// 2. pressAction has the opt-out sentinel id: user explicitly passed
|
|
129
|
+
// pressAction: null in JS — pass null to createIntent so no launch intent
|
|
130
|
+
// is created (non-tappable notification).
|
|
131
|
+
// 3. pressAction is a normal bundle: pass through unchanged.
|
|
132
|
+
// Resolve the effective pressAction bundle for the content intent.
|
|
133
|
+
// Three cases:
|
|
134
|
+
// 1. pressAction is null (absent from bundle, e.g. trigger rehydrated from Room DB
|
|
135
|
+
// after app kill): synthesize default { id:'default', launchActivity:'default' }
|
|
136
|
+
// so tapping the notification opens the app (defense-in-depth for paths that
|
|
137
|
+
// bypass the TS validator).
|
|
138
|
+
// 2. pressAction has the opt-out sentinel id: user explicitly passed
|
|
139
|
+
// pressAction: null in JS — pass null to createIntent so no launch intent
|
|
140
|
+
// is created (non-tappable notification).
|
|
141
|
+
// 3. pressAction is a normal bundle: pass through unchanged.
|
|
142
|
+
//
|
|
143
|
+
// pressActionForIntent → used for creating the launch intent (or null for opt-out)
|
|
144
|
+
// pressActionForExtras → used in the receiver intent extras (event payload);
|
|
145
|
+
// null for cases 1 & 2 to avoid leaking synthesized defaults
|
|
146
|
+
// or the sentinel id into the JS event.
|
|
147
|
+
Bundle pressActionForIntent = androidModel.getPressAction();
|
|
148
|
+
Bundle pressActionForExtras = pressActionForIntent;
|
|
149
|
+
if (pressActionForIntent == null) {
|
|
150
|
+
// Case 1: absent — synthesize default for launch, null for extras
|
|
151
|
+
pressActionForIntent = new Bundle();
|
|
152
|
+
pressActionForIntent.putString("id", "default");
|
|
153
|
+
pressActionForIntent.putString("launchActivity", "default");
|
|
154
|
+
// pressActionForExtras stays null: the original notification didn't have
|
|
155
|
+
// pressAction, so the event shouldn't either.
|
|
156
|
+
} else if (NotificationPendingIntent.PRESS_ACTION_OPT_OUT_ID.equals(
|
|
157
|
+
pressActionForIntent.getString("id"))) {
|
|
158
|
+
// Case 2: explicit opt-out sentinel — no launch intent, no sentinel in extras
|
|
159
|
+
pressActionForIntent = null;
|
|
160
|
+
pressActionForExtras = null;
|
|
161
|
+
}
|
|
162
|
+
// Case 3: normal pressAction — both variables point to the original bundle
|
|
163
|
+
|
|
164
|
+
int targetSdkVersion =
|
|
165
|
+
ContextHolder.getApplicationContext().getApplicationInfo().targetSdkVersion;
|
|
166
|
+
if (targetSdkVersion >= Build.VERSION_CODES.S
|
|
167
|
+
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
168
|
+
builder.setContentIntent(
|
|
169
|
+
NotificationPendingIntent.createIntent(
|
|
170
|
+
notificationModel.getHashCode(),
|
|
171
|
+
pressActionForIntent,
|
|
172
|
+
TYPE_PRESS,
|
|
173
|
+
new String[] {"notification", "pressAction"},
|
|
174
|
+
notificationModel.toBundle(),
|
|
175
|
+
pressActionForExtras));
|
|
176
|
+
} else {
|
|
177
|
+
builder.setContentIntent(
|
|
178
|
+
ReceiverService.createIntent(
|
|
179
|
+
ReceiverService.PRESS_INTENT,
|
|
180
|
+
new String[] {"notification", "pressAction"},
|
|
181
|
+
notificationModel.toBundle(),
|
|
182
|
+
pressActionForIntent));
|
|
183
|
+
}
|
|
138
184
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
185
|
+
if (notificationModel.getTitle() != null) {
|
|
186
|
+
builder.setContentTitle(TextUtils.fromHtml(notificationModel.getTitle()));
|
|
187
|
+
}
|
|
142
188
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
189
|
+
if (notificationModel.getSubTitle() != null) {
|
|
190
|
+
builder.setSubText(TextUtils.fromHtml(notificationModel.getSubTitle()));
|
|
191
|
+
}
|
|
146
192
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
193
|
+
if (notificationModel.getBody() != null) {
|
|
194
|
+
builder.setContentText(TextUtils.fromHtml(notificationModel.getBody()));
|
|
195
|
+
}
|
|
150
196
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
197
|
+
if (androidModel.getBadgeIconType() != null) {
|
|
198
|
+
builder.setBadgeIconType(androidModel.getBadgeIconType());
|
|
199
|
+
}
|
|
154
200
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
201
|
+
if (androidModel.getCategory() != null) {
|
|
202
|
+
builder.setCategory(androidModel.getCategory());
|
|
203
|
+
}
|
|
158
204
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
205
|
+
if (androidModel.getColor() != null) {
|
|
206
|
+
builder.setColor(androidModel.getColor());
|
|
207
|
+
}
|
|
162
208
|
|
|
163
|
-
|
|
209
|
+
builder.setColorized(androidModel.getColorized());
|
|
164
210
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
211
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
|
212
|
+
builder.setChronometerCountDown(androidModel.getChronometerCountDown());
|
|
213
|
+
}
|
|
168
214
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
215
|
+
if (androidModel.getGroup() != null) {
|
|
216
|
+
builder.setGroup(androidModel.getGroup());
|
|
217
|
+
}
|
|
172
218
|
|
|
173
|
-
|
|
174
|
-
|
|
219
|
+
builder.setGroupAlertBehavior(androidModel.getGroupAlertBehaviour());
|
|
220
|
+
builder.setGroupSummary(androidModel.getGroupSummary());
|
|
175
221
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
222
|
+
if (androidModel.getInputHistory() != null) {
|
|
223
|
+
builder.setRemoteInputHistory(androidModel.getInputHistory());
|
|
224
|
+
}
|
|
179
225
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
226
|
+
if (androidModel.getLights() != null) {
|
|
227
|
+
ArrayList<Integer> lights = androidModel.getLights();
|
|
228
|
+
builder.setLights(lights.get(0), lights.get(1), lights.get(2));
|
|
229
|
+
}
|
|
184
230
|
|
|
185
|
-
|
|
231
|
+
builder.setLocalOnly(androidModel.getLocalOnly());
|
|
186
232
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
233
|
+
if (androidModel.getNumber() != null) {
|
|
234
|
+
builder.setNumber(androidModel.getNumber());
|
|
235
|
+
}
|
|
190
236
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
237
|
+
if (androidModel.getSound() != null) {
|
|
238
|
+
Uri soundUri = ResourceUtils.getSoundUri(androidModel.getSound());
|
|
239
|
+
if (soundUri != null) {
|
|
240
|
+
hasCustomSound = true;
|
|
241
|
+
builder.setSound(soundUri);
|
|
242
|
+
} else {
|
|
243
|
+
Logger.w(
|
|
244
|
+
TAG,
|
|
245
|
+
"Unable to retrieve sound for notification, sound was specified as: "
|
|
246
|
+
+ androidModel.getSound());
|
|
247
|
+
}
|
|
201
248
|
}
|
|
202
|
-
}
|
|
203
249
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
250
|
+
builder.setDefaults(androidModel.getDefaults(hasCustomSound));
|
|
251
|
+
builder.setOngoing(androidModel.getOngoing());
|
|
252
|
+
builder.setOnlyAlertOnce(androidModel.getOnlyAlertOnce());
|
|
253
|
+
builder.setPriority(androidModel.getPriority());
|
|
208
254
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
255
|
+
NotificationAndroidModel.AndroidProgress progress = androidModel.getProgress();
|
|
256
|
+
if (progress != null) {
|
|
257
|
+
builder.setProgress(
|
|
258
|
+
progress.getMax(), progress.getCurrent(), progress.getIndeterminate());
|
|
259
|
+
}
|
|
214
260
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
261
|
+
if (androidModel.getShortcutId() != null) {
|
|
262
|
+
builder.setShortcutId(androidModel.getShortcutId());
|
|
263
|
+
}
|
|
218
264
|
|
|
219
|
-
|
|
265
|
+
builder.setShowWhen(androidModel.getShowTimestamp());
|
|
220
266
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
267
|
+
Integer smallIconId = androidModel.getSmallIcon();
|
|
268
|
+
if (smallIconId != null) {
|
|
269
|
+
Integer smallIconLevel = androidModel.getSmallIconLevel();
|
|
270
|
+
if (smallIconLevel != null) {
|
|
271
|
+
builder.setSmallIcon(smallIconId, smallIconLevel);
|
|
272
|
+
} else {
|
|
273
|
+
builder.setSmallIcon(smallIconId);
|
|
274
|
+
}
|
|
228
275
|
}
|
|
229
|
-
}
|
|
230
276
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
277
|
+
if (androidModel.getSortKey() != null) {
|
|
278
|
+
builder.setSortKey(androidModel.getSortKey());
|
|
279
|
+
}
|
|
234
280
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
281
|
+
if (androidModel.getTicker() != null) {
|
|
282
|
+
builder.setTicker(androidModel.getTicker());
|
|
283
|
+
}
|
|
238
284
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
285
|
+
if (androidModel.getTimeoutAfter() != null) {
|
|
286
|
+
builder.setTimeoutAfter(androidModel.getTimeoutAfter());
|
|
287
|
+
}
|
|
242
288
|
|
|
243
|
-
|
|
289
|
+
builder.setUsesChronometer(androidModel.getShowChronometer());
|
|
244
290
|
|
|
245
|
-
|
|
246
|
-
|
|
291
|
+
long[] vibrationPattern = androidModel.getVibrationPattern();
|
|
292
|
+
if (vibrationPattern.length > 0) builder.setVibrate(vibrationPattern);
|
|
247
293
|
|
|
248
|
-
|
|
294
|
+
builder.setVisibility(androidModel.getVisibility());
|
|
249
295
|
|
|
250
|
-
|
|
251
|
-
|
|
296
|
+
long timestamp = androidModel.getTimestamp();
|
|
297
|
+
if (timestamp > -1) builder.setWhen(timestamp);
|
|
252
298
|
|
|
253
|
-
|
|
299
|
+
builder.setAutoCancel(androidModel.getAutoCancel());
|
|
254
300
|
|
|
255
|
-
|
|
301
|
+
return builder;
|
|
302
|
+
} finally {
|
|
303
|
+
Trace.endSection();
|
|
304
|
+
}
|
|
256
305
|
};
|
|
257
306
|
|
|
258
307
|
/*
|
|
@@ -534,7 +583,8 @@ class NotificationManager {
|
|
|
534
583
|
Logger.e(
|
|
535
584
|
TAG,
|
|
536
585
|
"cancelAllNotificationsWithIds -> Failed to parse id as integer "
|
|
537
|
-
+ id
|
|
586
|
+
+ id,
|
|
587
|
+
e);
|
|
538
588
|
}
|
|
539
589
|
|
|
540
590
|
if (integerId != null) {
|
|
@@ -577,47 +627,64 @@ class NotificationManager {
|
|
|
577
627
|
return new ExtendedListenableFuture<>(notificationBundleToBuilder(notificationModel))
|
|
578
628
|
.continueWith(
|
|
579
629
|
(taskResult) -> {
|
|
580
|
-
|
|
630
|
+
Trace.beginSection("notifee:displayNotification");
|
|
631
|
+
try {
|
|
632
|
+
NotificationCompat.Builder builder = taskResult;
|
|
633
|
+
|
|
634
|
+
// Add the following extras for `getDisplayedNotifications()`
|
|
635
|
+
Bundle extrasBundle = new Bundle();
|
|
636
|
+
extrasBundle.putBundle(EXTRA_NOTIFEE_NOTIFICATION, notificationModel.toBundle());
|
|
637
|
+
if (triggerBundle != null) {
|
|
638
|
+
extrasBundle.putBundle(EXTRA_NOTIFEE_TRIGGER, triggerBundle);
|
|
639
|
+
}
|
|
640
|
+
builder.addExtras(extrasBundle);
|
|
581
641
|
|
|
582
|
-
|
|
583
|
-
Bundle extrasBundle = new Bundle();
|
|
584
|
-
extrasBundle.putBundle(EXTRA_NOTIFEE_NOTIFICATION, notificationModel.toBundle());
|
|
585
|
-
if (triggerBundle != null) {
|
|
586
|
-
extrasBundle.putBundle(EXTRA_NOTIFEE_TRIGGER, triggerBundle);
|
|
587
|
-
}
|
|
588
|
-
builder.addExtras(extrasBundle);
|
|
642
|
+
NotificationAndroidModel androidBundle = notificationModel.getAndroid();
|
|
589
643
|
|
|
590
|
-
|
|
591
|
-
|
|
644
|
+
// Set foreground service behavior before building (only for FGS notifications).
|
|
645
|
+
// IMMEDIATE eliminates the 10-second display delay on Android 12+.
|
|
646
|
+
if (androidBundle.getAsForegroundService()) {
|
|
647
|
+
builder.setForegroundServiceBehavior(
|
|
648
|
+
androidBundle.getForegroundServiceBehavior());
|
|
649
|
+
}
|
|
592
650
|
|
|
593
|
-
|
|
651
|
+
// build notification
|
|
652
|
+
Notification notification = Objects.requireNonNull(builder).build();
|
|
594
653
|
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
654
|
+
int hashCode = notificationModel.getHashCode();
|
|
655
|
+
if (androidBundle.getLoopSound()) {
|
|
656
|
+
notification.flags |= Notification.FLAG_INSISTENT;
|
|
657
|
+
}
|
|
599
658
|
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
659
|
+
if (androidBundle.getFlags() != null && androidBundle.getFlags().length > 0) {
|
|
660
|
+
for (int flag : androidBundle.getFlags()) {
|
|
661
|
+
notification.flags |= flag;
|
|
662
|
+
}
|
|
603
663
|
}
|
|
604
|
-
}
|
|
605
664
|
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
665
|
+
if (androidBundle.getLightUpScreen()) {
|
|
666
|
+
PowerManagerUtils.lightUpScreenIfNeeded(ContextHolder.getApplicationContext());
|
|
667
|
+
}
|
|
609
668
|
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
669
|
+
if (androidBundle.getAsForegroundService()) {
|
|
670
|
+
Trace.beginSection("notifee:startForegroundService");
|
|
671
|
+
try {
|
|
672
|
+
ForegroundService.start(hashCode, notification, notificationModel.toBundle());
|
|
673
|
+
} finally {
|
|
674
|
+
Trace.endSection();
|
|
675
|
+
}
|
|
676
|
+
} else {
|
|
677
|
+
NotificationManagerCompat.from(getApplicationContext())
|
|
678
|
+
.notify(androidBundle.getTag(), hashCode, notification);
|
|
679
|
+
}
|
|
616
680
|
|
|
617
|
-
|
|
618
|
-
|
|
681
|
+
EventBus.post(
|
|
682
|
+
new NotificationEvent(NotificationEvent.TYPE_DELIVERED, notificationModel));
|
|
619
683
|
|
|
620
|
-
|
|
684
|
+
return Futures.immediateFuture(null);
|
|
685
|
+
} finally {
|
|
686
|
+
Trace.endSection();
|
|
687
|
+
}
|
|
621
688
|
},
|
|
622
689
|
CACHED_THREAD_POOL);
|
|
623
690
|
}
|
|
@@ -31,6 +31,16 @@ public class NotificationPendingIntent {
|
|
|
31
31
|
public static final String NOTIFICATION_ID_INTENT_KEY = "notification_id";
|
|
32
32
|
private static final String TAG = "NotificationPendingIntent";
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Reserved sentinel pressAction id used to signal an explicit opt-out from tap-to-open behavior.
|
|
36
|
+
* When the TS validator sees {@code pressAction: null}, it emits a pressAction bundle with this
|
|
37
|
+
* id so the native layer can distinguish "user opted out" from "pressAction absent" (which
|
|
38
|
+
* triggers the default). DO NOT use this as a real pressAction id.
|
|
39
|
+
*
|
|
40
|
+
* <p>The same constant is mirrored in {@code validateAndroidNotification.ts}.
|
|
41
|
+
*/
|
|
42
|
+
public static final String PRESS_ACTION_OPT_OUT_ID = "__NOTIFEE_OPT_OUT__";
|
|
43
|
+
|
|
34
44
|
/**
|
|
35
45
|
* Creates a PendingIntent, which when sent triggers this class.
|
|
36
46
|
*
|
|
@@ -0,0 +1,84 @@
|
|
|
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.content.Context;
|
|
21
|
+
import android.os.Trace;
|
|
22
|
+
import androidx.core.app.NotificationManagerCompat;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Encapsulates the foreground-service warmup logic so it can be called from both {@link
|
|
26
|
+
* InitProvider} (automatic, at app startup) and the JS bridge ({@code prewarmForegroundService()},
|
|
27
|
+
* on-demand).
|
|
28
|
+
*
|
|
29
|
+
* <p>This class runs <b>synchronously</b> on the caller's thread. Callers are responsible for
|
|
30
|
+
* choosing the appropriate thread/executor.
|
|
31
|
+
*
|
|
32
|
+
* <p>Safe to call multiple times (idempotent). After the first call, class loading is a no-op from
|
|
33
|
+
* ART's perspective and the Binder proxy call is cheap.
|
|
34
|
+
*/
|
|
35
|
+
@KeepForSdk
|
|
36
|
+
public final class WarmupHelper {
|
|
37
|
+
|
|
38
|
+
private static final String TAG = "WarmupHelper";
|
|
39
|
+
|
|
40
|
+
static final String[] WARMUP_CLASSES = {
|
|
41
|
+
"app.notifee.core.ForegroundService",
|
|
42
|
+
"app.notifee.core.NotificationManager",
|
|
43
|
+
"app.notifee.core.model.NotificationModel",
|
|
44
|
+
"app.notifee.core.model.NotificationAndroidModel",
|
|
45
|
+
"app.notifee.core.model.ChannelModel",
|
|
46
|
+
"androidx.core.app.NotificationCompat$Builder",
|
|
47
|
+
"androidx.core.app.NotificationManagerCompat",
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
private WarmupHelper() {}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Pre-loads critical foreground-service classes and warms the INotificationManager Binder proxy.
|
|
54
|
+
* Runs synchronously on the caller's thread.
|
|
55
|
+
*
|
|
56
|
+
* @param context Application context used for class loading and Binder warmup.
|
|
57
|
+
*/
|
|
58
|
+
@KeepForSdk
|
|
59
|
+
public static void runWarmup(Context context) {
|
|
60
|
+
Trace.beginSection("notifee:warmup");
|
|
61
|
+
try {
|
|
62
|
+
ClassLoader classLoader = context.getClassLoader();
|
|
63
|
+
|
|
64
|
+
// Pre-load critical foreground service classes to move ART class loading/verification
|
|
65
|
+
// cost from the first displayNotification() call to whenever the caller chooses.
|
|
66
|
+
for (String className : WARMUP_CLASSES) {
|
|
67
|
+
try {
|
|
68
|
+
Class.forName(className, true, classLoader);
|
|
69
|
+
} catch (ClassNotFoundException e) {
|
|
70
|
+
Logger.d(TAG, "Warmup class not found: " + className);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Pre-warm INotificationManager Binder proxy by touching NotificationManagerCompat.
|
|
75
|
+
try {
|
|
76
|
+
NotificationManagerCompat.from(context).getNotificationChannels();
|
|
77
|
+
} catch (Exception e) {
|
|
78
|
+
Logger.d(TAG, "Warmup Binder pre-warm failed: " + e.getMessage());
|
|
79
|
+
}
|
|
80
|
+
} finally {
|
|
81
|
+
Trace.endSection();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -74,6 +74,11 @@ public class NotificationAndroidModel {
|
|
|
74
74
|
Objects.requireNonNull(
|
|
75
75
|
mNotificationAndroidBundle.getParcelableArrayList("foregroundServiceTypes"));
|
|
76
76
|
|
|
77
|
+
if (foregroundServiceTypesArrayList.isEmpty()) {
|
|
78
|
+
Logger.w(TAG, "foregroundServiceTypes is empty; treating as absent");
|
|
79
|
+
return ServiceInfo.FOREGROUND_SERVICE_TYPE_MANIFEST;
|
|
80
|
+
}
|
|
81
|
+
|
|
77
82
|
int foregroundServiceType = 0;
|
|
78
83
|
for (int i = 0; i < foregroundServiceTypesArrayList.size(); i++) {
|
|
79
84
|
foregroundServiceType |= ObjectUtils.getInt(foregroundServiceTypesArrayList.get(i));
|
|
@@ -97,6 +102,17 @@ public class NotificationAndroidModel {
|
|
|
97
102
|
return mNotificationAndroidBundle.getBoolean("asForegroundService", false);
|
|
98
103
|
}
|
|
99
104
|
|
|
105
|
+
/**
|
|
106
|
+
* Gets the foreground service behavior for the notification. Controls whether the foreground
|
|
107
|
+
* service notification is shown immediately or deferred on Android 12+.
|
|
108
|
+
*
|
|
109
|
+
* @return int matching a NotificationCompat.FOREGROUND_SERVICE_* constant
|
|
110
|
+
*/
|
|
111
|
+
public int getForegroundServiceBehavior() {
|
|
112
|
+
return mNotificationAndroidBundle.getInt(
|
|
113
|
+
"foregroundServiceBehavior", NotificationCompat.FOREGROUND_SERVICE_DEFAULT);
|
|
114
|
+
}
|
|
115
|
+
|
|
100
116
|
/**
|
|
101
117
|
* Gets if the notification should light up the screen when displayed
|
|
102
118
|
*
|
|
@@ -328,7 +344,7 @@ public class NotificationAndroidModel {
|
|
|
328
344
|
|
|
329
345
|
return lights;
|
|
330
346
|
} catch (Exception e) {
|
|
331
|
-
Logger.e(TAG, "getLights -> Failed to parse lights");
|
|
347
|
+
Logger.e(TAG, "getLights -> Failed to parse lights", e);
|
|
332
348
|
return null;
|
|
333
349
|
}
|
|
334
350
|
}
|
|
@@ -317,6 +317,36 @@ class NotifeeApiModule(reactContext: ReactApplicationContext) :
|
|
|
317
317
|
}
|
|
318
318
|
}
|
|
319
319
|
|
|
320
|
+
override fun prewarmForegroundService(promise: Promise) {
|
|
321
|
+
val context = reactApplicationContext.applicationContext ?: run {
|
|
322
|
+
promise.reject("ERR_CONTEXT", "ReactApplicationContext is null")
|
|
323
|
+
return
|
|
324
|
+
}
|
|
325
|
+
val executor = java.util.concurrent.Executors.newSingleThreadExecutor { r ->
|
|
326
|
+
Thread(r, "notifee-prewarm").apply {
|
|
327
|
+
isDaemon = true
|
|
328
|
+
priority = Thread.MIN_PRIORITY
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
executor.submit {
|
|
332
|
+
android.os.Trace.beginSection("notifee:prewarm")
|
|
333
|
+
try {
|
|
334
|
+
app.notifee.core.WarmupHelper.runWarmup(context)
|
|
335
|
+
promise.resolve(null)
|
|
336
|
+
} catch (t: Throwable) {
|
|
337
|
+
// Best-effort semantics: warmup failures don't reject the promise.
|
|
338
|
+
// WarmupHelper logs and swallows internal errors; if something
|
|
339
|
+
// unexpectedly escapes, log it and still resolve so the JS-side
|
|
340
|
+
// await does not hang.
|
|
341
|
+
Logger.e("NotifeeApiModule", "prewarmForegroundService unexpectedly threw", t)
|
|
342
|
+
promise.resolve(null)
|
|
343
|
+
} finally {
|
|
344
|
+
android.os.Trace.endSection()
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
executor.shutdown()
|
|
348
|
+
}
|
|
349
|
+
|
|
320
350
|
override fun hideNotificationDrawer() {
|
|
321
351
|
NotifeeReactUtils.hideNotificationDrawer()
|
|
322
352
|
}
|