react-native-notify-kit 10.5.0 → 10.6.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 (34) hide show
  1. package/README.md +57 -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 +1 -1
  26. package/server/dist/validation.d.ts.map +1 -1
  27. package/server/dist/validation.js +14 -0
  28. package/server/dist/validation.js.map +1 -1
  29. package/server/src/validation.ts +20 -0
  30. package/src/NotifeeApiModule.ts +4 -0
  31. package/src/types/Notification.ts +2 -1
  32. package/src/types/NotificationAndroid.ts +18 -5
  33. package/src/validators/validateAndroidNotification.ts +7 -0
  34. package/src/version.ts +1 -1
@@ -0,0 +1,303 @@
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
+ import static org.junit.Assert.assertEquals;
20
+ import static org.junit.Assert.assertFalse;
21
+ import static org.junit.Assert.assertNotEquals;
22
+ import static org.junit.Assert.assertNotNull;
23
+ import static org.junit.Assert.assertNull;
24
+ import static org.junit.Assert.assertTrue;
25
+
26
+ import android.app.PendingIntent;
27
+ import android.content.Intent;
28
+ import android.net.Uri;
29
+ import android.os.Bundle;
30
+ import org.junit.After;
31
+ import org.junit.Before;
32
+ import org.junit.Test;
33
+ import org.junit.runner.RunWith;
34
+ import org.robolectric.RobolectricTestRunner;
35
+ import org.robolectric.RuntimeEnvironment;
36
+ import org.robolectric.Shadows;
37
+ import org.robolectric.annotation.Config;
38
+ import org.robolectric.shadows.ShadowPendingIntent;
39
+
40
+ @RunWith(RobolectricTestRunner.class)
41
+ @Config(sdk = 34)
42
+ public class ReceiverServicePendingIntentIdentityTest {
43
+
44
+ @Before
45
+ public void setUp() {
46
+ ContextHolder.setApplicationContext(RuntimeEnvironment.getApplication());
47
+ ShadowPendingIntent.reset();
48
+ }
49
+
50
+ @After
51
+ public void tearDown() {
52
+ ShadowPendingIntent.reset();
53
+ }
54
+
55
+ @Test
56
+ public void differentNotifications_doNotCollide() {
57
+ PendingIntent notificationA =
58
+ createReceiverIntent(
59
+ ReceiverService.ACTION_PRESS_INTENT, "notification-a", null, "done", "payload-a");
60
+ PendingIntent notificationB =
61
+ createReceiverIntent(
62
+ ReceiverService.ACTION_PRESS_INTENT, "notification-b", null, "done", "payload-b");
63
+
64
+ assertDistinctAndroidIdentity(notificationA, notificationB);
65
+ assertEquals("payload-a", payloadVersion(savedIntent(notificationA)));
66
+ assertEquals("payload-b", payloadVersion(savedIntent(notificationB)));
67
+ }
68
+
69
+ @Test
70
+ public void deleteIntents_forDifferentNotificationsAreDistinct() {
71
+ PendingIntent deleteA =
72
+ createReceiverIntent(ReceiverService.DELETE_INTENT, "notification-a", "tag", null, null);
73
+ PendingIntent deleteB =
74
+ createReceiverIntent(ReceiverService.DELETE_INTENT, "notification-b", "tag", null, null);
75
+
76
+ assertDistinctAndroidIdentity(deleteA, deleteB);
77
+ }
78
+
79
+ @Test
80
+ public void actionPressIntents_forDifferentNotificationsAreDistinct() {
81
+ PendingIntent actionA =
82
+ createReceiverIntent(
83
+ ReceiverService.ACTION_PRESS_INTENT, "notification-a", "tag", "done", null);
84
+ PendingIntent actionB =
85
+ createReceiverIntent(
86
+ ReceiverService.ACTION_PRESS_INTENT, "notification-b", "tag", "done", null);
87
+
88
+ assertDistinctAndroidIdentity(actionA, actionB);
89
+ }
90
+
91
+ @Test
92
+ public void actionPressIntents_forDifferentActionsOnSameNotificationAreDistinct() {
93
+ PendingIntent done =
94
+ createReceiverIntent(
95
+ ReceiverService.ACTION_PRESS_INTENT, "notification", "tag", "done", null);
96
+ PendingIntent snooze =
97
+ createReceiverIntent(
98
+ ReceiverService.ACTION_PRESS_INTENT, "notification", "tag", "snooze", null);
99
+
100
+ assertDistinctAndroidIdentity(done, snooze);
101
+ }
102
+
103
+ @Test
104
+ public void sameInteractionReposted_reusesTokenAndUpdatesExtras() {
105
+ PendingIntent original =
106
+ createReceiverIntent(
107
+ ReceiverService.ACTION_PRESS_INTENT, "notification", "tag", "done", "v1");
108
+ Intent originalIntent = new Intent(Shadows.shadowOf(original).getSavedIntent());
109
+ assertEquals("v1", payloadVersion(originalIntent));
110
+
111
+ PendingIntent reposted =
112
+ createReceiverIntent(
113
+ ReceiverService.ACTION_PRESS_INTENT, "notification", "tag", "done", "v2");
114
+
115
+ assertEquals(original, reposted);
116
+ Intent updatedIntent = Shadows.shadowOf(reposted).getSavedIntent();
117
+ assertTrue(originalIntent.filterEquals(updatedIntent));
118
+ assertEquals(originalIntent.getData(), updatedIntent.getData());
119
+ assertEquals("v2", payloadVersion(updatedIntent));
120
+ }
121
+
122
+ @Test
123
+ public void sameNotificationId_withDifferentTagsIsDistinct() {
124
+ PendingIntent tagA =
125
+ createReceiverIntent(ReceiverService.ACTION_PRESS_INTENT, "same-id", "tag-a", "done", null);
126
+ PendingIntent tagB =
127
+ createReceiverIntent(ReceiverService.ACTION_PRESS_INTENT, "same-id", "tag-b", "done", null);
128
+
129
+ assertDistinctAndroidIdentity(tagA, tagB);
130
+ }
131
+
132
+ @Test
133
+ public void deletePressAndActionPress_areDistinctInteractionTypes() {
134
+ PendingIntent delete =
135
+ createReceiverIntent(ReceiverService.DELETE_INTENT, "notification", "tag", null, null);
136
+ PendingIntent press =
137
+ createReceiverIntent(ReceiverService.PRESS_INTENT, "notification", "tag", "done", null);
138
+ PendingIntent actionPress =
139
+ createReceiverIntent(
140
+ ReceiverService.ACTION_PRESS_INTENT, "notification", "tag", "done", null);
141
+
142
+ assertDistinctAndroidIdentity(delete, press);
143
+ assertDistinctAndroidIdentity(delete, actionPress);
144
+ assertDistinctAndroidIdentity(press, actionPress);
145
+ }
146
+
147
+ @Test
148
+ public void delimiterHeavyValues_doNotCreateAmbiguousIdentity() {
149
+ String delimiterHeavyValue = "/?&=%# spazio 雪";
150
+ String tagA = "tag:" + delimiterHeavyValue + ":part";
151
+ String notificationIdA = "item";
152
+ String tagB = "tag:" + delimiterHeavyValue;
153
+ String notificationIdB = "part:item";
154
+
155
+ assertEquals(
156
+ naiveColonIdentity(tagA, notificationIdA, "done"),
157
+ naiveColonIdentity(tagB, notificationIdB, "done"));
158
+
159
+ PendingIntent interactionA =
160
+ createReceiverIntent(
161
+ ReceiverService.ACTION_PRESS_INTENT, notificationIdA, tagA, "done", null);
162
+ PendingIntent interactionB =
163
+ createReceiverIntent(
164
+ ReceiverService.ACTION_PRESS_INTENT, notificationIdB, tagB, "done", null);
165
+
166
+ assertDistinctAndroidIdentity(interactionA, interactionB);
167
+
168
+ Uri dataA = savedIntent(interactionA).getData();
169
+ Uri dataB = savedIntent(interactionB).getData();
170
+ assertNotNull(dataA);
171
+ assertNotNull(dataB);
172
+ assertEquals(tagA, dataA.getQueryParameter("notificationTag"));
173
+ assertEquals(notificationIdA, dataA.getQueryParameter("notificationId"));
174
+ assertEquals(tagB, dataB.getQueryParameter("notificationTag"));
175
+ assertEquals(notificationIdB, dataB.getQueryParameter("notificationId"));
176
+ }
177
+
178
+ @Test
179
+ public void nullEmptyAndLiteralNullTags_areDistinct() {
180
+ PendingIntent nullTag =
181
+ createReceiverIntent(
182
+ ReceiverService.ACTION_PRESS_INTENT, "notification", null, "done", null);
183
+ PendingIntent emptyTag =
184
+ createReceiverIntent(ReceiverService.ACTION_PRESS_INTENT, "notification", "", "done", null);
185
+ PendingIntent literalNullTag =
186
+ createReceiverIntent(
187
+ ReceiverService.ACTION_PRESS_INTENT, "notification", "null", "done", null);
188
+
189
+ assertDistinctAndroidIdentity(nullTag, emptyTag);
190
+ assertDistinctAndroidIdentity(nullTag, literalNullTag);
191
+ assertDistinctAndroidIdentity(emptyTag, literalNullTag);
192
+
193
+ Uri nullData = savedIntent(nullTag).getData();
194
+ Uri emptyData = savedIntent(emptyTag).getData();
195
+ Uri literalNullData = savedIntent(literalNullTag).getData();
196
+ assertNotNull(nullData);
197
+ assertNotNull(emptyData);
198
+ assertNotNull(literalNullData);
199
+ assertEquals("0", nullData.getQueryParameter("notificationTagPresent"));
200
+ assertNull(nullData.getQueryParameter("notificationTag"));
201
+ assertEquals("1", emptyData.getQueryParameter("notificationTagPresent"));
202
+ assertEquals("", emptyData.getQueryParameter("notificationTag"));
203
+ assertEquals("1", literalNullData.getQueryParameter("notificationTagPresent"));
204
+ assertEquals("null", literalNullData.getQueryParameter("notificationTag"));
205
+ }
206
+
207
+ @Test
208
+ public void requestCode_isStableAndDoesNotDependOnConstructionOrder() {
209
+ PendingIntent originalA =
210
+ createReceiverIntent(
211
+ ReceiverService.ACTION_PRESS_INTENT, "notification-a", "tag", "done", "v1");
212
+ PendingIntent notificationB =
213
+ createReceiverIntent(
214
+ ReceiverService.ACTION_PRESS_INTENT, "notification-b", "tag", "done", null);
215
+ PendingIntent repostedA =
216
+ createReceiverIntent(
217
+ ReceiverService.ACTION_PRESS_INTENT, "notification-a", "tag", "done", "v2");
218
+
219
+ ShadowPendingIntent shadowA = Shadows.shadowOf(originalA);
220
+ ShadowPendingIntent shadowB = Shadows.shadowOf(notificationB);
221
+ ShadowPendingIntent shadowRepostedA = Shadows.shadowOf(repostedA);
222
+
223
+ assertEquals(shadowA.getRequestCode(), shadowB.getRequestCode());
224
+ assertEquals(shadowA.getRequestCode(), shadowRepostedA.getRequestCode());
225
+ assertNotEquals(originalA, notificationB);
226
+ assertEquals(originalA, repostedA);
227
+ assertEquals("v2", payloadVersion(shadowA.getSavedIntent()));
228
+ assertTrue((shadowA.getFlags() & PendingIntent.FLAG_UPDATE_CURRENT) != 0);
229
+ assertTrue((shadowA.getFlags() & PendingIntent.FLAG_MUTABLE) != 0);
230
+ }
231
+
232
+ private static PendingIntent createReceiverIntent(
233
+ String action,
234
+ String notificationId,
235
+ String notificationTag,
236
+ String pressActionId,
237
+ String payloadVersion) {
238
+ Bundle notification = new Bundle();
239
+ notification.putString("id", notificationId);
240
+
241
+ Bundle android = new Bundle();
242
+ if (notificationTag != null) {
243
+ android.putString("tag", notificationTag);
244
+ }
245
+ notification.putBundle("android", android);
246
+
247
+ if (payloadVersion != null) {
248
+ Bundle data = new Bundle();
249
+ data.putString("version", payloadVersion);
250
+ notification.putBundle("data", data);
251
+ }
252
+
253
+ if (pressActionId == null) {
254
+ return ReceiverService.createIntent(action, new String[] {"notification"}, notification);
255
+ }
256
+
257
+ Bundle pressAction = new Bundle();
258
+ pressAction.putString("id", pressActionId);
259
+ return ReceiverService.createIntent(
260
+ action, new String[] {"notification", "pressAction"}, notification, pressAction);
261
+ }
262
+
263
+ private static void assertDistinctAndroidIdentity(PendingIntent first, PendingIntent second) {
264
+ ShadowPendingIntent firstShadow = Shadows.shadowOf(first);
265
+ ShadowPendingIntent secondShadow = Shadows.shadowOf(second);
266
+ Intent firstIntent = firstShadow.getSavedIntent();
267
+ Intent secondIntent = secondShadow.getSavedIntent();
268
+
269
+ assertNotEquals(first, second);
270
+ assertTrue(firstShadow.isService());
271
+ assertTrue(secondShadow.isService());
272
+ assertEquals(firstShadow.getRequestCode(), secondShadow.getRequestCode());
273
+ assertNotNull(firstIntent.getData());
274
+ assertNotNull(secondIntent.getData());
275
+ assertFalse(firstIntent.filterEquals(secondIntent));
276
+ assertNotEquals(firstIntent.getData(), secondIntent.getData());
277
+ }
278
+
279
+ private static Intent savedIntent(PendingIntent pendingIntent) {
280
+ Intent intent = Shadows.shadowOf(pendingIntent).getSavedIntent();
281
+ assertNotNull(intent);
282
+ return intent;
283
+ }
284
+
285
+ private static String payloadVersion(Intent intent) {
286
+ Bundle notification = intent.getBundleExtra("notification");
287
+ assertNotNull(notification);
288
+ Bundle data = notification.getBundle("data");
289
+ assertNotNull(data);
290
+ return data.getString("version");
291
+ }
292
+
293
+ private static String naiveColonIdentity(
294
+ String notificationTag, String notificationId, String pressActionId) {
295
+ return ReceiverService.ACTION_PRESS_INTENT
296
+ + ":"
297
+ + notificationTag
298
+ + ":"
299
+ + notificationId
300
+ + ":"
301
+ + pressActionId;
302
+ }
303
+ }
@@ -0,0 +1,337 @@
1
+ package app.notifee.core.model;
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
+ import static org.junit.Assert.assertEquals;
20
+ import static org.junit.Assert.assertNotNull;
21
+ import static org.junit.Assert.assertNull;
22
+ import static org.junit.Assert.assertSame;
23
+ import static org.junit.Assert.assertThrows;
24
+ import static org.junit.Assert.assertTrue;
25
+ import static org.mockito.Mockito.mockStatic;
26
+
27
+ import android.graphics.Bitmap;
28
+ import android.os.Bundle;
29
+ import androidx.core.app.NotificationCompat;
30
+ import androidx.core.app.Person;
31
+ import androidx.core.graphics.drawable.IconCompat;
32
+ import app.notifee.core.utility.ResourceUtils;
33
+ import com.google.common.util.concurrent.ForwardingListeningExecutorService;
34
+ import com.google.common.util.concurrent.Futures;
35
+ import com.google.common.util.concurrent.ListenableFuture;
36
+ import com.google.common.util.concurrent.ListeningExecutorService;
37
+ import com.google.common.util.concurrent.MoreExecutors;
38
+ import java.util.ArrayList;
39
+ import java.util.List;
40
+ import java.util.concurrent.Callable;
41
+ import java.util.concurrent.ExecutionException;
42
+ import java.util.concurrent.Executor;
43
+ import java.util.concurrent.TimeUnit;
44
+ import java.util.concurrent.TimeoutException;
45
+ import org.junit.Test;
46
+ import org.junit.runner.RunWith;
47
+ import org.mockito.MockedStatic;
48
+ import org.robolectric.RobolectricTestRunner;
49
+
50
+ /**
51
+ * Regression tests for the messaging style's person lookups.
52
+ *
53
+ * <p>The person task has a shorter icon timeout, but scheduling delays, process suspension, or
54
+ * other stalls can still exhaust the outer timed wait. {@link PersonFutureFailureExecutor} models
55
+ * outer-wait outcomes deterministically; it does not reproduce or identify their runtime cause.
56
+ */
57
+ @RunWith(RobolectricTestRunner.class)
58
+ public class NotificationAndroidStyleModelPersonFailureTest {
59
+
60
+ private static final int STYLE_TYPE_MESSAGING = 3;
61
+ private static final int USER_PERSON_INDEX = 0;
62
+ private static final int MESSAGE_PERSON_INDEX = 1;
63
+ private static final String USER_ICON_URL = "https://example.invalid/user.png";
64
+
65
+ @Test
66
+ public void messagingStyle_userPersonTimesOut_keepsEverythingButTheIcon() throws Exception {
67
+ NotificationAndroidStyleModel model =
68
+ NotificationAndroidStyleModel.fromBundle(messagingStyleBundle(USER_ICON_URL, null));
69
+
70
+ NotificationCompat.Style resolvedStyle =
71
+ model.getStyleTask(timesOutAt(USER_PERSON_INDEX)).get();
72
+
73
+ assertTrue(resolvedStyle instanceof NotificationCompat.MessagingStyle);
74
+ NotificationCompat.MessagingStyle style = (NotificationCompat.MessagingStyle) resolvedStyle;
75
+ Person user = style.getUser();
76
+ assertPersonFields(user, "Me", "viewer-1", "mailto:me@example.com");
77
+ assertNull(user.getIcon());
78
+ }
79
+
80
+ @Test
81
+ public void messagingStyle_messagePersonTimesOut_keepsEveryFieldButTheIcon() throws Exception {
82
+ NotificationAndroidStyleModel model =
83
+ NotificationAndroidStyleModel.fromBundle(messagingStyleBundle(null, USER_ICON_URL));
84
+
85
+ NotificationCompat.Style resolvedStyle =
86
+ model.getStyleTask(timesOutAt(MESSAGE_PERSON_INDEX)).get();
87
+
88
+ assertTrue(resolvedStyle instanceof NotificationCompat.MessagingStyle);
89
+ NotificationCompat.MessagingStyle style = (NotificationCompat.MessagingStyle) resolvedStyle;
90
+ List<NotificationCompat.MessagingStyle.Message> messages = style.getMessages();
91
+ assertEquals(1, messages.size());
92
+ assertEquals("hello", messages.get(0).getText().toString());
93
+ Person messagePerson = messages.get(0).getPerson();
94
+ assertPersonFields(messagePerson, "Alice", "alice-1", "mailto:a@example.com");
95
+ assertNull(messagePerson.getIcon());
96
+ }
97
+
98
+ @Test
99
+ public void messagingStyle_iconFutureFails_keepsEverythingButTheIcon() throws Exception {
100
+ IllegalStateException iconFailure = new IllegalStateException("simulated icon load failure");
101
+ NotificationAndroidStyleModel model =
102
+ NotificationAndroidStyleModel.fromBundle(messagingStyleBundle(USER_ICON_URL, null));
103
+
104
+ try (MockedStatic<ResourceUtils> resourceUtils = mockStatic(ResourceUtils.class)) {
105
+ resourceUtils
106
+ .when(() -> ResourceUtils.getImageBitmapFromUrl(USER_ICON_URL))
107
+ .thenReturn(Futures.immediateFailedFuture(iconFailure));
108
+
109
+ NotificationCompat.MessagingStyle style =
110
+ (NotificationCompat.MessagingStyle)
111
+ model.getStyleTask(MoreExecutors.newDirectExecutorService()).get();
112
+
113
+ Person user = style.getUser();
114
+ assertPersonFields(user, "Me", "viewer-1", "mailto:me@example.com");
115
+ assertNull(user.getIcon());
116
+ }
117
+ }
118
+
119
+ @Test
120
+ public void messagingStyle_unexpectedPersonFailure_propagates() {
121
+ IllegalStateException unexpectedFailure =
122
+ new IllegalStateException("simulated unexpected person task failure");
123
+ NotificationAndroidStyleModel model =
124
+ NotificationAndroidStyleModel.fromBundle(messagingStyleBundle());
125
+
126
+ ExecutionException failure =
127
+ assertThrows(
128
+ ExecutionException.class,
129
+ () ->
130
+ model
131
+ .getStyleTask(failsUnexpectedlyAt(USER_PERSON_INDEX, unexpectedFailure))
132
+ .get());
133
+
134
+ assertTrue(failure.getCause() instanceof ExecutionException);
135
+ assertSame(unexpectedFailure, failure.getCause().getCause());
136
+ }
137
+
138
+ @Test
139
+ public void messagingStyle_outerPersonWaitIsInterrupted_propagates() {
140
+ InterruptedException interruption = new InterruptedException("simulated outer interruption");
141
+ NotificationAndroidStyleModel model =
142
+ NotificationAndroidStyleModel.fromBundle(messagingStyleBundle());
143
+
144
+ ExecutionException failure =
145
+ assertThrows(
146
+ ExecutionException.class,
147
+ () -> model.getStyleTask(interruptedAt(USER_PERSON_INDEX, interruption)).get());
148
+
149
+ assertSame(interruption, failure.getCause());
150
+ }
151
+
152
+ @Test
153
+ public void messagingStyle_personIconResolves_mapsEveryBundleField() throws Exception {
154
+ Bitmap iconBitmap = Bitmap.createBitmap(2, 2, Bitmap.Config.ARGB_8888);
155
+ NotificationAndroidStyleModel model =
156
+ NotificationAndroidStyleModel.fromBundle(messagingStyleBundle(USER_ICON_URL, null));
157
+
158
+ try (MockedStatic<ResourceUtils> resourceUtils = mockStatic(ResourceUtils.class)) {
159
+ resourceUtils
160
+ .when(() -> ResourceUtils.getImageBitmapFromUrl(USER_ICON_URL))
161
+ .thenReturn(Futures.immediateFuture(iconBitmap));
162
+
163
+ NotificationCompat.MessagingStyle style =
164
+ (NotificationCompat.MessagingStyle)
165
+ model.getStyleTask(MoreExecutors.newDirectExecutorService()).get();
166
+
167
+ Person user = style.getUser();
168
+ assertPersonFields(user, "Me", "viewer-1", "mailto:me@example.com");
169
+ assertNotNull(user.getIcon());
170
+ assertEquals(IconCompat.TYPE_ADAPTIVE_BITMAP, user.getIcon().getType());
171
+ assertEquals("Room", style.getConversationTitle().toString());
172
+ assertTrue(style.isGroupConversation());
173
+ assertEquals("Alice", style.getMessages().get(0).getPerson().getName());
174
+ } finally {
175
+ iconBitmap.recycle();
176
+ }
177
+ }
178
+
179
+ private static Bundle messagingStyleBundle() {
180
+ return messagingStyleBundle(null, null);
181
+ }
182
+
183
+ private static Bundle messagingStyleBundle(String userIcon, String messageIcon) {
184
+ Bundle messagePerson = personBundle("Alice", "alice-1");
185
+ if (messageIcon != null) {
186
+ messagePerson.putString("icon", messageIcon);
187
+ }
188
+
189
+ Bundle message = new Bundle();
190
+ message.putString("text", "hello");
191
+ message.putLong("timestamp", 1_700_000_000_000L);
192
+ message.putBundle("person", messagePerson);
193
+
194
+ ArrayList<Bundle> messages = new ArrayList<>();
195
+ messages.add(message);
196
+
197
+ Bundle user = personBundle("Me", "viewer-1");
198
+ if (userIcon != null) {
199
+ user.putString("icon", userIcon);
200
+ }
201
+
202
+ Bundle styleBundle = new Bundle();
203
+ styleBundle.putInt("type", STYLE_TYPE_MESSAGING);
204
+ styleBundle.putString("title", "Room");
205
+ styleBundle.putBoolean("group", true);
206
+ styleBundle.putBundle("person", user);
207
+ styleBundle.putParcelableArrayList("messages", messages);
208
+ return styleBundle;
209
+ }
210
+
211
+ private static Bundle personBundle(String name, String id) {
212
+ Bundle person = new Bundle();
213
+ person.putString("name", name);
214
+ person.putString("id", id);
215
+ person.putBoolean("important", true);
216
+ person.putBoolean("bot", true);
217
+ person.putString("uri", name.equals("Me") ? "mailto:me@example.com" : "mailto:a@example.com");
218
+ return person;
219
+ }
220
+
221
+ private static void assertPersonFields(Person person, String name, String key, String uri) {
222
+ assertEquals(name, person.getName());
223
+ assertEquals(key, person.getKey());
224
+ assertEquals(uri, person.getUri());
225
+ assertTrue(person.isImportant());
226
+ assertTrue(person.isBot());
227
+ }
228
+
229
+ private interface PersonFailure {
230
+ void raise() throws InterruptedException, ExecutionException, TimeoutException;
231
+ }
232
+
233
+ private static PersonFutureFailureExecutor timesOutAt(int personIndex) {
234
+ return new PersonFutureFailureExecutor(
235
+ personIndex,
236
+ () -> {
237
+ throw new TimeoutException("simulated person task stall");
238
+ });
239
+ }
240
+
241
+ private static PersonFutureFailureExecutor interruptedAt(
242
+ int personIndex, InterruptedException interruption) {
243
+ return new PersonFutureFailureExecutor(
244
+ personIndex,
245
+ () -> {
246
+ throw interruption;
247
+ });
248
+ }
249
+
250
+ private static PersonFutureFailureExecutor failsUnexpectedlyAt(
251
+ int personIndex, RuntimeException failure) {
252
+ return new PersonFutureFailureExecutor(
253
+ personIndex,
254
+ () -> {
255
+ throw new ExecutionException(failure);
256
+ });
257
+ }
258
+
259
+ /**
260
+ * Runs the style task and non-target person tasks inline, but makes one indexed person future
261
+ * fail when its outer timed wait is invoked.
262
+ */
263
+ private static final class PersonFutureFailureExecutor
264
+ extends ForwardingListeningExecutorService {
265
+ private final ListeningExecutorService delegate = MoreExecutors.newDirectExecutorService();
266
+ private final int failingPersonIndex;
267
+ private final PersonFailure failure;
268
+ private boolean styleTaskSubmitted = false;
269
+ private int personTaskCount = 0;
270
+
271
+ PersonFutureFailureExecutor(int failingPersonIndex, PersonFailure failure) {
272
+ this.failingPersonIndex = failingPersonIndex;
273
+ this.failure = failure;
274
+ }
275
+
276
+ @Override
277
+ protected ListeningExecutorService delegate() {
278
+ return delegate;
279
+ }
280
+
281
+ @Override
282
+ public <T> ListenableFuture<T> submit(Callable<T> task) {
283
+ if (!styleTaskSubmitted) {
284
+ styleTaskSubmitted = true;
285
+ return runImmediately(task);
286
+ }
287
+
288
+ if (personTaskCount++ == failingPersonIndex) {
289
+ return failingFuture();
290
+ }
291
+
292
+ return runImmediately(task);
293
+ }
294
+
295
+ private static <T> ListenableFuture<T> runImmediately(Callable<T> task) {
296
+ try {
297
+ return Futures.immediateFuture(task.call());
298
+ } catch (Exception e) {
299
+ return Futures.immediateFailedFuture(e);
300
+ }
301
+ }
302
+
303
+ private <T> ListenableFuture<T> failingFuture() {
304
+ return new ListenableFuture<T>() {
305
+ @Override
306
+ public void addListener(Runnable listener, Executor executor) {}
307
+
308
+ @Override
309
+ public boolean cancel(boolean mayInterruptIfRunning) {
310
+ return false;
311
+ }
312
+
313
+ @Override
314
+ public boolean isCancelled() {
315
+ return false;
316
+ }
317
+
318
+ @Override
319
+ public boolean isDone() {
320
+ return false;
321
+ }
322
+
323
+ @Override
324
+ public T get() {
325
+ throw new UnsupportedOperationException("the style awaits persons with a timeout");
326
+ }
327
+
328
+ @Override
329
+ public T get(long timeout, TimeUnit unit)
330
+ throws InterruptedException, ExecutionException, TimeoutException {
331
+ failure.raise();
332
+ throw new AssertionError("unreachable");
333
+ }
334
+ };
335
+ }
336
+ }
337
+ }