Skip to content

Commit

Permalink
Merge pull request #27 from MaikuB/dev
Browse files Browse the repository at this point in the history
Add ability to periodically show a notification
  • Loading branch information
MaikuB committed Apr 28, 2018
2 parents e305f0b + a1743d1 commit 8e10eba
Show file tree
Hide file tree
Showing 15 changed files with 260 additions and 47 deletions.
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
* [Android] Fix bug in cancelling all notifications

## [0.2.1]
* [Android & iOS] Add ability to set a notification to be periodically displayed
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <flutter_local_notifications/FlutterLocalNotificationsPlugin.h>
...
- (void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification
{
if(@available(iOS 10.0, *)) {
Expand Down Expand Up @@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -67,16 +71,21 @@ private FlutterLocalNotificationsPlugin(Registrar registrar) {

public static void rescheduleNotifications(Context context) {
ArrayList<NotificationDetails> scheduledNotifications = loadScheduledNotifications(context);
for (int i = 0; i < scheduledNotifications.size(); i++) {
scheduleNotification(context, scheduledNotifications.get(i), false);
for (Iterator<NotificationDetails> it = scheduledNotifications.iterator(); it.hasNext();) {
NotificationDetails scheduledNotification = it.next();
if(scheduledNotification.repeatInterval == null) {
scheduleNotification(context, scheduledNotification, false);
}
else {
repeatNotification(context, scheduledNotification, false);
}
}
}

private static ArrayList<NotificationDetails> loadScheduledNotifications(Context context) {
ArrayList<NotificationDetails> scheduledNotifications = new ArrayList<>();
SharedPreferences sharedPreferences = context.getSharedPreferences(SCHEDULED_NOTIFICATIONS, Context.MODE_PRIVATE);
String json = sharedPreferences.getString(SCHEDULED_NOTIFICATIONS, null);
System.out.println("json " + json);
if (json != null) {
Gson gson = getGsonBuilder();
Type type = new TypeToken<ArrayList<NotificationDetails>>() {
Expand Down Expand Up @@ -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);
Expand All @@ -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<NotificationDetails> 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));
Expand Down Expand Up @@ -339,6 +394,13 @@ public void onMethodCall(MethodCall call, Result result) {
result.success(null);
break;
}
case PERIODICALLY_SHOW_METHOD: {
Map<String, Object> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ public enum NotificationStyle{
BigText,
Inbox
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.dexterous.flutterlocalnotifications;

public enum RepeatInterval {
EveryMinute,
Hourly,
Daily,
Weekly
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -27,14 +28,19 @@ 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;
public Integer groupAlertBehavior;
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<String, Object> arguments) {
NotificationDetails notificationDetails = new NotificationDetails();
notificationDetails.payload = (String) arguments.get("payload");
Expand All @@ -44,6 +50,12 @@ public static NotificationDetails from(Map<String, Object> 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<String, Object> platformChannelSpecifics = (Map<String, Object>) arguments.get("platformSpecifics");
if (platformChannelSpecifics != null) {
Expand Down
2 changes: 1 addition & 1 deletion example/android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
defined in @style/LaunchTheme). -->
<meta-data
android:name="io.flutter.app.android.SplashScreenUntilFirstFrame"
android:value="true" />s
android:value="true" />
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
Expand Down
4 changes: 2 additions & 2 deletions example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion example/ios/Runner.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 = (
Expand Down
5 changes: 5 additions & 0 deletions example/ios/Runner/AppDelegate.m
Original file line number Diff line number Diff line change
Expand Up @@ -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];
}
Expand Down
19 changes: 19 additions & 0 deletions example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,13 @@ class _MyAppState extends State<MyApp> {
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(
Expand Down Expand Up @@ -309,6 +316,18 @@ class _MyAppState extends State<MyApp> {
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 {
Expand Down

0 comments on commit 8e10eba

Please sign in to comment.