react-native-notify-kit 9.3.0 → 9.5.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 +68 -2
- package/android/build.gradle +5 -0
- package/android/proguard-rules.pro +1 -0
- package/android/src/androidTest/java/app/notifee/core/DoScheduledWorkOrderingTest.java +234 -0
- package/android/src/androidTest/java/app/notifee/core/RebootRecoveryTest.java +204 -0
- package/android/src/androidTest/java/app/notifee/core/database/TimingWorkDataRepository.java +56 -0
- package/android/src/androidTest/java/app/notifee/core/database/WorkDataRepositoryRaceTest.java +221 -0
- package/android/src/androidTest/res/drawable/test_icon.xml +14 -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/NotifeeAlarmManager.java +160 -79
- package/android/src/main/java/app/notifee/core/NotificationManager.java +370 -288
- package/android/src/main/java/app/notifee/core/WarmupHelper.java +84 -0
- package/android/src/main/java/app/notifee/core/database/WorkDataRepository.java +38 -34
- 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/database/WorkDataRepositoryFutureContractTest.java +279 -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
|
/*
|
|
@@ -536,15 +543,13 @@ class NotificationManager {
|
|
|
536
543
|
task -> {
|
|
537
544
|
if (notificationType == NOTIFICATION_TYPE_TRIGGER
|
|
538
545
|
|| notificationType == NOTIFICATION_TYPE_ALL) {
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
},
|
|
547
|
-
LISTENING_CACHED_THREAD_POOL);
|
|
546
|
+
// Chain the Room delete onto the alarm-manager cancel so the outer
|
|
547
|
+
// future — and therefore the JS Promise — only completes after Room
|
|
548
|
+
// has drained. Fixes upstream invertase/notifee#549.
|
|
549
|
+
return Futures.transformAsync(
|
|
550
|
+
NotifeeAlarmManager.cancelAllNotifications(),
|
|
551
|
+
ignored -> WorkDataRepository.getInstance(getApplicationContext()).deleteAll(),
|
|
552
|
+
LISTENING_CACHED_THREAD_POOL);
|
|
548
553
|
}
|
|
549
554
|
return Futures.immediateFuture(null);
|
|
550
555
|
},
|
|
@@ -576,7 +581,8 @@ class NotificationManager {
|
|
|
576
581
|
Logger.e(
|
|
577
582
|
TAG,
|
|
578
583
|
"cancelAllNotificationsWithIds -> Failed to parse id as integer "
|
|
579
|
-
+ id
|
|
584
|
+
+ id,
|
|
585
|
+
e);
|
|
580
586
|
}
|
|
581
587
|
|
|
582
588
|
if (integerId != null) {
|
|
@@ -605,9 +611,11 @@ class NotificationManager {
|
|
|
605
611
|
}))
|
|
606
612
|
.continueWith(
|
|
607
613
|
task -> {
|
|
608
|
-
// delete
|
|
614
|
+
// Chain the Room delete so the outer future — and therefore the JS
|
|
615
|
+
// Promise — only completes after Room has drained. Fixes upstream
|
|
616
|
+
// invertase/notifee#549 for the per-id cancel path.
|
|
609
617
|
if (notificationType != NOTIFICATION_TYPE_DISPLAYED) {
|
|
610
|
-
WorkDataRepository.getInstance(getApplicationContext()).deleteByIds(ids);
|
|
618
|
+
return WorkDataRepository.getInstance(getApplicationContext()).deleteByIds(ids);
|
|
611
619
|
}
|
|
612
620
|
return Futures.immediateFuture(null);
|
|
613
621
|
},
|
|
@@ -619,103 +627,139 @@ 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
|
}
|
|
666
691
|
|
|
667
692
|
static ListenableFuture<Void> createTriggerNotification(
|
|
668
693
|
NotificationModel notificationModel, Bundle triggerBundle) {
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
694
|
+
int triggerType = ObjectUtils.getInt(triggerBundle.get("type"));
|
|
695
|
+
ListenableFuture<Void> scheduleFuture;
|
|
696
|
+
switch (triggerType) {
|
|
697
|
+
case 0:
|
|
698
|
+
scheduleFuture = createTimestampTriggerNotification(notificationModel, triggerBundle);
|
|
699
|
+
break;
|
|
700
|
+
case 1:
|
|
701
|
+
scheduleFuture = createIntervalTriggerNotification(notificationModel, triggerBundle);
|
|
702
|
+
break;
|
|
703
|
+
default:
|
|
704
|
+
scheduleFuture = Futures.immediateFuture(null);
|
|
705
|
+
break;
|
|
706
|
+
}
|
|
680
707
|
|
|
708
|
+
return Futures.transform(
|
|
709
|
+
scheduleFuture,
|
|
710
|
+
unused -> {
|
|
681
711
|
EventBus.post(
|
|
682
712
|
new NotificationEvent(
|
|
683
713
|
NotificationEvent.TYPE_TRIGGER_NOTIFICATION_CREATED, notificationModel));
|
|
684
|
-
|
|
685
714
|
return null;
|
|
686
|
-
}
|
|
715
|
+
},
|
|
716
|
+
LISTENING_CACHED_THREAD_POOL);
|
|
687
717
|
}
|
|
688
718
|
|
|
689
|
-
|
|
719
|
+
// Returns a future that completes only after the Room insert has persisted AND
|
|
720
|
+
// WorkManager has enqueued the periodic work. Chaining Room-first guarantees the
|
|
721
|
+
// worker reads the row on its first fire even if the process is killed between
|
|
722
|
+
// the JS Promise resolving and WorkManager scheduling. Fixes upstream
|
|
723
|
+
// invertase/notifee#549 for the interval-trigger path.
|
|
724
|
+
static ListenableFuture<Void> createIntervalTriggerNotification(
|
|
690
725
|
NotificationModel notificationModel, Bundle triggerBundle) {
|
|
691
726
|
IntervalTriggerModel trigger = IntervalTriggerModel.fromBundle(triggerBundle);
|
|
692
727
|
String uniqueWorkName = "trigger:" + notificationModel.getId();
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
728
|
+
Context context = getApplicationContext();
|
|
729
|
+
|
|
730
|
+
ListenableFuture<Void> insertFuture =
|
|
731
|
+
WorkDataRepository.insertTriggerNotification(
|
|
732
|
+
context, notificationModel, triggerBundle, false);
|
|
733
|
+
|
|
734
|
+
return Futures.transform(
|
|
735
|
+
insertFuture,
|
|
736
|
+
unused -> {
|
|
737
|
+
WorkManager workManager = WorkManager.getInstance(context);
|
|
738
|
+
Data.Builder workDataBuilder =
|
|
739
|
+
new Data.Builder()
|
|
740
|
+
.putString(Worker.KEY_WORK_TYPE, Worker.WORK_TYPE_NOTIFICATION_TRIGGER)
|
|
741
|
+
.putString(Worker.KEY_WORK_REQUEST, Worker.WORK_REQUEST_PERIODIC)
|
|
742
|
+
.putString("id", notificationModel.getId());
|
|
743
|
+
|
|
744
|
+
long interval = trigger.getInterval();
|
|
745
|
+
PeriodicWorkRequest.Builder workRequestBuilder =
|
|
746
|
+
new PeriodicWorkRequest.Builder(Worker.class, interval, trigger.getTimeUnit())
|
|
747
|
+
.setInitialDelay(interval, trigger.getTimeUnit());
|
|
748
|
+
workRequestBuilder.addTag(Worker.WORK_TYPE_NOTIFICATION_TRIGGER);
|
|
749
|
+
workRequestBuilder.addTag(uniqueWorkName);
|
|
750
|
+
workRequestBuilder.setInputData(workDataBuilder.build());
|
|
751
|
+
workManager.enqueueUniquePeriodicWork(
|
|
752
|
+
uniqueWorkName, ExistingPeriodicWorkPolicy.UPDATE, workRequestBuilder.build());
|
|
753
|
+
return null;
|
|
754
|
+
},
|
|
755
|
+
LISTENING_CACHED_THREAD_POOL);
|
|
716
756
|
}
|
|
717
757
|
|
|
718
|
-
|
|
758
|
+
// Returns a future that completes only after the Room insert has persisted AND
|
|
759
|
+
// the AlarmManager / WorkManager schedule call has run. Same rationale as the
|
|
760
|
+
// interval path above. Fixes upstream invertase/notifee#549 for the timestamp
|
|
761
|
+
// trigger path (AlarmManager by default since 9.1.12).
|
|
762
|
+
static ListenableFuture<Void> createTimestampTriggerNotification(
|
|
719
763
|
NotificationModel notificationModel, Bundle triggerBundle) {
|
|
720
764
|
TimestampTriggerModel trigger = TimestampTriggerModel.fromBundle(triggerBundle);
|
|
721
765
|
|
|
@@ -724,52 +768,56 @@ class NotificationManager {
|
|
|
724
768
|
long delay = trigger.getDelay();
|
|
725
769
|
int interval = trigger.getInterval();
|
|
726
770
|
|
|
727
|
-
// Save in DB
|
|
728
771
|
Data.Builder workDataBuilder =
|
|
729
772
|
new Data.Builder()
|
|
730
773
|
.putString(Worker.KEY_WORK_TYPE, Worker.WORK_TYPE_NOTIFICATION_TRIGGER)
|
|
731
774
|
.putString("id", notificationModel.getId());
|
|
732
775
|
|
|
733
776
|
Boolean withAlarmManager = trigger.getWithAlarmManager();
|
|
777
|
+
Context context = getApplicationContext();
|
|
778
|
+
|
|
779
|
+
ListenableFuture<Void> insertFuture =
|
|
780
|
+
WorkDataRepository.insertTriggerNotification(
|
|
781
|
+
context, notificationModel, triggerBundle, withAlarmManager);
|
|
782
|
+
|
|
783
|
+
return Futures.transform(
|
|
784
|
+
insertFuture,
|
|
785
|
+
unused -> {
|
|
786
|
+
if (withAlarmManager) {
|
|
787
|
+
NotifeeAlarmManager.scheduleTimestampTriggerNotification(notificationModel, trigger);
|
|
788
|
+
return null;
|
|
789
|
+
}
|
|
734
790
|
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
workRequestBuilder.addTag(Worker.WORK_TYPE_NOTIFICATION_TRIGGER);
|
|
766
|
-
workRequestBuilder.addTag(uniqueWorkName);
|
|
767
|
-
workRequestBuilder.setInitialDelay(delay, TimeUnit.SECONDS);
|
|
768
|
-
workDataBuilder.putString(Worker.KEY_WORK_REQUEST, Worker.WORK_REQUEST_PERIODIC);
|
|
769
|
-
workRequestBuilder.setInputData(workDataBuilder.build());
|
|
770
|
-
workManager.enqueueUniquePeriodicWork(
|
|
771
|
-
uniqueWorkName, ExistingPeriodicWorkPolicy.UPDATE, workRequestBuilder.build());
|
|
772
|
-
}
|
|
791
|
+
WorkManager workManager = WorkManager.getInstance(context);
|
|
792
|
+
|
|
793
|
+
// WorkManager - One time trigger
|
|
794
|
+
if (interval == -1) {
|
|
795
|
+
OneTimeWorkRequest.Builder workRequestBuilder =
|
|
796
|
+
new OneTimeWorkRequest.Builder(Worker.class);
|
|
797
|
+
workRequestBuilder.addTag(Worker.WORK_TYPE_NOTIFICATION_TRIGGER);
|
|
798
|
+
workRequestBuilder.addTag(uniqueWorkName);
|
|
799
|
+
workDataBuilder.putString(Worker.KEY_WORK_REQUEST, Worker.WORK_REQUEST_ONE_TIME);
|
|
800
|
+
workRequestBuilder.setInputData(workDataBuilder.build());
|
|
801
|
+
workRequestBuilder.setInitialDelay(delay, TimeUnit.SECONDS);
|
|
802
|
+
workManager.enqueueUniqueWork(
|
|
803
|
+
uniqueWorkName, ExistingWorkPolicy.REPLACE, workRequestBuilder.build());
|
|
804
|
+
} else {
|
|
805
|
+
// WorkManager - repeat trigger
|
|
806
|
+
PeriodicWorkRequest.Builder workRequestBuilder =
|
|
807
|
+
new PeriodicWorkRequest.Builder(
|
|
808
|
+
Worker.class, trigger.getInterval(), trigger.getTimeUnit());
|
|
809
|
+
|
|
810
|
+
workRequestBuilder.addTag(Worker.WORK_TYPE_NOTIFICATION_TRIGGER);
|
|
811
|
+
workRequestBuilder.addTag(uniqueWorkName);
|
|
812
|
+
workRequestBuilder.setInitialDelay(delay, TimeUnit.SECONDS);
|
|
813
|
+
workDataBuilder.putString(Worker.KEY_WORK_REQUEST, Worker.WORK_REQUEST_PERIODIC);
|
|
814
|
+
workRequestBuilder.setInputData(workDataBuilder.build());
|
|
815
|
+
workManager.enqueueUniquePeriodicWork(
|
|
816
|
+
uniqueWorkName, ExistingPeriodicWorkPolicy.UPDATE, workRequestBuilder.build());
|
|
817
|
+
}
|
|
818
|
+
return null;
|
|
819
|
+
},
|
|
820
|
+
LISTENING_CACHED_THREAD_POOL);
|
|
773
821
|
}
|
|
774
822
|
|
|
775
823
|
static ListenableFuture<List<Bundle>> getDisplayedNotifications() {
|
|
@@ -957,17 +1005,51 @@ class NotificationManager {
|
|
|
957
1005
|
new ExtendedListenableFuture<>(result)
|
|
958
1006
|
.addOnCompleteListener(
|
|
959
1007
|
(e2, _unused) -> {
|
|
960
|
-
completer.set(Result.success());
|
|
961
1008
|
if (e2 != null) {
|
|
962
1009
|
Logger.e(TAG, "Failed to display notification", e2);
|
|
1010
|
+
completer.set(Result.success());
|
|
1011
|
+
return;
|
|
1012
|
+
}
|
|
1013
|
+
String workerRequestType = data.getString(Worker.KEY_WORK_REQUEST);
|
|
1014
|
+
if (workerRequestType != null
|
|
1015
|
+
&& workerRequestType.equals(Worker.WORK_REQUEST_ONE_TIME)) {
|
|
1016
|
+
// DO NOT reorder — completer.set must only run after the
|
|
1017
|
+
// delete future completes, otherwise WorkManager may start
|
|
1018
|
+
// a new work instance that reads the stale row. Previously
|
|
1019
|
+
// completer.set fired before the delete was enqueued, leaving
|
|
1020
|
+
// a zombie row that reboot recovery would resurrect as a
|
|
1021
|
+
// ghost alarm. See #549 audit Part B, Caller #5.
|
|
1022
|
+
//
|
|
1023
|
+
// Note: CallbackToFutureAdapter.Completer.set() returns
|
|
1024
|
+
// false on double-set, it does NOT throw. This is an
|
|
1025
|
+
// androidx contract. Do not refactor under the assumption
|
|
1026
|
+
// that double-set is dangerous.
|
|
1027
|
+
Futures.addCallback(
|
|
1028
|
+
WorkDataRepository.getInstance(getApplicationContext())
|
|
1029
|
+
.deleteById(id),
|
|
1030
|
+
new FutureCallback<Void>() {
|
|
1031
|
+
@Override
|
|
1032
|
+
public void onSuccess(Void unused) {
|
|
1033
|
+
completer.set(Result.success());
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
@Override
|
|
1037
|
+
public void onFailure(@NonNull Throwable t) {
|
|
1038
|
+
// Notification was already displayed; a failed
|
|
1039
|
+
// delete leaves an orphan row that the next
|
|
1040
|
+
// cancelAll or app restart will clean up. Still
|
|
1041
|
+
// report success so WorkManager doesn't retry
|
|
1042
|
+
// the already-displayed notification.
|
|
1043
|
+
Logger.e(
|
|
1044
|
+
TAG,
|
|
1045
|
+
"Failed to delete one-time trigger row after" + " display",
|
|
1046
|
+
new Exception(t));
|
|
1047
|
+
completer.set(Result.success());
|
|
1048
|
+
}
|
|
1049
|
+
},
|
|
1050
|
+
LISTENING_CACHED_THREAD_POOL);
|
|
963
1051
|
} else {
|
|
964
|
-
|
|
965
|
-
if (workerRequestType != null
|
|
966
|
-
&& workerRequestType.equals(Worker.WORK_REQUEST_ONE_TIME)) {
|
|
967
|
-
// delete database entry if work is a one-time request
|
|
968
|
-
WorkDataRepository.getInstance(getApplicationContext())
|
|
969
|
-
.deleteById(id);
|
|
970
|
-
}
|
|
1052
|
+
completer.set(Result.success());
|
|
971
1053
|
}
|
|
972
1054
|
},
|
|
973
1055
|
LISTENING_CACHED_THREAD_POOL);
|