diff --git a/CHANGELOG.md b/CHANGELOG.md index b6f2dfa98..39cb7eda6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -76,4 +76,7 @@ ## [0.2.0] * [Android] Add ability to specify if notifications should automatically be dismissed upon touching them * [Android] Add ability to specify in notifications are ongoing -* [Android] Fix bug in cancelling all notifications \ No newline at end of file +* [Android] Fix bug in cancelling all notifications + +## [0.2.1] +* [Android & iOS] Add ability to set a notification to be periodically displayed \ No newline at end of file diff --git a/README.md b/README.md index a6eeb8378..f4ea7e1dc 100644 --- a/README.md +++ b/README.md @@ -209,6 +209,10 @@ Note that with Android 8.0+, sounds and vibrations are associated with notificat By design, iOS applications do not display notifications when they're in the foreground. For iOS 10+, use the presentation options to control the behaviour for when a notification is triggered while the app is in the foreground. For older versions of iOS, you will need update the AppDelegate class to handle when a local notification is received to display an alert. This is shown in the sample app within the `didReceiveLocalNotification` method of the `AppDelegate` class. The notification title can be found by looking up the `title` within the `userInfo` dictionary of the `UILocalNotification` object ``` +#import + +... + - (void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification { if(@available(iOS 10.0, *)) { @@ -243,6 +247,15 @@ By design, iOS applications do not display notifications when they're in the for In theory, it should be possible for the plugin to handle this but this the method doesn't seem to fire. The Flutter team has acknowledged that the method hasn't been wired up to enable this https://github.com/flutter/flutter/issues/16662 +Also if you have set notifications to be periodically shown, then on older iOS versions (< 10), if the application was uninstalled without cancelling all alarms then the next time it's installed you may see the "old" notifications being fired. If this is not the desired behaviour, then you can add the following to the `didFinishLaunchingWithOptions` method of your `AppDelegate` class. + +``` +if(![[NSUserDefaults standardUserDefaults]objectForKey:@"Notification"]){ + [[UIApplication sharedApplication] cancelAllLocalNotifications]; + [[NSUserDefaults standardUserDefaults]setBool:YES forKey:@"Notification"]; +} +``` + ## Testing As the plugin class is not static, it is possible to mock and verify it's behaviour when writing tests as part of your application. Check the source code for a sample test suite can be found at _test/flutter_local_notifications_test.dart_ that demonstrates how this can be done. diff --git a/android/src/main/java/com/dexterous/flutterlocalnotifications/FlutterLocalNotificationsPlugin.java b/android/src/main/java/com/dexterous/flutterlocalnotifications/FlutterLocalNotificationsPlugin.java index cab7a42f6..9035f419e 100644 --- a/android/src/main/java/com/dexterous/flutterlocalnotifications/FlutterLocalNotificationsPlugin.java +++ b/android/src/main/java/com/dexterous/flutterlocalnotifications/FlutterLocalNotificationsPlugin.java @@ -53,8 +53,12 @@ public class FlutterLocalNotificationsPlugin implements MethodCallHandler, Plugi private static final String CANCEL_METHOD = "cancel"; private static final String CANCEL_ALL_METHOD = "cancelAll"; private static final String SCHEDULE_METHOD = "schedule"; + private static final String PERIODICALLY_SHOW_METHOD = "periodicallyShow"; private static final String METHOD_CHANNEL = "dexterous.com/flutter/local_notifications"; private static final String PAYLOAD = "payload"; + public static String NOTIFICATION_ID = "notification_id"; + public static String NOTIFICATION = "notification"; + public static String REPEAT = "repeat"; private static MethodChannel channel; private static int defaultIconResourceId; private final Registrar registrar; @@ -67,8 +71,14 @@ private FlutterLocalNotificationsPlugin(Registrar registrar) { public static void rescheduleNotifications(Context context) { ArrayList scheduledNotifications = loadScheduledNotifications(context); - for (int i = 0; i < scheduledNotifications.size(); i++) { - scheduleNotification(context, scheduledNotifications.get(i), false); + for (Iterator it = scheduledNotifications.iterator(); it.hasNext();) { + NotificationDetails scheduledNotification = it.next(); + if(scheduledNotification.repeatInterval == null) { + scheduleNotification(context, scheduledNotification, false); + } + else { + repeatNotification(context, scheduledNotification, false); + } } } @@ -76,7 +86,6 @@ private static ArrayList loadScheduledNotifications(Context ArrayList scheduledNotifications = new ArrayList<>(); SharedPreferences sharedPreferences = context.getSharedPreferences(SCHEDULED_NOTIFICATIONS, Context.MODE_PRIVATE); String json = sharedPreferences.getString(SCHEDULED_NOTIFICATIONS, null); - System.out.println("json " + json); if (json != null) { Gson gson = getGsonBuilder(); Type type = new TypeToken>() { @@ -139,8 +148,8 @@ private static Spanned fromHtml(String html) { private static void scheduleNotification(Context context, NotificationDetails notificationDetails, Boolean updateScheduledNotificationsCache) { Notification notification = createNotification(context, notificationDetails); Intent notificationIntent = new Intent(context, ScheduledNotificationReceiver.class); - notificationIntent.putExtra(ScheduledNotificationReceiver.NOTIFICATION_ID, notificationDetails.id); - notificationIntent.putExtra(ScheduledNotificationReceiver.NOTIFICATION, notification); + notificationIntent.putExtra(NOTIFICATION_ID, notificationDetails.id); + notificationIntent.putExtra(NOTIFICATION, notification); PendingIntent pendingIntent = PendingIntent.getBroadcast(context, notificationDetails.id, notificationIntent, PendingIntent.FLAG_CANCEL_CURRENT); AlarmManager alarmManager = getAlarmManager(context); @@ -152,12 +161,58 @@ private static void scheduleNotification(Context context, NotificationDetails no } } + private static void repeatNotification(Context context, NotificationDetails notificationDetails, Boolean updateScheduledNotificationsCache) { + Notification notification = createNotification(context, notificationDetails); + Intent notificationIntent = new Intent(context, ScheduledNotificationReceiver.class); + notificationIntent.putExtra(NOTIFICATION_ID, notificationDetails.id); + notificationIntent.putExtra(NOTIFICATION, notification); + notificationIntent.putExtra(REPEAT, true); + PendingIntent pendingIntent = PendingIntent.getBroadcast(context, notificationDetails.id, notificationIntent, PendingIntent.FLAG_CANCEL_CURRENT); + + AlarmManager alarmManager = getAlarmManager(context); + long startTimeMilliseconds = notificationDetails.calledAt; + long repeatInterval = 0; + switch(notificationDetails.repeatInterval) { + + case EveryMinute: + repeatInterval = 60000; + break; + case Hourly: + repeatInterval = 60000 * 60; + break; + case Daily: + repeatInterval = 60000 * 60 * 24; + break; + case Weekly: + repeatInterval = 60000 * 60 * 24 * 7; + break; + } + + long currentTime = System.currentTimeMillis(); + while(startTimeMilliseconds < currentTime) { + startTimeMilliseconds += repeatInterval; + } + + + alarmManager.setInexactRepeating(AlarmManager.RTC_WAKEUP, startTimeMilliseconds, repeatInterval, pendingIntent); + if (updateScheduledNotificationsCache) { + ArrayList scheduledNotifications = loadScheduledNotifications(context); + scheduledNotifications.add(notificationDetails); + saveScheduledNotifications(context, scheduledNotifications); + } + } + private static Notification createNotification(Context context, NotificationDetails notificationDetails) { int resourceId; - if (notificationDetails.icon != null) { - resourceId = context.getResources().getIdentifier(notificationDetails.icon, "drawable", context.getPackageName()); + if(notificationDetails.iconResourceId == null) { + if (notificationDetails.icon != null) { + resourceId = context.getResources().getIdentifier(notificationDetails.icon, "drawable", context.getPackageName()); + } else { + resourceId = defaultIconResourceId; + } + notificationDetails.iconResourceId = resourceId; } else { - resourceId = defaultIconResourceId; + resourceId = notificationDetails.iconResourceId; } setupNotificationChannel(context, notificationDetails); Intent intent = new Intent(context, getMainActivityClass(context)); @@ -339,6 +394,13 @@ public void onMethodCall(MethodCall call, Result result) { result.success(null); break; } + case PERIODICALLY_SHOW_METHOD: { + Map arguments = call.arguments(); + NotificationDetails notificationDetails = NotificationDetails.from(arguments); + repeatNotification(registrar.context(), notificationDetails, true); + result.success(null); + break; + } case CANCEL_METHOD: Integer id = call.arguments(); cancelNotification(id); diff --git a/android/src/main/java/com/dexterous/flutterlocalnotifications/NotificationStyle.java b/android/src/main/java/com/dexterous/flutterlocalnotifications/NotificationStyle.java index 32e7c4af4..e80e6acd8 100644 --- a/android/src/main/java/com/dexterous/flutterlocalnotifications/NotificationStyle.java +++ b/android/src/main/java/com/dexterous/flutterlocalnotifications/NotificationStyle.java @@ -5,3 +5,4 @@ public enum NotificationStyle{ BigText, Inbox } + diff --git a/android/src/main/java/com/dexterous/flutterlocalnotifications/RepeatInterval.java b/android/src/main/java/com/dexterous/flutterlocalnotifications/RepeatInterval.java new file mode 100644 index 000000000..b9ff05eba --- /dev/null +++ b/android/src/main/java/com/dexterous/flutterlocalnotifications/RepeatInterval.java @@ -0,0 +1,8 @@ +package com.dexterous.flutterlocalnotifications; + +public enum RepeatInterval { + EveryMinute, + Hourly, + Daily, + Weekly +} diff --git a/android/src/main/java/com/dexterous/flutterlocalnotifications/ScheduledNotificationReceiver.java b/android/src/main/java/com/dexterous/flutterlocalnotifications/ScheduledNotificationReceiver.java index 9bc753379..d38105d3f 100644 --- a/android/src/main/java/com/dexterous/flutterlocalnotifications/ScheduledNotificationReceiver.java +++ b/android/src/main/java/com/dexterous/flutterlocalnotifications/ScheduledNotificationReceiver.java @@ -12,16 +12,18 @@ public class ScheduledNotificationReceiver extends BroadcastReceiver { - public static String NOTIFICATION_ID = "notification_id"; - public static String NOTIFICATION = "notification"; @Override public void onReceive(final Context context, Intent intent) { - NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context); - Notification notification = intent.getParcelableExtra(NOTIFICATION); - int notificationId = intent.getIntExtra(NOTIFICATION_ID, 0); - notificationManager.notify(notificationId, notification); - FlutterLocalNotificationsPlugin.removeNotificationFromCache(notificationId, context); + NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context); + Notification notification = intent.getParcelableExtra(FlutterLocalNotificationsPlugin.NOTIFICATION); + int notificationId = intent.getIntExtra(FlutterLocalNotificationsPlugin.NOTIFICATION_ID, 0); + notificationManager.notify(notificationId, notification); + boolean repeat = intent.getBooleanExtra(FlutterLocalNotificationsPlugin.REPEAT, false); + if (repeat) { + return; + } + FlutterLocalNotificationsPlugin.removeNotificationFromCache(notificationId, context); } } diff --git a/android/src/main/java/com/dexterous/flutterlocalnotifications/models/NotificationDetails.java b/android/src/main/java/com/dexterous/flutterlocalnotifications/models/NotificationDetails.java index eeb775690..19bfbdc38 100644 --- a/android/src/main/java/com/dexterous/flutterlocalnotifications/models/NotificationDetails.java +++ b/android/src/main/java/com/dexterous/flutterlocalnotifications/models/NotificationDetails.java @@ -3,6 +3,7 @@ import android.os.Build; import com.dexterous.flutterlocalnotifications.NotificationStyle; +import com.dexterous.flutterlocalnotifications.RepeatInterval; import com.dexterous.flutterlocalnotifications.models.styles.BigTextStyleInformation; import com.dexterous.flutterlocalnotifications.models.styles.DefaultStyleInformation; import com.dexterous.flutterlocalnotifications.models.styles.InboxStyleInformation; @@ -27,7 +28,9 @@ public class NotificationDetails { public long[] vibrationPattern; public NotificationStyle style; public StyleInformation styleInformation; + public RepeatInterval repeatInterval; public Long millisecondsSinceEpoch; + public Long calledAt; public String payload; public String groupKey; public Boolean setAsGroupSummary; @@ -35,6 +38,9 @@ public class NotificationDetails { public Boolean autoCancel; public Boolean ongoing; + // Note: this is set on the Android to save details about the icon that should be used when re-shydrating scheduled notifications when a device has been restarted + public Integer iconResourceId; + public static NotificationDetails from(Map arguments) { NotificationDetails notificationDetails = new NotificationDetails(); notificationDetails.payload = (String) arguments.get("payload"); @@ -44,6 +50,12 @@ public static NotificationDetails from(Map arguments) { if (arguments.containsKey("millisecondsSinceEpoch")) { notificationDetails.millisecondsSinceEpoch = (Long) arguments.get("millisecondsSinceEpoch"); } + if(arguments.containsKey("calledAt")) { + notificationDetails.calledAt = (Long) arguments.get("calledAt"); + } + if(arguments.containsKey("repeatInterval")) { + notificationDetails.repeatInterval = RepeatInterval.values()[(Integer)arguments.get("repeatInterval")]; + } @SuppressWarnings("unchecked") Map platformChannelSpecifics = (Map) arguments.get("platformSpecifics"); if (platformChannelSpecifics != null) { diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index 7f7e0c621..84bd1cb1e 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -31,7 +31,7 @@ defined in @style/LaunchTheme). --> s + android:value="true" /> diff --git a/example/ios/Podfile.lock b/example/ios/Podfile.lock index 01a962735..f5b647cd0 100644 --- a/example/ios/Podfile.lock +++ b/example/ios/Podfile.lock @@ -4,12 +4,12 @@ PODS: - Flutter DEPENDENCIES: - - Flutter (from `Pods/.symlinks/flutter/ios`) + - Flutter (from `Pods/.symlinks/flutter/ios-release`) - flutter_local_notifications (from `Pods/.symlinks/plugins/flutter_local_notifications/ios`) EXTERNAL SOURCES: Flutter: - :path: Pods/.symlinks/flutter/ios + :path: Pods/.symlinks/flutter/ios-release flutter_local_notifications: :path: Pods/.symlinks/plugins/flutter_local_notifications/ios diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index c44ad4216..9fb6ad695 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -260,7 +260,7 @@ ); inputPaths = ( "${SRCROOT}/Pods/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", - "${PODS_ROOT}/.symlinks/flutter/ios/Flutter.framework", + "${PODS_ROOT}/.symlinks/flutter/ios-release/Flutter.framework", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( diff --git a/example/ios/Runner/AppDelegate.m b/example/ios/Runner/AppDelegate.m index 8cf1468e5..e7f13b5c7 100644 --- a/example/ios/Runner/AppDelegate.m +++ b/example/ios/Runner/AppDelegate.m @@ -6,6 +6,11 @@ @implementation AppDelegate - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { [GeneratedPluginRegistrant registerWithRegistry:self]; + // cancel old notifications that were scheduled to be periodically shown upon a reinstallation of the app + if(![[NSUserDefaults standardUserDefaults]objectForKey:@"Notification"]){ + [[UIApplication sharedApplication] cancelAllLocalNotifications]; + [[NSUserDefaults standardUserDefaults]setBool:YES forKey:@"Notification"]; + } // Override point for customization after application launch. return [super application:application didFinishLaunchingWithOptions:launchOptions]; } diff --git a/example/lib/main.dart b/example/lib/main.dart index 3242b02f5..ee24c037d 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -79,6 +79,13 @@ class _MyAppState extends State { onPressed: () async { await _scheduleNotification(); })), + new Padding( + padding: new EdgeInsets.fromLTRB(0.0, 0.0, 0.0, 8.0), + child: new RaisedButton( + child: new Text('Repeat notification every minute'), + onPressed: () async { + await _repeatNotification(); + })), new Padding( padding: new EdgeInsets.fromLTRB(0.0, 0.0, 0.0, 8.0), child: new RaisedButton( @@ -309,6 +316,18 @@ class _MyAppState extends State { await flutterLocalNotificationsPlugin.show(0, 'ongoing notification title', 'ongoing notification body', platformChannelSpecifics); } + + Future _repeatNotification() async { + NotificationDetailsAndroid androidPlatformChannelSpecifics = + new NotificationDetailsAndroid('repeating channel id', + 'repeating channel name', 'repeating description'); + NotificationDetailsIOS iOSPlatformChannelSpecifics = + new NotificationDetailsIOS(); + NotificationDetails platformChannelSpecifics = new NotificationDetails( + androidPlatformChannelSpecifics, iOSPlatformChannelSpecifics); + await flutterLocalNotificationsPlugin.periodicallyShow(0, 'repeating title', + 'repeating body', RepeatInterval.EveryMinute, platformChannelSpecifics); + } } class SecondScreen extends StatefulWidget { diff --git a/ios/Classes/FlutterLocalNotificationsPlugin.m b/ios/Classes/FlutterLocalNotificationsPlugin.m index 0585b3ac2..54d8e9057 100644 --- a/ios/Classes/FlutterLocalNotificationsPlugin.m +++ b/ios/Classes/FlutterLocalNotificationsPlugin.m @@ -8,6 +8,7 @@ @implementation FlutterLocalNotificationsPlugin NSString *const INITIALIZE_METHOD = @"initialize"; NSString *const SHOW_METHOD = @"show"; NSString *const SCHEDULE_METHOD = @"schedule"; +NSString *const PERIODICALLY_SHOW_METHOD = @"periodicallyShow"; NSString *const CANCEL_METHOD = @"cancel"; NSString *const CANCEL_ALL_METHOD = @"cancelAll"; NSString *const CHANNEL = @"dexterous.com/flutter/local_notifications"; @@ -27,6 +28,7 @@ @implementation FlutterLocalNotificationsPlugin NSString *const PRESENT_SOUND = @"presentSound"; NSString *const PRESENT_BADGE = @"presentBadge"; NSString *const MILLISECONDS_SINCE_EPOCH = @"millisecondsSinceEpoch"; +NSString *const REPEAT_INTERVAL = @"repeatInterval"; NSString *const NOTIFICATION_ID = @"NotificationId"; NSString *const PAYLOAD = @"payload"; @@ -38,6 +40,13 @@ @implementation FlutterLocalNotificationsPlugin + (bool) resumingFromBackground { return appResumingFromBackground; } UILocalNotification *launchNotification; +typedef NS_ENUM(NSInteger, RepeatInterval) { + EveryMinute, + Hourly, + Daily, + Weekly +}; + + (void)registerWithRegistrar:(NSObject*)registrar { channel = [FlutterMethodChannel methodChannelWithName:CHANNEL @@ -51,7 +60,6 @@ + (void)registerWithRegistrar:(NSObject*)registrar { [registrar addMethodCallDelegate:instance channel:channel]; } - - (void)initialize:(FlutterMethodCall * _Nonnull)call result:(FlutterResult _Nonnull)result { appResumingFromBackground = false; NSDictionary *arguments = [call arguments]; @@ -140,13 +148,16 @@ - (void)showNotification:(FlutterMethodCall * _Nonnull)call result:(FlutterResul sound = platformSpecifics[SOUND]; } NSNumber *secondsSinceEpoch; + NSNumber *repeatInterval; if([SCHEDULE_METHOD isEqualToString:call.method]) { secondsSinceEpoch = @([call.arguments[MILLISECONDS_SINCE_EPOCH] integerValue] / 1000); + } else if([PERIODICALLY_SHOW_METHOD isEqualToString:call.method]) { + repeatInterval = @([call.arguments[REPEAT_INTERVAL] integerValue]); } if(@available(iOS 10.0, *)) { - [self showUserNotification:id title:title body:body secondsSinceEpoch:secondsSinceEpoch presentAlert:presentAlert presentSound:presentSound presentBadge:presentBadge sound:sound payload:payload]; + [self showUserNotification:id title:title body:body secondsSinceEpoch:secondsSinceEpoch repeatInterval:repeatInterval presentAlert:presentAlert presentSound:presentSound presentBadge:presentBadge sound:sound payload:payload]; } else { - [self showLocalNotification:id title:title body:body secondsSinceEpoch:secondsSinceEpoch presentAlert:presentAlert presentSound:presentSound presentBadge:presentBadge sound:sound payload:payload]; + [self showLocalNotification:id title:title body:body secondsSinceEpoch:secondsSinceEpoch repeatInterval:repeatInterval presentAlert:presentAlert presentSound:presentSound presentBadge:presentBadge sound:sound payload:payload]; } result(nil); } @@ -187,7 +198,7 @@ - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result { if([INITIALIZE_METHOD isEqualToString:call.method]) { [self initialize:call result:result]; - } else if ([SHOW_METHOD isEqualToString:call.method] || [SCHEDULE_METHOD isEqualToString:call.method]) { + } else if ([SHOW_METHOD isEqualToString:call.method] || [SCHEDULE_METHOD isEqualToString:call.method] || [PERIODICALLY_SHOW_METHOD isEqualToString:call.method]) { [self showNotification:call result:result]; } else if([CANCEL_METHOD isEqualToString:call.method]) { [self cancelNotification:call result:result]; @@ -204,7 +215,7 @@ - (NSDictionary*)buildUserDict:(NSNumber *)id title:(NSString *)title presentAle return userDict; } -- (void) showUserNotification:(NSNumber *)id title:(NSString *)title body:(NSString *)body secondsSinceEpoch:(NSNumber *)secondsSinceEpoch presentAlert:(bool)presentAlert presentSound:(bool)presentSound presentBadge:(bool)presentBadge sound:(NSString*)sound payload:(NSString *)payload NS_AVAILABLE_IOS(10.0) { +- (void) showUserNotification:(NSNumber *)id title:(NSString *)title body:(NSString *)body secondsSinceEpoch:(NSNumber *)secondsSinceEpoch repeatInterval:(NSNumber *)repeatInterval presentAlert:(bool)presentAlert presentSound:(bool)presentSound presentBadge:(bool)presentBadge sound:(NSString*)sound payload:(NSString *)payload NS_AVAILABLE_IOS(10.0) { UNMutableNotificationContent* content = [[UNMutableNotificationContent alloc] init]; UNNotificationTrigger *trigger; content.title = title; @@ -218,8 +229,27 @@ - (void) showUserNotification:(NSNumber *)id title:(NSString *)title body:(NSStr } content.userInfo = [self buildUserDict:id title:title presentAlert:presentAlert presentSound:presentSound presentBadge:presentBadge payload:payload]; if(secondsSinceEpoch == nil) { - trigger = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:0.1 - repeats:NO]; + NSTimeInterval timeInterval = 0.1; + Boolean repeats = NO; + if(repeatInterval != nil) { + switch([repeatInterval integerValue]) { + case EveryMinute: + timeInterval = 60; + break; + case Hourly: + timeInterval = 60 * 60; + break; + case Daily: + timeInterval = 60 * 60 * 24; + break; + case Weekly: + timeInterval = 60 * 60 * 24 * 7; + break; + } + repeats = YES; + } + trigger = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:timeInterval + repeats:repeats]; } else { NSDate *date = [NSDate dateWithTimeIntervalSince1970:[secondsSinceEpoch integerValue]]; NSCalendar *currentCalendar = [NSCalendar currentCalendar]; @@ -242,12 +272,13 @@ - (void) showUserNotification:(NSNumber *)id title:(NSString *)title body:(NSStr } -- (void) showLocalNotification:(NSNumber *)id title:(NSString *)title body:(NSString *)body secondsSinceEpoch:(NSNumber *)secondsSinceEpoch presentAlert:(bool)presentAlert presentSound:(bool)presentSound presentBadge:(bool)presentBadge sound:(NSString*)sound payload:(NSString *)payload { +- (void) showLocalNotification:(NSNumber *)id title:(NSString *)title body:(NSString *)body secondsSinceEpoch:(NSNumber *)secondsSinceEpoch repeatInterval:(NSNumber *)repeatInterval presentAlert:(bool)presentAlert presentSound:(bool)presentSound presentBadge:(bool)presentBadge sound:(NSString*)sound payload:(NSString *)payload { UILocalNotification *notification = [[UILocalNotification alloc] init]; notification.alertBody = body; if(@available(iOS 8.2, *)) { notification.alertTitle = title; } + if(presentSound) { if(!sound || [sound isKindOfClass:[NSNull class]]){ notification.soundName = UILocalNotificationDefaultSoundName; @@ -255,8 +286,34 @@ - (void) showLocalNotification:(NSNumber *)id title:(NSString *)title body:(NSSt notification.soundName = sound; } } + notification.userInfo = [self buildUserDict:id title:title presentAlert:presentAlert presentSound:presentSound presentBadge:presentBadge payload:payload]; if(secondsSinceEpoch == nil) { + if(repeatInterval != nil) { + NSTimeInterval timeInterval = 0; + + switch([repeatInterval 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]; + return; + } [[UIApplication sharedApplication] presentLocalNotificationNow:notification]; } else { notification.fireDate = [NSDate dateWithTimeIntervalSince1970:[secondsSinceEpoch integerValue]]; diff --git a/lib/flutter_local_notifications.dart b/lib/flutter_local_notifications.dart index 4530ba3df..bff51d561 100644 --- a/lib/flutter_local_notifications.dart +++ b/lib/flutter_local_notifications.dart @@ -7,6 +7,9 @@ import 'package:platform/platform.dart'; typedef Future MessageHandler(String message); +/// The available intervals for periodically showing notifications +enum RepeatInterval { EveryMinute, Daily, Hourly, Weekly } + class FlutterLocalNotificationsPlugin { factory FlutterLocalNotificationsPlugin() => _instance; @@ -30,12 +33,8 @@ class FlutterLocalNotificationsPlugin { Future initialize(InitializationSettings initializationSettings, {MessageHandler selectNotification}) async { onSelectNotification = selectNotification; - Map serializedPlatformSpecifics; - if (_platform.isAndroid) { - serializedPlatformSpecifics = initializationSettings.android.toMap(); - } else if (_platform.isIOS) { - serializedPlatformSpecifics = initializationSettings.ios.toMap(); - } + Map serializedPlatformSpecifics = + _retrievePlatformSpecificInitializationSettings(initializationSettings); _channel.setMethodCallHandler(_handleMethod); var result = await _channel.invokeMethod('initialize', serializedPlatformSpecifics); @@ -46,12 +45,8 @@ class FlutterLocalNotificationsPlugin { Future show(int id, String title, String body, NotificationDetails notificationDetails, {String payload}) async { - Map serializedPlatformSpecifics; - if (_platform.isAndroid) { - serializedPlatformSpecifics = notificationDetails?.android?.toMap(); - } else if (_platform.isIOS) { - serializedPlatformSpecifics = notificationDetails?.iOS?.toMap(); - } + Map serializedPlatformSpecifics = + _retrievePlatformSpecificNotificationDetails(notificationDetails); await _channel.invokeMethod('show', { 'id': id, 'title': title, @@ -75,12 +70,8 @@ class FlutterLocalNotificationsPlugin { Future schedule(int id, String title, String body, DateTime scheduledDate, NotificationDetails notificationDetails, {String payload}) async { - Map serializedPlatformSpecifics; - if (_platform.isAndroid) { - serializedPlatformSpecifics = notificationDetails?.android?.toMap(); - } else if (_platform.isIOS) { - serializedPlatformSpecifics = notificationDetails?.iOS?.toMap(); - } + Map serializedPlatformSpecifics = + _retrievePlatformSpecificNotificationDetails(notificationDetails); await _channel.invokeMethod('schedule', { 'id': id, 'title': title, @@ -91,6 +82,46 @@ class FlutterLocalNotificationsPlugin { }); } + /// Periodically show a notification using the specified interval. + /// For example, specifying a hourly interval means the first time the notification will be an hour after the method has been called and then every hour after that. + Future periodicallyShow(int id, String title, String body, + RepeatInterval repeatInterval, NotificationDetails notificationDetails, + {String payload}) async { + Map serializedPlatformSpecifics = + _retrievePlatformSpecificNotificationDetails(notificationDetails); + await _channel.invokeMethod('periodicallyShow', { + 'id': id, + 'title': title, + 'body': body, + 'calledAt': new DateTime.now().millisecondsSinceEpoch, + 'repeatInterval': repeatInterval.index, + 'platformSpecifics': serializedPlatformSpecifics, + 'payload': payload ?? '' + }); + } + + Map _retrievePlatformSpecificNotificationDetails( + NotificationDetails notificationDetails) { + Map serializedPlatformSpecifics; + if (_platform.isAndroid) { + serializedPlatformSpecifics = notificationDetails?.android?.toMap(); + } else if (_platform.isIOS) { + serializedPlatformSpecifics = notificationDetails?.iOS?.toMap(); + } + return serializedPlatformSpecifics; + } + + Map _retrievePlatformSpecificInitializationSettings( + InitializationSettings initializationSettings) { + Map serializedPlatformSpecifics; + if (_platform.isAndroid) { + serializedPlatformSpecifics = initializationSettings?.android?.toMap(); + } else if (_platform.isIOS) { + serializedPlatformSpecifics = initializationSettings?.ios?.toMap(); + } + return serializedPlatformSpecifics; + } + Future _handleMethod(MethodCall call) { return onSelectNotification(call.arguments); } diff --git a/pubspec.yaml b/pubspec.yaml index de5a2ac6c..ef1e7cfd6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: flutter_local_notifications description: A cross platform plugin for displaying and scheduling local notifications for Flutter applications with the ability to customise for each platform. -version: 0.2.0 +version: 0.2.1 author: Michael Bui homepage: https://github.com/MaikuB/flutter_local_notifications