Skip to content

Commit 1066d26

Browse files
kaptnkoalaMaikuB
andauthored
[flutter_local_notifications] Android inexact notifications (#1881)
* Adapt code to allow inexact notifications on Android * Google Java Format * Change default setting of useInexactMode * Fix unit tests and periodicallyShow options * Change boolean flags to AndroidScheduleType parameter * Add check for exact alarm support in Dart * Export ScheduleType on putlie dart interface * Use specific type adapter for ScheduleType * Rename ScheduleType to ScheduleMode * Add permission exception * Handle permission exception in notification receivers * Fix tests * Google Java Format * Fix fallback schedule mode handling in deserializer * add guard around schedule mode to fix instant notifications not working whe * Google Java Format --------- Co-authored-by: github-actions <> Co-authored-by: Michael Bui <[email protected]>
1 parent d0cf654 commit 1066d26

11 files changed

+271
-108
lines changed

flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/FlutterLocalNotificationsPlugin.java

+143-48
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
import android.text.Spanned;
3131
import android.text.TextUtils;
3232
import android.text.style.ForegroundColorSpan;
33+
import android.util.Log;
3334

3435
import androidx.annotation.Keep;
3536
import androidx.annotation.NonNull;
@@ -54,7 +55,9 @@
5455
import com.dexterous.flutterlocalnotifications.models.NotificationChannelDetails;
5556
import com.dexterous.flutterlocalnotifications.models.NotificationChannelGroupDetails;
5657
import com.dexterous.flutterlocalnotifications.models.NotificationDetails;
58+
import com.dexterous.flutterlocalnotifications.models.NotificationStyle;
5759
import com.dexterous.flutterlocalnotifications.models.PersonDetails;
60+
import com.dexterous.flutterlocalnotifications.models.ScheduleMode;
5861
import com.dexterous.flutterlocalnotifications.models.ScheduledNotificationRepeatFrequency;
5962
import com.dexterous.flutterlocalnotifications.models.SoundSource;
6063
import com.dexterous.flutterlocalnotifications.models.styles.BigPictureStyleInformation;
@@ -126,6 +129,8 @@ public class FlutterLocalNotificationsPlugin
126129
private static final String INITIALIZE_METHOD = "initialize";
127130
private static final String GET_CALLBACK_HANDLE_METHOD = "getCallbackHandle";
128131
private static final String ARE_NOTIFICATIONS_ENABLED_METHOD = "areNotificationsEnabled";
132+
private static final String CAN_SCHEDULE_EXACT_NOTIFICATIONS_METHOD =
133+
"canScheduleExactNotifications";
129134
private static final String CREATE_NOTIFICATION_CHANNEL_GROUP_METHOD =
130135
"createNotificationChannelGroup";
131136
private static final String DELETE_NOTIFICATION_CHANNEL_GROUP_METHOD =
@@ -176,6 +181,7 @@ public class FlutterLocalNotificationsPlugin
176181
"permissionRequestInProgress";
177182
private static final String PERMISSION_REQUEST_IN_PROGRESS_ERROR_MESSAGE =
178183
"Another permission request is already in progress";
184+
private static final String EXACT_ALARMS_PERMISSION_ERROR_CODE = "exact_alarms_not_permitted";
179185
private static final String CANCEL_ID = "id";
180186
private static final String CANCEL_TAG = "tag";
181187
private static final String ACTION_ID = "actionId";
@@ -193,16 +199,36 @@ public class FlutterLocalNotificationsPlugin
193199

194200
static void rescheduleNotifications(Context context) {
195201
ArrayList<NotificationDetails> scheduledNotifications = loadScheduledNotifications(context);
196-
for (NotificationDetails scheduledNotification : scheduledNotifications) {
197-
if (scheduledNotification.repeatInterval == null) {
198-
if (scheduledNotification.timeZoneName == null) {
199-
scheduleNotification(context, scheduledNotification, false);
202+
for (NotificationDetails notificationDetails : scheduledNotifications) {
203+
try {
204+
if (notificationDetails.repeatInterval != null) {
205+
repeatNotification(context, notificationDetails, false);
206+
} else if (notificationDetails.timeZoneName != null) {
207+
zonedScheduleNotification(context, notificationDetails, false);
200208
} else {
201-
zonedScheduleNotification(context, scheduledNotification, false);
209+
scheduleNotification(context, notificationDetails, false);
202210
}
211+
} catch (ExactAlarmPermissionException e) {
212+
Log.e("notification", e.getMessage());
213+
removeNotificationFromCache(context, notificationDetails.id);
214+
}
215+
}
216+
}
217+
218+
static void scheduleNextNotification(Context context, NotificationDetails notificationDetails) {
219+
try {
220+
if (notificationDetails.scheduledNotificationRepeatFrequency != null) {
221+
zonedScheduleNextNotification(context, notificationDetails);
222+
} else if (notificationDetails.matchDateTimeComponents != null) {
223+
zonedScheduleNextNotificationMatchingDateComponents(context, notificationDetails);
224+
} else if (notificationDetails.repeatInterval != null) {
225+
scheduleNextRepeatingNotification(context, notificationDetails);
203226
} else {
204-
repeatNotification(context, scheduledNotification, false);
227+
removeNotificationFromCache(context, notificationDetails.id);
205228
}
229+
} catch (ExactAlarmPermissionException e) {
230+
Log.e("notification", e.getMessage());
231+
removeNotificationFromCache(context, notificationDetails.id);
206232
}
207233
}
208234

@@ -440,7 +466,10 @@ static Gson buildGson() {
440466
.registerSubtype(BigPictureStyleInformation.class)
441467
.registerSubtype(InboxStyleInformation.class)
442468
.registerSubtype(MessagingStyleInformation.class);
443-
GsonBuilder builder = new GsonBuilder().registerTypeAdapterFactory(styleInformationAdapter);
469+
GsonBuilder builder =
470+
new GsonBuilder()
471+
.registerTypeAdapter(ScheduleMode.class, new ScheduleMode.Deserializer())
472+
.registerTypeAdapterFactory(styleInformationAdapter);
444473
gson = builder.create();
445474
}
446475
return gson;
@@ -505,19 +534,11 @@ private static void scheduleNotification(
505534
getBroadcastPendingIntent(context, notificationDetails.id, notificationIntent);
506535

507536
AlarmManager alarmManager = getAlarmManager(context);
508-
if (BooleanUtils.getValue(notificationDetails.allowWhileIdle)) {
509-
AlarmManagerCompat.setExactAndAllowWhileIdle(
510-
alarmManager,
511-
AlarmManager.RTC_WAKEUP,
512-
notificationDetails.millisecondsSinceEpoch,
513-
pendingIntent);
514-
} else {
515-
AlarmManagerCompat.setExact(
516-
alarmManager,
517-
AlarmManager.RTC_WAKEUP,
518-
notificationDetails.millisecondsSinceEpoch,
519-
pendingIntent);
520-
}
537+
setupAlarm(
538+
notificationDetails,
539+
alarmManager,
540+
notificationDetails.millisecondsSinceEpoch,
541+
pendingIntent);
521542

522543
if (updateScheduledNotificationsCache) {
523544
saveScheduledNotification(context, notificationDetails);
@@ -542,19 +563,14 @@ private static void zonedScheduleNotification(
542563
.toInstant()
543564
.toEpochMilli();
544565

545-
if (BooleanUtils.getValue(notificationDetails.allowWhileIdle)) {
546-
AlarmManagerCompat.setExactAndAllowWhileIdle(
547-
alarmManager, AlarmManager.RTC_WAKEUP, epochMilli, pendingIntent);
548-
} else {
549-
AlarmManagerCompat.setExact(alarmManager, AlarmManager.RTC_WAKEUP, epochMilli, pendingIntent);
550-
}
566+
setupAlarm(notificationDetails, alarmManager, epochMilli, pendingIntent);
551567

552568
if (updateScheduledNotificationsCache) {
553569
saveScheduledNotification(context, notificationDetails);
554570
}
555571
}
556572

557-
static void scheduleNextRepeatingNotification(
573+
private static void scheduleNextRepeatingNotification(
558574
Context context, NotificationDetails notificationDetails) {
559575
long repeatInterval = calculateRepeatIntervalMilliseconds(notificationDetails);
560576
long notificationTriggerTime =
@@ -566,8 +582,10 @@ static void scheduleNextRepeatingNotification(
566582
PendingIntent pendingIntent =
567583
getBroadcastPendingIntent(context, notificationDetails.id, notificationIntent);
568584
AlarmManager alarmManager = getAlarmManager(context);
569-
AlarmManagerCompat.setExactAndAllowWhileIdle(
570-
alarmManager, AlarmManager.RTC_WAKEUP, notificationTriggerTime, pendingIntent);
585+
586+
setupAllowWhileIdleAlarm(
587+
notificationDetails, alarmManager, notificationTriggerTime, pendingIntent);
588+
571589
saveScheduledNotification(context, notificationDetails);
572590
}
573591

@@ -635,18 +653,58 @@ private static void repeatNotification(
635653
getBroadcastPendingIntent(context, notificationDetails.id, notificationIntent);
636654
AlarmManager alarmManager = getAlarmManager(context);
637655

638-
if (BooleanUtils.getValue(notificationDetails.allowWhileIdle)) {
639-
AlarmManagerCompat.setExactAndAllowWhileIdle(
640-
alarmManager, AlarmManager.RTC_WAKEUP, notificationTriggerTime, pendingIntent);
656+
if (notificationDetails.scheduleMode.useAllowWhileIdle()) {
657+
setupAllowWhileIdleAlarm(
658+
notificationDetails, alarmManager, notificationTriggerTime, pendingIntent);
641659
} else {
642660
alarmManager.setInexactRepeating(
643661
AlarmManager.RTC_WAKEUP, notificationTriggerTime, repeatInterval, pendingIntent);
644662
}
663+
645664
if (updateScheduledNotificationsCache) {
646665
saveScheduledNotification(context, notificationDetails);
647666
}
648667
}
649668

669+
private static void setupAlarm(
670+
NotificationDetails notificationDetails,
671+
AlarmManager alarmManager,
672+
long epochMilli,
673+
PendingIntent pendingIntent) {
674+
if (notificationDetails.scheduleMode.useAllowWhileIdle()) {
675+
setupAllowWhileIdleAlarm(notificationDetails, alarmManager, epochMilli, pendingIntent);
676+
} else {
677+
if (notificationDetails.scheduleMode.useExactAlarm()) {
678+
checkCanScheduleExactAlarms(alarmManager);
679+
AlarmManagerCompat.setExact(
680+
alarmManager, AlarmManager.RTC_WAKEUP, epochMilli, pendingIntent);
681+
} else {
682+
alarmManager.set(AlarmManager.RTC_WAKEUP, epochMilli, pendingIntent);
683+
}
684+
}
685+
}
686+
687+
private static void setupAllowWhileIdleAlarm(
688+
NotificationDetails notificationDetails,
689+
AlarmManager alarmManager,
690+
long epochMilli,
691+
PendingIntent pendingIntent) {
692+
if (notificationDetails.scheduleMode.useExactAlarm()) {
693+
checkCanScheduleExactAlarms(alarmManager);
694+
AlarmManagerCompat.setExactAndAllowWhileIdle(
695+
alarmManager, AlarmManager.RTC_WAKEUP, epochMilli, pendingIntent);
696+
} else {
697+
AlarmManagerCompat.setAndAllowWhileIdle(
698+
alarmManager, AlarmManager.RTC_WAKEUP, epochMilli, pendingIntent);
699+
}
700+
}
701+
702+
private static void checkCanScheduleExactAlarms(AlarmManager alarmManager) {
703+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && !alarmManager.canScheduleExactAlarms()) {
704+
throw new ExactAlarmPermissionException();
705+
}
706+
}
707+
650708
private static long calculateNextNotificationTrigger(
651709
long notificationTriggerTime, long repeatInterval) {
652710
// ensures that time is in the future
@@ -1154,7 +1212,7 @@ static void showNotification(Context context, NotificationDetails notificationDe
11541212
}
11551213
}
11561214

1157-
static void zonedScheduleNextNotification(
1215+
private static void zonedScheduleNextNotification(
11581216
Context context, NotificationDetails notificationDetails) {
11591217
String nextFireDate = getNextFireDate(notificationDetails);
11601218
if (nextFireDate == null) {
@@ -1164,7 +1222,7 @@ static void zonedScheduleNextNotification(
11641222
zonedScheduleNotification(context, notificationDetails, true);
11651223
}
11661224

1167-
static void zonedScheduleNextNotificationMatchingDateComponents(
1225+
private static void zonedScheduleNextNotificationMatchingDateComponents(
11681226
Context context, NotificationDetails notificationDetails) {
11691227
String nextFireDate = getNextFireDateMatchingDateTimeComponents(notificationDetails);
11701228
if (nextFireDate == null) {
@@ -1174,7 +1232,7 @@ static void zonedScheduleNextNotificationMatchingDateComponents(
11741232
zonedScheduleNotification(context, notificationDetails, true);
11751233
}
11761234

1177-
static String getNextFireDate(NotificationDetails notificationDetails) {
1235+
private static String getNextFireDate(NotificationDetails notificationDetails) {
11781236
if (notificationDetails.scheduledNotificationRepeatFrequency
11791237
== ScheduledNotificationRepeatFrequency.Daily) {
11801238
LocalDateTime localDateTime =
@@ -1189,7 +1247,8 @@ static String getNextFireDate(NotificationDetails notificationDetails) {
11891247
return null;
11901248
}
11911249

1192-
static String getNextFireDateMatchingDateTimeComponents(NotificationDetails notificationDetails) {
1250+
private static String getNextFireDateMatchingDateTimeComponents(
1251+
NotificationDetails notificationDetails) {
11931252
ZoneId zoneId = ZoneId.of(notificationDetails.timeZoneName);
11941253
ZonedDateTime scheduledDateTime =
11951254
ZonedDateTime.of(LocalDateTime.parse(notificationDetails.scheduledDateTime), zoneId);
@@ -1335,6 +1394,9 @@ public void fail(String message) {
13351394
case ARE_NOTIFICATIONS_ENABLED_METHOD:
13361395
areNotificationsEnabled(result);
13371396
break;
1397+
case CAN_SCHEDULE_EXACT_NOTIFICATIONS_METHOD:
1398+
setCanScheduleExactNotifications(result);
1399+
break;
13381400
case CREATE_NOTIFICATION_CHANNEL_GROUP_METHOD:
13391401
createNotificationChannelGroup(call, result);
13401402
break;
@@ -1425,33 +1487,42 @@ private void cancel(MethodCall call, Result result) {
14251487
}
14261488

14271489
private void repeat(MethodCall call, Result result) {
1428-
Map<String, Object> arguments = call.arguments();
1429-
NotificationDetails notificationDetails = extractNotificationDetails(result, arguments);
1490+
NotificationDetails notificationDetails = extractNotificationDetails(result, call.arguments());
14301491
if (notificationDetails != null) {
1431-
repeatNotification(applicationContext, notificationDetails, true);
1432-
result.success(null);
1492+
try {
1493+
repeatNotification(applicationContext, notificationDetails, true);
1494+
result.success(null);
1495+
} catch (PluginException e) {
1496+
result.error(e.code, e.getMessage(), null);
1497+
}
14331498
}
14341499
}
14351500

14361501
private void schedule(MethodCall call, Result result) {
1437-
Map<String, Object> arguments = call.arguments();
1438-
NotificationDetails notificationDetails = extractNotificationDetails(result, arguments);
1502+
NotificationDetails notificationDetails = extractNotificationDetails(result, call.arguments());
14391503
if (notificationDetails != null) {
1440-
scheduleNotification(applicationContext, notificationDetails, true);
1441-
result.success(null);
1504+
try {
1505+
scheduleNotification(applicationContext, notificationDetails, true);
1506+
result.success(null);
1507+
} catch (PluginException e) {
1508+
result.error(e.code, e.getMessage(), null);
1509+
}
14421510
}
14431511
}
14441512

14451513
private void zonedSchedule(MethodCall call, Result result) {
1446-
Map<String, Object> arguments = call.arguments();
1447-
NotificationDetails notificationDetails = extractNotificationDetails(result, arguments);
1514+
NotificationDetails notificationDetails = extractNotificationDetails(result, call.arguments());
14481515
if (notificationDetails != null) {
14491516
if (notificationDetails.matchDateTimeComponents != null) {
14501517
notificationDetails.scheduledDateTime =
14511518
getNextFireDateMatchingDateTimeComponents(notificationDetails);
14521519
}
1453-
zonedScheduleNotification(applicationContext, notificationDetails, true);
1454-
result.success(null);
1520+
try {
1521+
zonedScheduleNotification(applicationContext, notificationDetails, true);
1522+
result.success(null);
1523+
} catch (PluginException e) {
1524+
result.error(e.code, e.getMessage(), null);
1525+
}
14551526
}
14561527
}
14571528

@@ -1970,4 +2041,28 @@ private void areNotificationsEnabled(Result result) {
19702041
NotificationManagerCompat notificationManager = getNotificationManager(applicationContext);
19712042
result.success(notificationManager.areNotificationsEnabled());
19722043
}
2044+
2045+
private void setCanScheduleExactNotifications(Result result) {
2046+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
2047+
result.success(true);
2048+
} else {
2049+
AlarmManager alarmManager = getAlarmManager(applicationContext);
2050+
result.success(alarmManager.canScheduleExactAlarms());
2051+
}
2052+
}
2053+
2054+
private static class PluginException extends RuntimeException {
2055+
public final String code;
2056+
2057+
PluginException(String code, String message) {
2058+
super(message);
2059+
this.code = code;
2060+
}
2061+
}
2062+
2063+
private static class ExactAlarmPermissionException extends PluginException {
2064+
public ExactAlarmPermissionException() {
2065+
super(EXACT_ALARMS_PERMISSION_ERROR_CODE, "Exact alarms are not permitted");
2066+
}
2067+
}
19732068
}

flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/ScheduledNotificationReceiver.java

+2-12
Original file line numberDiff line numberDiff line change
@@ -44,19 +44,9 @@ public void onReceive(final Context context, Intent intent) {
4444
Gson gson = FlutterLocalNotificationsPlugin.buildGson();
4545
Type type = new TypeToken<NotificationDetails>() {}.getType();
4646
NotificationDetails notificationDetails = gson.fromJson(notificationDetailsJson, type);
47+
4748
FlutterLocalNotificationsPlugin.showNotification(context, notificationDetails);
48-
if (notificationDetails.scheduledNotificationRepeatFrequency != null) {
49-
FlutterLocalNotificationsPlugin.zonedScheduleNextNotification(context, notificationDetails);
50-
} else if (notificationDetails.matchDateTimeComponents != null) {
51-
FlutterLocalNotificationsPlugin.zonedScheduleNextNotificationMatchingDateComponents(
52-
context, notificationDetails);
53-
} else if (notificationDetails.repeatInterval != null) {
54-
FlutterLocalNotificationsPlugin.scheduleNextRepeatingNotification(
55-
context, notificationDetails);
56-
} else {
57-
FlutterLocalNotificationsPlugin.removeNotificationFromCache(
58-
context, notificationDetails.id);
59-
}
49+
FlutterLocalNotificationsPlugin.scheduleNextNotification(context, notificationDetails);
6050
}
6151
}
6252
}

0 commit comments

Comments
 (0)