react-native-notify-kit 10.0.0 → 10.2.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 +168 -100
- package/android/build.gradle +2 -2
- package/android/src/androidTest/java/app/notifee/core/RebootRecoveryTest.java +94 -2
- package/android/src/main/AndroidManifest.xml +8 -7
- package/android/src/main/java/app/notifee/core/ForegroundService.java +1 -0
- package/android/src/main/java/app/notifee/core/NotificationManager.java +8 -0
- package/android/src/main/java/app/notifee/core/RebootBroadcastReceiver.java +16 -0
- package/android/src/main/java/app/notifee/core/model/NotificationAndroidModel.java +8 -3
- package/android/src/main/java/app/notifee/core/model/TimestampTriggerModel.java +42 -27
- package/android/src/main/java/app/notifee/core/utility/ResourceUtils.java +30 -1
- package/android/src/main/kotlin/io/invertase/notifee/NotifeeApiModule.kt +6 -6
- package/android/src/test/java/app/notifee/core/model/NotificationAndroidModelSmallIconFallbackTest.java +74 -0
- package/android/src/test/java/app/notifee/core/model/TimestampTriggerModelTest.java +154 -0
- package/android/src/test/java/app/notifee/core/utility/ResourceUtilsFallbackIconTest.java +116 -0
- package/dist/types/Module.d.ts +7 -7
- package/dist/types/Notification.d.ts +1 -1
- package/dist/types/NotificationIOS.d.ts +44 -21
- package/dist/types/NotificationIOS.js +15 -1
- package/dist/types/NotificationIOS.js.map +1 -1
- package/dist/types/Trigger.d.ts +45 -2
- package/dist/types/Trigger.js +22 -0
- package/dist/types/Trigger.js.map +1 -1
- package/dist/validators/validateIOSCategory.js +2 -2
- package/dist/validators/validateIOSCategory.js.map +1 -1
- package/dist/validators/validateTrigger.js +42 -1
- package/dist/validators/validateTrigger.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/ios/NotifeeCore/NotifeeCore+NSNotificationCenter.m +20 -10
- package/ios/NotifeeCore/NotifeeCore+UNUserNotificationCenter.h +1 -2
- package/ios/NotifeeCore/NotifeeCore+UNUserNotificationCenter.m +44 -35
- package/ios/NotifeeCore/NotifeeCore.m +1035 -99
- package/ios/NotifeeCore/NotifeeCoreUtil.h +36 -1
- package/ios/NotifeeCore/NotifeeCoreUtil.m +533 -9
- package/ios/RNNotifee/NotifeeApiModule.mm +2 -2
- package/package.json +1 -1
- package/server/README.md +4 -4
- package/src/types/Module.ts +7 -7
- package/src/types/Notification.ts +1 -1
- package/src/types/NotificationIOS.ts +43 -28
- package/src/types/Trigger.ts +45 -1
- package/src/validators/validateIOSCategory.ts +4 -2
- package/src/validators/validateTrigger.ts +62 -0
- package/src/version.ts +1 -1
package/android/build.gradle
CHANGED
|
@@ -53,7 +53,7 @@ android {
|
|
|
53
53
|
defaultConfig {
|
|
54
54
|
minSdk = 24
|
|
55
55
|
targetSdk = 35
|
|
56
|
-
multiDexEnabled true
|
|
56
|
+
multiDexEnabled = true
|
|
57
57
|
|
|
58
58
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
|
59
59
|
|
|
@@ -66,7 +66,7 @@ android {
|
|
|
66
66
|
|
|
67
67
|
lint {
|
|
68
68
|
disable 'GradleCompatible'
|
|
69
|
-
abortOnError false
|
|
69
|
+
abortOnError = false
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
compileOptions {
|
|
@@ -14,8 +14,8 @@ package app.notifee.core;
|
|
|
14
14
|
* NOT RUN IN CI.
|
|
15
15
|
*
|
|
16
16
|
* Instrumented regression suite for the reboot-recovery path in
|
|
17
|
-
* NotifeeAlarmManager.rescheduleNotifications, covering
|
|
18
|
-
* issues across
|
|
17
|
+
* NotifeeAlarmManager.rescheduleNotifications, covering four upstream
|
|
18
|
+
* issues across six test cases:
|
|
19
19
|
*
|
|
20
20
|
* 1. rescheduleNotifications_advancesEveryAnchorBeforeCompleting
|
|
21
21
|
* (upstream invertase/notifee#549 — reboot-recovery anchor persistence
|
|
@@ -54,6 +54,15 @@ package app.notifee.core;
|
|
|
54
54
|
* handleStaleNonRepeatingTrigger routes the failure through the
|
|
55
55
|
* resilient branch that still runs deleteById.
|
|
56
56
|
*
|
|
57
|
+
* 6. rescheduleNotifications_nonRepeatingFuture_preservesAnchorAndRearmsAlarm
|
|
58
|
+
* (upstream invertase/notifee#991 — non-repeating TIMESTAMP triggers lost
|
|
59
|
+
* after device reboot on Android 14). Seeds a non-repeating row 5 minutes
|
|
60
|
+
* in the future and asserts the row survives reboot recovery with its
|
|
61
|
+
* anchor preserved AND a fresh AlarmManager PendingIntent registered.
|
|
62
|
+
* Complements the past-stale paths above by covering the happy-path
|
|
63
|
+
* branch where setNextTimestamp early-returns at TimestampTriggerModel:136
|
|
64
|
+
* and rescheduleNotification re-arms the original anchor.
|
|
65
|
+
*
|
|
57
66
|
* Run manually via the smoke harness (recommended — wraps gradle + copies
|
|
58
67
|
* reports + fails loud if no XML reports are found):
|
|
59
68
|
*
|
|
@@ -133,6 +142,9 @@ public class RebootRecoveryTest {
|
|
|
133
142
|
/** 48 hours before "now" — well beyond the 24h grace period. */
|
|
134
143
|
private static final long STALE_BEYOND_GRACE_OFFSET_MS = 48 * HOUR_IN_MS;
|
|
135
144
|
|
|
145
|
+
/** 5 minutes after "now" — the future-non-repeating happy path for upstream #991. */
|
|
146
|
+
private static final long FUTURE_NON_REPEATING_OFFSET_MS = 5L * 60 * 1000;
|
|
147
|
+
|
|
136
148
|
/**
|
|
137
149
|
* Channel id used by the within-grace stale-trigger test fixture. Must match a real Android
|
|
138
150
|
* notification channel created in {@link #setUp()} so {@code
|
|
@@ -181,6 +193,7 @@ public class RebootRecoveryTest {
|
|
|
181
193
|
NotifeeAlarmManager.cancelNotification(staleWithinGraceTestId());
|
|
182
194
|
NotifeeAlarmManager.cancelNotification(staleBeyondGraceTestId());
|
|
183
195
|
NotifeeAlarmManager.cancelNotification(staleDisplayFailsTestId());
|
|
196
|
+
NotifeeAlarmManager.cancelNotification(nonRepeatingFutureTestId());
|
|
184
197
|
// Dismiss any notifications the within-grace tests may have posted so the device
|
|
185
198
|
// notification shade is left clean — the Room row is the test's contract, but the
|
|
186
199
|
// visible notification is a side effect we should not leave behind.
|
|
@@ -387,6 +400,81 @@ public class RebootRecoveryTest {
|
|
|
387
400
|
repo.getWorkDataById(id).get(5, TimeUnit.SECONDS) == null);
|
|
388
401
|
}
|
|
389
402
|
|
|
403
|
+
/**
|
|
404
|
+
* Regression test for upstream invertase/notifee#991 — non-repeating TIMESTAMP triggers whose
|
|
405
|
+
* fire time is still in the future at boot must be re-armed with their original anchor preserved.
|
|
406
|
+
*
|
|
407
|
+
* <p>The existing #734 tests cover the past-stale branches of {@code
|
|
408
|
+
* handleStaleNonRepeatingTrigger}; this fills the happy-path gap. Seeds a non-repeating trigger 5
|
|
409
|
+
* minutes in the future, invokes reboot recovery, and asserts that:
|
|
410
|
+
*
|
|
411
|
+
* <ul>
|
|
412
|
+
* <li>the Room row survives (not stale → not deleted),
|
|
413
|
+
* <li>the timestamp is unchanged (non-repeating → {@code setNextTimestamp} early-returns at
|
|
414
|
+
* {@code TimestampTriggerModel.java:136-139}),
|
|
415
|
+
* <li>an {@code AlarmManager} {@code PendingIntent} has been re-registered for the row, proving
|
|
416
|
+
* {@code rescheduleNotification} → {@code scheduleTimestampTriggerNotification} reached the
|
|
417
|
+
* alarm-set call.
|
|
418
|
+
* </ul>
|
|
419
|
+
*/
|
|
420
|
+
@Test
|
|
421
|
+
public void rescheduleNotifications_nonRepeatingFuture_preservesAnchorAndRearmsAlarm()
|
|
422
|
+
throws Exception {
|
|
423
|
+
long now = System.currentTimeMillis();
|
|
424
|
+
long futureAnchor = now + FUTURE_NON_REPEATING_OFFSET_MS;
|
|
425
|
+
String id = nonRepeatingFutureTestId();
|
|
426
|
+
|
|
427
|
+
repo.insert(buildNonRepeatingEntity(id, futureAnchor)).get(5, TimeUnit.SECONDS);
|
|
428
|
+
assertEquals(1, repo.getAll().get(5, TimeUnit.SECONDS).size());
|
|
429
|
+
|
|
430
|
+
new NotifeeAlarmManager().rescheduleNotifications(null);
|
|
431
|
+
|
|
432
|
+
PendingIntent pi = awaitPendingIntentRegistered(id);
|
|
433
|
+
assertNotNull(
|
|
434
|
+
"AlarmManager PendingIntent must be re-registered for the non-repeating future row " + id,
|
|
435
|
+
pi);
|
|
436
|
+
|
|
437
|
+
WorkDataEntity row = repo.getWorkDataById(id).get(5, TimeUnit.SECONDS);
|
|
438
|
+
assertNotNull("non-repeating future row must survive reboot recovery", row);
|
|
439
|
+
Bundle triggerBundle = ObjectUtils.bytesToBundle(row.getTrigger());
|
|
440
|
+
long preservedAnchor = ObjectUtils.getLong(triggerBundle.get("timestamp"));
|
|
441
|
+
assertEquals(
|
|
442
|
+
"non-repeating future trigger anchor must NOT be advanced — setNextTimestamp early-returns"
|
|
443
|
+
+ " when repeatFrequency is -1 and timestamp is in the future",
|
|
444
|
+
futureAnchor,
|
|
445
|
+
preservedAnchor);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Polls until an {@code AlarmManager} {@code PendingIntent} keyed off {@code id.hashCode()} is
|
|
450
|
+
* registered (FLAG_NO_CREATE returns non-null). Times out via {@link #fail(String)} if the
|
|
451
|
+
* reschedule pass never reaches {@code alarmManager.set*}.
|
|
452
|
+
*/
|
|
453
|
+
private PendingIntent awaitPendingIntentRegistered(String id) throws Exception {
|
|
454
|
+
Intent intent = new Intent(context, NotificationAlarmReceiver.class);
|
|
455
|
+
intent.putExtra("notificationId", id);
|
|
456
|
+
long deadline = System.currentTimeMillis() + POLL_DEADLINE_MS;
|
|
457
|
+
while (System.currentTimeMillis() < deadline) {
|
|
458
|
+
PendingIntent pi =
|
|
459
|
+
PendingIntent.getBroadcast(
|
|
460
|
+
context,
|
|
461
|
+
id.hashCode(),
|
|
462
|
+
intent,
|
|
463
|
+
PendingIntent.FLAG_NO_CREATE | PendingIntent.FLAG_MUTABLE);
|
|
464
|
+
if (pi != null) {
|
|
465
|
+
return pi;
|
|
466
|
+
}
|
|
467
|
+
Thread.sleep(POLL_INTERVAL_MS);
|
|
468
|
+
}
|
|
469
|
+
fail(
|
|
470
|
+
"AlarmManager PendingIntent for "
|
|
471
|
+
+ id
|
|
472
|
+
+ " was not registered within "
|
|
473
|
+
+ POLL_DEADLINE_MS
|
|
474
|
+
+ "ms — the #991 fix in rescheduleNotification may be broken");
|
|
475
|
+
return null; // unreachable
|
|
476
|
+
}
|
|
477
|
+
|
|
390
478
|
/** Polls {@link WorkDataRepository#getWorkDataById(String)} until it returns {@code null}. */
|
|
391
479
|
private void awaitRowDeleted(String id) throws Exception {
|
|
392
480
|
long deadline = System.currentTimeMillis() + POLL_DEADLINE_MS;
|
|
@@ -566,4 +654,8 @@ public class RebootRecoveryTest {
|
|
|
566
654
|
private static String staleDisplayFailsTestId() {
|
|
567
655
|
return "reboot-recovery-stale-display-fails-test";
|
|
568
656
|
}
|
|
657
|
+
|
|
658
|
+
private static String nonRepeatingFutureTestId() {
|
|
659
|
+
return "reboot-recovery-non-repeating-future-test";
|
|
660
|
+
}
|
|
569
661
|
}
|
|
@@ -49,15 +49,16 @@
|
|
|
49
49
|
</intent-filter>
|
|
50
50
|
</receiver>
|
|
51
51
|
|
|
52
|
+
<!--
|
|
53
|
+
NotificationAlarmReceiver is invoked exclusively via PendingIntent broadcasts from
|
|
54
|
+
AlarmManager (NotifeeAlarmManager.scheduleTimestampTriggerNotification). It does not
|
|
55
|
+
handle any system-broadcast Intent action — onReceive dispatches intent.getExtras()
|
|
56
|
+
to NotifeeAlarmManager.displayScheduledNotification, which early-returns on null
|
|
57
|
+
extras. Reboot recovery is owned by RebootBroadcastReceiver above.
|
|
58
|
+
-->
|
|
52
59
|
<receiver
|
|
53
60
|
android:name="app.notifee.core.NotificationAlarmReceiver"
|
|
54
|
-
android:exported="false"
|
|
55
|
-
<intent-filter>
|
|
56
|
-
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
|
57
|
-
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
|
|
58
|
-
<action android:name="com.htc.intent.action.QUICKBOOT_POWERON"/>
|
|
59
|
-
</intent-filter>
|
|
60
|
-
</receiver>
|
|
61
|
+
android:exported="false" />
|
|
61
62
|
|
|
62
63
|
<!-- Broadcast Receiver -->
|
|
63
64
|
<receiver
|
|
@@ -415,6 +415,7 @@ public class ForegroundService extends Service {
|
|
|
415
415
|
* @return the raw {@code foregroundServiceType} bitmask from the manifest, or 0 if the service is
|
|
416
416
|
* not declared or has no explicit type
|
|
417
417
|
*/
|
|
418
|
+
@RequiresApi(Build.VERSION_CODES.Q)
|
|
418
419
|
private int getDeclaredForegroundServiceType() {
|
|
419
420
|
try {
|
|
420
421
|
ComponentName component = new ComponentName(this, ForegroundService.class);
|
|
@@ -312,6 +312,8 @@ class NotificationManager {
|
|
|
312
312
|
} else {
|
|
313
313
|
builder.setSmallIcon(smallIconId);
|
|
314
314
|
}
|
|
315
|
+
} else {
|
|
316
|
+
builder.setSmallIcon(ResourceUtils.getFallbackSmallIconId(getApplicationContext()));
|
|
315
317
|
}
|
|
316
318
|
|
|
317
319
|
if (androidModel.getSortKey() != null) {
|
|
@@ -816,6 +818,12 @@ class NotificationManager {
|
|
|
816
818
|
Boolean withAlarmManager = trigger.getWithAlarmManager();
|
|
817
819
|
Context context = getApplicationContext();
|
|
818
820
|
|
|
821
|
+
if (!withAlarmManager && TimestampTriggerModel.MONTHLY.equals(trigger.getRepeatFrequency())) {
|
|
822
|
+
String message = "RepeatFrequency.MONTHLY is not supported without AlarmManager.";
|
|
823
|
+
Logger.e(TAG, message);
|
|
824
|
+
return Futures.immediateFailedFuture(new IllegalArgumentException(message));
|
|
825
|
+
}
|
|
826
|
+
|
|
819
827
|
ListenableFuture<Void> insertFuture =
|
|
820
828
|
WorkDataRepository.insertTriggerNotification(
|
|
821
829
|
context, notificationModel, triggerBundle, withAlarmManager);
|
|
@@ -27,9 +27,25 @@ import android.content.Intent;
|
|
|
27
27
|
*/
|
|
28
28
|
public class RebootBroadcastReceiver extends BroadcastReceiver {
|
|
29
29
|
private static final String TAG = "RebootReceiver";
|
|
30
|
+
private static final String ACTION_QUICKBOOT_POWERON = "android.intent.action.QUICKBOOT_POWERON";
|
|
31
|
+
private static final String ACTION_HTC_QUICKBOOT_POWERON =
|
|
32
|
+
"com.htc.intent.action.QUICKBOOT_POWERON";
|
|
30
33
|
|
|
31
34
|
@Override
|
|
32
35
|
public void onReceive(Context context, Intent intent) {
|
|
36
|
+
if (intent == null) {
|
|
37
|
+
Logger.w(TAG, "Ignoring reboot event with null intent");
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
String action = intent.getAction();
|
|
42
|
+
if (!Intent.ACTION_BOOT_COMPLETED.equals(action)
|
|
43
|
+
&& !ACTION_QUICKBOOT_POWERON.equals(action)
|
|
44
|
+
&& !ACTION_HTC_QUICKBOOT_POWERON.equals(action)) {
|
|
45
|
+
Logger.w(TAG, "Ignoring unsupported reboot event action: " + action);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
33
49
|
PendingResult pendingResult = goAsync();
|
|
34
50
|
// Tracks whether the synchronous section successfully handed off to the
|
|
35
51
|
// async reschedule path. If not, the finally block must call finish() to
|
|
@@ -533,9 +533,14 @@ public class NotificationAndroidModel {
|
|
|
533
533
|
int smallIconId = ResourceUtils.getImageResourceId(rawIcon);
|
|
534
534
|
|
|
535
535
|
if (smallIconId == 0) {
|
|
536
|
-
Logger.
|
|
537
|
-
|
|
538
|
-
String.format(
|
|
536
|
+
Logger.w(
|
|
537
|
+
TAG,
|
|
538
|
+
String.format(
|
|
539
|
+
"Notification small icon '%s' could not be resolved; falling back to the app"
|
|
540
|
+
+ " launcher icon. Common causes: asset only in src/debug/res, R8 resource"
|
|
541
|
+
+ " shrinking, or name mismatch (names must be lowercase with underscores)."
|
|
542
|
+
+ " See README Troubleshooting.",
|
|
543
|
+
rawIcon));
|
|
539
544
|
return null;
|
|
540
545
|
}
|
|
541
546
|
|
|
@@ -30,13 +30,13 @@ public class TimestampTriggerModel {
|
|
|
30
30
|
private Boolean mWithAlarmManager = false;
|
|
31
31
|
private AlarmType mAlarmType = AlarmType.SET_EXACT;
|
|
32
32
|
private String mRepeatFrequency = null;
|
|
33
|
+
private int mRepeatInterval = 1;
|
|
33
34
|
private Long mTimestamp = null;
|
|
34
35
|
|
|
35
36
|
public static final String HOURLY = "HOURLY";
|
|
36
37
|
public static final String DAILY = "DAILY";
|
|
37
38
|
public static final String WEEKLY = "WEEKLY";
|
|
38
|
-
|
|
39
|
-
private static final long HOUR_IN_MS = 60L * 60 * 1000;
|
|
39
|
+
public static final String MONTHLY = "MONTHLY";
|
|
40
40
|
|
|
41
41
|
private static final String TAG = "TimeTriggerModel";
|
|
42
42
|
|
|
@@ -47,6 +47,7 @@ public class TimestampTriggerModel {
|
|
|
47
47
|
TimeUnit timeUnit = null;
|
|
48
48
|
if (mTimeTriggerBundle.containsKey("repeatFrequency")) {
|
|
49
49
|
int repeatFrequency = ObjectUtils.getInt(mTimeTriggerBundle.get("repeatFrequency"));
|
|
50
|
+
mRepeatInterval = getRepeatInterval(mTimeTriggerBundle.get("repeatInterval"));
|
|
50
51
|
mTimestamp = ObjectUtils.getLong(mTimeTriggerBundle.get("timestamp"));
|
|
51
52
|
|
|
52
53
|
switch (repeatFrequency) {
|
|
@@ -54,21 +55,24 @@ public class TimestampTriggerModel {
|
|
|
54
55
|
// default value for one-time trigger
|
|
55
56
|
break;
|
|
56
57
|
case 0:
|
|
57
|
-
mInterval =
|
|
58
|
+
mInterval = mRepeatInterval;
|
|
58
59
|
mTimeUnit = TimeUnit.HOURS;
|
|
59
60
|
mRepeatFrequency = HOURLY;
|
|
60
61
|
break;
|
|
61
62
|
case 1:
|
|
62
|
-
mInterval =
|
|
63
|
+
mInterval = mRepeatInterval;
|
|
63
64
|
mTimeUnit = TimeUnit.DAYS;
|
|
64
65
|
mRepeatFrequency = DAILY;
|
|
65
66
|
break;
|
|
66
67
|
case 2:
|
|
67
68
|
// weekly, 7 days
|
|
68
|
-
mInterval = 7;
|
|
69
|
+
mInterval = 7 * mRepeatInterval;
|
|
69
70
|
mTimeUnit = TimeUnit.DAYS;
|
|
70
71
|
mRepeatFrequency = WEEKLY;
|
|
71
72
|
break;
|
|
73
|
+
case 3:
|
|
74
|
+
mRepeatFrequency = MONTHLY;
|
|
75
|
+
break;
|
|
72
76
|
}
|
|
73
77
|
}
|
|
74
78
|
|
|
@@ -139,33 +143,27 @@ public class TimestampTriggerModel {
|
|
|
139
143
|
return;
|
|
140
144
|
}
|
|
141
145
|
|
|
142
|
-
|
|
146
|
+
Calendar cal = Calendar.getInstance();
|
|
147
|
+
cal.setTimeInMillis(getTimestamp());
|
|
143
148
|
|
|
149
|
+
int field;
|
|
144
150
|
if (HOURLY.equals(mRepeatFrequency)) {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
151
|
+
field = Calendar.HOUR_OF_DAY;
|
|
152
|
+
} else if (DAILY.equals(mRepeatFrequency)) {
|
|
153
|
+
field = Calendar.DAY_OF_MONTH;
|
|
154
|
+
} else if (WEEKLY.equals(mRepeatFrequency)) {
|
|
155
|
+
field = Calendar.WEEK_OF_YEAR;
|
|
156
|
+
} else if (MONTHLY.equals(mRepeatFrequency)) {
|
|
157
|
+
field = Calendar.MONTH;
|
|
150
158
|
} else {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
Calendar cal = Calendar.getInstance();
|
|
154
|
-
cal.setTimeInMillis(timestamp);
|
|
155
|
-
|
|
156
|
-
int field;
|
|
157
|
-
if (WEEKLY.equals(mRepeatFrequency)) {
|
|
158
|
-
field = Calendar.WEEK_OF_YEAR;
|
|
159
|
-
} else {
|
|
160
|
-
field = Calendar.DAY_OF_MONTH;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
while (cal.getTimeInMillis() < System.currentTimeMillis()) {
|
|
164
|
-
cal.add(field, 1);
|
|
165
|
-
}
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
166
161
|
|
|
167
|
-
|
|
162
|
+
while (cal.getTimeInMillis() < System.currentTimeMillis()) {
|
|
163
|
+
cal.add(field, mRepeatInterval);
|
|
168
164
|
}
|
|
165
|
+
|
|
166
|
+
this.mTimestamp = cal.getTimeInMillis();
|
|
169
167
|
}
|
|
170
168
|
|
|
171
169
|
public enum AlarmType {
|
|
@@ -196,6 +194,23 @@ public class TimestampTriggerModel {
|
|
|
196
194
|
return mRepeatFrequency;
|
|
197
195
|
}
|
|
198
196
|
|
|
197
|
+
private int getRepeatInterval(Object repeatInterval) {
|
|
198
|
+
if (!(repeatInterval instanceof Number)) {
|
|
199
|
+
return 1;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
double interval = ((Number) repeatInterval).doubleValue();
|
|
203
|
+
if (Double.isNaN(interval)
|
|
204
|
+
|| Double.isInfinite(interval)
|
|
205
|
+
|| interval <= 0
|
|
206
|
+
|| interval % 1 != 0
|
|
207
|
+
|| interval > Integer.MAX_VALUE) {
|
|
208
|
+
return 1;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return (int) interval;
|
|
212
|
+
}
|
|
213
|
+
|
|
199
214
|
public Bundle toBundle() {
|
|
200
215
|
Bundle bundle = (Bundle) mTimeTriggerBundle.clone();
|
|
201
216
|
if (mTimestamp != null) {
|
|
@@ -18,6 +18,7 @@ package app.notifee.core.utility;
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
import android.content.Context;
|
|
21
|
+
import android.content.pm.ApplicationInfo;
|
|
21
22
|
import android.graphics.Bitmap;
|
|
22
23
|
import android.graphics.Canvas;
|
|
23
24
|
import android.graphics.Color;
|
|
@@ -43,6 +44,7 @@ import com.facebook.imagepipeline.request.ImageRequestBuilder;
|
|
|
43
44
|
import com.google.common.util.concurrent.ListenableFuture;
|
|
44
45
|
import com.google.common.util.concurrent.SettableFuture;
|
|
45
46
|
import java.util.HashMap;
|
|
47
|
+
import java.util.Locale;
|
|
46
48
|
import java.util.Map;
|
|
47
49
|
|
|
48
50
|
public class ResourceUtils {
|
|
@@ -223,13 +225,40 @@ public class ResourceUtils {
|
|
|
223
225
|
return resourceId;
|
|
224
226
|
}
|
|
225
227
|
|
|
228
|
+
/**
|
|
229
|
+
* Returns a resource id guaranteed to be usable as a smallIcon. Three-layer fallback: (1) {@code
|
|
230
|
+
* applicationInfo.icon}, (2) {@code applicationInfo.logo}, (3) the system {@code
|
|
231
|
+
* android.R.drawable.ic_dialog_info} drawable. Never returns 0, never throws.
|
|
232
|
+
*
|
|
233
|
+
* <p>Used by {@link app.notifee.core.NotificationManager} to keep a notification valid when the
|
|
234
|
+
* user-supplied {@code smallIcon} string cannot be resolved to a drawable at runtime.
|
|
235
|
+
*/
|
|
236
|
+
public static int getFallbackSmallIconId(Context context) {
|
|
237
|
+
try {
|
|
238
|
+
if (context != null) {
|
|
239
|
+
ApplicationInfo info = context.getApplicationInfo();
|
|
240
|
+
if (info != null) {
|
|
241
|
+
if (info.icon != 0) {
|
|
242
|
+
return info.icon;
|
|
243
|
+
}
|
|
244
|
+
if (info.logo != 0) {
|
|
245
|
+
return info.logo;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
} catch (Exception e) {
|
|
250
|
+
Logger.w(TAG, "getFallbackSmallIconId -> falling back to system default icon", e);
|
|
251
|
+
}
|
|
252
|
+
return android.R.drawable.ic_dialog_info;
|
|
253
|
+
}
|
|
254
|
+
|
|
226
255
|
/** Attempts to find a resource id by name and type */
|
|
227
256
|
private static int getResourceIdByName(String name, String type) {
|
|
228
257
|
if (name == null || name.isEmpty()) {
|
|
229
258
|
return 0;
|
|
230
259
|
}
|
|
231
260
|
|
|
232
|
-
name = name.toLowerCase().replace("-", "_");
|
|
261
|
+
name = name.toLowerCase(Locale.ROOT).replace("-", "_");
|
|
233
262
|
|
|
234
263
|
String key = name + "_" + type;
|
|
235
264
|
|
|
@@ -157,7 +157,7 @@ class NotifeeApiModule(reactContext: ReactApplicationContext) :
|
|
|
157
157
|
|
|
158
158
|
override fun openAlarmPermissionSettings(promise: Promise) {
|
|
159
159
|
Notifee.getInstance()
|
|
160
|
-
.openAlarmPermissionSettings(getCurrentActivity()) { e, _ ->
|
|
160
|
+
.openAlarmPermissionSettings(getReactApplicationContext().getCurrentActivity()) { e, _ ->
|
|
161
161
|
NotifeeReactUtils.promiseResolver(promise, e)
|
|
162
162
|
}
|
|
163
163
|
}
|
|
@@ -220,7 +220,7 @@ class NotifeeApiModule(reactContext: ReactApplicationContext) :
|
|
|
220
220
|
|
|
221
221
|
override fun getInitialNotification(promise: Promise) {
|
|
222
222
|
Notifee.getInstance()
|
|
223
|
-
.getInitialNotification(getCurrentActivity()) { e, bundle ->
|
|
223
|
+
.getInitialNotification(getReactApplicationContext().getCurrentActivity()) { e, bundle ->
|
|
224
224
|
NotifeeReactUtils.promiseResolver(promise, e, bundle)
|
|
225
225
|
}
|
|
226
226
|
}
|
|
@@ -242,7 +242,7 @@ class NotifeeApiModule(reactContext: ReactApplicationContext) :
|
|
|
242
242
|
return
|
|
243
243
|
}
|
|
244
244
|
|
|
245
|
-
val activity = getCurrentActivity() as? PermissionAwareActivity
|
|
245
|
+
val activity = getReactApplicationContext().getCurrentActivity() as? PermissionAwareActivity
|
|
246
246
|
if (activity == null) {
|
|
247
247
|
Logger.d(
|
|
248
248
|
"requestPermission",
|
|
@@ -277,14 +277,14 @@ class NotifeeApiModule(reactContext: ReactApplicationContext) :
|
|
|
277
277
|
|
|
278
278
|
override fun openNotificationSettings(channelId: String?, promise: Promise) {
|
|
279
279
|
Notifee.getInstance()
|
|
280
|
-
.openNotificationSettings(channelId, getCurrentActivity()) { e, _ ->
|
|
280
|
+
.openNotificationSettings(channelId, getReactApplicationContext().getCurrentActivity()) { e, _ ->
|
|
281
281
|
NotifeeReactUtils.promiseResolver(promise, e)
|
|
282
282
|
}
|
|
283
283
|
}
|
|
284
284
|
|
|
285
285
|
override fun openBatteryOptimizationSettings(promise: Promise) {
|
|
286
286
|
Notifee.getInstance()
|
|
287
|
-
.openBatteryOptimizationSettings(getCurrentActivity()) { e, _ ->
|
|
287
|
+
.openBatteryOptimizationSettings(getReactApplicationContext().getCurrentActivity()) { e, _ ->
|
|
288
288
|
NotifeeReactUtils.promiseResolver(promise, e)
|
|
289
289
|
}
|
|
290
290
|
}
|
|
@@ -305,7 +305,7 @@ class NotifeeApiModule(reactContext: ReactApplicationContext) :
|
|
|
305
305
|
|
|
306
306
|
override fun openPowerManagerSettings(promise: Promise) {
|
|
307
307
|
Notifee.getInstance()
|
|
308
|
-
.openPowerManagerSettings(getCurrentActivity()) { e, _ ->
|
|
308
|
+
.openPowerManagerSettings(getReactApplicationContext().getCurrentActivity()) { e, _ ->
|
|
309
309
|
NotifeeReactUtils.promiseResolver(promise, e)
|
|
310
310
|
}
|
|
311
311
|
}
|
|
@@ -0,0 +1,74 @@
|
|
|
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.assertNull;
|
|
20
|
+
|
|
21
|
+
import android.os.Bundle;
|
|
22
|
+
import app.notifee.core.ContextHolder;
|
|
23
|
+
import org.junit.Before;
|
|
24
|
+
import org.junit.Test;
|
|
25
|
+
import org.junit.runner.RunWith;
|
|
26
|
+
import org.robolectric.RobolectricTestRunner;
|
|
27
|
+
import org.robolectric.RuntimeEnvironment;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Unit coverage for {@link NotificationAndroidModel#getSmallIcon()}.
|
|
31
|
+
*
|
|
32
|
+
* <p>The method returns {@code null} when the smallIcon string cannot be resolved to a valid
|
|
33
|
+
* resource id. This is the trigger condition that {@link app.notifee.core.NotificationManager}
|
|
34
|
+
* translates into the launcher-icon fallback (upstream invertase/notifee#733).
|
|
35
|
+
*
|
|
36
|
+
* <p>The happy path (valid resource id returned) is covered end-to-end by smoke tests on real
|
|
37
|
+
* devices — unit-testing it here would require a real drawable registered against the test
|
|
38
|
+
* application package, which Robolectric does not stage by default.
|
|
39
|
+
*/
|
|
40
|
+
@RunWith(RobolectricTestRunner.class)
|
|
41
|
+
public class NotificationAndroidModelSmallIconFallbackTest {
|
|
42
|
+
|
|
43
|
+
@Before
|
|
44
|
+
public void setUp() {
|
|
45
|
+
// getSmallIcon() -> ResourceUtils.getImageResourceId() -> getResourceIdByName() reads
|
|
46
|
+
// ContextHolder.getApplicationContext() to call context.getResources().getIdentifier(...).
|
|
47
|
+
// Robolectric does not auto-populate ContextHolder, so we wire it up here. Same pattern
|
|
48
|
+
// used by ForegroundServiceTest and NotifeeAlarmManagerHandleStaleTest.
|
|
49
|
+
ContextHolder.setApplicationContext(RuntimeEnvironment.getApplication());
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
@Test
|
|
53
|
+
public void returnsNull_whenSmallIconKeyMissing() {
|
|
54
|
+
Bundle bundle = new Bundle();
|
|
55
|
+
NotificationAndroidModel model = NotificationAndroidModel.fromBundle(bundle);
|
|
56
|
+
assertNull(model.getSmallIcon());
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@Test
|
|
60
|
+
public void returnsNull_whenSmallIconCannotBeResolved() {
|
|
61
|
+
Bundle bundle = new Bundle();
|
|
62
|
+
bundle.putString("smallIcon", "nonexistent_icon_name_for_test");
|
|
63
|
+
NotificationAndroidModel model = NotificationAndroidModel.fromBundle(bundle);
|
|
64
|
+
assertNull(model.getSmallIcon());
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
@Test
|
|
68
|
+
public void returnsNull_whenSmallIconEmptyString() {
|
|
69
|
+
Bundle bundle = new Bundle();
|
|
70
|
+
bundle.putString("smallIcon", "");
|
|
71
|
+
NotificationAndroidModel model = NotificationAndroidModel.fromBundle(bundle);
|
|
72
|
+
assertNull(model.getSmallIcon());
|
|
73
|
+
}
|
|
74
|
+
}
|