react-native-notify-kit 9.5.0 → 9.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 +204 -2
- package/android/src/androidTest/java/app/notifee/core/RebootRecoveryTest.java +378 -13
- package/android/src/main/java/app/notifee/core/AlarmPermissionBroadcastReceiver.java +17 -6
- package/android/src/main/java/app/notifee/core/InitProvider.java +109 -2
- package/android/src/main/java/app/notifee/core/NotifeeAlarmManager.java +228 -17
- package/android/src/main/java/app/notifee/core/NotificationAlarmReceiver.java +16 -5
- package/android/src/main/java/app/notifee/core/NotificationManager.java +98 -0
- package/android/src/main/java/app/notifee/core/Preferences.java +9 -0
- package/android/src/main/java/app/notifee/core/RebootBroadcastReceiver.java +20 -5
- package/android/src/main/kotlin/io/invertase/notifee/HeadlessTask.kt +3 -2
- package/android/src/test/java/app/notifee/core/ForegroundServiceTest.java +251 -4
- package/android/src/test/java/app/notifee/core/GetDisplayedNotificationsDataTest.java +242 -0
- package/android/src/test/java/app/notifee/core/InitProviderBootCheckTest.java +81 -0
- package/android/src/test/java/app/notifee/core/NotifeeAlarmManagerHandleStaleTest.java +242 -0
- package/android/src/test/java/app/notifee/core/NotifeeAlarmManagerSetAlarmClockTest.java +264 -0
- package/android/src/test/java/app/notifee/core/model/TimestampTriggerModelTest.java +199 -3
- package/android/src/test/java/io/invertase/notifee/HeadlessTaskConfigTest.kt +34 -0
- package/dist/types/Notification.d.ts +35 -4
- package/dist/types/Notification.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/jest-mock.js +1 -0
- package/package.json +1 -1
- package/src/types/Notification.ts +35 -4
- package/src/version.ts +1 -1
|
@@ -1,11 +1,26 @@
|
|
|
1
1
|
package app.notifee.core;
|
|
2
2
|
|
|
3
3
|
import static org.junit.Assert.assertEquals;
|
|
4
|
+
import static org.junit.Assert.assertNotNull;
|
|
5
|
+
import static org.junit.Assert.assertNull;
|
|
4
6
|
import static org.junit.Assert.assertTrue;
|
|
5
7
|
import static org.junit.Assert.fail;
|
|
6
8
|
|
|
9
|
+
import android.app.Notification;
|
|
10
|
+
import android.app.NotificationChannel;
|
|
11
|
+
import android.app.NotificationManager;
|
|
7
12
|
import android.app.Service;
|
|
13
|
+
import android.content.Context;
|
|
8
14
|
import android.content.Intent;
|
|
15
|
+
import android.content.pm.ServiceInfo;
|
|
16
|
+
import android.os.Bundle;
|
|
17
|
+
import androidx.core.app.NotificationCompat;
|
|
18
|
+
import app.notifee.core.event.NotificationEvent;
|
|
19
|
+
import java.lang.reflect.Field;
|
|
20
|
+
import java.util.ArrayList;
|
|
21
|
+
import java.util.List;
|
|
22
|
+
import org.greenrobot.eventbus.Subscribe;
|
|
23
|
+
import org.greenrobot.eventbus.ThreadMode;
|
|
9
24
|
import org.junit.After;
|
|
10
25
|
import org.junit.Before;
|
|
11
26
|
import org.junit.Test;
|
|
@@ -26,12 +41,15 @@ public class ForegroundServiceTest {
|
|
|
26
41
|
}
|
|
27
42
|
|
|
28
43
|
@After
|
|
29
|
-
public void tearDown() {
|
|
30
|
-
// Reset
|
|
31
|
-
//
|
|
32
|
-
//
|
|
44
|
+
public void tearDown() throws Exception {
|
|
45
|
+
// Reset public and private static fields to prevent cross-test pollution. The three private
|
|
46
|
+
// statics are cleared via reflection here so that tests which seed them directly (to exercise
|
|
47
|
+
// onTimeout without running the full START path) cannot leak state into neighbouring tests.
|
|
33
48
|
ForegroundService.mCurrentNotificationId = null;
|
|
34
49
|
ForegroundService.mCurrentForegroundServiceType = -1;
|
|
50
|
+
setPrivateStatic("mCurrentNotificationBundle", null);
|
|
51
|
+
setPrivateStatic("mCurrentNotification", null);
|
|
52
|
+
setPrivateStatic("mCurrentHashCode", 0);
|
|
35
53
|
}
|
|
36
54
|
|
|
37
55
|
/**
|
|
@@ -179,4 +197,233 @@ public class ForegroundServiceTest {
|
|
|
179
197
|
int result = service.onStartCommand(stopIntent, 0, 2);
|
|
180
198
|
assertEquals(Service.START_STICKY_COMPATIBILITY, result);
|
|
181
199
|
}
|
|
200
|
+
|
|
201
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
202
|
+
// START path and onTimeout event emission (regression guards for 9.1.13)
|
|
203
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
204
|
+
|
|
205
|
+
private static final String TEST_CHANNEL_ID = "fgs-test-channel";
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* START happy path: a valid intent with a notification payload must drive the service through
|
|
209
|
+
* {@code startForeground()} and leave {@code mCurrentNotificationId} populated so a subsequent
|
|
210
|
+
* STOP/onTimeout can reference it. Runs on SDK 33 to avoid the API 34+ manifest-type check, which
|
|
211
|
+
* would require a test-specific {@code foregroundServiceType} declaration.
|
|
212
|
+
*/
|
|
213
|
+
@Test
|
|
214
|
+
@Config(sdk = 33)
|
|
215
|
+
public void onStartCommand_startIntent_setsCurrentNotificationIdAndCallsStartForeground()
|
|
216
|
+
throws Exception {
|
|
217
|
+
createChannel();
|
|
218
|
+
ServiceController<ForegroundService> controller =
|
|
219
|
+
Robolectric.buildService(ForegroundService.class);
|
|
220
|
+
ForegroundService service = controller.create().get();
|
|
221
|
+
|
|
222
|
+
String id = "fgs-start-happy";
|
|
223
|
+
int result =
|
|
224
|
+
service.onStartCommand(
|
|
225
|
+
buildStartIntent(id, id.hashCode()), /* flags= */ 0, /* startId= */ 1);
|
|
226
|
+
|
|
227
|
+
assertEquals(Service.START_NOT_STICKY, result);
|
|
228
|
+
assertEquals(id, ForegroundService.mCurrentNotificationId);
|
|
229
|
+
// Robolectric's ShadowService records the most recent Notification passed to
|
|
230
|
+
// startForeground(); a non-null result proves the 3-arg startForeground() overload on the
|
|
231
|
+
// API 33 branch of onStartCommand was exercised and Android's contract was satisfied.
|
|
232
|
+
Notification posted = org.robolectric.Shadows.shadowOf(service).getLastForegroundNotification();
|
|
233
|
+
assertNotNull("startForeground() must have been called during the START path", posted);
|
|
234
|
+
// The private static mCurrentHashCode tracks the caller-supplied hash; verifying it via
|
|
235
|
+
// reflection proves the START branch fully ran (not just the early-return path).
|
|
236
|
+
Field hashField = ForegroundService.class.getDeclaredField("mCurrentHashCode");
|
|
237
|
+
hashField.setAccessible(true);
|
|
238
|
+
assertEquals(id.hashCode(), hashField.getInt(null));
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Regression guard for the 9.1.13 {@code onTimeout(int)} fix (upstream invertase/notifee#703). On
|
|
243
|
+
* API 34, Android's single-argument {@code onTimeout} fires when a {@code shortService} FGS
|
|
244
|
+
* exceeds its 3-minute budget. The handler must:
|
|
245
|
+
*
|
|
246
|
+
* <ol>
|
|
247
|
+
* <li>emit a {@link NotificationEvent} with type {@link NotificationEvent#TYPE_FG_TIMEOUT},
|
|
248
|
+
* <li>carry the originating notification model so JS can correlate the event,
|
|
249
|
+
* <li>populate {@code startId} and {@code fgsType} extras (the latter is {@code -1} as a
|
|
250
|
+
* sentinel on the single-argument variant), and
|
|
251
|
+
* <li>reset the service's static tracking state.
|
|
252
|
+
* </ol>
|
|
253
|
+
*/
|
|
254
|
+
@Test
|
|
255
|
+
@Config(sdk = 34)
|
|
256
|
+
public void onTimeout_api34_emitsFgTimeoutEventWithStartIdAndSentinelFgsType() throws Exception {
|
|
257
|
+
ServiceController<ForegroundService> controller =
|
|
258
|
+
Robolectric.buildService(ForegroundService.class);
|
|
259
|
+
ForegroundService service = controller.create().get();
|
|
260
|
+
|
|
261
|
+
String id = "fgs-timeout-api34";
|
|
262
|
+
seedActiveForegroundServiceState(id);
|
|
263
|
+
|
|
264
|
+
FgsEventCapture capture = new FgsEventCapture();
|
|
265
|
+
EventBus.register(capture);
|
|
266
|
+
try {
|
|
267
|
+
int startId = 42;
|
|
268
|
+
service.onTimeout(startId);
|
|
269
|
+
|
|
270
|
+
assertEquals(
|
|
271
|
+
"exactly one NotificationEvent should be emitted on timeout", 1, capture.events.size());
|
|
272
|
+
NotificationEvent event = capture.events.get(0);
|
|
273
|
+
assertEquals(NotificationEvent.TYPE_FG_TIMEOUT, event.getType());
|
|
274
|
+
assertNotNull(
|
|
275
|
+
"timeout event must carry the originating notification", event.getNotification());
|
|
276
|
+
assertEquals(id, event.getNotification().getId());
|
|
277
|
+
assertNotNull("timeout event must carry startId/fgsType extras", event.getExtras());
|
|
278
|
+
assertEquals(startId, event.getExtras().getInt("startId"));
|
|
279
|
+
// handleTimeout is called with -1 as the fgsType sentinel from the single-argument overload.
|
|
280
|
+
assertEquals(-1, event.getExtras().getInt("fgsType"));
|
|
281
|
+
} finally {
|
|
282
|
+
EventBus.unregister(capture);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
assertNull(
|
|
286
|
+
"mCurrentNotificationId must be cleared after onTimeout",
|
|
287
|
+
ForegroundService.mCurrentNotificationId);
|
|
288
|
+
assertEquals(-1, ForegroundService.mCurrentForegroundServiceType);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Regression guard for the API 35+ {@code onTimeout(int, int)} overload, which supersedes the
|
|
293
|
+
* single-argument variant and surfaces the type-specific timeout cause (e.g. {@code
|
|
294
|
+
* FOREGROUND_SERVICE_TYPE_DATA_SYNC}'s new Android 15 cumulative cap). The emitted event must
|
|
295
|
+
* carry the explicit {@code fgsType} value, not the sentinel.
|
|
296
|
+
*/
|
|
297
|
+
@Test
|
|
298
|
+
@Config(sdk = 35)
|
|
299
|
+
public void onTimeout_api35_emitsFgTimeoutEventWithExplicitFgsType() throws Exception {
|
|
300
|
+
ServiceController<ForegroundService> controller =
|
|
301
|
+
Robolectric.buildService(ForegroundService.class);
|
|
302
|
+
ForegroundService service = controller.create().get();
|
|
303
|
+
|
|
304
|
+
String id = "fgs-timeout-api35";
|
|
305
|
+
seedActiveForegroundServiceState(id);
|
|
306
|
+
|
|
307
|
+
FgsEventCapture capture = new FgsEventCapture();
|
|
308
|
+
EventBus.register(capture);
|
|
309
|
+
try {
|
|
310
|
+
int startId = 7;
|
|
311
|
+
int fgsType = ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC;
|
|
312
|
+
service.onTimeout(startId, fgsType);
|
|
313
|
+
|
|
314
|
+
assertEquals(1, capture.events.size());
|
|
315
|
+
NotificationEvent event = capture.events.get(0);
|
|
316
|
+
assertEquals(NotificationEvent.TYPE_FG_TIMEOUT, event.getType());
|
|
317
|
+
assertEquals(id, event.getNotification().getId());
|
|
318
|
+
assertNotNull(event.getExtras());
|
|
319
|
+
assertEquals(startId, event.getExtras().getInt("startId"));
|
|
320
|
+
assertEquals(fgsType, event.getExtras().getInt("fgsType"));
|
|
321
|
+
} finally {
|
|
322
|
+
EventBus.unregister(capture);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
assertNull(ForegroundService.mCurrentNotificationId);
|
|
326
|
+
assertEquals(-1, ForegroundService.mCurrentForegroundServiceType);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Defensive behaviour: if onTimeout fires on a service instance whose static state has already
|
|
331
|
+
* been cleared (a race between STOP and the Android system delivering a delayed timeout), the
|
|
332
|
+
* handler must not crash and must not post a stray event.
|
|
333
|
+
*/
|
|
334
|
+
@Test
|
|
335
|
+
@Config(sdk = 34)
|
|
336
|
+
public void onTimeout_withNoActiveState_doesNotCrashOrEmitEvent() {
|
|
337
|
+
ServiceController<ForegroundService> controller =
|
|
338
|
+
Robolectric.buildService(ForegroundService.class);
|
|
339
|
+
ForegroundService service = controller.create().get();
|
|
340
|
+
|
|
341
|
+
FgsEventCapture capture = new FgsEventCapture();
|
|
342
|
+
EventBus.register(capture);
|
|
343
|
+
try {
|
|
344
|
+
service.onTimeout(/* startId= */ 99);
|
|
345
|
+
assertEquals(0, capture.events.size());
|
|
346
|
+
} finally {
|
|
347
|
+
EventBus.unregister(capture);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
352
|
+
// Helpers
|
|
353
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
354
|
+
|
|
355
|
+
private static void createChannel() {
|
|
356
|
+
Context context = RuntimeEnvironment.getApplication();
|
|
357
|
+
NotificationManager nm =
|
|
358
|
+
(NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
|
359
|
+
if (nm != null && nm.getNotificationChannel(TEST_CHANNEL_ID) == null) {
|
|
360
|
+
NotificationChannel channel =
|
|
361
|
+
new NotificationChannel(
|
|
362
|
+
TEST_CHANNEL_ID, "FGS test channel", NotificationManager.IMPORTANCE_LOW);
|
|
363
|
+
nm.createNotificationChannel(channel);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
private static Bundle buildNotificationBundle(String id) {
|
|
368
|
+
Bundle bundle = new Bundle();
|
|
369
|
+
bundle.putString("id", id);
|
|
370
|
+
bundle.putString("title", "FGS test " + id);
|
|
371
|
+
Bundle androidBundle = new Bundle();
|
|
372
|
+
androidBundle.putString("channelId", TEST_CHANNEL_ID);
|
|
373
|
+
bundle.putBundle("android", androidBundle);
|
|
374
|
+
return bundle;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
private static Notification buildNotification() {
|
|
378
|
+
Context context = RuntimeEnvironment.getApplication();
|
|
379
|
+
return new NotificationCompat.Builder(context, TEST_CHANNEL_ID)
|
|
380
|
+
.setSmallIcon(android.R.drawable.ic_dialog_info)
|
|
381
|
+
.setContentTitle("FGS test")
|
|
382
|
+
.build();
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
private static Intent buildStartIntent(String id, int hashCode) {
|
|
386
|
+
Intent intent = new Intent();
|
|
387
|
+
intent.setAction(ForegroundService.START_FOREGROUND_SERVICE_ACTION);
|
|
388
|
+
intent.putExtra("hashCode", hashCode);
|
|
389
|
+
intent.putExtra("notification", buildNotification());
|
|
390
|
+
intent.putExtra("notificationBundle", buildNotificationBundle(id));
|
|
391
|
+
return intent;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Populates the service's private static tracking fields directly, simulating the post-START
|
|
396
|
+
* state that onTimeout expects to observe. Using reflection here (rather than running a full
|
|
397
|
+
* START first) keeps the onTimeout tests SDK-independent — the START path would trip the API 34+
|
|
398
|
+
* manifest-type check, but onTimeout itself is sdk-agnostic because its body only uses pre-API-34
|
|
399
|
+
* primitives.
|
|
400
|
+
*/
|
|
401
|
+
private static void seedActiveForegroundServiceState(String id) throws Exception {
|
|
402
|
+
ForegroundService.mCurrentNotificationId = id;
|
|
403
|
+
ForegroundService.mCurrentForegroundServiceType = 1;
|
|
404
|
+
setPrivateStatic("mCurrentNotificationBundle", buildNotificationBundle(id));
|
|
405
|
+
setPrivateStatic("mCurrentNotification", buildNotification());
|
|
406
|
+
setPrivateStatic("mCurrentHashCode", id.hashCode());
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
private static void setPrivateStatic(String name, Object value) throws Exception {
|
|
410
|
+
Field field = ForegroundService.class.getDeclaredField(name);
|
|
411
|
+
field.setAccessible(true);
|
|
412
|
+
field.set(null, value);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* greenrobot EventBus subscriber that records every {@link NotificationEvent} posted during a
|
|
417
|
+
* test. {@link ThreadMode#POSTING} fires the subscriber synchronously on the caller thread, so
|
|
418
|
+
* the test can inspect {@code events} immediately after {@code post()} returns without any looper
|
|
419
|
+
* advancement.
|
|
420
|
+
*/
|
|
421
|
+
public static class FgsEventCapture {
|
|
422
|
+
final List<NotificationEvent> events = new ArrayList<>();
|
|
423
|
+
|
|
424
|
+
@Subscribe(threadMode = ThreadMode.POSTING)
|
|
425
|
+
public void onNotificationEvent(NotificationEvent event) {
|
|
426
|
+
events.add(event);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
182
429
|
}
|
|
@@ -0,0 +1,242 @@
|
|
|
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.assertNotNull;
|
|
22
|
+
import static org.junit.Assert.assertTrue;
|
|
23
|
+
|
|
24
|
+
import android.os.Bundle;
|
|
25
|
+
import java.util.Arrays;
|
|
26
|
+
import java.util.HashSet;
|
|
27
|
+
import java.util.Set;
|
|
28
|
+
import org.junit.Test;
|
|
29
|
+
import org.junit.runner.RunWith;
|
|
30
|
+
import org.robolectric.RobolectricTestRunner;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Unit tests for {@link NotificationManager#shouldIncludeInData(String)} and {@link
|
|
34
|
+
* NotificationManager#extractDataFromExtras(android.os.Bundle)} — the denylist filter and
|
|
35
|
+
* bundle-to-bundle helper added for upstream invertase/notifee#393.
|
|
36
|
+
*
|
|
37
|
+
* <p>The end-to-end {@code getDisplayedNotifications()} path is intentionally not exercised here
|
|
38
|
+
* because it depends on {@code android.app.NotificationManager.getActiveNotifications()}, which
|
|
39
|
+
* would require a system service mock. Extracting the pure predicate and the extras iteration into
|
|
40
|
+
* helper methods lets these tests run under Robolectric with a real {@link Bundle} implementation
|
|
41
|
+
* and no system service at all.
|
|
42
|
+
*/
|
|
43
|
+
@RunWith(RobolectricTestRunner.class)
|
|
44
|
+
public class GetDisplayedNotificationsDataTest {
|
|
45
|
+
|
|
46
|
+
// -------- shouldIncludeInData (pure predicate) --------
|
|
47
|
+
|
|
48
|
+
@Test
|
|
49
|
+
public void shouldIncludeInData_plainKey_returnsTrue() {
|
|
50
|
+
assertTrue(NotificationManager.shouldIncludeInData("event"));
|
|
51
|
+
assertTrue(NotificationManager.shouldIncludeInData("userId"));
|
|
52
|
+
assertTrue(NotificationManager.shouldIncludeInData("conversation_id"));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@Test
|
|
56
|
+
public void shouldIncludeInData_androidPrefix_returnsFalse() {
|
|
57
|
+
assertFalse(NotificationManager.shouldIncludeInData("android.title"));
|
|
58
|
+
assertFalse(NotificationManager.shouldIncludeInData("android.text"));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
@Test
|
|
62
|
+
public void shouldIncludeInData_androidLookalike_returnsTrue() {
|
|
63
|
+
// The `android.` prefix intentionally includes the dot, so custom keys that
|
|
64
|
+
// merely begin with the substring `android` are not accidentally filtered.
|
|
65
|
+
assertTrue(NotificationManager.shouldIncludeInData("androidify"));
|
|
66
|
+
assertTrue(NotificationManager.shouldIncludeInData("androidish"));
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@Test
|
|
70
|
+
public void shouldIncludeInData_googlePrefix_returnsFalse() {
|
|
71
|
+
assertFalse(NotificationManager.shouldIncludeInData("google.sent_time"));
|
|
72
|
+
assertFalse(NotificationManager.shouldIncludeInData("google.delivered_priority"));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
@Test
|
|
76
|
+
public void shouldIncludeInData_googleLookalike_returnsTrue() {
|
|
77
|
+
// Same reasoning as androidLookalike — `google.` with dot keeps the filter precise.
|
|
78
|
+
assertTrue(NotificationManager.shouldIncludeInData("googleish"));
|
|
79
|
+
assertTrue(NotificationManager.shouldIncludeInData("googlebot"));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
@Test
|
|
83
|
+
public void shouldIncludeInData_gcmPrefix_returnsFalse() {
|
|
84
|
+
assertFalse(NotificationManager.shouldIncludeInData("gcm.notification.foo"));
|
|
85
|
+
assertFalse(NotificationManager.shouldIncludeInData("gcm.n.e"));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
@Test
|
|
89
|
+
public void shouldIncludeInData_notifeePrefix_returnsFalse() {
|
|
90
|
+
// "notifee.notification" and "notifee.trigger" are the current internal
|
|
91
|
+
// constants (EXTRA_NOTIFEE_NOTIFICATION / EXTRA_NOTIFEE_TRIGGER). They
|
|
92
|
+
// are caught by the no-dot `notifee` prefix.
|
|
93
|
+
assertFalse(NotificationManager.shouldIncludeInData("notifee.notification"));
|
|
94
|
+
assertFalse(NotificationManager.shouldIncludeInData("notifee.trigger"));
|
|
95
|
+
|
|
96
|
+
// `notifee` is INTENTIONALLY kept without the dot: the library reserves
|
|
97
|
+
// its own namespace entirely, so `notifeeFoo` is also filtered. This
|
|
98
|
+
// differs from `fcm.` (with dot, see below): for `notifee` the whole
|
|
99
|
+
// prefix is library-owned and can collide with future internal constants,
|
|
100
|
+
// whereas `fcm` is a third-party namespace where we prefer to let
|
|
101
|
+
// realistic user keys like `fcmRegion` or `fcmToken` survive.
|
|
102
|
+
assertFalse(NotificationManager.shouldIncludeInData("notifeeFoo"));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
@Test
|
|
106
|
+
public void shouldIncludeInData_fcmDottedPrefix_returnsFalse() {
|
|
107
|
+
// `fcm.` uses the trailing dot so realistic user custom keys (fcmRegion,
|
|
108
|
+
// fcmToken, fcmlike) can round-trip through `data`. This diverges from
|
|
109
|
+
// iOS parseDataFromUserInfo:, which uses bare `[key hasPrefix:@"fcm"]`
|
|
110
|
+
// specifically to catch `fcm_options` (see the `// fcm_options` marker
|
|
111
|
+
// at NotifeeCoreUtil.m:627-628 and the dedicated
|
|
112
|
+
// shouldIncludeInData_fcmOptionsExactKey_returnsFalse test above). Both
|
|
113
|
+
// platforms drop `fcm_options`; Android additionally preserves bare-fcm
|
|
114
|
+
// user keys (fcmRegion, fcmToken, …) that iOS drops as collateral.
|
|
115
|
+
assertFalse(NotificationManager.shouldIncludeInData("fcm.notification"));
|
|
116
|
+
assertFalse(NotificationManager.shouldIncludeInData("fcm.foo"));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
@Test
|
|
120
|
+
public void shouldIncludeInData_fcmLookalike_returnsTrue() {
|
|
121
|
+
// These survive the Android filter. They would NOT survive the iOS
|
|
122
|
+
// filter, which is an accepted cross-platform divergence in favor of
|
|
123
|
+
// preserving realistic user data on Android. Note: `fcm_options` is a
|
|
124
|
+
// deliberate EXCEPTION to this rule — see the dedicated test
|
|
125
|
+
// shouldIncludeInData_fcmOptionsExactKey_returnsFalse below.
|
|
126
|
+
assertTrue(NotificationManager.shouldIncludeInData("fcm"));
|
|
127
|
+
assertTrue(NotificationManager.shouldIncludeInData("fcmlike"));
|
|
128
|
+
assertTrue(NotificationManager.shouldIncludeInData("fcmRegion"));
|
|
129
|
+
assertTrue(NotificationManager.shouldIncludeInData("fcmToken"));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
@Test
|
|
133
|
+
public void shouldIncludeInData_fcmOptionsExactKey_returnsFalse() {
|
|
134
|
+
// `fcm_options` is the Firebase HTTP v1 Message.fcm_options analytics
|
|
135
|
+
// label. On iOS it is caught by the bare `[key hasPrefix:@"fcm"]` filter
|
|
136
|
+
// in NotifeeCoreUtil.m:627-628, which has an inline `// fcm_options`
|
|
137
|
+
// comment documenting that the whole reason the iOS prefix is bare (not
|
|
138
|
+
// dotted) is specifically to drop this key. On Android we switched the
|
|
139
|
+
// prefix to `fcm.` (with dot) so realistic user keys like `fcmRegion`
|
|
140
|
+
// can survive — but that alone would leak `fcm_options`. This test
|
|
141
|
+
// guards the exact-match entry in EXCLUDED_DATA_KEYS that restores
|
|
142
|
+
// parity with iOS for the one Firebase-reserved bare-`fcm` key.
|
|
143
|
+
assertFalse(NotificationManager.shouldIncludeInData("fcm_options"));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
@Test
|
|
147
|
+
public void shouldIncludeInData_exactKeyBlocklist_returnsFalse() {
|
|
148
|
+
assertFalse(NotificationManager.shouldIncludeInData("from"));
|
|
149
|
+
assertFalse(NotificationManager.shouldIncludeInData("collapse_key"));
|
|
150
|
+
assertFalse(NotificationManager.shouldIncludeInData("message_type"));
|
|
151
|
+
assertFalse(NotificationManager.shouldIncludeInData("message_id"));
|
|
152
|
+
assertFalse(NotificationManager.shouldIncludeInData("aps"));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
@Test
|
|
156
|
+
public void shouldIncludeInData_nullKey_returnsFalse() {
|
|
157
|
+
assertFalse(NotificationManager.shouldIncludeInData(null));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// -------- extractDataFromExtras (bundle → bundle) --------
|
|
161
|
+
|
|
162
|
+
@Test
|
|
163
|
+
public void extractDataFromExtras_nullExtras_returnsEmptyBundle() {
|
|
164
|
+
Bundle result = NotificationManager.extractDataFromExtras(null);
|
|
165
|
+
assertNotNull(result);
|
|
166
|
+
assertEquals(0, result.size());
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
@Test
|
|
170
|
+
public void extractDataFromExtras_emptyExtras_returnsEmptyBundle() {
|
|
171
|
+
Bundle result = NotificationManager.extractDataFromExtras(new Bundle());
|
|
172
|
+
assertNotNull(result);
|
|
173
|
+
assertEquals(0, result.size());
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
@Test
|
|
177
|
+
public void extractDataFromExtras_mixedKeys_returnsOnlyCustomKeys() {
|
|
178
|
+
Bundle extras = new Bundle();
|
|
179
|
+
|
|
180
|
+
// Custom keys that should survive the filter.
|
|
181
|
+
extras.putString("event", "chat_msg");
|
|
182
|
+
extras.putString("userId", "42");
|
|
183
|
+
|
|
184
|
+
// System keys that must be dropped.
|
|
185
|
+
extras.putString("android.title", "Hello");
|
|
186
|
+
extras.putString("android.text", "Body");
|
|
187
|
+
extras.putString("google.sent_time", "123456");
|
|
188
|
+
extras.putString("gcm.notification.e", "1");
|
|
189
|
+
extras.putString("from", "12345");
|
|
190
|
+
extras.putString("collapse_key", "do_not_collapse");
|
|
191
|
+
extras.putString("message_id", "msg-abc");
|
|
192
|
+
extras.putString("notifee.notification", "internal");
|
|
193
|
+
extras.putString("fcm.foo", "reserved-dotted");
|
|
194
|
+
|
|
195
|
+
// Custom key matching the iOS-divergent survivor pattern: `fcmRegion`
|
|
196
|
+
// must appear in the result on Android, even though iOS would drop it.
|
|
197
|
+
extras.putString("fcmRegion", "eu-west-1");
|
|
198
|
+
|
|
199
|
+
// Non-String value to verify toString() coercion for the rare survivor.
|
|
200
|
+
extras.putInt("count", 5);
|
|
201
|
+
|
|
202
|
+
Bundle result = NotificationManager.extractDataFromExtras(extras);
|
|
203
|
+
assertNotNull(result);
|
|
204
|
+
|
|
205
|
+
Set<String> expected = new HashSet<>(Arrays.asList("event", "userId", "fcmRegion", "count"));
|
|
206
|
+
assertEquals(expected, result.keySet());
|
|
207
|
+
assertEquals("chat_msg", result.getString("event"));
|
|
208
|
+
assertEquals("42", result.getString("userId"));
|
|
209
|
+
assertEquals("eu-west-1", result.getString("fcmRegion"));
|
|
210
|
+
assertEquals("5", result.getString("count"));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
@Test
|
|
214
|
+
public void extractDataFromExtras_onlySystemKeys_returnsEmptyBundle() {
|
|
215
|
+
Bundle extras = new Bundle();
|
|
216
|
+
extras.putString("android.title", "Hello");
|
|
217
|
+
extras.putString("google.sent_time", "123");
|
|
218
|
+
extras.putString("gcm.notification.foo", "bar");
|
|
219
|
+
extras.putString("notifee.trigger", "internal");
|
|
220
|
+
extras.putString("from", "12345");
|
|
221
|
+
extras.putString("fcm.options", "reserved-dotted");
|
|
222
|
+
// Firebase analytics label — exact-match filtered for iOS parity,
|
|
223
|
+
// see shouldIncludeInData_fcmOptionsExactKey_returnsFalse.
|
|
224
|
+
extras.putString("fcm_options", "{\"analytics_label\":\"foo\"}");
|
|
225
|
+
|
|
226
|
+
Bundle result = NotificationManager.extractDataFromExtras(extras);
|
|
227
|
+
assertNotNull(result);
|
|
228
|
+
assertEquals(0, result.size());
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
@Test
|
|
232
|
+
public void extractDataFromExtras_nullValueForCustomKey_isSkipped() {
|
|
233
|
+
Bundle extras = new Bundle();
|
|
234
|
+
extras.putString("event", "chat_msg");
|
|
235
|
+
extras.putString("nullable", null);
|
|
236
|
+
|
|
237
|
+
Bundle result = NotificationManager.extractDataFromExtras(extras);
|
|
238
|
+
assertNotNull(result);
|
|
239
|
+
assertEquals(1, result.size());
|
|
240
|
+
assertEquals("chat_msg", result.getString("event"));
|
|
241
|
+
}
|
|
242
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
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.assertFalse;
|
|
20
|
+
import static org.junit.Assert.assertTrue;
|
|
21
|
+
|
|
22
|
+
import org.junit.Test;
|
|
23
|
+
import org.junit.runner.RunWith;
|
|
24
|
+
import org.robolectric.RobolectricTestRunner;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Unit tests for the BOOT_COUNT-based cold-start reschedule decision logic added for upstream
|
|
28
|
+
* invertase/notifee#734. Exhaustively covers the pure decision helper {@link
|
|
29
|
+
* InitProvider#shouldRescheduleAfterBoot}.
|
|
30
|
+
*
|
|
31
|
+
* <p>The integration path {@code runBootCheck → rescheduleNotifications} is intentionally not
|
|
32
|
+
* exercised here because {@code rescheduleNotifications} touches a real Room database via {@code
|
|
33
|
+
* WorkDataRepository.getInstance}, which is not set up in this unit-test environment. That path is
|
|
34
|
+
* covered by the instrumented {@code RebootRecoveryTest} and by the manual Step 4 smoke test.
|
|
35
|
+
*/
|
|
36
|
+
@RunWith(RobolectricTestRunner.class)
|
|
37
|
+
public class InitProviderBootCheckTest {
|
|
38
|
+
|
|
39
|
+
@Test
|
|
40
|
+
public void shouldRescheduleAfterBoot_firstRun_withValidBootCount_returnsFalse() {
|
|
41
|
+
// lastKnown == -1 means "no baseline recorded yet". We record the baseline
|
|
42
|
+
// and skip reschedule to avoid firing stale triggers on fresh installs.
|
|
43
|
+
assertFalse(InitProvider.shouldRescheduleAfterBoot(5, -1));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
@Test
|
|
47
|
+
public void shouldRescheduleAfterBoot_sameBoot_returnsFalse() {
|
|
48
|
+
// Same app process, same device boot → nothing to recover.
|
|
49
|
+
assertFalse(InitProvider.shouldRescheduleAfterBoot(5, 5));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@Test
|
|
53
|
+
public void shouldRescheduleAfterBoot_newBoot_returnsTrue() {
|
|
54
|
+
// Reboot detected since last run → recover scheduled alarms.
|
|
55
|
+
assertTrue(InitProvider.shouldRescheduleAfterBoot(6, 5));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
@Test
|
|
59
|
+
public void shouldRescheduleAfterBoot_bootCountDecreased_returnsTrue() {
|
|
60
|
+
// BOOT_COUNT decreasing is unexpected but possible (factory reset, counter
|
|
61
|
+
// rollover on exotic ROMs). Any change is treated as "unknown transition
|
|
62
|
+
// since last run" → conservative reschedule.
|
|
63
|
+
assertTrue(InitProvider.shouldRescheduleAfterBoot(3, 5));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@Test
|
|
67
|
+
public void shouldRescheduleAfterBoot_bootCountUnavailable_returnsTrue() {
|
|
68
|
+
// BOOT_COUNT couldn't be read → we can't tell if a reboot happened, so we
|
|
69
|
+
// reschedule conservatively. This also covers emulators and rooted ROMs
|
|
70
|
+
// where the Settings.Global row is missing or throws.
|
|
71
|
+
assertTrue(InitProvider.shouldRescheduleAfterBoot(-1, 5));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
@Test
|
|
75
|
+
public void shouldRescheduleAfterBoot_bootCountUnavailable_andFirstRun_returnsTrue() {
|
|
76
|
+
// Degenerate case: first run AND BOOT_COUNT unavailable. The "unavailable"
|
|
77
|
+
// branch wins over the "first run" branch because we genuinely don't know
|
|
78
|
+
// if the device has rebooted since install.
|
|
79
|
+
assertTrue(InitProvider.shouldRescheduleAfterBoot(-1, -1));
|
|
80
|
+
}
|
|
81
|
+
}
|