react-native-notify-kit 10.5.0 → 10.7.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 +107 -1
- package/android/src/main/java/app/notifee/core/Notifee.java +9 -0
- package/android/src/main/java/app/notifee/core/ReceiverService.java +41 -4
- package/android/src/main/java/app/notifee/core/model/NotificationAndroidStyleModel.java +63 -34
- package/android/src/main/java/app/notifee/core/utility/FullScreenIntentUtils.java +53 -0
- package/android/src/main/java/app/notifee/core/utility/ResourceUtils.java +15 -1
- package/android/src/test/java/app/notifee/core/ReceiverServicePendingIntentIdentityTest.java +303 -0
- package/android/src/test/java/app/notifee/core/model/NotificationAndroidStyleModelPersonFailureTest.java +337 -0
- package/android/src/test/java/app/notifee/core/model/TimestampTriggerModelTest.java +27 -70
- package/android/src/test/java/app/notifee/core/utility/FullScreenIntentUtilsTest.java +83 -0
- package/android/src/test/java/app/notifee/core/utility/ResourceUtilsBitmapOwnershipTest.java +145 -0
- package/dist/NotifeeApiModule.js +4 -0
- package/dist/NotifeeApiModule.js.map +1 -1
- package/dist/types/Notification.d.ts +2 -1
- package/dist/types/Notification.js.map +1 -1
- package/dist/types/NotificationAndroid.d.ts +17 -5
- package/dist/types/NotificationAndroid.js.map +1 -1
- package/dist/validators/validateAndroidNotification.js +4 -0
- package/dist/validators/validateAndroidNotification.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/ios/NotifeeCore/NotifeeCoreExtensionHelper.m +285 -91
- package/ios/NotifeeCore/NotifeeCoreUtil.m +159 -13
- package/jest-mock.js +1 -0
- package/package.json +4 -1
- package/plugin/build/android/withNotifyKitAndroidNotificationIcons.js +498 -0
- package/plugin/build/index.js +13 -18
- package/plugin/build/options.js +84 -0
- package/server/dist/validation.d.ts.map +1 -1
- package/server/dist/validation.js +14 -0
- package/server/dist/validation.js.map +1 -1
- package/server/src/validation.ts +20 -0
- package/src/NotifeeApiModule.ts +4 -0
- package/src/types/Notification.ts +2 -1
- package/src/types/NotificationAndroid.ts +18 -5
- package/src/validators/validateAndroidNotification.ts +7 -0
- package/src/version.ts +1 -1
package/README.md
CHANGED
|
@@ -148,6 +148,8 @@ useEffect(() => {
|
|
|
148
148
|
|
|
149
149
|
> **Android event reliability:** Registering `onForegroundEvent()` initializes the native event relay. Pending events received while the `ReactContext` is unavailable are buffered. If the context becomes unavailable after a flush takes its buffer snapshot, undelivered events are requeued so a later native event relay flush can deliver them without duplication in the validated recovery path, while preserving FIFO, the 10-event capacity, and drop-oldest behavior. No application code changes or warm-up calls such as `getDisplayedNotifications()` are required; continue registering foreground and background handlers at the recommended points shown above. This hardens the Android event path investigated in [react-native-notify-kit#47](https://github.com/marcocrupi/react-native-notify-kit/issues/47) and relates to the event reliability and buffering problem tracked in [invertase/notifee#1279](https://github.com/invertase/notifee/issues/1279); the scenarios are not necessarily identical.
|
|
150
150
|
|
|
151
|
+
<!-- Keep these callouts separate. -->
|
|
152
|
+
|
|
151
153
|
> **Which handler fires when (iOS):**
|
|
152
154
|
>
|
|
153
155
|
> - Tap a notification while the app is **active in foreground** → `onForegroundEvent` receives `PRESS`.
|
|
@@ -156,6 +158,42 @@ useEffect(() => {
|
|
|
156
158
|
>
|
|
157
159
|
> Register **both** handlers if you need to react to taps in every app state. Resolves the confusion reported in upstream [invertase/notifee#1155](https://github.com/invertase/notifee/issues/1155).
|
|
158
160
|
|
|
161
|
+
### Android notification numbers and launcher badges
|
|
162
|
+
|
|
163
|
+
For notifications displayed through `notifee.displayNotification()`, use `android.badgeCount` to associate a number with
|
|
164
|
+
an individual Android notification:
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
await notifee.createChannel({
|
|
168
|
+
id: 'messages',
|
|
169
|
+
name: 'Messages',
|
|
170
|
+
badge: true,
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
await notifee.displayNotification({
|
|
174
|
+
id: 'messages',
|
|
175
|
+
title: 'New messages',
|
|
176
|
+
body: 'You have 5 unread messages',
|
|
177
|
+
android: {
|
|
178
|
+
channelId: 'messages',
|
|
179
|
+
badgeCount: 5,
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
`android.badgeCount` belongs to that notification and maps conceptually to AndroidX
|
|
185
|
+
`NotificationCompat.Builder.setNumber(...)`. Display the notification again with the same `id` and a new `badgeCount`
|
|
186
|
+
to update or replace it. The notification number itself is not limited to API level 26.
|
|
187
|
+
|
|
188
|
+
On Android 8.0 (API level 26) and above, `badge: true` allows notifications in the channel to contribute to launcher
|
|
189
|
+
badging; it does not set a count. Launcher and OEM behavior determines whether a number, only a notification dot, or a
|
|
190
|
+
different aggregation is shown, so a numeric badge on the app icon is not guaranteed.
|
|
191
|
+
|
|
192
|
+
Unlike the global iOS `notifee.setBadgeCount()` API, Android has no equivalent standard public API independent of posted
|
|
193
|
+
notifications. `badgeCount: 0` neither cancels the posted notification nor provides a portable way to clear a global
|
|
194
|
+
Android badge. See [Android Appearance: Badges](https://docs.page/marcocrupi/react-native-notify-kit/react-native/android/appearance#badges)
|
|
195
|
+
for complete details.
|
|
196
|
+
|
|
159
197
|
## Notifee FCM Mode (NEW in 10.0.0, Expo CNG in 10.4.0)
|
|
160
198
|
|
|
161
199
|
**Use `react-native-notify-kit` as the sole FCM display layer on both Android and iOS**: one developer API, no duplicate notifications on Android, no silent-push drops on iOS. Ship a server SDK payload, let the client handle it in one line, and set up the iOS Notification Service Extension with the Expo config plugin or, for bare React Native, the `init-nse` CLI. On Android, FCM Mode remains data-only: RNFirebase receives the message and calls `notifee.handleFcmMessage`.
|
|
@@ -204,6 +242,54 @@ export default {
|
|
|
204
242
|
|
|
205
243
|
On Android, FCM Mode stays data-only. Configure Firebase and RNFirebase in the app, create the Android channel used by your payloads, then call `notifee.handleFcmMessage(remoteMessage)` from RNFirebase `onMessage` and `setBackgroundMessageHandler`. The Expo smoke app validates foreground and background delivery with the `android-expo-smoke` scenario on a real device.
|
|
206
244
|
|
|
245
|
+
#### Android notification small icons
|
|
246
|
+
|
|
247
|
+
For Expo CNG/prebuild projects, the config plugin can generate small notification icon resources without manually editing generated `android/` directories. This resource-generation option is independent of FCM Mode:
|
|
248
|
+
|
|
249
|
+
```ts
|
|
250
|
+
export default {
|
|
251
|
+
expo: {
|
|
252
|
+
plugins: [
|
|
253
|
+
[
|
|
254
|
+
'react-native-notify-kit',
|
|
255
|
+
{
|
|
256
|
+
android: {
|
|
257
|
+
icons: [
|
|
258
|
+
{
|
|
259
|
+
name: 'notification_message',
|
|
260
|
+
path: './assets/notification-message.png',
|
|
261
|
+
type: 'small',
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
name: 'notification_warning',
|
|
265
|
+
path: './assets/notification-warning.png',
|
|
266
|
+
type: 'small',
|
|
267
|
+
},
|
|
268
|
+
],
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
],
|
|
272
|
+
],
|
|
273
|
+
},
|
|
274
|
+
};
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
During prebuild, each entry generates density-specific PNG drawable resources (`mdpi` 24×24, `hdpi` 36×36, `xhdpi` 48×48, `xxhdpi` 72×72, and `xxxhdpi` 96×96) with a transparent background and `cover` resizing. The source `path` must be a project-relative PNG inside the project root. The `name` must start with a lowercase letter and contain only `a-z`, `0-9`, and `_`; invalid names are rejected rather than normalized. The array supports multiple icons and preserves their order. Each entry must include `type: 'small'`; omitting it or using another type fails during configuration/prebuild. The plugin also writes an Android resource-shrinker keep rule for the generated icons.
|
|
278
|
+
|
|
279
|
+
`android.icons` generates resources; it does not select an icon for a notification. Pass the configured resource `name`—not its file path—to `android.smallIcon`:
|
|
280
|
+
|
|
281
|
+
```ts
|
|
282
|
+
await notifee.displayNotification({
|
|
283
|
+
title: 'New message',
|
|
284
|
+
android: {
|
|
285
|
+
channelId: 'messages',
|
|
286
|
+
smallIcon: 'notification_message',
|
|
287
|
+
},
|
|
288
|
+
});
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
This option does not configure Firebase or set `com.google.firebase.messaging.default_notification_icon`.
|
|
292
|
+
|
|
207
293
|
Android foreground service manifest configuration is also available through the NotifyKit config plugin, but it is explicit opt-in:
|
|
208
294
|
|
|
209
295
|
```ts
|
|
@@ -503,7 +589,7 @@ This fork fixes the following bugs that were never resolved in the original Noti
|
|
|
503
589
|
>
|
|
504
590
|
> **Reserved keys filtered from `data`** (custom payload keys matching these are dropped): prefixes `android.`, `google.`, `gcm.`, `fcm.` (with trailing dot — `fcmRegion`, `googleish` survive), `notifee` (no trailing dot — library's reserved namespace, `notifeeFoo` is also filtered), plus exact keys `from`, `collapse_key`, `message_type`, `message_id`, `aps`, `fcm_options`. The `fcm_options` exact-match matches iOS behavior on the Firebase analytics-label key. **Cross-platform note**: bare-`fcm` keys other than `fcm_options` (e.g. `fcmRegion`, `fcmToken`) are preserved on Android but filtered on iOS — rename server-side if you need strict parity.
|
|
505
591
|
|
|
506
|
-
<!--
|
|
592
|
+
<!-- Keep these callouts separate. -->
|
|
507
593
|
|
|
508
594
|
> **Note for policy-eligible apps requiring exact alarm APIs (alarm clocks, timers, calendars):**
|
|
509
595
|
> Add `<uses-permission android:name="android.permission.USE_EXACT_ALARM" />` to your app's
|
|
@@ -800,6 +886,24 @@ await notifee.displayNotification({
|
|
|
800
886
|
|
|
801
887
|
## New APIs
|
|
802
888
|
|
|
889
|
+
### Full-Screen Intent availability (Android)
|
|
890
|
+
|
|
891
|
+
Use `getNotificationSettings()` to check whether the app currently has access to use Full-Screen Intents:
|
|
892
|
+
|
|
893
|
+
```typescript
|
|
894
|
+
import notifee, { AndroidNotificationSetting } from 'react-native-notify-kit';
|
|
895
|
+
|
|
896
|
+
const settings = await notifee.getNotificationSettings();
|
|
897
|
+
|
|
898
|
+
if (settings.android.fullScreenIntent === AndroidNotificationSetting.DISABLED) {
|
|
899
|
+
// Full-Screen Intent access is currently unavailable.
|
|
900
|
+
}
|
|
901
|
+
```
|
|
902
|
+
|
|
903
|
+
`AndroidNotificationSetting.ENABLED` reports Full-Screen Intent access; it does not guarantee that a particular
|
|
904
|
+
notification will be presented full-screen. See [Android Behaviour: Full-screen](https://docs.page/marcocrupi/react-native-notify-kit/react-native/android/behaviour#full-screen)
|
|
905
|
+
for the full requirements and presentation behavior.
|
|
906
|
+
|
|
803
907
|
### `setNotificationConfig` (iOS)
|
|
804
908
|
|
|
805
909
|
Controls whether Notifee intercepts remote (push) notification tap events on iOS.
|
|
@@ -870,6 +974,8 @@ Three causes account for the vast majority of reports:
|
|
|
870
974
|
|
|
871
975
|
**Naming mismatch.** Android resource names are case-sensitive and only accept `[a-z0-9_]`. A string like `smallIcon: 'icNotification'` will not resolve `ic_notification.png`. Rename the `smallIcon` value to match the file on disk exactly.
|
|
872
976
|
|
|
977
|
+
For Expo CNG/prebuild apps, use the NotifyKit config plugin's `android.icons` option instead of editing generated `android/` directories. It generates the density-specific drawables and writes a resource-shrinker keep rule during prebuild; continue to pass the configured resource `name` to `android.smallIcon`.
|
|
978
|
+
|
|
873
979
|
For the full procedure on creating a small-icon asset via Android Studio, see [docs/react-native/android/appearance.mdx](docs/react-native/android/appearance.mdx).
|
|
874
980
|
|
|
875
981
|
#### Custom sounds for push notifications in background or killed state
|
|
@@ -36,6 +36,7 @@ import app.notifee.core.model.ChannelGroupModel;
|
|
|
36
36
|
import app.notifee.core.model.ChannelModel;
|
|
37
37
|
import app.notifee.core.model.NotificationModel;
|
|
38
38
|
import app.notifee.core.utility.AlarmUtils;
|
|
39
|
+
import app.notifee.core.utility.FullScreenIntentUtils;
|
|
39
40
|
import app.notifee.core.utility.PowerManagerUtils;
|
|
40
41
|
import com.google.common.util.concurrent.FutureCallback;
|
|
41
42
|
import com.google.common.util.concurrent.Futures;
|
|
@@ -508,6 +509,14 @@ public class Notifee {
|
|
|
508
509
|
androidSettingsBundle.putInt("alarm", 0);
|
|
509
510
|
}
|
|
510
511
|
|
|
512
|
+
boolean canUseFullScreenIntent = FullScreenIntentUtils.canUseFullScreenIntent();
|
|
513
|
+
|
|
514
|
+
if (canUseFullScreenIntent) {
|
|
515
|
+
androidSettingsBundle.putInt("fullScreenIntent", 1);
|
|
516
|
+
} else {
|
|
517
|
+
androidSettingsBundle.putInt("fullScreenIntent", 0);
|
|
518
|
+
}
|
|
519
|
+
|
|
511
520
|
notificationSettingsBundle.putBundle("android", androidSettingsBundle);
|
|
512
521
|
result.onComplete(null, notificationSettingsBundle);
|
|
513
522
|
}
|
|
@@ -26,6 +26,7 @@ import android.app.PendingIntent;
|
|
|
26
26
|
import android.app.Service;
|
|
27
27
|
import android.content.Context;
|
|
28
28
|
import android.content.Intent;
|
|
29
|
+
import android.net.Uri;
|
|
29
30
|
import android.os.Build;
|
|
30
31
|
import android.os.Bundle;
|
|
31
32
|
import android.os.IBinder;
|
|
@@ -39,14 +40,16 @@ import app.notifee.core.model.NotificationAndroidModel;
|
|
|
39
40
|
import app.notifee.core.model.NotificationAndroidPressActionModel;
|
|
40
41
|
import app.notifee.core.model.NotificationModel;
|
|
41
42
|
import app.notifee.core.utility.IntentUtils;
|
|
42
|
-
import java.util.concurrent.atomic.AtomicInteger;
|
|
43
43
|
|
|
44
44
|
public class ReceiverService extends Service {
|
|
45
45
|
private static final String TAG = "ReceiverService";
|
|
46
46
|
public static final String REMOTE_INPUT_RECEIVER_KEY =
|
|
47
47
|
"app.notifee.core.ReceiverService.REMOTE_INPUT_RECEIVER_KEY";
|
|
48
48
|
|
|
49
|
-
private static final
|
|
49
|
+
private static final int PENDING_INTENT_REQUEST_CODE = 0;
|
|
50
|
+
private static final String PENDING_INTENT_URI_SCHEME = "notifee";
|
|
51
|
+
private static final String PENDING_INTENT_URI_AUTHORITY = "receiver-service";
|
|
52
|
+
private static final String PENDING_INTENT_URI_PATH = "pending-intent";
|
|
50
53
|
|
|
51
54
|
static final String DELETE_INTENT = "app.notifee.core.ReceiverService.DELETE_INTENT";
|
|
52
55
|
static final String PRESS_INTENT = "app.notifee.core.ReceiverService.PRESS_INTENT";
|
|
@@ -76,9 +79,43 @@ public class ReceiverService extends Service {
|
|
|
76
79
|
}
|
|
77
80
|
}
|
|
78
81
|
|
|
79
|
-
|
|
82
|
+
intent.setData(createPendingIntentData(intent));
|
|
83
|
+
|
|
80
84
|
return PendingIntent.getService(
|
|
81
|
-
context,
|
|
85
|
+
context,
|
|
86
|
+
PENDING_INTENT_REQUEST_CODE,
|
|
87
|
+
intent,
|
|
88
|
+
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private static Uri createPendingIntentData(Intent intent) {
|
|
92
|
+
Bundle notification = intent.getBundleExtra("notification");
|
|
93
|
+
Bundle android = notification == null ? null : notification.getBundle("android");
|
|
94
|
+
Bundle pressAction = intent.getBundleExtra("pressAction");
|
|
95
|
+
|
|
96
|
+
String notificationId = notification == null ? null : notification.getString("id");
|
|
97
|
+
String notificationTag = android == null ? null : android.getString("tag");
|
|
98
|
+
String pressActionId = pressAction == null ? null : pressAction.getString("id");
|
|
99
|
+
|
|
100
|
+
Uri.Builder builder =
|
|
101
|
+
new Uri.Builder()
|
|
102
|
+
.scheme(PENDING_INTENT_URI_SCHEME)
|
|
103
|
+
.authority(PENDING_INTENT_URI_AUTHORITY)
|
|
104
|
+
.appendPath(PENDING_INTENT_URI_PATH);
|
|
105
|
+
|
|
106
|
+
appendNullableIdentityValue(builder, "interactionType", intent.getAction());
|
|
107
|
+
appendNullableIdentityValue(builder, "notificationTag", notificationTag);
|
|
108
|
+
appendNullableIdentityValue(builder, "notificationId", notificationId);
|
|
109
|
+
appendNullableIdentityValue(builder, "pressActionId", pressActionId);
|
|
110
|
+
return builder.build();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private static void appendNullableIdentityValue(
|
|
114
|
+
Uri.Builder builder, String name, @Nullable String value) {
|
|
115
|
+
builder.appendQueryParameter(name + "Present", value == null ? "0" : "1");
|
|
116
|
+
if (value != null) {
|
|
117
|
+
builder.appendQueryParameter(name, value);
|
|
118
|
+
}
|
|
82
119
|
}
|
|
83
120
|
|
|
84
121
|
@Nullable
|
|
@@ -34,6 +34,7 @@ import com.google.common.util.concurrent.ListenableFuture;
|
|
|
34
34
|
import com.google.common.util.concurrent.ListeningExecutorService;
|
|
35
35
|
import java.util.ArrayList;
|
|
36
36
|
import java.util.Objects;
|
|
37
|
+
import java.util.concurrent.ExecutionException;
|
|
37
38
|
import java.util.concurrent.TimeUnit;
|
|
38
39
|
import java.util.concurrent.TimeoutException;
|
|
39
40
|
|
|
@@ -50,6 +51,31 @@ public class NotificationAndroidStyleModel {
|
|
|
50
51
|
return new NotificationAndroidStyleModel(styleBundle);
|
|
51
52
|
}
|
|
52
53
|
|
|
54
|
+
/** Builds a person from everything in its bundle except the remotely fetched icon. */
|
|
55
|
+
private static Person.Builder getPersonBuilder(Bundle personBundle) {
|
|
56
|
+
Person.Builder personBuilder = new Person.Builder();
|
|
57
|
+
|
|
58
|
+
personBuilder.setName(personBundle.getString("name"));
|
|
59
|
+
|
|
60
|
+
if (personBundle.containsKey("id")) {
|
|
61
|
+
personBuilder.setKey(personBundle.getString("id"));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (personBundle.containsKey("bot")) {
|
|
65
|
+
personBuilder.setBot(personBundle.getBoolean("bot"));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (personBundle.containsKey("important")) {
|
|
69
|
+
personBuilder.setImportant(personBundle.getBoolean("important"));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (personBundle.containsKey("uri")) {
|
|
73
|
+
personBuilder.setUri(personBundle.getString("uri"));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return personBuilder;
|
|
77
|
+
}
|
|
78
|
+
|
|
53
79
|
/**
|
|
54
80
|
* Converts a person bundle from JS into a Person
|
|
55
81
|
*
|
|
@@ -60,54 +86,59 @@ public class NotificationAndroidStyleModel {
|
|
|
60
86
|
ListeningExecutorService lExecutor, Bundle personBundle) {
|
|
61
87
|
return lExecutor.submit(
|
|
62
88
|
() -> {
|
|
63
|
-
Person.Builder personBuilder =
|
|
64
|
-
|
|
65
|
-
personBuilder.setName(personBundle.getString("name"));
|
|
66
|
-
|
|
67
|
-
if (personBundle.containsKey("id")) {
|
|
68
|
-
personBuilder.setKey(personBundle.getString("id"));
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
if (personBundle.containsKey("bot")) {
|
|
72
|
-
personBuilder.setBot(personBundle.getBoolean("bot"));
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
if (personBundle.containsKey("important")) {
|
|
76
|
-
personBuilder.setImportant(personBundle.getBoolean("important"));
|
|
77
|
-
}
|
|
89
|
+
Person.Builder personBuilder = getPersonBuilder(personBundle);
|
|
78
90
|
|
|
79
91
|
if (personBundle.containsKey("icon")) {
|
|
80
92
|
String personIcon = Objects.requireNonNull(personBundle.getString("icon"));
|
|
81
|
-
Bitmap personIconBitmap = null;
|
|
82
93
|
|
|
83
94
|
try {
|
|
84
|
-
personIconBitmap =
|
|
95
|
+
Bitmap personIconBitmap =
|
|
85
96
|
ResourceUtils.getImageBitmapFromUrl(personIcon).get(10, TimeUnit.SECONDS);
|
|
97
|
+
|
|
98
|
+
if (personIconBitmap != null) {
|
|
99
|
+
personBuilder.setIcon(IconCompat.createWithAdaptiveBitmap(personIconBitmap));
|
|
100
|
+
}
|
|
86
101
|
} catch (TimeoutException e) {
|
|
87
102
|
Logger.e(
|
|
88
103
|
TAG,
|
|
89
104
|
"Timeout occurred whilst trying to retrieve a person icon: " + personIcon,
|
|
90
105
|
e);
|
|
91
|
-
} catch (
|
|
106
|
+
} catch (ExecutionException | RuntimeException e) {
|
|
92
107
|
Logger.e(
|
|
93
108
|
TAG,
|
|
94
|
-
"An error occurred whilst trying to retrieve a person icon: "
|
|
109
|
+
"An error occurred whilst trying to retrieve or create a person icon: "
|
|
110
|
+
+ personIcon,
|
|
95
111
|
e);
|
|
96
112
|
}
|
|
97
|
-
|
|
98
|
-
if (personIconBitmap != null) {
|
|
99
|
-
personBuilder.setIcon(IconCompat.createWithAdaptiveBitmap(personIconBitmap));
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
if (personBundle.containsKey("uri")) {
|
|
104
|
-
personBuilder.setUri(personBundle.getString("uri"));
|
|
105
113
|
}
|
|
106
114
|
|
|
107
115
|
return personBuilder.build();
|
|
108
116
|
});
|
|
109
117
|
}
|
|
110
118
|
|
|
119
|
+
/**
|
|
120
|
+
* Awaits a person, degrading to an icon-less one only if the outer deadline expires.
|
|
121
|
+
*
|
|
122
|
+
* <p>The icon lookup has a shorter timeout, but scheduling delays, process suspension, or other
|
|
123
|
+
* task stalls can still exhaust this outer deadline. The runtime cause is not inferred here.
|
|
124
|
+
*
|
|
125
|
+
* <p>ExecutionException and InterruptedException propagate because neither proves that only the
|
|
126
|
+
* icon failed.
|
|
127
|
+
*/
|
|
128
|
+
private static Person awaitPerson(ListenableFuture<Person> personTask, Bundle personBundle)
|
|
129
|
+
throws InterruptedException, ExecutionException {
|
|
130
|
+
try {
|
|
131
|
+
return personTask.get(20, TimeUnit.SECONDS);
|
|
132
|
+
} catch (TimeoutException e) {
|
|
133
|
+
Logger.e(TAG, "Timeout occurred whilst trying to retrieve a messaging style person", e);
|
|
134
|
+
} catch (ExecutionException e) {
|
|
135
|
+
Logger.e(TAG, "Unexpected failure whilst building a messaging style person", e);
|
|
136
|
+
throw e;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return getPersonBuilder(personBundle).build();
|
|
140
|
+
}
|
|
141
|
+
|
|
111
142
|
public Bundle toBundle() {
|
|
112
143
|
return (Bundle) mNotificationAndroidStyleBundle.clone();
|
|
113
144
|
}
|
|
@@ -291,11 +322,9 @@ public class NotificationAndroidStyleModel {
|
|
|
291
322
|
ListeningExecutorService lExecutor) {
|
|
292
323
|
return lExecutor.submit(
|
|
293
324
|
() -> {
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
Objects.requireNonNull(mNotificationAndroidStyleBundle.getBundle("person")))
|
|
298
|
-
.get(20, TimeUnit.SECONDS);
|
|
325
|
+
Bundle personBundle =
|
|
326
|
+
Objects.requireNonNull(mNotificationAndroidStyleBundle.getBundle("person"));
|
|
327
|
+
Person person = awaitPerson(getPerson(lExecutor, personBundle), personBundle);
|
|
299
328
|
|
|
300
329
|
NotificationCompat.MessagingStyle messagingStyle =
|
|
301
330
|
new NotificationCompat.MessagingStyle(person);
|
|
@@ -322,9 +351,9 @@ public class NotificationAndroidStyleModel {
|
|
|
322
351
|
long timestamp = BundleValueReader.getLongPreserving(message, "timestamp");
|
|
323
352
|
|
|
324
353
|
if (message.containsKey("person")) {
|
|
354
|
+
Bundle messagePersonBundle = Objects.requireNonNull(message.getBundle("person"));
|
|
325
355
|
messagePerson =
|
|
326
|
-
getPerson(lExecutor,
|
|
327
|
-
.get(20, TimeUnit.SECONDS);
|
|
356
|
+
awaitPerson(getPerson(lExecutor, messagePersonBundle), messagePersonBundle);
|
|
328
357
|
}
|
|
329
358
|
|
|
330
359
|
messagingStyle =
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
package app.notifee.core.utility;
|
|
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 static app.notifee.core.ContextHolder.getApplicationContext;
|
|
21
|
+
|
|
22
|
+
import android.content.Context;
|
|
23
|
+
import androidx.annotation.Nullable;
|
|
24
|
+
import androidx.annotation.VisibleForTesting;
|
|
25
|
+
import androidx.core.app.NotificationManagerCompat;
|
|
26
|
+
|
|
27
|
+
public class FullScreenIntentUtils {
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Whether the app currently has the Android access required to use full-screen intents.
|
|
31
|
+
*
|
|
32
|
+
* <p>API levels below 29 do not use the full-screen intent permission model. API levels 29
|
|
33
|
+
* through 33 require {@code USE_FULL_SCREEN_INTENT}. API level 34 and above use Android's
|
|
34
|
+
* full-screen intent special app access.
|
|
35
|
+
*
|
|
36
|
+
* @return true when the required access is currently available. This does not guarantee that a
|
|
37
|
+
* particular notification will be presented full-screen.
|
|
38
|
+
*/
|
|
39
|
+
public static boolean canUseFullScreenIntent() {
|
|
40
|
+
return canUseFullScreenIntent(getApplicationContext());
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
@VisibleForTesting
|
|
44
|
+
static boolean canUseFullScreenIntent(@Nullable Context context) {
|
|
45
|
+
// The application context is expected to exist on the public path. Preserve the existing
|
|
46
|
+
// defensive fail-open behavior for an internal state that cannot prove access is denied.
|
|
47
|
+
if (context == null) {
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return NotificationManagerCompat.from(context).canUseFullScreenIntent();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -170,7 +170,21 @@ public class ResourceUtils {
|
|
|
170
170
|
new BaseBitmapDataSubscriber() {
|
|
171
171
|
@Override
|
|
172
172
|
protected void onNewResultImpl(@Nullable Bitmap bitmap) {
|
|
173
|
-
|
|
173
|
+
// Fresco owns this pooled bitmap and only guarantees it is valid for the
|
|
174
|
+
// duration of this callback. If it escapes without a copy, Fresco may
|
|
175
|
+
// recycle it on memory-cache eviction (e.g. onTrimMemory) while the
|
|
176
|
+
// notification is still being built, posted or re-parceled, crashing with
|
|
177
|
+
// "Can't copy/parcel a recycled bitmap" or "Canvas: trying to use a
|
|
178
|
+
// recycled bitmap".
|
|
179
|
+
if (bitmap == null) {
|
|
180
|
+
bitmapTCS.set(null);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
Bitmap.Config config = bitmap.getConfig();
|
|
184
|
+
if (config == null) {
|
|
185
|
+
config = Bitmap.Config.ARGB_8888;
|
|
186
|
+
}
|
|
187
|
+
bitmapTCS.set(bitmap.copy(config, false));
|
|
174
188
|
}
|
|
175
189
|
|
|
176
190
|
@Override
|