Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,11 @@
import java.util.Arrays;
import java.util.Calendar;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

/** FlutterLocalNotificationsPlugin */
@Keep
Expand All @@ -99,7 +101,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_SET = "scheduled_notifications_set";
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";
Expand Down Expand Up @@ -392,24 +396,63 @@ static Gson buildGson() {
private static ArrayList<NotificationDetails> loadScheduledNotifications(Context context) {
ArrayList<NotificationDetails> 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);
Set<String> jsonNotifications = getScheduledNotificationsJsonSet(sharedPreferences);
if (jsonNotifications != null) {
Gson gson = buildGson();
Type type = new TypeToken<ArrayList<NotificationDetails>>() {}.getType();
scheduledNotifications = gson.fromJson(json, type);
Type type = new TypeToken<NotificationDetails>() {}.getType();
for (String json : jsonNotifications) {
scheduledNotifications.add(gson.fromJson(json, type));
}
}
return scheduledNotifications;
}

/**
* 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<String> getScheduledNotificationsJsonSet(SharedPreferences sharedPreferences) {
Set<String> jsonNotifications = sharedPreferences.getStringSet(SCHEDULED_NOTIFICATIONS_SET, null);
if (jsonNotifications != null) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check implies that we cannot switch back to the old implementation and again to this one, the migration will always happen only once

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is true, in an attempt to reduce the number of extra queries to Shared Preferences made during normal use.

When switching back to the old implementation, the notifications will be lost. When re-switching to the new implementation, the lost notifications will be restored and any new ones will be lost.

Preventing the initial loss is only possible by additionally saving the alarms the old way, which would defeat the purpose of the change.
Doing a second migration is only possible by always checking for presence of an old data object, which would incur a performance penalty each time.

My suggestion to solve the second migration problem is to make it the responsibility of the app and not the plugin. App developers can ensure notifications are explicitly re-scheduled by the app if such a rare situation occurs.

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();
Type type = new TypeToken<ArrayList<NotificationDetails>>() {}.getType();
ArrayList<NotificationDetails> 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<String> scheduledNotifications = Set.of(jsonNotificationsToSave);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.remove(SCHEDULED_NOTIFICATIONS_STRING);
editor.putStringSet(SCHEDULED_NOTIFICATIONS_SET, scheduledNotifications);
editor.apply();
return scheduledNotifications;
}

private static void saveScheduledNotifications(
Context context, ArrayList<NotificationDetails> scheduledNotifications) {
Gson gson = buildGson();
String json = gson.toJson(scheduledNotifications);
int amount = scheduledNotifications.size();
String[] jsonNotificationsToSave = new String[amount];
for (int i = 0; i < amount; i++) {
jsonNotificationsToSave[i] = gson.toJson(scheduledNotifications.get(i));
}
SharedPreferences sharedPreferences =
context.getSharedPreferences(SCHEDULED_NOTIFICATIONS, Context.MODE_PRIVATE);
context.getSharedPreferences(SCHEDULED_NOTIFICATIONS_FILE, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString(SCHEDULED_NOTIFICATIONS, json);
editor.putStringSet(SCHEDULED_NOTIFICATIONS_SET, Set.of(jsonNotificationsToSave));
editor.apply();
}

Expand Down Expand Up @@ -450,11 +493,8 @@ private static void scheduleNotification(

AlarmManager alarmManager = getAlarmManager(context);
if (BooleanUtils.getValue(notificationDetails.allowWhileIdle)) {
AlarmManagerCompat.setExactAndAllowWhileIdle(
alarmManager,
AlarmManager.RTC_WAKEUP,
notificationDetails.millisecondsSinceEpoch,
pendingIntent);
AlarmManagerCompat.setAlarmClock(
alarmManager, notificationDetails.millisecondsSinceEpoch, pendingIntent, pendingIntent);
} else {
AlarmManagerCompat.setExact(
alarmManager,
Expand Down Expand Up @@ -492,8 +532,7 @@ private static void zonedScheduleNotification(
.toInstant()
.toEpochMilli();
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);
}
Expand All @@ -515,8 +554,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);
}

Expand Down Expand Up @@ -568,8 +607,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);
Expand Down Expand Up @@ -610,18 +649,24 @@ 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<NotificationDetails> scheduledNotifications = loadScheduledNotifications(context);
ArrayList<NotificationDetails> scheduledNotificationsToSave = new ArrayList<>();
for (NotificationDetails scheduledNotification : scheduledNotifications) {
if (scheduledNotification.id.equals(notificationDetails.id)) {
continue;
}
scheduledNotificationsToSave.add(scheduledNotification);
SharedPreferences sharedPreferences =
context.getSharedPreferences(SCHEDULED_NOTIFICATIONS_FILE, Context.MODE_PRIVATE);
Set<String> jsonNotifications = getScheduledNotificationsJsonSet(sharedPreferences);
if (jsonNotifications == null) {
jsonNotifications = new HashSet<String>();
}
scheduledNotificationsToSave.add(notificationDetails);
saveScheduledNotifications(context, scheduledNotificationsToSave);

// Mutations on the Set from getStringSet() are not allowed!
Set<String> jsonNotificationsToSave = new HashSet<String>(jsonNotifications);

Gson gson = buildGson();
jsonNotificationsToSave.add(gson.toJson(notificationDetails));

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

notifications should have unique ids not unique json strings

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I ignored this explicitly, although we can solve this by using a HashMap instead of ArrayList in loadScheduledNotifications.

SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putStringSet(SCHEDULED_NOTIFICATIONS_SET, jsonNotificationsToSave);
editor.apply();
}

private static int getDrawableResourceId(Context context, String name) {
Expand Down Expand Up @@ -1619,7 +1664,11 @@ private void cancelAllNotifications(Result result) {
alarmManager.cancel(pendingIntent);
}

saveScheduledNotifications(applicationContext, new ArrayList<>());
SharedPreferences sharedPreferences =
applicationContext.getSharedPreferences(SCHEDULED_NOTIFICATIONS_FILE, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.remove(SCHEDULED_NOTIFICATIONS_SET);
editor.apply();
result.success(null);
}

Expand Down