react-native-notify-kit 10.4.9 → 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.
- package/README.md +60 -2
- 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 +2 -2
- package/server/README.md +6 -4
- package/server/dist/android.d.ts +8 -0
- package/server/dist/android.d.ts.map +1 -0
- package/server/dist/android.js +20 -0
- package/server/dist/android.js.map +1 -0
- package/server/dist/buildPayload.d.ts +3 -0
- package/server/dist/buildPayload.d.ts.map +1 -0
- package/server/dist/buildPayload.js +82 -0
- package/server/dist/buildPayload.js.map +1 -0
- package/server/dist/index.d.ts +6 -0
- package/server/dist/index.d.ts.map +1 -0
- package/server/dist/index.js +12 -0
- package/server/dist/index.js.map +1 -0
- package/server/dist/ios.d.ts +11 -0
- package/server/dist/ios.d.ts.map +1 -0
- package/server/dist/ios.js +61 -0
- package/server/dist/ios.js.map +1 -0
- package/server/dist/serialize.d.ts +11 -0
- package/server/dist/serialize.d.ts.map +1 -0
- package/server/dist/serialize.js +32 -0
- package/server/dist/serialize.js.map +1 -0
- package/server/dist/types.d.ts +2 -0
- package/server/dist/types.d.ts.map +1 -0
- package/server/dist/types.js +3 -0
- package/server/dist/types.js.map +1 -0
- package/server/dist/validation.d.ts +3 -0
- package/server/dist/validation.d.ts.map +1 -0
- package/server/dist/validation.js +115 -0
- package/server/dist/validation.js.map +1 -0
- package/server/src/android.ts +4 -4
- package/server/src/validation.ts +20 -0
- package/src/NotifeeApiModule.ts +4 -0
- package/src/internal/fcmContract.d.ts +10 -8
- 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
|
@@ -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
|
+
}
|