Skip to content

Commit cf8d389

Browse files
committed
[flutter_local_notifications] let apps choose the isolate for the dismissal callback
Adds a `dismissIsolate` preference (`NotificationDismissedIsolate`) to the platform-specific details, replacing the boolean flag, so apps can choose whether a dismissal fires on the main or a background isolate. Where a platform can't honour the choice it degrades: macOS has no background isolate so it always reports on the main isolate, and the main isolate only fires while the app is running.
1 parent a11db4f commit cf8d389

14 files changed

Lines changed: 181 additions & 66 deletions

File tree

flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/ActionBroadcastReceiver.java

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,17 @@
2323
import io.flutter.plugin.common.EventChannel;
2424
import io.flutter.plugin.common.EventChannel.EventSink;
2525
import io.flutter.plugin.common.EventChannel.StreamHandler;
26+
import io.flutter.plugin.common.MethodChannel;
2627
import io.flutter.view.FlutterCallbackInformation;
2728

2829
public class ActionBroadcastReceiver extends BroadcastReceiver {
2930
public static final String ACTION_TAPPED =
3031
"com.dexterous.flutterlocalnotifications.ActionBroadcastReceiver.ACTION_TAPPED";
3132
public static final String ACTION_DISMISSED =
3233
"com.dexterous.flutterlocalnotifications.ActionBroadcastReceiver.ACTION_DISMISSED";
34+
public static final String DISMISS_ISOLATE = "dismissIsolate";
35+
private static final int DISMISS_ISOLATE_MAIN = 0;
36+
private static final int DISMISS_ISOLATE_BACKGROUND = 1;
3337
private static final String TAG = "ActionBroadcastReceiver";
3438
@Nullable private static ActionEventSink actionEventSink;
3539
@Nullable private static FlutterEngine engine;
@@ -50,11 +54,22 @@ public void onReceive(Context context, Intent intent) {
5054
return;
5155
}
5256

53-
preferences = preferences == null ? new IsolatePreferences(context) : preferences;
54-
5557
final Map<String, Object> action =
5658
FlutterLocalNotificationsPlugin.extractNotificationResponseMap(intent);
5759

60+
// A main-isolate dismissal is only delivered while the app is running.
61+
if (ACTION_DISMISSED.equalsIgnoreCase(intent.getAction())
62+
&& intent.getIntExtra(DISMISS_ISOLATE, DISMISS_ISOLATE_BACKGROUND)
63+
== DISMISS_ISOLATE_MAIN) {
64+
MethodChannel liveChannel = FlutterLocalNotificationsPlugin.liveChannel;
65+
if (liveChannel != null) {
66+
liveChannel.invokeMethod("didReceiveNotificationResponse", action);
67+
}
68+
return;
69+
}
70+
71+
preferences = preferences == null ? new IsolatePreferences(context) : preferences;
72+
5873
if (intent.getBooleanExtra(FlutterLocalNotificationsPlugin.CANCEL_NOTIFICATION, false)) {
5974
int notificationId = (int) action.get(FlutterLocalNotificationsPlugin.NOTIFICATION_ID);
6075
Object tag = action.get(FlutterLocalNotificationsPlugin.NOTIFICATION_TAG);

flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/FlutterLocalNotificationsPlugin.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,7 @@ public class FlutterLocalNotificationsPlugin
211211
static String NOTIFICATION_DETAILS = "notificationDetails";
212212
static Gson gson;
213213
private MethodChannel channel;
214+
static MethodChannel liveChannel;
214215
private Context applicationContext;
215216
private Activity mainActivity;
216217
static final int NOTIFICATION_PERMISSION_REQUEST_CODE = 1;
@@ -298,13 +299,14 @@ protected static Notification createNotification(
298299
.setSilent(BooleanUtils.getValue(notificationDetails.silent))
299300
.setOnlyAlertOnce(BooleanUtils.getValue(notificationDetails.onlyAlertOnce));
300301

301-
if (BooleanUtils.getValue(notificationDetails.emitDismissEvent)) {
302+
if (notificationDetails.dismissIsolate != null) {
302303
Intent deleteIntent = new Intent(context, ActionBroadcastReceiver.class);
303304
deleteIntent.setAction(ActionBroadcastReceiver.ACTION_DISMISSED);
304305
deleteIntent
305306
.putExtra(NOTIFICATION_ID, notificationDetails.id)
306307
.putExtra(NOTIFICATION_TAG, notificationDetails.tag)
307-
.putExtra(PAYLOAD, notificationDetails.payload);
308+
.putExtra(PAYLOAD, notificationDetails.payload)
309+
.putExtra(ActionBroadcastReceiver.DISMISS_ISOLATE, notificationDetails.dismissIsolate);
308310
int deleteFlags = PendingIntent.FLAG_UPDATE_CURRENT;
309311
if (VERSION.SDK_INT >= VERSION_CODES.M) {
310312
deleteFlags |= PendingIntent.FLAG_IMMUTABLE;
@@ -1422,11 +1424,15 @@ public void onAttachedToEngine(FlutterPluginBinding binding) {
14221424
this.applicationContext = binding.getApplicationContext();
14231425
this.channel = new MethodChannel(binding.getBinaryMessenger(), METHOD_CHANNEL);
14241426
this.channel.setMethodCallHandler(this);
1427+
liveChannel = this.channel;
14251428
}
14261429

14271430
@Override
14281431
public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {
14291432
this.channel.setMethodCallHandler(null);
1433+
if (liveChannel == this.channel) {
1434+
liveChannel = null;
1435+
}
14301436
this.channel = null;
14311437
this.applicationContext = null;
14321438
}

flutter_local_notifications/android/src/main/java/com/dexterous/flutterlocalnotifications/models/NotificationDetails.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ public class NotificationDetails implements Serializable {
122122
private static final String MATCH_DATE_TIME_COMPONENTS = "matchDateTimeComponents";
123123

124124
private static final String FULL_SCREEN_INTENT = "fullScreenIntent";
125-
private static final String EMIT_DISMISS_EVENT = "emitDismissEvent";
125+
private static final String DISMISS_ISOLATE = "dismissIsolate";
126126
private static final String SHORTCUT_ID = "shortcutId";
127127
private static final String SUB_TEXT = "subText";
128128
private static final String ACTIONS = "actions";
@@ -192,7 +192,7 @@ public class NotificationDetails implements Serializable {
192192
public DateTimeComponents matchDateTimeComponents;
193193
public Long when;
194194
public Boolean fullScreenIntent;
195-
public Boolean emitDismissEvent;
195+
public Integer dismissIsolate;
196196
public String shortcutId;
197197
public String subText;
198198
public @Nullable List<NotificationAction> actions;
@@ -292,8 +292,8 @@ private static void readPlatformSpecifics(
292292
notificationDetails.category = (String) platformChannelSpecifics.get(CATEGORY);
293293
notificationDetails.fullScreenIntent =
294294
(Boolean) platformChannelSpecifics.get((FULL_SCREEN_INTENT));
295-
notificationDetails.emitDismissEvent =
296-
(Boolean) platformChannelSpecifics.get(EMIT_DISMISS_EVENT);
295+
notificationDetails.dismissIsolate =
296+
(Integer) platformChannelSpecifics.get(DISMISS_ISOLATE);
297297
notificationDetails.shortcutId = (String) platformChannelSpecifics.get(SHORTCUT_ID);
298298
notificationDetails.additionalFlags = (int[]) platformChannelSpecifics.get(ADDITIONAL_FLAGS);
299299
notificationDetails.subText = (String) platformChannelSpecifics.get(SUB_TEXT);

flutter_local_notifications/example/lib/main.dart

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,8 @@ void notificationTapBackground(NotificationResponse notificationResponse) {
7171
NotificationResponseType.notificationDismissed) {
7272
// ignore: avoid_print
7373
print(
74-
'notification(${notificationResponse.id}) dismissed with'
75-
' payload: ${notificationResponse.payload}',
74+
'notification(${notificationResponse.id}) dismissed on background isolate'
75+
' with payload: ${notificationResponse.payload}',
7676
);
7777
return;
7878
}
@@ -396,8 +396,8 @@ class _HomePageState extends State<HomePage> {
396396
NotificationResponseType.notificationDismissed) {
397397
// ignore: avoid_print
398398
print(
399-
'notification(${response?.id}) dismissed with'
400-
' payload: ${response?.payload}',
399+
'notification(${response?.id}) dismissed on main isolate'
400+
' with payload: ${response?.payload}',
401401
);
402402
return;
403403
}
@@ -478,9 +478,22 @@ class _HomePageState extends State<HomePage> {
478478
),
479479
PaddedElevatedButton(
480480
buttonText:
481-
'Show notification that reports when it is dismissed',
481+
'Show notification that reports its dismissal on '
482+
'the main isolate',
482483
onPressed: () async {
483-
await _showDismissibleNotification();
484+
await _showDismissibleNotification(
485+
NotificationDismissedIsolate.main,
486+
);
487+
},
488+
),
489+
PaddedElevatedButton(
490+
buttonText:
491+
'Show notification that reports its dismissal on '
492+
'a background isolate',
493+
onPressed: () async {
494+
await _showDismissibleNotification(
495+
NotificationDismissedIsolate.background,
496+
);
484497
},
485498
),
486499
PaddedElevatedButton(
@@ -1182,22 +1195,25 @@ class _HomePageState extends State<HomePage> {
11821195
);
11831196
}
11841197

1185-
Future<void> _showDismissibleNotification() async {
1186-
const AndroidNotificationDetails androidNotificationDetails =
1198+
Future<void> _showDismissibleNotification(
1199+
NotificationDismissedIsolate isolate,
1200+
) async {
1201+
final AndroidNotificationDetails androidNotificationDetails =
11871202
AndroidNotificationDetails(
11881203
'your channel id',
11891204
'your channel name',
11901205
channelDescription: 'your channel description',
11911206
importance: Importance.max,
11921207
priority: Priority.high,
11931208
ticker: 'ticker',
1194-
emitDismissEvent: true,
1209+
dismissIsolate: isolate,
11951210
);
1196-
const DarwinNotificationDetails darwinNotificationDetails =
1211+
final DarwinNotificationDetails darwinNotificationDetails =
11971212
DarwinNotificationDetails(
11981213
categoryIdentifier: dismissableNotificationCategory,
1214+
dismissIsolate: isolate,
11991215
);
1200-
const NotificationDetails notificationDetails = NotificationDetails(
1216+
final NotificationDetails notificationDetails = NotificationDetails(
12011217
android: androidNotificationDetails,
12021218
iOS: darwinNotificationDetails,
12031219
macOS: darwinNotificationDetails,

flutter_local_notifications/ios/flutter_local_notifications/Sources/flutter_local_notifications/FlutterLocalNotificationsPlugin.m

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ @implementation FlutterLocalNotificationsPlugin {
9999
NSString *const NOTIFICATION_LAUNCHED_APP = @"notificationLaunchedApp";
100100
NSString *const ACTION_ID = @"actionId";
101101
NSString *const NOTIFICATION_RESPONSE_TYPE = @"notificationResponseType";
102+
NSString *const DISMISS_ISOLATE = @"dismissIsolate";
102103

103104
NSString *const UNSUPPORTED_OS_VERSION_ERROR_CODE = @"unsupported_os_version";
104105
NSString *const GET_ACTIVE_NOTIFICATIONS_ERROR_MESSAGE =
@@ -798,14 +799,21 @@ - (void)cancelAllPendingNotifications:(FlutterResult _Nonnull)result
798799
if (presentSound && content.sound == nil) {
799800
content.sound = UNNotificationSound.defaultSound;
800801
}
801-
content.userInfo = [self buildUserDict:arguments[ID]
802-
title:content.title
803-
presentAlert:presentAlert
804-
presentSound:presentSound
805-
presentBadge:presentBadge
806-
presentBanner:presentBanner
807-
presentList:presentList
808-
payload:arguments[PAYLOAD]];
802+
NSMutableDictionary *userDict = [self buildUserDict:arguments[ID]
803+
title:content.title
804+
presentAlert:presentAlert
805+
presentSound:presentSound
806+
presentBadge:presentBadge
807+
presentBanner:presentBanner
808+
presentList:presentList
809+
payload:arguments[PAYLOAD]];
810+
if (arguments[PLATFORM_SPECIFICS] != [NSNull null]) {
811+
id dismissIsolate = arguments[PLATFORM_SPECIFICS][DISMISS_ISOLATE];
812+
if (dismissIsolate != nil && dismissIsolate != [NSNull null]) {
813+
userDict[DISMISS_ISOLATE] = dismissIsolate;
814+
}
815+
}
816+
content.userInfo = userDict;
809817
return content;
810818
}
811819

@@ -1070,6 +1078,28 @@ - (void)userNotificationCenter:(UNUserNotificationCenter *)center
10701078
_launchingAppFromNotification = true;
10711079
}
10721080
completionHandler();
1081+
} else if ([response.actionIdentifier
1082+
isEqualToString:UNNotificationDismissActionIdentifier]) {
1083+
id dismissIsolate =
1084+
response.notification.request.content.userInfo[DISMISS_ISOLATE];
1085+
if (dismissIsolate != nil && dismissIsolate != [NSNull null]) {
1086+
NSMutableDictionary *notificationResponseDict =
1087+
[self extractNotificationResponseDict:response];
1088+
if ([dismissIsolate integerValue] == 0) {
1089+
if (_initialized) {
1090+
[_channel invokeMethod:@"didReceiveNotificationResponse"
1091+
arguments:notificationResponseDict];
1092+
}
1093+
} else {
1094+
if (!actionEventSink) {
1095+
actionEventSink = [[ActionEventSink alloc] init];
1096+
}
1097+
[actionEventSink addItem:notificationResponseDict];
1098+
[_flutterEngineManager startEngineIfNeeded:actionEventSink
1099+
registerPlugins:registerPlugins];
1100+
}
1101+
}
1102+
completionHandler();
10731103
} else if (response.actionIdentifier != nil) {
10741104
NSMutableDictionary *notificationResponseDict =
10751105
[self extractNotificationResponseDict:response];

flutter_local_notifications/lib/src/platform_specifics/android/method_channel_mappers.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ extension AndroidNotificationDetailsMapper on AndroidNotificationDetails {
233233
'colorized': colorized,
234234
'number': number,
235235
'audioAttributesUsage': audioAttributesUsage.value,
236-
'emitDismissEvent': emitDismissEvent,
236+
'dismissIsolate': dismissIsolate?.index,
237237
}
238238
..addAll(_convertActionsToMap(actions))
239239
..addAll(_convertStyleInformationToMap())

flutter_local_notifications/lib/src/platform_specifics/android/notification_details.dart

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import 'dart:typed_data';
22
import 'dart:ui';
33

4+
import '../../types.dart';
45
import 'bitmap.dart';
56
import 'enums.dart';
67
import 'notification_sound.dart';
@@ -156,7 +157,7 @@ class AndroidNotificationDetails {
156157
this.colorized = false,
157158
this.number,
158159
this.audioAttributesUsage = AudioAttributesUsage.notification,
159-
this.emitDismissEvent = false,
160+
this.dismissIsolate,
160161
});
161162

162163
/// The icon that should be used when displaying the notification.
@@ -428,12 +429,12 @@ class AndroidNotificationDetails {
428429
/// https://developer.android.com/reference/android/media/AudioAttributes
429430
final AudioAttributesUsage audioAttributesUsage;
430431

431-
/// Whether to report when the notification is dismissed by the user.
432+
/// The isolate a dismissal is reported on, or `null` to not report it.
432433
///
433-
/// When set to `true`, swiping the notification away or clearing it triggers
434-
/// the `onDidReceiveBackgroundNotificationResponse` callback with a
435-
/// [NotificationResponse] of type
436-
/// [NotificationResponseType.notificationDismissed]. Dismissals caused by
437-
/// tapping the notification or by calling `cancel` are not reported.
438-
final bool emitDismissEvent;
434+
/// When set, swiping the notification away triggers a [NotificationResponse]
435+
/// of type [NotificationResponseType.notificationDismissed] on that isolate.
436+
/// [NotificationDismissedIsolate.background] fires even when the app has been
437+
/// terminated; [NotificationDismissedIsolate.main] fires only while it runs.
438+
/// Dismissing via a tap or `cancel` is never reported.
439+
final NotificationDismissedIsolate? dismissIsolate;
439440
}

flutter_local_notifications/lib/src/platform_specifics/darwin/mappers.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,5 +86,6 @@ extension DarwinNotificationDetailsMapper on DarwinNotificationDetails {
8686
'attachments': attachments?.map((a) => a.toMap()).toList(),
8787
'categoryIdentifier': categoryIdentifier,
8888
'criticalSoundVolume': criticalSoundVolume,
89+
'dismissIsolate': dismissIsolate?.index,
8990
};
9091
}

flutter_local_notifications/lib/src/platform_specifics/darwin/notification_details.dart

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import '../../types.dart';
12
import 'interruption_level.dart';
23
import 'notification_attachment.dart';
34

@@ -19,6 +20,7 @@ class DarwinNotificationDetails {
1920
this.categoryIdentifier,
2021
this.interruptionLevel,
2122
this.criticalSoundVolume,
23+
this.dismissIsolate,
2224
});
2325

2426
/// Indicates if an alert should be display when the notification is triggered
@@ -153,4 +155,13 @@ class DarwinNotificationDetails {
153155
/// On iOS, this property is only applicable to iOS 12.0 or newer.
154156
/// On macOS, this property is only applicable to macOS 10.14 or newer.
155157
final double? criticalSoundVolume;
158+
159+
/// The isolate a dismissal is reported on, or `null` to not report it.
160+
///
161+
/// Requires the notification's category to be configured with the
162+
/// `customDismissAction` option. When set, swiping the notification away
163+
/// triggers a [NotificationResponse] of type
164+
/// [NotificationResponseType.notificationDismissed] on that isolate. macOS
165+
/// has no background isolate so it always reports on the main isolate.
166+
final NotificationDismissedIsolate? dismissIsolate;
156167
}

flutter_local_notifications/lib/src/types.dart

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,12 @@ enum Day {
2727
/// The integer representation of [Day].
2828
final int value;
2929
}
30+
31+
/// The isolate a notification dismissal is reported on.
32+
enum NotificationDismissedIsolate {
33+
/// The application's main isolate.
34+
main,
35+
36+
/// A background isolate.
37+
background,
38+
}

0 commit comments

Comments
 (0)