From 4f36494981ce7e6a0b471e228bf040185e74a86d Mon Sep 17 00:00:00 2001 From: Jeroen van de Ven Date: Mon, 10 Jan 2022 17:28:11 +0100 Subject: [PATCH 1/8] [WIP] New data model for scheduled notifications Google Java Format Update Gradle Remove new code, format Small fixes Fix rebase bug --- .../FlutterLocalNotificationsPlugin.java | 257 +++++++++++------- .../ScheduledNotificationReceiver.java | 1 - .../example/android/app/build.gradle | 6 +- .../example/android/build.gradle | 2 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- 5 files changed, 163 insertions(+), 105 deletions(-) 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 1ce44bdc7..2e5d9a0c4 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 @@ -38,6 +38,7 @@ import androidx.core.app.RemoteInput; import androidx.core.content.ContextCompat; import androidx.core.graphics.drawable.IconCompat; + import com.dexterous.flutterlocalnotifications.isolate.IsolatePreferences; import com.dexterous.flutterlocalnotifications.models.BitmapSource; import com.dexterous.flutterlocalnotifications.models.DateTimeComponents; @@ -64,17 +65,7 @@ import com.google.gson.GsonBuilder; import com.google.gson.reflect.TypeToken; import com.jakewharton.threetenabp.AndroidThreeTen; -import io.flutter.FlutterInjector; -import io.flutter.embedding.engine.loader.FlutterLoader; -import io.flutter.embedding.engine.plugins.FlutterPlugin; -import io.flutter.embedding.engine.plugins.activity.ActivityAware; -import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; -import io.flutter.plugin.common.BinaryMessenger; -import io.flutter.plugin.common.MethodCall; -import io.flutter.plugin.common.MethodChannel; -import io.flutter.plugin.common.MethodChannel.MethodCallHandler; -import io.flutter.plugin.common.MethodChannel.Result; -import io.flutter.plugin.common.PluginRegistry; + import java.io.FileInputStream; import java.io.IOException; import java.lang.reflect.Type; @@ -86,10 +77,21 @@ 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 io.flutter.FlutterInjector; +import io.flutter.embedding.engine.loader.FlutterLoader; +import io.flutter.embedding.engine.plugins.FlutterPlugin; +import io.flutter.embedding.engine.plugins.activity.ActivityAware; +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; +import io.flutter.plugin.common.BinaryMessenger; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; +import io.flutter.plugin.common.MethodChannel.MethodCallHandler; +import io.flutter.plugin.common.MethodChannel.Result; +import io.flutter.plugin.common.PluginRegistry; + /** FlutterLocalNotificationsPlugin */ @Keep public class FlutterLocalNotificationsPlugin @@ -101,7 +103,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"; @@ -119,6 +123,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"; @@ -151,6 +156,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; @@ -409,36 +415,64 @@ 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) { - Gson gson = buildGson(); - Type type = new TypeToken>() {}.getType(); - scheduledNotifications = gson.fromJson(json, type); + context.getSharedPreferences(SCHEDULED_NOTIFICATIONS_FILE, Context.MODE_PRIVATE); + Map jsonNotifications = sharedPreferences.getAll(); + Log.v("FLN", "Got all " + jsonNotifications.size() + " notification JSONs"); + Gson gson = buildGson(); + Type type = new TypeToken() {}.getType(); + for (Object value : jsonNotifications.values()) { + if (value instanceof String) { + String json = (String) value; + scheduledNotifications.add(gson.fromJson(json, type)); + } else { + Log.v("FLN", "Found non-string value: " + value); + } } return scheduledNotifications; } - private static void saveScheduledNotifications( - Context context, ArrayList scheduledNotifications) { - Gson gson = buildGson(); - String json = gson.toJson(scheduledNotifications); - SharedPreferences sharedPreferences = - context.getSharedPreferences(SCHEDULED_NOTIFICATIONS, Context.MODE_PRIVATE); - SharedPreferences.Editor editor = sharedPreferences.edit(); - editor.putString(SCHEDULED_NOTIFICATIONS, json).apply(); - } + // 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(); + // 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.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 sharedPreferences = + context.getSharedPreferences(SCHEDULED_NOTIFICATIONS_FILE, Context.MODE_PRIVATE); + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.remove(SCHEDULED_NOTIFICATIONS_ID + notificationId); + editor.apply(); } @SuppressWarnings("deprecation") @@ -457,51 +491,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)) { - // Workaround for Android 12 issue regarding extra com.android.systemui alarms that - // are not cancelled by AlarmManager. - if (VERSION.SDK_INT >= VERSION_CODES.S) { - AlarmManagerCompat.setExactAndAllowWhileIdle( - alarmManager, - AlarmManager.RTC_WAKEUP, - notificationDetails.millisecondsSinceEpoch, - pendingIntent); - } else { - AlarmManagerCompat.setAlarmClock( - alarmManager, notificationDetails.millisecondsSinceEpoch, pendingIntent, 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( @@ -514,6 +514,23 @@ 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)) { // Workaround for Android 12 issue regarding extra com.android.systemui alarms that // are not cancelled by AlarmManager. @@ -648,16 +665,13 @@ private static long calculateRepeatIntervalMilliseconds(NotificationDetails noti 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) { @@ -1355,6 +1369,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; @@ -1468,6 +1485,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 = @@ -1478,6 +1500,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); @@ -1639,6 +1676,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); @@ -1653,26 +1696,42 @@ 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 */ + private void cancelAllNotifications(Result result) { + NotificationManagerCompat notificationManager = getNotificationManager(applicationContext); + notificationManager.cancelAll(); + + cancelAllPending(result); } @Override diff --git a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/ScheduledNotificationReceiver.java b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/ScheduledNotificationReceiver.java index 7e4b55e30..a52b3005d 100644 --- a/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/ScheduledNotificationReceiver.java +++ b/flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/ScheduledNotificationReceiver.java @@ -16,7 +16,6 @@ import java.lang.reflect.Type; - /** Created by michaelbui on 24/3/18. */ @Keep public class ScheduledNotificationReceiver extends BroadcastReceiver { diff --git a/flutter_local_notifications/example/android/app/build.gradle b/flutter_local_notifications/example/android/app/build.gradle index b77e3da89..5d859e141 100644 --- a/flutter_local_notifications/example/android/app/build.gradle +++ b/flutter_local_notifications/example/android/app/build.gradle @@ -54,11 +54,11 @@ android { signingConfig signingConfigs.debug } } - - // Temporary workaround as per https://issuetracker.google.com/issues/158060799 - lintOptions { + lint { checkReleaseBuilds false } + + // Temporary workaround as per https://issuetracker.google.com/issues/158060799 } flutter { diff --git a/flutter_local_notifications/example/android/build.gradle b/flutter_local_notifications/example/android/build.gradle index df7a3ddd8..7f0bb8f1c 100644 --- a/flutter_local_notifications/example/android/build.gradle +++ b/flutter_local_notifications/example/android/build.gradle @@ -6,7 +6,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:4.1.3' + classpath 'com.android.tools.build:gradle:7.1.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } diff --git a/flutter_local_notifications/example/android/gradle/wrapper/gradle-wrapper.properties b/flutter_local_notifications/example/android/gradle/wrapper/gradle-wrapper.properties index bc6a58afd..595fb867a 100644 --- a/flutter_local_notifications/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/flutter_local_notifications/example/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-all.zip From 78ae826b87e422e7270a8e80325b36f4417f2dcd Mon Sep 17 00:00:00 2001 From: Pieter van Loon Date: Fri, 25 Mar 2022 09:52:33 +0100 Subject: [PATCH 2/8] Add stub for cancelAllPending in platform interface --- .../lib/flutter_local_notifications_platform_interface.dart | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/flutter_local_notifications_platform_interface/lib/flutter_local_notifications_platform_interface.dart b/flutter_local_notifications_platform_interface/lib/flutter_local_notifications_platform_interface.dart index 80c3d0612..a87cdbfc0 100644 --- a/flutter_local_notifications_platform_interface/lib/flutter_local_notifications_platform_interface.dart +++ b/flutter_local_notifications_platform_interface/lib/flutter_local_notifications_platform_interface.dart @@ -65,6 +65,11 @@ abstract class FlutterLocalNotificationsPlatform extends PlatformInterface { throw UnimplementedError('cancelAll() has not been implemented'); } + /// Cancels/removes all pending notifications. This applies to notifications that have been scheduled but not those that have already been presented. + Future cancelAllPending() async { + throw UnimplementedError('cancelAllPending() has not been implemented'); + } + /// Returns a list of notifications pending to be delivered/shown Future> pendingNotificationRequests() { throw UnimplementedError( From c3879199e90abafa69952ea94295929d775f486f Mon Sep 17 00:00:00 2001 From: Pieter van Loon Date: Fri, 25 Mar 2022 09:53:30 +0100 Subject: [PATCH 3/8] Call plugin for cancelAllPending --- .../lib/src/platform_flutter_local_notifications.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart b/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart index 6a6c54c84..67a0fc13a 100644 --- a/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart +++ b/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart @@ -39,6 +39,9 @@ class MethodChannelFlutterLocalNotificationsPlugin @override Future cancelAll() => _channel.invokeMethod('cancelAll'); + @override + Future cancelAllPending() => _channel.invokeMethod('cancelAllPending'); + @override Future getNotificationAppLaunchDetails() async { From a68af4972e2215f43dd1c0d8a25fb2d44544a4d4 Mon Sep 17 00:00:00 2001 From: Pieter van Loon Date: Fri, 25 Mar 2022 10:01:47 +0100 Subject: [PATCH 4/8] Implemented cancelAllPending on iOS --- .../ios/Classes/FlutterLocalNotificationsPlugin.m | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/flutter_local_notifications/ios/Classes/FlutterLocalNotificationsPlugin.m b/flutter_local_notifications/ios/Classes/FlutterLocalNotificationsPlugin.m index 201934436..8e3c2267a 100644 --- a/flutter_local_notifications/ios/Classes/FlutterLocalNotificationsPlugin.m +++ b/flutter_local_notifications/ios/Classes/FlutterLocalNotificationsPlugin.m @@ -29,6 +29,7 @@ @implementation FlutterLocalNotificationsPlugin { NSString *const SHOW_WEEKLY_AT_DAY_AND_TIME_METHOD = @"showWeeklyAtDayAndTime"; NSString *const CANCEL_METHOD = @"cancel"; NSString *const CANCEL_ALL_METHOD = @"cancelAll"; +NSString *const CANCEL_ALL_PENDING_METHOD = @"cancelAllPending"; NSString *const PENDING_NOTIFICATIONS_REQUESTS_METHOD = @"pendingNotificationRequests"; NSString *const GET_ACTIVE_NOTIFICATIONS_METHOD = @"getActiveNotifications"; @@ -176,6 +177,8 @@ - (void)handleMethodCall:(FlutterMethodCall *)call [self cancel:((NSNumber *)call.arguments) result:result]; } else if ([CANCEL_ALL_METHOD isEqualToString:call.method]) { [self cancelAll:result]; + } else if ([CANCEL_ALL_PENDING_METHOD isEqualToString:call.method]) { + [self cancelAllPending:result]; } else if ([GET_NOTIFICATION_APP_LAUNCH_DETAILS_METHOD isEqualToString:call.method]) { NSString *payload; @@ -800,6 +803,17 @@ - (void)cancel:(NSNumber *)id result:(FlutterResult _Nonnull)result { result(nil); } +- (void)cancelAllPending:(FlutterResult _Nonnull)result { + if (@available(iOS 10.0, *)) { + UNUserNotificationCenter *center = + [UNUserNotificationCenter currentNotificationCenter]; + [center removeAllPendingNotificationRequests]; + } else { + [[UIApplication sharedApplication] cancelAllLocalNotifications]; + } + result(nil); +} + - (void)cancelAll:(FlutterResult _Nonnull)result { if (@available(iOS 10.0, *)) { UNUserNotificationCenter *center = From 736cdfe9314e0e330a90406bef7b099f7c6435d7 Mon Sep 17 00:00:00 2001 From: Pieter van Loon Date: Fri, 25 Mar 2022 11:13:42 +0100 Subject: [PATCH 5/8] Removed support for deprecated methods and set minimum ios to 10 --- .../Classes/FlutterLocalNotificationsPlugin.m | 385 ++---------------- .../ios/flutter_local_notifications.podspec | 2 +- .../platform_flutter_local_notifications.dart | 50 --- 3 files changed, 44 insertions(+), 393 deletions(-) diff --git a/flutter_local_notifications/ios/Classes/FlutterLocalNotificationsPlugin.m b/flutter_local_notifications/ios/Classes/FlutterLocalNotificationsPlugin.m index 8e3c2267a..7d0b541d5 100644 --- a/flutter_local_notifications/ios/Classes/FlutterLocalNotificationsPlugin.m +++ b/flutter_local_notifications/ios/Classes/FlutterLocalNotificationsPlugin.m @@ -12,7 +12,6 @@ @implementation FlutterLocalNotificationsPlugin { bool _launchingAppFromNotification; NSObject *_registrar; NSString *_launchPayload; - UILocalNotification *_launchNotification; FlutterEngineManager *_flutterEngineManager; } @@ -22,11 +21,8 @@ @implementation FlutterLocalNotificationsPlugin { NSString *const INITIALIZE_METHOD = @"initialize"; NSString *const GET_CALLBACK_METHOD = @"getCallbackHandle"; NSString *const SHOW_METHOD = @"show"; -NSString *const SCHEDULE_METHOD = @"schedule"; NSString *const ZONED_SCHEDULE_METHOD = @"zonedSchedule"; NSString *const PERIODICALLY_SHOW_METHOD = @"periodicallyShow"; -NSString *const SHOW_DAILY_AT_TIME_METHOD = @"showDailyAtTime"; -NSString *const SHOW_WEEKLY_AT_DAY_AND_TIME_METHOD = @"showWeeklyAtDayAndTime"; NSString *const CANCEL_METHOD = @"cancel"; NSString *const CANCEL_ALL_METHOD = @"cancelAll"; NSString *const CANCEL_ALL_PENDING_METHOD = @"cancelAllPending"; @@ -163,14 +159,8 @@ - (void)handleMethodCall:(FlutterMethodCall *)call [self show:call.arguments result:result]; } else if ([ZONED_SCHEDULE_METHOD isEqualToString:call.method]) { [self zonedSchedule:call.arguments result:result]; - } else if ([SCHEDULE_METHOD isEqualToString:call.method]) { - [self schedule:call.arguments result:result]; } else if ([PERIODICALLY_SHOW_METHOD isEqualToString:call.method]) { [self periodicallyShow:call.arguments result:result]; - } else if ([SHOW_DAILY_AT_TIME_METHOD isEqualToString:call.method]) { - [self showDailyAtTime:call.arguments result:result]; - } else if ([SHOW_WEEKLY_AT_DAY_AND_TIME_METHOD isEqualToString:call.method]) { - [self showWeeklyAtDayAndTime:call.arguments result:result]; } else if ([REQUEST_PERMISSIONS_METHOD isEqualToString:call.method]) { [self requestPermissions:call.arguments result:result]; } else if ([CANCEL_METHOD isEqualToString:call.method]) { @@ -181,16 +171,10 @@ - (void)handleMethodCall:(FlutterMethodCall *)call [self cancelAllPending:result]; } else if ([GET_NOTIFICATION_APP_LAUNCH_DETAILS_METHOD isEqualToString:call.method]) { - NSString *payload; - if (_launchNotification != nil) { - payload = _launchNotification.userInfo[PAYLOAD]; - } else { - payload = _launchPayload; - } NSDictionary *notificationAppLaunchDetails = [NSDictionary dictionaryWithObjectsAndKeys: [NSNumber numberWithBool:_launchingAppFromNotification], - NOTIFICATION_LAUNCHED_APP, payload, PAYLOAD, nil]; + NOTIFICATION_LAUNCHED_APP, _launchPayload, PAYLOAD, nil]; result(notificationAppLaunchDetails); } else if ([PENDING_NOTIFICATIONS_REQUESTS_METHOD isEqualToString:call.method]) { @@ -463,7 +447,6 @@ - (void)requestPermissionsImpl:(bool)soundPermission result(@NO); return; } - if (@available(iOS 10.0, *)) { UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; @@ -487,79 +470,8 @@ - (void)requestPermissionsImpl:(bool)soundPermission NSError *_Nullable error) { result(@(granted)); }]; - } else { - UIUserNotificationType notificationTypes = 0; - if (soundPermission) { - notificationTypes |= UIUserNotificationTypeSound; - } - if (alertPermission) { - notificationTypes |= UIUserNotificationTypeAlert; - } - if (badgePermission) { - notificationTypes |= UIUserNotificationTypeBadge; - } - UIUserNotificationSettings *settings = - [UIUserNotificationSettings settingsForTypes:notificationTypes - categories:nil]; - [[UIApplication sharedApplication] - registerUserNotificationSettings:settings]; - result(@YES); - } } -- (UILocalNotification *)buildStandardUILocalNotification: - (NSDictionary *)arguments { - UILocalNotification *notification = [[UILocalNotification alloc] init]; - if ([self containsKey:BODY forDictionary:arguments]) { - notification.alertBody = arguments[BODY]; - } - - NSString *title; - if ([self containsKey:TITLE forDictionary:arguments]) { - title = arguments[TITLE]; - if (@available(iOS 8.2, *)) { - notification.alertTitle = title; - } - } - - bool presentAlert = _displayAlert; - bool presentSound = _playSound; - bool presentBadge = _updateBadge; - if (arguments[PLATFORM_SPECIFICS] != [NSNull null]) { - NSDictionary *platformSpecifics = arguments[PLATFORM_SPECIFICS]; - - if ([self containsKey:PRESENT_ALERT forDictionary:platformSpecifics]) { - presentAlert = [[platformSpecifics objectForKey:PRESENT_ALERT] boolValue]; - } - if ([self containsKey:PRESENT_SOUND forDictionary:platformSpecifics]) { - presentSound = [[platformSpecifics objectForKey:PRESENT_SOUND] boolValue]; - } - if ([self containsKey:PRESENT_BADGE forDictionary:platformSpecifics]) { - presentBadge = [[platformSpecifics objectForKey:PRESENT_BADGE] boolValue]; - } - - if ([self containsKey:BADGE_NUMBER forDictionary:platformSpecifics]) { - notification.applicationIconBadgeNumber = - [platformSpecifics[BADGE_NUMBER] integerValue]; - } - - if ([self containsKey:SOUND forDictionary:platformSpecifics]) { - notification.soundName = [platformSpecifics[SOUND] stringValue]; - } - } - - if (presentSound && notification.soundName == nil) { - notification.soundName = UILocalNotificationDefaultSoundName; - } - - notification.userInfo = [self buildUserDict:arguments[ID] - title:title - presentAlert:presentAlert - presentSound:presentSound - presentBadge:presentBadge - payload:arguments[PAYLOAD]]; - return notification; -} - (NSString *)getIdentifier:(id)arguments { return [arguments[ID] stringValue]; @@ -567,215 +479,66 @@ - (NSString *)getIdentifier:(id)arguments { - (void)show:(NSDictionary *_Nonnull)arguments result:(FlutterResult _Nonnull)result { - if (@available(iOS 10.0, *)) { UNMutableNotificationContent *content = [self buildStandardNotificationContent:arguments result:result]; - [self addNotificationRequest:[self getIdentifier:arguments] - content:content - result:result - trigger:nil]; - } else { - UILocalNotification *notification = - [self buildStandardUILocalNotification:arguments]; - [[UIApplication sharedApplication] - presentLocalNotificationNow:notification]; - result(nil); - } + UNNotificationRequest *notificationRequest = + [UNNotificationRequest requestWithIdentifier:[self getIdentifier:arguments] + content:content + trigger:nil]; + UNUserNotificationCenter *center = + [UNUserNotificationCenter currentNotificationCenter]; + [center addNotificationRequest:notificationRequest + withCompletionHandler:^(NSError *_Nullable error) { + if (error == nil) { + result(nil); + return; + } + result(getFlutterError(error)); + }]; } - (void)zonedSchedule:(NSDictionary *_Nonnull)arguments result:(FlutterResult _Nonnull)result { - if (@available(iOS 10.0, *)) { UNMutableNotificationContent *content = [self buildStandardNotificationContent:arguments result:result]; UNCalendarNotificationTrigger *trigger = [self buildUserNotificationCalendarTrigger:arguments]; - [self addNotificationRequest:[self getIdentifier:arguments] - content:content - result:result - trigger:trigger]; - - } else { - UILocalNotification *notification = - [self buildStandardUILocalNotification:arguments]; - NSString *scheduledDateTime = arguments[SCHEDULED_DATE_TIME]; - NSString *timeZoneName = arguments[TIME_ZONE_NAME]; - NSNumber *matchDateComponents = arguments[MATCH_DATE_TIME_COMPONENTS]; - NSNumber *uiLocalNotificationDateInterpretation = - arguments[UILOCALNOTIFICATION_DATE_INTERPRETATION]; - NSTimeZone *timezone = [NSTimeZone timeZoneWithName:timeZoneName]; - NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; - [dateFormatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss"]; - [dateFormatter setTimeZone:timezone]; - NSDate *date = [dateFormatter dateFromString:scheduledDateTime]; - notification.fireDate = date; - if (uiLocalNotificationDateInterpretation != nil) { - if ([uiLocalNotificationDateInterpretation integerValue] == - AbsoluteGMTTime) { - notification.timeZone = nil; - } else if ([uiLocalNotificationDateInterpretation integerValue] == - WallClockTime) { - notification.timeZone = timezone; - } - } - if (matchDateComponents != nil) { - if ([matchDateComponents integerValue] == Time) { - notification.repeatInterval = NSCalendarUnitDay; - } else if ([matchDateComponents integerValue] == DayOfWeekAndTime) { - notification.repeatInterval = NSCalendarUnitWeekOfYear; - } else if ([matchDateComponents integerValue] == DayOfMonthAndTime) { - notification.repeatInterval = NSCalendarUnitMonth; - } else if ([matchDateComponents integerValue] == DateAndTime) { - notification.repeatInterval = NSCalendarUnitYear; - } - } - [[UIApplication sharedApplication] scheduleLocalNotification:notification]; - result(nil); - } -} - -- (void)schedule:(NSDictionary *_Nonnull)arguments - result:(FlutterResult _Nonnull)result { - NSNumber *secondsSinceEpoch = - @([arguments[MILLISECONDS_SINCE_EPOCH] longLongValue] / 1000); - if (@available(iOS 10.0, *)) { - UNMutableNotificationContent *content = - [self buildStandardNotificationContent:arguments result:result]; - NSDate *date = [NSDate - dateWithTimeIntervalSince1970:[secondsSinceEpoch longLongValue]]; - NSCalendar *calendar = [NSCalendar currentCalendar]; - NSDateComponents *dateComponents = - [calendar components:(NSCalendarUnitYear | NSCalendarUnitMonth | - NSCalendarUnitDay | NSCalendarUnitHour | - NSCalendarUnitMinute | NSCalendarUnitSecond) - fromDate:date]; - UNCalendarNotificationTrigger *trigger = [UNCalendarNotificationTrigger - triggerWithDateMatchingComponents:dateComponents - repeats:false]; - [self addNotificationRequest:[self getIdentifier:arguments] - content:content - result:result - trigger:trigger]; - } else { - UILocalNotification *notification = - [self buildStandardUILocalNotification:arguments]; - notification.fireDate = [NSDate - dateWithTimeIntervalSince1970:[secondsSinceEpoch longLongValue]]; - [[UIApplication sharedApplication] scheduleLocalNotification:notification]; - result(nil); - } + UNNotificationRequest *notificationRequest = + [UNNotificationRequest requestWithIdentifier:[self getIdentifier:arguments] + content:content + trigger:trigger]; + UNUserNotificationCenter *center = + [UNUserNotificationCenter currentNotificationCenter]; + [center addNotificationRequest:notificationRequest + withCompletionHandler:^(NSError *_Nullable error) { + if (error == nil) { + result(nil); + return; + } + result(getFlutterError(error)); + }]; } - (void)periodicallyShow:(NSDictionary *_Nonnull)arguments result:(FlutterResult _Nonnull)result { - if (@available(iOS 10.0, *)) { UNMutableNotificationContent *content = [self buildStandardNotificationContent:arguments result:result]; UNTimeIntervalNotificationTrigger *trigger = [self buildUserNotificationTimeIntervalTrigger:arguments]; - [self addNotificationRequest:[self getIdentifier:arguments] - content:content - result:result - trigger:trigger]; - } else { - UILocalNotification *notification = - [self buildStandardUILocalNotification:arguments]; - NSTimeInterval timeInterval = 0; - switch ([arguments[REPEAT_INTERVAL] integerValue]) { - case EveryMinute: - timeInterval = 60; - notification.repeatInterval = NSCalendarUnitMinute; - break; - case Hourly: - timeInterval = 60 * 60; - notification.repeatInterval = NSCalendarUnitHour; - break; - case Daily: - timeInterval = 60 * 60 * 24; - notification.repeatInterval = NSCalendarUnitDay; - break; - case Weekly: - timeInterval = 60 * 60 * 24 * 7; - notification.repeatInterval = NSCalendarUnitWeekOfYear; - break; - } - notification.fireDate = [NSDate dateWithTimeIntervalSinceNow:timeInterval]; - [[UIApplication sharedApplication] scheduleLocalNotification:notification]; - result(nil); - } -} - -- (void)showDailyAtTime:(NSDictionary *_Nonnull)arguments - result:(FlutterResult _Nonnull)result { - NSDictionary *timeArguments = (NSDictionary *)arguments[REPEAT_TIME]; - NSNumber *hourComponent = timeArguments[HOUR]; - NSNumber *minutesComponent = timeArguments[MINUTE]; - NSNumber *secondsComponent = timeArguments[SECOND]; - if (@available(iOS 10.0, *)) { - UNMutableNotificationContent *content = - [self buildStandardNotificationContent:arguments result:result]; - NSDateComponents *dateComponents = [[NSDateComponents alloc] init]; - [dateComponents setHour:[hourComponent integerValue]]; - [dateComponents setMinute:[minutesComponent integerValue]]; - [dateComponents setSecond:[secondsComponent integerValue]]; - UNCalendarNotificationTrigger *trigger = [UNCalendarNotificationTrigger - triggerWithDateMatchingComponents:dateComponents - repeats:YES]; - [self addNotificationRequest:[self getIdentifier:arguments] - content:content - result:result - trigger:trigger]; - } else { - UILocalNotification *notification = - [self buildStandardUILocalNotification:arguments]; - notification.repeatInterval = NSCalendarUnitDay; - NSDateComponents *dateComponents = [[NSDateComponents alloc] init]; - [dateComponents setHour:[hourComponent integerValue]]; - [dateComponents setMinute:[minutesComponent integerValue]]; - [dateComponents setSecond:[secondsComponent integerValue]]; - NSCalendar *calendar = [NSCalendar currentCalendar]; - notification.fireDate = [calendar dateFromComponents:dateComponents]; - [[UIApplication sharedApplication] scheduleLocalNotification:notification]; - result(nil); - } -} - -- (void)showWeeklyAtDayAndTime:(NSDictionary *_Nonnull)arguments - result:(FlutterResult _Nonnull)result { - NSDictionary *timeArguments = (NSDictionary *)arguments[REPEAT_TIME]; - NSNumber *dayOfWeekComponent = arguments[DAY]; - NSNumber *hourComponent = timeArguments[HOUR]; - NSNumber *minutesComponent = timeArguments[MINUTE]; - NSNumber *secondsComponent = timeArguments[SECOND]; - if (@available(iOS 10.0, *)) { - UNMutableNotificationContent *content = - [self buildStandardNotificationContent:arguments result:result]; - NSDateComponents *dateComponents = [[NSDateComponents alloc] init]; - [dateComponents setHour:[hourComponent integerValue]]; - [dateComponents setMinute:[minutesComponent integerValue]]; - [dateComponents setSecond:[secondsComponent integerValue]]; - [dateComponents setWeekday:[dayOfWeekComponent integerValue]]; - UNCalendarNotificationTrigger *trigger = [UNCalendarNotificationTrigger - triggerWithDateMatchingComponents:dateComponents - repeats:YES]; - [self addNotificationRequest:[self getIdentifier:arguments] - content:content - result:result - trigger:trigger]; - } else { - UILocalNotification *notification = - [self buildStandardUILocalNotification:arguments]; - notification.repeatInterval = NSCalendarUnitWeekOfYear; - NSDateComponents *dateComponents = [[NSDateComponents alloc] init]; - [dateComponents setHour:[hourComponent integerValue]]; - [dateComponents setMinute:[minutesComponent integerValue]]; - [dateComponents setSecond:[secondsComponent integerValue]]; - [dateComponents setWeekday:[dayOfWeekComponent integerValue]]; - NSCalendar *calendar = [NSCalendar currentCalendar]; - notification.fireDate = [calendar dateFromComponents:dateComponents]; - [[UIApplication sharedApplication] scheduleLocalNotification:notification]; - result(nil); - } + UNNotificationRequest *notificationRequest = + [UNNotificationRequest requestWithIdentifier:[self getIdentifier:arguments] + content:content + trigger:trigger]; + UNUserNotificationCenter *center = + [UNUserNotificationCenter currentNotificationCenter]; + [center addNotificationRequest:notificationRequest + withCompletionHandler:^(NSError *_Nullable error) { + if (error == nil) { + result(nil); + return; + } + result(getFlutterError(error)); + }]; } - (void)cancel:(NSNumber *)id result:(FlutterResult _Nonnull)result { @@ -1027,27 +790,6 @@ - (NSDictionary *)buildUserDict:(NSNumber *)id return userDict; } -- (void)addNotificationRequest:(NSString *)identifier - content:(UNMutableNotificationContent *)content - result:(FlutterResult _Nonnull)result - trigger:(UNNotificationTrigger *)trigger - API_AVAILABLE(ios(10.0)) { - UNNotificationRequest *notificationRequest = - [UNNotificationRequest requestWithIdentifier:identifier - content:content - trigger:trigger]; - UNUserNotificationCenter *center = - [UNUserNotificationCenter currentNotificationCenter]; - [center addNotificationRequest:notificationRequest - withCompletionHandler:^(NSError *_Nullable error) { - if (error == nil) { - result(nil); - return; - } - result(getFlutterError(error)); - }]; -} - - (BOOL)isAFlutterLocalNotification:(NSDictionary *)userInfo { return userInfo != nil && userInfo[NOTIFICATION_ID] && userInfo[PRESENT_ALERT] && userInfo[PRESENT_SOUND] && @@ -1137,45 +879,4 @@ - (void)userNotificationCenter:(UNUserNotificationCenter *)center } } -#pragma mark - AppDelegate -- (BOOL)application:(UIApplication *)application - didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { - if (launchOptions != nil) { - UILocalNotification *launchNotification = - (UILocalNotification *)[launchOptions - objectForKey:UIApplicationLaunchOptionsLocalNotificationKey]; - _launchingAppFromNotification = - launchNotification != nil && - [self isAFlutterLocalNotification:launchNotification.userInfo]; - if (_launchingAppFromNotification) { - _launchNotification = launchNotification; - } - } - - return YES; -} - -- (void)application:(UIApplication *)application - didReceiveLocalNotification:(UILocalNotification *)notification { - if (@available(iOS 10.0, *)) { - return; - } - if (![self isAFlutterLocalNotification:notification.userInfo]) { - return; - } - - NSMutableDictionary *arguments = [[NSMutableDictionary alloc] init]; - arguments[ID] = notification.userInfo[NOTIFICATION_ID]; - if (notification.userInfo[TITLE] != [NSNull null]) { - arguments[TITLE] = notification.userInfo[TITLE]; - } - if (notification.alertBody != nil) { - arguments[BODY] = notification.alertBody; - } - if (notification.userInfo[PAYLOAD] != [NSNull null]) { - arguments[PAYLOAD] = notification.userInfo[PAYLOAD]; - } - [_channel invokeMethod:DID_RECEIVE_LOCAL_NOTIFICATION arguments:arguments]; -} - @end diff --git a/flutter_local_notifications/ios/flutter_local_notifications.podspec b/flutter_local_notifications/ios/flutter_local_notifications.podspec index ddc954e1f..42d80b605 100644 --- a/flutter_local_notifications/ios/flutter_local_notifications.podspec +++ b/flutter_local_notifications/ios/flutter_local_notifications.podspec @@ -16,6 +16,6 @@ Flutter plugin for displaying local notifications. s.public_header_files = 'Classes/**/*.h' s.dependency 'Flutter' - s.ios.deployment_target = '8.0' + s.ios.deployment_target = '10.0' end diff --git a/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart b/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart index 67a0fc13a..8e6b86928 100644 --- a/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart +++ b/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart @@ -608,56 +608,6 @@ class IOSFlutterLocalNotificationsPlugin })); } - /// Shows a notification on a daily interval at the specified time. - @Deprecated( - 'Deprecated due to problems with time zones. Use zonedSchedule instead.') - Future showDailyAtTime( - int id, - String? title, - String? body, - Time notificationTime, - DarwinNotificationDetails? notificationDetails, { - String? payload, - }) async { - validateId(id); - await _channel.invokeMethod('showDailyAtTime', { - 'id': id, - 'title': title, - 'body': body, - 'calledAt': clock.now().millisecondsSinceEpoch, - 'repeatInterval': RepeatInterval.daily.index, - 'repeatTime': notificationTime.toMap(), - 'platformSpecifics': notificationDetails?.toMap(), - 'payload': payload ?? '' - }); - } - - /// Shows a notification on weekly interval at the specified day and time. - @Deprecated( - 'Deprecated due to problems with time zones. Use zonedSchedule instead.') - Future showWeeklyAtDayAndTime( - int id, - String? title, - String? body, - Day day, - Time notificationTime, - DarwinNotificationDetails? notificationDetails, { - String? payload, - }) async { - validateId(id); - await _channel.invokeMethod('showWeeklyAtDayAndTime', { - 'id': id, - 'title': title, - 'body': body, - 'calledAt': clock.now().millisecondsSinceEpoch, - 'repeatInterval': RepeatInterval.weekly.index, - 'repeatTime': notificationTime.toMap(), - 'day': day.value, - 'platformSpecifics': notificationDetails?.toMap(), - 'payload': payload ?? '' - }); - } - @override Future show( int id, From e86a1c25158389a94d654d270fe239061b27bdbb Mon Sep 17 00:00:00 2001 From: Pieter van Loon Date: Fri, 25 Mar 2022 12:11:59 +0100 Subject: [PATCH 6/8] Implemented batch scheduling on android --- .../FlutterLocalNotificationsPlugin.java | 43 ++++++++----- .../Classes/FlutterLocalNotificationsPlugin.m | 2 - .../flutter_local_notifications_plugin.dart | 31 +++++----- .../platform_flutter_local_notifications.dart | 62 +++++++++++++++++++ .../lib/src/types.dart | 26 ++++++++ 5 files changed, 133 insertions(+), 31 deletions(-) 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 2e5d9a0c4..3347230ef 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 @@ -103,7 +103,7 @@ 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_FILE = "scheduled_notifications"; + private static final String SCHEDULED_NOTIFICATIONS_FILE = "scheduled_notifications_file"; private static final String SCHEDULED_NOTIFICATIONS_STRING = "scheduled_notifications"; private static final String SCHEDULED_NOTIFICATIONS_ID = "snid"; private static final String INITIALIZE_METHOD = "initialize"; @@ -126,6 +126,7 @@ public class FlutterLocalNotificationsPlugin 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 ZONED_SCHEDULE_BATCH_METHOD = "zonedScheduleBatch"; private static final String PERIODICALLY_SHOW_METHOD = "periodicallyShow"; private static final String SHOW_DAILY_AT_TIME_METHOD = "showDailyAtTime"; private static final String SHOW_WEEKLY_AT_DAY_AND_TIME_METHOD = "showWeeklyAtDayAndTime"; @@ -1356,6 +1357,11 @@ public void onMethodCall(MethodCall call, @NonNull Result result) { zonedSchedule(call, result); break; } + case ZONED_SCHEDULE_BATCH_METHOD: + { + zonedScheduleBatch(call, result); + break; + } case PERIODICALLY_SHOW_METHOD: case SHOW_DAILY_AT_TIME_METHOD: case SHOW_WEEKLY_AT_DAY_AND_TIME_METHOD: @@ -1500,20 +1506,27 @@ private static void zonedScheduleDetails( } } - // 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 zonedScheduleBatch(MethodCall call, Result result) { + ArrayList> arguments = call.arguments(); + ArrayList requests = new ArrayList<>(); + + for (Map arg : arguments) { + NotificationDetails details = extractNotificationDetails(result, arg); + if (details == null) { + return; + } + requests.add(details); + } + + for (NotificationDetails details : requests) { + if (details.matchDateTimeComponents != null) { + details.scheduledDateTime = + getNextFireDateMatchingDateTimeComponents(details); + } + zonedScheduleNotification(applicationContext, details, true); + } + result.success(null); + } private void show(MethodCall call, Result result) { Map arguments = call.arguments(); diff --git a/flutter_local_notifications/ios/Classes/FlutterLocalNotificationsPlugin.m b/flutter_local_notifications/ios/Classes/FlutterLocalNotificationsPlugin.m index 7d0b541d5..e33f21466 100644 --- a/flutter_local_notifications/ios/Classes/FlutterLocalNotificationsPlugin.m +++ b/flutter_local_notifications/ios/Classes/FlutterLocalNotificationsPlugin.m @@ -78,8 +78,6 @@ @implementation FlutterLocalNotificationsPlugin { NSString *const SCHEDULED_DATE_TIME = @"scheduledDateTime"; NSString *const TIME_ZONE_NAME = @"timeZoneName"; NSString *const MATCH_DATE_TIME_COMPONENTS = @"matchDateTimeComponents"; -NSString *const UILOCALNOTIFICATION_DATE_INTERPRETATION = - @"uiLocalNotificationDateInterpretation"; NSString *const NOTIFICATION_ID = @"NotificationId"; NSString *const PAYLOAD = @"payload"; diff --git a/flutter_local_notifications/lib/src/flutter_local_notifications_plugin.dart b/flutter_local_notifications/lib/src/flutter_local_notifications_plugin.dart index dd1caf4dd..e6cdaa262 100644 --- a/flutter_local_notifications/lib/src/flutter_local_notifications_plugin.dart +++ b/flutter_local_notifications/lib/src/flutter_local_notifications_plugin.dart @@ -360,6 +360,21 @@ class FlutterLocalNotificationsPlugin { } } + Future zonedScheduleBatch(List requests) async { + if (kIsWeb) { + return; + } + if (defaultTargetPlatform == TargetPlatform.android) { + await resolvePlatformSpecificImplementation< + AndroidFlutterLocalNotificationsPlugin>()! + .zonedScheduleBatch(requests); + } else if (defaultTargetPlatform == TargetPlatform.iOS) { + await resolvePlatformSpecificImplementation< + IOSFlutterLocalNotificationsPlugin>()! + .zonedScheduleBatch(requests); + } + } + /// Periodically show a notification using the specified interval. /// /// For example, specifying a hourly interval means the first time the @@ -432,13 +447,7 @@ class FlutterLocalNotificationsPlugin { ?.showDailyAtTime( id, title, body, notificationTime, notificationDetails.android, payload: payload); - } else if (defaultTargetPlatform == TargetPlatform.iOS) { - await resolvePlatformSpecificImplementation< - IOSFlutterLocalNotificationsPlugin>() - ?.showDailyAtTime( - id, title, body, notificationTime, notificationDetails.iOS, - payload: payload); - } else if (defaultTargetPlatform == TargetPlatform.macOS) { + } else { throw UnimplementedError(); } } @@ -467,13 +476,7 @@ class FlutterLocalNotificationsPlugin { ?.showWeeklyAtDayAndTime(id, title, body, day, notificationTime, notificationDetails.android, payload: payload); - } else if (defaultTargetPlatform == TargetPlatform.iOS) { - await resolvePlatformSpecificImplementation< - IOSFlutterLocalNotificationsPlugin>() - ?.showWeeklyAtDayAndTime( - id, title, body, day, notificationTime, notificationDetails.iOS, - payload: payload); - } else if (defaultTargetPlatform == TargetPlatform.macOS) { + } else { throw UnimplementedError(); } } diff --git a/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart b/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart index 8e6b86928..15c753cae 100644 --- a/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart +++ b/flutter_local_notifications/lib/src/platform_flutter_local_notifications.dart @@ -190,6 +190,38 @@ class AndroidFlutterLocalNotificationsPlugin })); } + /// Schedules a batch of notifications to be shown at the specified date + /// and time relative to a specific time zone. + Future zonedScheduleBatch(List requests) async { + final List> serializedRequests = + >[]; + for (final NotificationRequest request in requests) { + validateId(request.id); + validateDateIsInTheFuture(request.date, request.matchDateTimeComponents); + final Map serializedPlatformSpecifics = + request.details.android?.toMap() ?? {}; + serializedPlatformSpecifics['allowWhileIdle'] = + request.androidAllowWhileIdle; + serializedRequests.add( + { + 'id': request.id, + 'title': request.title, + 'body': request.body, + 'platformSpecifics': serializedPlatformSpecifics, + 'payload': request.payload ?? '' + } + ..addAll(request.date.toMap()) + ..addAll(request.matchDateTimeComponents == null + ? {} + : { + 'matchDateTimeComponents': + request.matchDateTimeComponents!.index + }), + ); + } + await _channel.invokeMethod('zonedScheduleBatch', serializedRequests); + } + /// Shows a notification on a daily interval at the specified time. @Deprecated( 'Deprecated due to problems with time zones. Use zonedSchedule instead.') @@ -608,6 +640,36 @@ class IOSFlutterLocalNotificationsPlugin })); } + /// Schedules a batch of notifications to be shown at the specified date + /// and time relative to a specific time zone. + Future zonedScheduleBatch(List requests) async { + final List> serializedRequests = + >[]; + for (final NotificationRequest request in requests) { + validateId(request.id); + validateDateIsInTheFuture(request.date, request.matchDateTimeComponents); + final Map serializedPlatformSpecifics = + request.details.iOS?.toMap() ?? {}; + serializedRequests.add( + { + 'id': request.id, + 'title': request.title, + 'body': request.body, + 'platformSpecifics': serializedPlatformSpecifics, + 'payload': request.payload ?? '', + } + ..addAll(request.date.toMap()) + ..addAll(request.matchDateTimeComponents == null + ? {} + : { + 'matchDateTimeComponents': + request.matchDateTimeComponents!.index + }), + ); + } + await _channel.invokeMethod('zonedScheduleBatch', serializedRequests); + } + @override Future show( int id, diff --git a/flutter_local_notifications/lib/src/types.dart b/flutter_local_notifications/lib/src/types.dart index e0748d417..c8798124d 100644 --- a/flutter_local_notifications/lib/src/types.dart +++ b/flutter_local_notifications/lib/src/types.dart @@ -1,3 +1,7 @@ +import 'package:timezone/timezone.dart'; + +import 'notification_details.dart'; + /// The days of the week. class Day { /// Constructs an instance of [Day]. @@ -73,3 +77,25 @@ enum DateTimeComponents { /// The date and time. dateAndTime, } + +class NotificationRequest { + NotificationRequest({ + required this.id, + required this.title, + required this.body, + required this.details, + required this.date, + required this.androidAllowWhileIdle, + this.payload, + this.matchDateTimeComponents, + }); + + final int id; + final String? title; + final String? body; + final String? payload; + final TZDateTime date; + final NotificationDetails details; + final DateTimeComponents? matchDateTimeComponents; + final bool androidAllowWhileIdle; +} From 05812db76e89720b8e247095eca1ba9748a6b6dc Mon Sep 17 00:00:00 2001 From: Pieter van Loon Date: Tue, 5 Apr 2022 11:03:36 +0200 Subject: [PATCH 7/8] Added migration to new persistence format --- .../FlutterLocalNotificationsPlugin.java | 89 +++++++++---------- 1 file changed, 42 insertions(+), 47 deletions(-) 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 3347230ef..afd37c4cb 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 @@ -30,6 +30,7 @@ import android.util.Log; import androidx.annotation.Keep; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.core.app.AlarmManagerCompat; import androidx.core.app.NotificationCompat; import androidx.core.app.NotificationCompat.Action.Builder; @@ -66,6 +67,8 @@ import com.google.gson.reflect.TypeToken; import com.jakewharton.threetenabp.AndroidThreeTen; +import org.w3c.dom.Node; + import java.io.FileInputStream; import java.io.IOException; import java.lang.reflect.Type; @@ -414,6 +417,7 @@ static Gson buildGson() { } private static ArrayList loadScheduledNotifications(Context context) { + migrateScheduledNotificationsIfNeeded(context); ArrayList scheduledNotifications = new ArrayList<>(); SharedPreferences sharedPreferences = context.getSharedPreferences(SCHEDULED_NOTIFICATIONS_FILE, Context.MODE_PRIVATE); @@ -432,42 +436,33 @@ private static ArrayList loadScheduledNotifications(Context return scheduledNotifications; } - // TODO rewrite migration code + private static void migrateScheduledNotificationsIfNeeded(Context context) { + SharedPreferences sharedPreferences = + context.getSharedPreferences(SCHEDULED_NOTIFICATIONS_STRING, Context.MODE_PRIVATE); + String json = sharedPreferences.getString(SCHEDULED_NOTIFICATIONS_STRING, null); + if (json == null) { + Log.i("FLN", "No old notifications to migrate"); + return; + } + Gson gson = buildGson(); + Type type = new TypeToken>(){}.getType(); + ArrayList oldNotifications = gson.fromJson(json, type); + Log.i("FLN", "Migrating " + oldNotifications.size() + " notifications"); + // Deleting existing new notifications prevents duplicates + context.getSharedPreferences(SCHEDULED_NOTIFICATIONS_FILE, Context.MODE_PRIVATE) + .edit() + .clear() + .apply(); + Log.i("FLN", "Deleted any currently existing new notifications"); + for (NotificationDetails notification : oldNotifications) { + saveScheduledNotification(context, notification); + } + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.remove(SCHEDULED_NOTIFICATIONS_STRING); + editor.apply(); + Log.i("FLN", "Migrated " + oldNotifications.size() + " notifications"); + } - /** - * 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(); - // 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.remove(SCHEDULED_NOTIFICATIONS_STRING); - // editor.putStringSet(SCHEDULED_NOTIFICATIONS_SET, scheduledNotifications); - // editor.apply(); - // return scheduledNotifications; - // } static void removeNotificationFromCache(Context context, Integer notificationId) { SharedPreferences sharedPreferences = context.getSharedPreferences(SCHEDULED_NOTIFICATIONS_FILE, Context.MODE_PRIVATE); @@ -1373,10 +1368,10 @@ public void onMethodCall(MethodCall call, @NonNull Result result) { cancel(call, result); break; case CANCEL_ALL_METHOD: - cancelAllNotifications(result); + cancelAllNotifications(result, applicationContext); break; case CANCEL_ALL_PENDING_METHOD: - cancelAllPending(result); + cancelAllPending(result, applicationContext); break; case PENDING_NOTIFICATION_REQUESTS_METHOD: pendingNotificationRequests(result); @@ -1710,27 +1705,27 @@ private void cancelNotification(Integer id, String tag) { } /** Cancels only all pending notifications, leaving active ones. */ - private void cancelAllPending(Result result) { - cancelAllPendingNotifications(); + private static void cancelAllPending(Result result, Context context) { + cancelAllPendingNotifications(context); result.success(null); } /** Cancels all pending notifications without parsing any JSON */ - private void cancelAllPendingNotifications() { + private static void cancelAllPendingNotifications(Context context) { SharedPreferences sharedPreferences = - applicationContext.getSharedPreferences(SCHEDULED_NOTIFICATIONS_FILE, Context.MODE_PRIVATE); + context.getSharedPreferences(SCHEDULED_NOTIFICATIONS_FILE, Context.MODE_PRIVATE); Map jsonNotifications = (Map) sharedPreferences.getAll(); if (jsonNotifications.isEmpty()) { return; } - Intent intent = new Intent(applicationContext, ScheduledNotificationReceiver.class); - AlarmManager alarmManager = getAlarmManager(applicationContext); + Intent intent = new Intent(context, ScheduledNotificationReceiver.class); + AlarmManager alarmManager = getAlarmManager(context); 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); + PendingIntent pendingIntent = getBroadcastPendingIntent(context, intId, intent); alarmManager.cancel(pendingIntent); } @@ -1740,11 +1735,11 @@ private void cancelAllPendingNotifications() { } /** Cancels all notifications, both scheduled and active */ - private void cancelAllNotifications(Result result) { - NotificationManagerCompat notificationManager = getNotificationManager(applicationContext); + private static void cancelAllNotifications(Result result, Context context) { + NotificationManagerCompat notificationManager = getNotificationManager(context); notificationManager.cancelAll(); - cancelAllPending(result); + cancelAllPending(result, context); } @Override From 846b48369ef1b31eb916397b00f8263f9138e6c8 Mon Sep 17 00:00:00 2001 From: Jeroen van de Ven Date: Tue, 10 May 2022 13:18:29 +0200 Subject: [PATCH 8/8] [MAF-2043] Fix bug with substring length --- .../FlutterLocalNotificationsPlugin.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 afd37c4cb..45d2f66c0 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 @@ -1724,7 +1724,7 @@ private static void cancelAllPendingNotifications(Context context) { AlarmManager alarmManager = getAlarmManager(context); for (String id : jsonNotifications.keySet()) { // TODO filter out non-matching IDs - int intId = Integer.parseInt(id.substring(SCHEDULED_NOTIFICATIONS_ID.length() - 1)); + int intId = Integer.parseInt(id.substring(SCHEDULED_NOTIFICATIONS_ID.length())); PendingIntent pendingIntent = getBroadcastPendingIntent(context, intId, intent); alarmManager.cancel(pendingIntent); }