diff --git a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/FlutterLocalNotificationsPlugin.java b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/FlutterLocalNotificationsPlugin.java index d417da74d..44378be0f 100644 --- a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/FlutterLocalNotificationsPlugin.java +++ b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/FlutterLocalNotificationsPlugin.java @@ -84,9 +84,9 @@ import java.util.Arrays; import java.util.Calendar; import java.util.HashMap; -import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Set; /** FlutterLocalNotificationsPlugin */ @Keep @@ -99,7 +99,9 @@ public class FlutterLocalNotificationsPlugin private static final String DRAWABLE = "drawable"; private static final String DEFAULT_ICON = "defaultIcon"; private static final String SELECT_NOTIFICATION = "SELECT_NOTIFICATION"; - private static final String SCHEDULED_NOTIFICATIONS = "scheduled_notifications"; + private static final String SCHEDULED_NOTIFICATIONS_FILE = "scheduled_notifications"; + private static final String SCHEDULED_NOTIFICATIONS_STRING = "scheduled_notifications"; + private static final String SCHEDULED_NOTIFICATIONS_ID = "snid"; private static final String INITIALIZE_METHOD = "initialize"; private static final String GET_CALLBACK_HANDLE_METHOD = "getCallbackHandle"; private static final String ARE_NOTIFICATIONS_ENABLED_METHOD = "areNotificationsEnabled"; @@ -117,6 +119,7 @@ public class FlutterLocalNotificationsPlugin private static final String SHOW_METHOD = "show"; private static final String CANCEL_METHOD = "cancel"; private static final String CANCEL_ALL_METHOD = "cancelAll"; + private static final String CANCEL_ALL_PENDING_METHOD = "cancelAllPending"; private static final String SCHEDULE_METHOD = "schedule"; private static final String ZONED_SCHEDULE_METHOD = "zonedSchedule"; private static final String PERIODICALLY_SHOW_METHOD = "periodicallyShow"; @@ -149,6 +152,7 @@ public class FlutterLocalNotificationsPlugin + " your Android head project."; private static final String CANCEL_ID = "id"; private static final String CANCEL_TAG = "tag"; + private static final String REPLACE = "replace"; static String NOTIFICATION_DETAILS = "notificationDetails"; static Gson gson; private MethodChannel channel; @@ -392,37 +396,58 @@ static Gson buildGson() { private static ArrayList loadScheduledNotifications(Context context) { ArrayList scheduledNotifications = new ArrayList<>(); SharedPreferences sharedPreferences = - context.getSharedPreferences(SCHEDULED_NOTIFICATIONS, Context.MODE_PRIVATE); - String json = sharedPreferences.getString(SCHEDULED_NOTIFICATIONS, null); - if (json != null) { + context.getSharedPreferences(SCHEDULED_NOTIFICATIONS_FILE, Context.MODE_PRIVATE); + Map jsonNotifications = (Map) sharedPreferences.getAll(); + if (jsonNotifications != null) { Gson gson = buildGson(); - Type type = new TypeToken>() {}.getType(); - scheduledNotifications = gson.fromJson(json, type); + Type type = new TypeToken() {}.getType(); + for (String json : jsonNotifications.values()) { + scheduledNotifications.add(gson.fromJson(json, type)); + } } return scheduledNotifications; } - private static void saveScheduledNotifications( - Context context, ArrayList scheduledNotifications) { + // TODO rewrite migration code + /** + * Get scheduled notifications from shared preferences, converting from old format if present. + * + *

Returns null if neither are present The returned Set may not be mutated! + */ + private static Set getScheduledNotificationsJsonSet(SharedPreferences sharedPreferences) { + Set jsonNotifications = + sharedPreferences.getStringSet(SCHEDULED_NOTIFICATIONS_SET, null); + if (jsonNotifications != null) { + return jsonNotifications; + } + String notificationsJson = sharedPreferences.getString(SCHEDULED_NOTIFICATIONS_STRING, null); + if (notificationsJson == null) { + return null; + } + + // Convert (once) from the old format to the new format and delete the old Gson gson = buildGson(); - String json = gson.toJson(scheduledNotifications); - SharedPreferences sharedPreferences = - context.getSharedPreferences(SCHEDULED_NOTIFICATIONS, Context.MODE_PRIVATE); + Type type = new TypeToken>() {}.getType(); + ArrayList notificationsList = gson.fromJson(notificationsJson, type); + int amount = notificationsList.size(); + String[] jsonNotificationsToSave = new String[amount]; + for (int i = 0; i < amount; i++) { + jsonNotificationsToSave[i] = gson.toJson(notificationsList.get(i)); + } + Set scheduledNotifications = Set.of(jsonNotificationsToSave); SharedPreferences.Editor editor = sharedPreferences.edit(); - editor.putString(SCHEDULED_NOTIFICATIONS, json); + editor.remove(SCHEDULED_NOTIFICATIONS_STRING); + editor.putStringSet(SCHEDULED_NOTIFICATIONS_SET, scheduledNotifications); editor.apply(); + return scheduledNotifications; } static void removeNotificationFromCache(Context context, Integer notificationId) { - ArrayList scheduledNotifications = loadScheduledNotifications(context); - for (Iterator it = scheduledNotifications.iterator(); it.hasNext(); ) { - NotificationDetails notificationDetails = it.next(); - if (notificationDetails.id.equals(notificationId)) { - it.remove(); - break; - } - } - saveScheduledNotifications(context, scheduledNotifications); + SharedPreferences sharedPreferences1 = + context.getSharedPreferences(SCHEDULED_NOTIFICATIONS_FILE, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = sharedPreferences1.edit(); + editor.remove(SCHEDULED_NOTIFICATIONS_ID + notificationId); + editor.apply(); } @SuppressWarnings("deprecation") @@ -441,44 +466,17 @@ private static void scheduleNotification( Context context, final NotificationDetails notificationDetails, Boolean updateScheduledNotificationsCache) { - Gson gson = buildGson(); - String notificationDetailsJson = gson.toJson(notificationDetails); - Intent notificationIntent = new Intent(context, ScheduledNotificationReceiver.class); - notificationIntent.putExtra(NOTIFICATION_DETAILS, notificationDetailsJson); - PendingIntent pendingIntent = - getBroadcastPendingIntent(context, notificationDetails.id, notificationIntent); - - AlarmManager alarmManager = getAlarmManager(context); - if (BooleanUtils.getValue(notificationDetails.allowWhileIdle)) { - AlarmManagerCompat.setExactAndAllowWhileIdle( - alarmManager, - AlarmManager.RTC_WAKEUP, - notificationDetails.millisecondsSinceEpoch, - pendingIntent); - } else { - AlarmManagerCompat.setExact( - alarmManager, - AlarmManager.RTC_WAKEUP, - notificationDetails.millisecondsSinceEpoch, - pendingIntent); - } - - if (updateScheduledNotificationsCache) { - saveScheduledNotification(context, notificationDetails); - } + scheduleNotificationAtTime( + context, + notificationDetails, + notificationDetails.millisecondsSinceEpoch, + updateScheduledNotificationsCache); } private static void zonedScheduleNotification( Context context, final NotificationDetails notificationDetails, Boolean updateScheduledNotificationsCache) { - Gson gson = buildGson(); - String notificationDetailsJson = gson.toJson(notificationDetails); - Intent notificationIntent = new Intent(context, ScheduledNotificationReceiver.class); - notificationIntent.putExtra(NOTIFICATION_DETAILS, notificationDetailsJson); - PendingIntent pendingIntent = - getBroadcastPendingIntent(context, notificationDetails.id, notificationIntent); - AlarmManager alarmManager = getAlarmManager(context); long epochMilli = VERSION.SDK_INT >= VERSION_CODES.O ? ZonedDateTime.of( @@ -491,9 +489,25 @@ private static void zonedScheduleNotification( org.threeten.bp.ZoneId.of(notificationDetails.timeZoneName)) .toInstant() .toEpochMilli(); + scheduleNotificationAtTime( + context, notificationDetails, epochMilli, updateScheduledNotificationsCache); + } + + private static void scheduleNotificationAtTime( + Context context, + NotificationDetails notificationDetails, + long epochMilli, + Boolean updateScheduledNotificationsCache) { + Gson gson = buildGson(); + String notificationDetailsJson = gson.toJson(notificationDetails); + Intent notificationIntent = new Intent(context, ScheduledNotificationReceiver.class); + notificationIntent.putExtra(NOTIFICATION_DETAILS, notificationDetailsJson); + PendingIntent pendingIntent = + getBroadcastPendingIntent(context, notificationDetails.id, notificationIntent); + + AlarmManager alarmManager = getAlarmManager(context); if (BooleanUtils.getValue(notificationDetails.allowWhileIdle)) { - AlarmManagerCompat.setExactAndAllowWhileIdle( - alarmManager, AlarmManager.RTC_WAKEUP, epochMilli, pendingIntent); + AlarmManagerCompat.setAlarmClock(alarmManager, epochMilli, pendingIntent, pendingIntent); } else { AlarmManagerCompat.setExact(alarmManager, AlarmManager.RTC_WAKEUP, epochMilli, pendingIntent); } @@ -515,8 +529,8 @@ static void scheduleNextRepeatingNotification( PendingIntent pendingIntent = getBroadcastPendingIntent(context, notificationDetails.id, notificationIntent); AlarmManager alarmManager = getAlarmManager(context); - AlarmManagerCompat.setExactAndAllowWhileIdle( - alarmManager, AlarmManager.RTC_WAKEUP, notificationTriggerTime, pendingIntent); + AlarmManagerCompat.setAlarmClock( + alarmManager, notificationTriggerTime, pendingIntent, pendingIntent); saveScheduledNotification(context, notificationDetails); } @@ -568,8 +582,8 @@ private static void repeatNotification( AlarmManager alarmManager = getAlarmManager(context); if (BooleanUtils.getValue(notificationDetails.allowWhileIdle)) { - AlarmManagerCompat.setExactAndAllowWhileIdle( - alarmManager, AlarmManager.RTC_WAKEUP, notificationTriggerTime, pendingIntent); + AlarmManagerCompat.setAlarmClock( + alarmManager, notificationTriggerTime, pendingIntent, pendingIntent); } else { alarmManager.setInexactRepeating( AlarmManager.RTC_WAKEUP, notificationTriggerTime, repeatInterval, pendingIntent); @@ -610,18 +624,16 @@ private static long calculateRepeatIntervalMilliseconds(NotificationDetails noti return repeatInterval; } + // Note: This will now allow duplicate notifications on ID which would be overwritten before. private static void saveScheduledNotification( Context context, NotificationDetails notificationDetails) { - ArrayList scheduledNotifications = loadScheduledNotifications(context); - ArrayList scheduledNotificationsToSave = new ArrayList<>(); - for (NotificationDetails scheduledNotification : scheduledNotifications) { - if (scheduledNotification.id.equals(notificationDetails.id)) { - continue; - } - scheduledNotificationsToSave.add(scheduledNotification); - } - scheduledNotificationsToSave.add(notificationDetails); - saveScheduledNotifications(context, scheduledNotificationsToSave); + SharedPreferences sharedPreferences = + context.getSharedPreferences(SCHEDULED_NOTIFICATIONS_FILE, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = sharedPreferences.edit(); + Gson gson = buildGson(); + String jsonNotificationToSave = gson.toJson(notificationDetails); + editor.putString(SCHEDULED_NOTIFICATIONS_ID + notificationDetails.id, jsonNotificationToSave); + editor.apply(); } private static int getDrawableResourceId(Context context, String name) { @@ -1319,6 +1331,9 @@ public void onMethodCall(MethodCall call, @NonNull Result result) { case CANCEL_ALL_METHOD: cancelAllNotifications(result); break; + case CANCEL_ALL_PENDING_METHOD: + cancelAllPending(result); + break; case PENDING_NOTIFICATION_REQUESTS_METHOD: pendingNotificationRequests(result); break; @@ -1431,6 +1446,11 @@ private void schedule(MethodCall call, Result result) { private void zonedSchedule(MethodCall call, Result result) { Map arguments = call.arguments(); NotificationDetails notificationDetails = extractNotificationDetails(result, arguments); + zonedScheduleDetails(result, notificationDetails, applicationContext); + } + + private static void zonedScheduleDetails( + Result result, NotificationDetails notificationDetails, Context applicationContext) { if (notificationDetails != null) { if (notificationDetails.matchDateTimeComponents != null) { notificationDetails.scheduledDateTime = @@ -1441,6 +1461,21 @@ private void zonedSchedule(MethodCall call, Result result) { } } + private void zonedScheduleMultiple(MethodCall call, Result result) { + Map arguments = call.arguments(); + boolean replace = arguments.get(REPLACE); + ArrayList multiNotificationDetails = + extractMultipleNotificationDetails(result, arguments); + + if (replace) { + cancelAllPendingNotifications(); + } + + for (NotificationDetails notificationDetails : multiNotificationDetails) { + zonedScheduleDetails(result, notificationDetails, applicationContext); + } + } + private void show(MethodCall call, Result result) { Map arguments = call.arguments(); NotificationDetails notificationDetails = extractNotificationDetails(result, arguments); @@ -1587,6 +1622,12 @@ private boolean hasInvalidIcon(Result result, String icon) { && !isValidDrawableResource(applicationContext, icon, result, INVALID_ICON_ERROR_CODE); } + /** + * Cancels a single notification by ID + * + * @param id + * @param tag + */ private void cancelNotification(Integer id, String tag) { Intent intent = new Intent(applicationContext, ScheduledNotificationReceiver.class); PendingIntent pendingIntent = getBroadcastPendingIntent(applicationContext, id, intent); @@ -1601,26 +1642,46 @@ private void cancelNotification(Integer id, String tag) { removeNotificationFromCache(applicationContext, id); } - private void cancelAllNotifications(Result result) { - NotificationManagerCompat notificationManager = getNotificationManager(applicationContext); - notificationManager.cancelAll(); - ArrayList scheduledNotifications = - loadScheduledNotifications(applicationContext); - if (scheduledNotifications == null || scheduledNotifications.isEmpty()) { - result.success(null); + /** Cancels only all pending notifications, leaving active ones. */ + private void cancelAllPending(Result result) { + cancelAllPendingNotifications(); + result.success(null); + } + + /** Cancels all pending notifications without parsing any JSON */ + private void cancelAllPendingNotifications() { + SharedPreferences sharedPreferences = + applicationContext.getSharedPreferences(SCHEDULED_NOTIFICATIONS_FILE, Context.MODE_PRIVATE); + Map jsonNotifications = (Map) sharedPreferences.getAll(); + + if (jsonNotifications.isEmpty()) { return; } Intent intent = new Intent(applicationContext, ScheduledNotificationReceiver.class); - for (NotificationDetails scheduledNotification : scheduledNotifications) { - PendingIntent pendingIntent = - getBroadcastPendingIntent(applicationContext, scheduledNotification.id, intent); - AlarmManager alarmManager = getAlarmManager(applicationContext); + AlarmManager alarmManager = getAlarmManager(applicationContext); + for (String id : jsonNotifications.keySet()) { + // TODO filter out non-matching IDs + int intId = Integer.parseInt(id.substring(SCHEDULED_NOTIFICATIONS_ID.length() - 1)); + PendingIntent pendingIntent = getBroadcastPendingIntent(applicationContext, intId, intent); alarmManager.cancel(pendingIntent); } - saveScheduledNotifications(applicationContext, new ArrayList<>()); - result.success(null); + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.clear(); + editor.apply(); + } + + /** + * Cancels all notifications, both scheduled and active + * + * @param result + */ + private void cancelAllNotifications(Result result) { + NotificationManagerCompat notificationManager = getNotificationManager(applicationContext); + notificationManager.cancelAll(); + + cancelAllPending(result); } @Override