react-native-notify-kit 9.3.0 → 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 +62 -2
- package/android/build.gradle +4 -0
- 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 -192
- 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/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 +58 -0
- 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 +36 -12
- package/dist/validators/validateAndroidNotification.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 +5 -5
- package/ios/NotifeeCore/NotifeeCore.m +28 -9
- package/ios/NotifeeCore/NotifeeCoreDelegateHolder.m +5 -1
- package/ios/NotifeeCore/NotifeeCoreExtensionHelper.m +7 -2
- package/ios/RNNotifee/NotifeeApiModule.mm +31 -10
- 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 +62 -0
- package/src/utils/validate.ts +12 -4
- package/src/validators/validateAndroidChannel.ts +3 -3
- package/src/validators/validateAndroidNotification.ts +43 -10
- 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,198 +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
|
-
// Resolve the effective pressAction bundle for the content intent.
|
|
119
|
-
// Three cases:
|
|
120
|
-
// 1. pressAction is null (absent from bundle, e.g. trigger rehydrated from Room DB
|
|
121
|
-
// after app kill): synthesize default { id:'default', launchActivity:'default' }
|
|
122
|
-
// so tapping the notification opens the app (defense-in-depth for paths that
|
|
123
|
-
// bypass the TS validator).
|
|
124
|
-
// 2. pressAction has the opt-out sentinel id: user explicitly passed
|
|
125
|
-
// pressAction: null in JS — pass null to createIntent so no launch intent
|
|
126
|
-
// is created (non-tappable notification).
|
|
127
|
-
// 3. pressAction is a normal bundle: pass through unchanged.
|
|
128
|
-
// Resolve the effective pressAction bundle for the content intent.
|
|
129
|
-
// Three cases:
|
|
130
|
-
// 1. pressAction is null (absent from bundle, e.g. trigger rehydrated from Room DB
|
|
131
|
-
// after app kill): synthesize default { id:'default', launchActivity:'default' }
|
|
132
|
-
// so tapping the notification opens the app (defense-in-depth for paths that
|
|
133
|
-
// bypass the TS validator).
|
|
134
|
-
// 2. pressAction has the opt-out sentinel id: user explicitly passed
|
|
135
|
-
// pressAction: null in JS — pass null to createIntent so no launch intent
|
|
136
|
-
// is created (non-tappable notification).
|
|
137
|
-
// 3. pressAction is a normal bundle: pass through unchanged.
|
|
138
|
-
//
|
|
139
|
-
// pressActionForIntent → used for creating the launch intent (or null for opt-out)
|
|
140
|
-
// pressActionForExtras → used in the receiver intent extras (event payload);
|
|
141
|
-
// null for cases 1 & 2 to avoid leaking synthesized defaults
|
|
142
|
-
// or the sentinel id into the JS event.
|
|
143
|
-
Bundle pressActionForIntent = androidModel.getPressAction();
|
|
144
|
-
Bundle pressActionForExtras = pressActionForIntent;
|
|
145
|
-
if (pressActionForIntent == null) {
|
|
146
|
-
// Case 1: absent — synthesize default for launch, null for extras
|
|
147
|
-
pressActionForIntent = new Bundle();
|
|
148
|
-
pressActionForIntent.putString("id", "default");
|
|
149
|
-
pressActionForIntent.putString("launchActivity", "default");
|
|
150
|
-
// pressActionForExtras stays null: the original notification didn't have
|
|
151
|
-
// pressAction, so the event shouldn't either.
|
|
152
|
-
} else if (NotificationPendingIntent.PRESS_ACTION_OPT_OUT_ID.equals(
|
|
153
|
-
pressActionForIntent.getString("id"))) {
|
|
154
|
-
// Case 2: explicit opt-out sentinel — no launch intent, no sentinel in extras
|
|
155
|
-
pressActionForIntent = null;
|
|
156
|
-
pressActionForExtras = null;
|
|
157
|
-
}
|
|
158
|
-
// Case 3: normal pressAction — both variables point to the original bundle
|
|
159
|
-
|
|
160
|
-
int targetSdkVersion =
|
|
161
|
-
ContextHolder.getApplicationContext().getApplicationInfo().targetSdkVersion;
|
|
162
|
-
if (targetSdkVersion >= Build.VERSION_CODES.S
|
|
163
|
-
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
164
|
-
builder.setContentIntent(
|
|
165
|
-
NotificationPendingIntent.createIntent(
|
|
166
|
-
notificationModel.getHashCode(),
|
|
167
|
-
pressActionForIntent,
|
|
168
|
-
TYPE_PRESS,
|
|
169
|
-
new String[] {"notification", "pressAction"},
|
|
170
|
-
notificationModel.toBundle(),
|
|
171
|
-
pressActionForExtras));
|
|
172
|
-
} else {
|
|
173
|
-
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(
|
|
174
118
|
ReceiverService.createIntent(
|
|
175
|
-
ReceiverService.
|
|
176
|
-
new String[] {"notification"
|
|
177
|
-
notificationModel.toBundle()
|
|
178
|
-
|
|
179
|
-
|
|
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
|
+
}
|
|
180
184
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
185
|
+
if (notificationModel.getTitle() != null) {
|
|
186
|
+
builder.setContentTitle(TextUtils.fromHtml(notificationModel.getTitle()));
|
|
187
|
+
}
|
|
184
188
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
189
|
+
if (notificationModel.getSubTitle() != null) {
|
|
190
|
+
builder.setSubText(TextUtils.fromHtml(notificationModel.getSubTitle()));
|
|
191
|
+
}
|
|
188
192
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
193
|
+
if (notificationModel.getBody() != null) {
|
|
194
|
+
builder.setContentText(TextUtils.fromHtml(notificationModel.getBody()));
|
|
195
|
+
}
|
|
192
196
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
197
|
+
if (androidModel.getBadgeIconType() != null) {
|
|
198
|
+
builder.setBadgeIconType(androidModel.getBadgeIconType());
|
|
199
|
+
}
|
|
196
200
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
201
|
+
if (androidModel.getCategory() != null) {
|
|
202
|
+
builder.setCategory(androidModel.getCategory());
|
|
203
|
+
}
|
|
200
204
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
205
|
+
if (androidModel.getColor() != null) {
|
|
206
|
+
builder.setColor(androidModel.getColor());
|
|
207
|
+
}
|
|
204
208
|
|
|
205
|
-
|
|
209
|
+
builder.setColorized(androidModel.getColorized());
|
|
206
210
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
211
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
|
212
|
+
builder.setChronometerCountDown(androidModel.getChronometerCountDown());
|
|
213
|
+
}
|
|
210
214
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
215
|
+
if (androidModel.getGroup() != null) {
|
|
216
|
+
builder.setGroup(androidModel.getGroup());
|
|
217
|
+
}
|
|
214
218
|
|
|
215
|
-
|
|
216
|
-
|
|
219
|
+
builder.setGroupAlertBehavior(androidModel.getGroupAlertBehaviour());
|
|
220
|
+
builder.setGroupSummary(androidModel.getGroupSummary());
|
|
217
221
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
222
|
+
if (androidModel.getInputHistory() != null) {
|
|
223
|
+
builder.setRemoteInputHistory(androidModel.getInputHistory());
|
|
224
|
+
}
|
|
221
225
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
+
if (androidModel.getLights() != null) {
|
|
227
|
+
ArrayList<Integer> lights = androidModel.getLights();
|
|
228
|
+
builder.setLights(lights.get(0), lights.get(1), lights.get(2));
|
|
229
|
+
}
|
|
226
230
|
|
|
227
|
-
|
|
231
|
+
builder.setLocalOnly(androidModel.getLocalOnly());
|
|
228
232
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
233
|
+
if (androidModel.getNumber() != null) {
|
|
234
|
+
builder.setNumber(androidModel.getNumber());
|
|
235
|
+
}
|
|
232
236
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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
|
+
}
|
|
243
248
|
}
|
|
244
|
-
}
|
|
245
249
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
+
builder.setDefaults(androidModel.getDefaults(hasCustomSound));
|
|
251
|
+
builder.setOngoing(androidModel.getOngoing());
|
|
252
|
+
builder.setOnlyAlertOnce(androidModel.getOnlyAlertOnce());
|
|
253
|
+
builder.setPriority(androidModel.getPriority());
|
|
250
254
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
255
|
+
NotificationAndroidModel.AndroidProgress progress = androidModel.getProgress();
|
|
256
|
+
if (progress != null) {
|
|
257
|
+
builder.setProgress(
|
|
258
|
+
progress.getMax(), progress.getCurrent(), progress.getIndeterminate());
|
|
259
|
+
}
|
|
256
260
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
261
|
+
if (androidModel.getShortcutId() != null) {
|
|
262
|
+
builder.setShortcutId(androidModel.getShortcutId());
|
|
263
|
+
}
|
|
260
264
|
|
|
261
|
-
|
|
265
|
+
builder.setShowWhen(androidModel.getShowTimestamp());
|
|
262
266
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
+
}
|
|
270
275
|
}
|
|
271
|
-
}
|
|
272
276
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
277
|
+
if (androidModel.getSortKey() != null) {
|
|
278
|
+
builder.setSortKey(androidModel.getSortKey());
|
|
279
|
+
}
|
|
276
280
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
281
|
+
if (androidModel.getTicker() != null) {
|
|
282
|
+
builder.setTicker(androidModel.getTicker());
|
|
283
|
+
}
|
|
280
284
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
285
|
+
if (androidModel.getTimeoutAfter() != null) {
|
|
286
|
+
builder.setTimeoutAfter(androidModel.getTimeoutAfter());
|
|
287
|
+
}
|
|
284
288
|
|
|
285
|
-
|
|
289
|
+
builder.setUsesChronometer(androidModel.getShowChronometer());
|
|
286
290
|
|
|
287
|
-
|
|
288
|
-
|
|
291
|
+
long[] vibrationPattern = androidModel.getVibrationPattern();
|
|
292
|
+
if (vibrationPattern.length > 0) builder.setVibrate(vibrationPattern);
|
|
289
293
|
|
|
290
|
-
|
|
294
|
+
builder.setVisibility(androidModel.getVisibility());
|
|
291
295
|
|
|
292
|
-
|
|
293
|
-
|
|
296
|
+
long timestamp = androidModel.getTimestamp();
|
|
297
|
+
if (timestamp > -1) builder.setWhen(timestamp);
|
|
294
298
|
|
|
295
|
-
|
|
299
|
+
builder.setAutoCancel(androidModel.getAutoCancel());
|
|
296
300
|
|
|
297
|
-
|
|
301
|
+
return builder;
|
|
302
|
+
} finally {
|
|
303
|
+
Trace.endSection();
|
|
304
|
+
}
|
|
298
305
|
};
|
|
299
306
|
|
|
300
307
|
/*
|
|
@@ -576,7 +583,8 @@ class NotificationManager {
|
|
|
576
583
|
Logger.e(
|
|
577
584
|
TAG,
|
|
578
585
|
"cancelAllNotificationsWithIds -> Failed to parse id as integer "
|
|
579
|
-
+ id
|
|
586
|
+
+ id,
|
|
587
|
+
e);
|
|
580
588
|
}
|
|
581
589
|
|
|
582
590
|
if (integerId != null) {
|
|
@@ -619,47 +627,64 @@ class NotificationManager {
|
|
|
619
627
|
return new ExtendedListenableFuture<>(notificationBundleToBuilder(notificationModel))
|
|
620
628
|
.continueWith(
|
|
621
629
|
(taskResult) -> {
|
|
622
|
-
|
|
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);
|
|
623
641
|
|
|
624
|
-
|
|
625
|
-
Bundle extrasBundle = new Bundle();
|
|
626
|
-
extrasBundle.putBundle(EXTRA_NOTIFEE_NOTIFICATION, notificationModel.toBundle());
|
|
627
|
-
if (triggerBundle != null) {
|
|
628
|
-
extrasBundle.putBundle(EXTRA_NOTIFEE_TRIGGER, triggerBundle);
|
|
629
|
-
}
|
|
630
|
-
builder.addExtras(extrasBundle);
|
|
642
|
+
NotificationAndroidModel androidBundle = notificationModel.getAndroid();
|
|
631
643
|
|
|
632
|
-
|
|
633
|
-
|
|
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
|
+
}
|
|
634
650
|
|
|
635
|
-
|
|
651
|
+
// build notification
|
|
652
|
+
Notification notification = Objects.requireNonNull(builder).build();
|
|
636
653
|
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
654
|
+
int hashCode = notificationModel.getHashCode();
|
|
655
|
+
if (androidBundle.getLoopSound()) {
|
|
656
|
+
notification.flags |= Notification.FLAG_INSISTENT;
|
|
657
|
+
}
|
|
641
658
|
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
659
|
+
if (androidBundle.getFlags() != null && androidBundle.getFlags().length > 0) {
|
|
660
|
+
for (int flag : androidBundle.getFlags()) {
|
|
661
|
+
notification.flags |= flag;
|
|
662
|
+
}
|
|
645
663
|
}
|
|
646
|
-
}
|
|
647
664
|
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
665
|
+
if (androidBundle.getLightUpScreen()) {
|
|
666
|
+
PowerManagerUtils.lightUpScreenIfNeeded(ContextHolder.getApplicationContext());
|
|
667
|
+
}
|
|
651
668
|
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
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
|
+
}
|
|
658
680
|
|
|
659
|
-
|
|
660
|
-
|
|
681
|
+
EventBus.post(
|
|
682
|
+
new NotificationEvent(NotificationEvent.TYPE_DELIVERED, notificationModel));
|
|
661
683
|
|
|
662
|
-
|
|
684
|
+
return Futures.immediateFuture(null);
|
|
685
|
+
} finally {
|
|
686
|
+
Trace.endSection();
|
|
687
|
+
}
|
|
663
688
|
},
|
|
664
689
|
CACHED_THREAD_POOL);
|
|
665
690
|
}
|
|
@@ -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
|
}
|