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.
Files changed (37) hide show
  1. package/README.md +107 -1
  2. package/android/src/main/java/app/notifee/core/Notifee.java +9 -0
  3. package/android/src/main/java/app/notifee/core/ReceiverService.java +41 -4
  4. package/android/src/main/java/app/notifee/core/model/NotificationAndroidStyleModel.java +63 -34
  5. package/android/src/main/java/app/notifee/core/utility/FullScreenIntentUtils.java +53 -0
  6. package/android/src/main/java/app/notifee/core/utility/ResourceUtils.java +15 -1
  7. package/android/src/test/java/app/notifee/core/ReceiverServicePendingIntentIdentityTest.java +303 -0
  8. package/android/src/test/java/app/notifee/core/model/NotificationAndroidStyleModelPersonFailureTest.java +337 -0
  9. package/android/src/test/java/app/notifee/core/model/TimestampTriggerModelTest.java +27 -70
  10. package/android/src/test/java/app/notifee/core/utility/FullScreenIntentUtilsTest.java +83 -0
  11. package/android/src/test/java/app/notifee/core/utility/ResourceUtilsBitmapOwnershipTest.java +145 -0
  12. package/dist/NotifeeApiModule.js +4 -0
  13. package/dist/NotifeeApiModule.js.map +1 -1
  14. package/dist/types/Notification.d.ts +2 -1
  15. package/dist/types/Notification.js.map +1 -1
  16. package/dist/types/NotificationAndroid.d.ts +17 -5
  17. package/dist/types/NotificationAndroid.js.map +1 -1
  18. package/dist/validators/validateAndroidNotification.js +4 -0
  19. package/dist/validators/validateAndroidNotification.js.map +1 -1
  20. package/dist/version.d.ts +1 -1
  21. package/dist/version.js +1 -1
  22. package/ios/NotifeeCore/NotifeeCoreExtensionHelper.m +285 -91
  23. package/ios/NotifeeCore/NotifeeCoreUtil.m +159 -13
  24. package/jest-mock.js +1 -0
  25. package/package.json +4 -1
  26. package/plugin/build/android/withNotifyKitAndroidNotificationIcons.js +498 -0
  27. package/plugin/build/index.js +13 -18
  28. package/plugin/build/options.js +84 -0
  29. package/server/dist/validation.d.ts.map +1 -1
  30. package/server/dist/validation.js +14 -0
  31. package/server/dist/validation.js.map +1 -1
  32. package/server/src/validation.ts +20 -0
  33. package/src/NotifeeApiModule.ts +4 -0
  34. package/src/types/Notification.ts +2 -1
  35. package/src/types/NotificationAndroid.ts +18 -5
  36. package/src/validators/validateAndroidNotification.ts +7 -0
  37. 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
- <!-- markdownlint-disable-line MD028 -->
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 AtomicInteger uniqueIds = new AtomicInteger(0);
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
- int uniqueInt = uniqueIds.getAndIncrement();
82
+ intent.setData(createPendingIntentData(intent));
83
+
80
84
  return PendingIntent.getService(
81
- context, uniqueInt, intent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_MUTABLE);
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 = new Person.Builder();
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 (Exception e) {
106
+ } catch (ExecutionException | RuntimeException e) {
92
107
  Logger.e(
93
108
  TAG,
94
- "An error occurred whilst trying to retrieve a person icon: " + personIcon,
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
- Person person =
295
- getPerson(
296
- lExecutor,
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, Objects.requireNonNull(message.getBundle("person")))
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
- bitmapTCS.set(bitmap);
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