Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reworked callbacks have separate callbacks to deal with foreground and background interactions #1548

Merged
merged 14 commits into from
Apr 3, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions flutter_local_notifications/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
# [10.0.0-dev.12]

* **Breaking change** callbacks have now been worked. There are now the following callbacks
* `onDidReceiveForegroundNotificationResponse`: invoked only when the app is running. This works for when a user has selected a notification or notification action. This replaces the `onSelectNotification` callback that existed before. For notification actions, the action needs to be configured to indicate the the app or user interface should be shown on invoking the action for this callback to be invoked i.e. by specifying the `DarwinNotificationActionOption.foreground` option on iOS and the `showsUserInterface` property on Android. On macOS and Linux, as there's no support for background isolates it will always invoke this callback
* `onDidReceiveBackgroundNotificationResponse`: invoked on a background isolate for when a user has selected a notification action. This replaces the `onSelectNotificationAction` callback
* **Breaking change** the `NotificationAppLaunchDetails` has been updated to contain `NotificationResponse` class with the `payload` belonging to the `NotificationResponse` class. This is to allow knowing more details about what caused the app to launch e.g. if a notification action was used to do so

# [10.0.0-dev.11]

* Includes changes from 9.4.0
Expand Down
34 changes: 17 additions & 17 deletions flutter_local_notifications/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ Capabilities depend on the system notification server implementation, therefore,

Scheduled/pending notifications is currently not supported due to the lack of a scheduler API.

`onSelectNotification` and `onSelectNotificationAction` callbacks are launched in the main isolate of the running application and cannot be launched in the background if the application is not running. To respond to notification after the application is terminated, your application should be registered as DBus activatable (please see [DBusApplicationLaunching](https://wiki.gnome.org/HowDoI/DBusApplicationLaunching) for more information), and register action before activating the application. This is difficult to do in a plugin because plugins instantiate during application activation, so `getNotificationAppLaunchDetails` can't be implemented without changing the main user application.
The `onDidReceiveForegroundNotificationResponse` callback runs on the main isolate of the running application and cannot be launched in the background if the application is not running. To respond to notification after the application is terminated, your application should be registered as DBus activatable (please see [DBusApplicationLaunching](https://wiki.gnome.org/HowDoI/DBusApplicationLaunching) for more information), and register action before activating the application. This is difficult to do in a plugin because plugins instantiate during application activation, so `getNotificationAppLaunchDetails` can't be implemented without changing the main user application.

## 📷 Screenshots

Expand Down Expand Up @@ -183,7 +183,7 @@ If your application needs the ability to schedule full-screen intent notificatio

For reference, the example app's `AndroidManifest.xml` file can be found [here](https://github.com/MaikuB/flutter_local_notifications/blob/master/flutter_local_notifications/example/android/app/src/main/AndroidManifest.xml).

Note that when a full-screen intent notification actually occurs (as opposed to a heads-up notification that the system may decide should occur), the plugin will act as though the user has tapped on a notification so handle those the same way (e.g. `onSelectNotification` callback) to display the appropriate page for your application.
Note that when a full-screen intent notification actually occurs (as opposed to a heads-up notification that the system may decide should occur), the plugin will act as though the user has tapped on a notification so handle those the same way (e.g. `onDidReceiveForegroundNotificationResponse` callback) to display the appropriate page for your application.


#### Release build configuration
Expand Down Expand Up @@ -240,7 +240,7 @@ final InitializationSettings initializationSettings = InitializationSettings(
iOS: initializationSettingsDarwin,
linux: initializationSettingsLinux);
flutterLocalNotificationsPlugin.initialize(initializationSettings,
onSelectNotification: onSelectNotification);
onDidReceiveForegroundNotificationResponse: onDidReceiveForegroundNotificationResponse);

...

Expand Down Expand Up @@ -281,11 +281,10 @@ If you encounter any issues please refer to the API docs and the sample code in

### Notification Actions

Notifications can now contain actions. These actions may be selected by the user when a App is sleeping or terminated and will wake up your app. However, it may not wake up the user-visible part of your App; but only the part of it which runs in the background. An exception is the Linux implementation: please see details in [Linux limitations](#linux-limitations) chapter.
Notifications can now contain actions. On macOS and Linux (see [Linux limitations](#linux-limitations) chapter), these will only run on the main isolate by calling the `onDidReceiveForegroundNotificationResponse` callback. On iOS and Android, these will run on the main isolate by calling the `onDidReceiveForegroundNotificationResponse` callback if the configuration has specified that the app/user interface should be shown i.e. by specifying the `DarwinNotificationActionOption.foreground` option on iOS and the `showsUserInterface` property on Android. If they haven't, then these actions may be selected by the user when an app is sleeping or terminated and will wake up your app. However, it may not wake up the user-visible part of your App; but only the part of it which runs in the background. This is done by spawning a background isolate.

This plugin contains handlers for iOS & Android to handle these cases and will allow you to specify a Dart entry point (a function).

When the user selects a action, the plugin will start a **separate Flutter Engine** which only exists to execute this callback. Linux implementation runs this callback in the current main isolate.
This plugin contains handlers for iOS & Android to handle these background isolate cases and will allow you to specify a Dart entry point (a function).
When the user selects a action, the plugin will start a **separate Flutter Engine** which will then invoke the `onDidReceiveBackgroundNotificationResponse` callback

**Configuration**:

Expand Down Expand Up @@ -370,7 +369,7 @@ On iOS/macOS, the notification category will define which actions are availble.
You need to configure a **top level** or **static** method which will handle the action:

``` dart
void notificationTapBackground(NotificationActionDetails notificationActionDetails) {
void notificationTapBackground(NotificationResponse notificationResponse) {
// handle action
}
```
Expand All @@ -380,10 +379,10 @@ Specify this function as a parameter in the `initialize` method of this plugin:
``` dart
await flutterLocalNotificationsPlugin.initialize(
initializationSettings,
onSelectNotification: (String payload) async {
onDidReceiveForegroundNotificationResponse: (NotificationResponse notificationResponse) async {
// ...
},
onSelectNotificationAction: notificationTapBackground,
onDidReceiveBackgroundNotificationResponse: notificationTapBackground,
);
```

Expand Down Expand Up @@ -452,14 +451,15 @@ final InitializationSettings initializationSettings = InitializationSettings(
macOS: initializationSettingsDarwin,
linux: initializationSettingsLinux);
await flutterLocalNotificationsPlugin.initialize(initializationSettings,
onSelectNotification: selectNotification);
onDidReceiveForegroundNotificationResponse: onDidReceiveForegroundNotificationResponse);
```

Initialisation can be done in the `main` function of your application or can be done within the first page shown in your app. Developers can refer to the example app that has code for the initialising within the `main` function. The code above has been simplified for explaining the concepts. Here we have specified the default icon to use for notifications on Android (refer to the *Android setup* section) and designated the function (`selectNotification`) that should fire when a notification has been tapped on via the `onSelectNotification` callback. Specifying this callback is entirely optional but here it will trigger navigation to another page and display the payload associated with the notification.
Initialisation can be done in the `main` function of your application or can be done within the first page shown in your app. Developers can refer to the example app that has code for the initialising within the `main` function. The code above has been simplified for explaining the concepts. Here we have specified the default icon to use for notifications on Android (refer to the *Android setup* section) and designated the function (`onDidReceiveForegroundNotificationResponse`) that should fire when a notification has been tapped on via the `onDidReceiveForegroundNotificationResponse` callback. Specifying this callback is entirely optional but here it will trigger navigation to another page and display the payload associated with the notification.

```dart
void selectNotification(String payload) async {
if (payload != null) {
void onDidReceiveForegroundNotificationResponse(NotificationResponse notificationResponse) async {
final String? payload = notificationResponse.payload;
if (notificationResponse.payload != null) {
debugPrint('notification payload: $payload');
}
await Navigator.push(
Expand All @@ -473,14 +473,14 @@ In the real world, this payload could represent the id of the item you want to d

The `DarwinInitializationSettings` class provides default settings on how the notification be presented when it is triggered and the application is in the foreground on iOS/macOS. There are optional named parameters that can be modified to suit your application's purposes. Here, it is omitted and the default values for these named properties is set such that all presentation options (alert, sound, badge) are enabled.

The `LinuxInitializationSettings` class requires a name for the default action that calls the `onSelectNotification` callback when the notification is clicked.
The `LinuxInitializationSettings` class requires a name for the default action that calls the `onDidReceiveForegroundNotificationResponse` callback when the notification is clicked.

On iOS and macOS, initialisation may show a prompt to requires users to give the application permission to display notifications (note: permissions don't need to be requested on Android). Depending on when this happens, this may not be the ideal user experience for your application. If so, please refer to the next section on how to work around this.

For an explanation of the `onDidReceiveLocalNotification` callback associated with the `DarwinInitializationSettings` class, please read [this](https://github.com/MaikuB/flutter_local_notifications/tree/master/flutter_local_notifications#handling-notifications-whilst-the-app-is-in-the-foreground).


*Note*: from version 4.0 of the plugin, calling `initialize` will not trigger the `onSelectNotification` callback when the application was started by tapping on a notification to trigger. Use the `getNotificationAppLaunchDetails` method that is available in the plugin if you need to handle a notification triggering the launch for an app e.g. change the home route of the app for deep-linking.
*Note*: from version 4.0 of the plugin, calling `initialize` will not trigger the `onDidReceiveForegroundNotificationResponse` callback when the application was started by tapping on a notification to trigger. Use the `getNotificationAppLaunchDetails` method that is available in the plugin if you need to handle a notification triggering the launch for an app e.g. change the home route of the app for deep-linking.

### [iOS (all supported versions) and macOS 10.14+] Requesting notification permissions

Expand Down Expand Up @@ -512,7 +512,7 @@ The constructor for the `DarwinInitializationSettings` class has three named pa
macOS: initializationSettingsDarwin,
linux: initializationSettingsLinux);
await flutterLocalNotificationsPlugin.initialize(initializationSettings,
onSelectNotification: onSelectNotification);
onDidReceiveForegroundNotificationResponse: onDidReceiveForegroundNotificationResponse);
```

Then call the `requestPermissions` method with desired permissions at the appropriate point in your application
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,16 @@
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;

import androidx.annotation.Keep;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.core.app.NotificationManagerCompat;
import androidx.core.app.RemoteInput;

import com.dexterous.flutterlocalnotifications.isolate.IsolatePreferences;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

Expand All @@ -31,11 +28,7 @@
public class ActionBroadcastReceiver extends BroadcastReceiver {
public static final String ACTION_TAPPED =
"com.dexterous.flutterlocalnotifications.ActionBroadcastReceiver.ACTION_TAPPED";
public static final String ACTION_ID = "actionId";
public static final String NOTIFICATION_ID = "notificationId";
public static final String INPUT_RESULT = "FlutterLocalNotificationsPluginInputResult";
private static final String TAG = "ActionBroadcastReceiver";
private static final String INPUT = "input";
@Nullable private static ActionEventSink actionEventSink;
@Nullable private static FlutterEngine engine;
IsolatePreferences preferences;
Expand All @@ -56,26 +49,12 @@ public void onReceive(Context context, Intent intent) {

preferences = preferences == null ? new IsolatePreferences(context) : preferences;

final Map<String, Object> action = new HashMap<>();
final int notificationId = intent.getIntExtra(NOTIFICATION_ID, -1);
action.put(NOTIFICATION_ID, notificationId);
action.put(
ACTION_ID, intent.hasExtra(ACTION_ID) ? intent.getStringExtra(ACTION_ID) : "unknown");
action.put(
FlutterLocalNotificationsPlugin.PAYLOAD,
intent.hasExtra(FlutterLocalNotificationsPlugin.PAYLOAD)
? intent.getStringExtra(FlutterLocalNotificationsPlugin.PAYLOAD)
: "");

Bundle remoteInput = RemoteInput.getResultsFromIntent(intent);
if (remoteInput != null) {
action.put(INPUT, remoteInput.getString(INPUT_RESULT));
} else {
action.put(INPUT, "");
}
final Map<String, Object> action =
FlutterLocalNotificationsPlugin.extractNotificationResponseMap(intent);

if (intent.getBooleanExtra(FlutterLocalNotificationsPlugin.CANCEL_NOTIFICATION, false)) {
NotificationManagerCompat.from(context).cancel(notificationId);
NotificationManagerCompat.from(context)
.cancel((int) action.get(FlutterLocalNotificationsPlugin.NOTIFICATION_ID));
}

if (actionEventSink == null) {
Expand Down
Loading