-
Notifications
You must be signed in to change notification settings - Fork 1.4k
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
Support for custom notification actions #17
Comments
Hi @MaikuB do you have an example use case where of using a custom notification action on iOS? |
I believe the support for this would be covered in flutter/flutter#3671 and help enable more fully featured apps. That thread covers other use cases that would require headless-Dart execution. It'd also be good if the team could provide more documentation on how headless execution works on Android. I tried to look at how to do this to implement custom actions on Android and to handle another request (#21) by mimicking the changes I saw in https://github.com/flutter/plugins/tree/master/packages/android_alarm_manager but wasn't successful. Haven't checked in that code can do so to another branch if your team has capacity to provide input |
@MaikuB You need to create an Android service, that will host your shared method channel and support handling the intent in the background. Make sure to set up the shared channel when the plugin is registered. Then when you are creating your notification, if it is meant to run in the background, the intent must be created with your Service class. Then in your Service's onHandleIntent, you want to get the shared channel and invoke your callback to dart. And then here's where I handle the call from the Android service in Dart. Hopefully that helps. Great plugin by the way. Your plugin has more features and you seem to be more active in development than I am, so I think I'll add a reference to your plugin in my readme, in case people want the features your plugin offers. |
@mitchhymel thanks for the tips and appreciation :) However, does that cover when the application has been terminated as well? That's the main scenario i'm looking at it that I have concerns about? If you look at what's in the android_alarm_manager plugin, you can see what they try to do is resolve an instance of the Flutter application etc to find the callback to invoke. That's what I believe to be crucial in handling that scenario and haven't seen in your code. I already handle intents via the plugin as it implements the NewIntentListener |
I'm not sure if it handles when the application has been terminated. I haven't tested it yet with my code. From your description, and looking at the AlarmService code, I'm thinking my code would not handle the case where the app is terminated. So maybe my code is not a helpful example. |
Fair enough. i've managed to at least handle it so that when app is terminated when comes to tapping the notification and then triggering a callback after launch. Anyway, appreciate you trying to provide help :) |
@mit-mit i've figured out what i was missing on the Android side of things now but be good to see if the team is able to find a solution for iOS :) |
@mit-mit i mentioned this on the main Flutter repo but don't know if anyone from your team has seen it given there hasn't been a response. I've taken a look at how Dart code is executed from Android and from what I can gather, it doesn't seem like the APIs that have been exposed allow for passing back arguments from the Andoid side back to the Dart function that will be invoked. This prevents being able to implement a lot of use cases e.g. knowing which alarm to snooze once the callback has been received on the Dart side |
@MaikuB let me ask around a bit, but may take a while as the whole team is really busy getting ready for I/O next week. |
Awesome, i understand so thanks for that. Looking forward to see what's on next week :) |
@mit-mit since I/O is done now, just wanted to see if were you able to find more information about this. Also. I know Hixie said iOS headless execution is being looked into but whilst doing that, it'd be good if the team looks at if it's possible to do that and pass payload back from the iOS side as well |
Doesn't the firebase messaging plugin handle this? I am pretty sure it does processing without pulling the app to the foreground... |
Nope, take a look at the README file for it on how it handles when the app is terminated and you'll see this
How I handle when the user taps on the notification at the moment is pretty much identical except it's consolidated to a single event handler |
I followed the idea above from @mitchhymel and it seems to work. It is opening up the app and calling the method on it to say the button was clicked on. How do you verify this scenario? I am swiping the app away in the activity list. I suspect this will make the startup tricky though since the ordering will likely be an issue when the activity starts. Since Mitch is using getActivitry on the pending intent it is opening up the app. Got it working correctly on ios too with categories. |
It won't do background processing in this case though by the looks :) Which is likely what you are worried about? It will only work if it forces the app into the foreground and does something. Which is a bit disappointing. |
What you're doing is actually not that much different to how I handle when the app is terminated and the user taps on the notification, i.e. launch the app and trigger the callback that allows apps to handle the action that occurred. Yes, I'm concerned with running code in the background even when the app is terminated and as @mitchhymel mentioned, I don't believe his code handled that either. This would help enable scenarios like being able to snooze an alarm and skipping music tracks when the app has been terminated. The mechanisms to achieve this are different (one of which is to have the intent trigger a service) and appear to also handled the case when the app is currently running. However, as I mentioned earlier, the APIs that are available don't allow passing parameters back. I'd much prefer to only add shipping this feature when it's possible to handle all the scenarios (i.e. when is terminated or running). Otherwise, I'd end up with code that needs to be changed later or devs seeing issues raised even when a limitation has been documentation but they didn't bother reading about it, which i'm also guilty of :) Anyway, hope you understand where I'm coming from |
I can see that. I just did a slightly different change that lets you open
a url on an action instead. This lets you do things like driving
directions from the buttons too. All of this sorts out what I need to do
with buttons... Although the fact that firebase messages get dropped on
the floor in the background is a bit of a bastard :)
…On 21 May 2018 at 17:58, Michael Bui ***@***.***> wrote:
What you're doing is actually not that much different to how I handle when
the app is terminated and the user taps on the notification, i.e. launch
the app and trigger the callback that allows apps to handle the action that
occurred.
Yes, I'm concerned with running code in the background even when the app
is terminated. This would help enable scenarios like being able to snooze
an alarm and skipping music tracks when the app has been terminated. The
mechanisms to achieve this are different (one of which is to have the
intent trigger a service) and appear to also handled the case when the app
is currently running. However, as I mentioned earlier, the APIs that are
available don't allow passing parameters back. I'd much prefer to only add
shipping this feature when it's possible to handle all the scenarios (i.e.
when is terminated or running). Otherwise, I'd end up with code that needs
to be changed later or devs seeing issues raised even when a limitation has
been documentation but they didn't bother reading about it, which i'm also
guilty of :)
Anyway, hope you understand where I'm coming from
—
You are receiving this because you commented.
Reply to this email directly, view it on GitHub
<#17 (comment)>,
or mute the thread
<https://github.com/notifications/unsubscribe-auth/ACbmMQYZU2dHCAV1s2t3D-CC52XH4Ldlks5t02KagaJpZM4TeOyk>
.
|
Great example @MaikuB - |
@pinkfish too right mate :) |
Any Update on this? There are quite a lot of use-cases that require custom actions! |
I'm well aware of that. You should follow the issues linked in the original post. There are open PRs related to this that need to go in first to enable this to work better, particularly when the app isn't running. If it's that urgent and you don't care about scenarios like that (via headless Dart execution) then I'd suggest you fork to roll a custom build. Edit: I can see you've already commented on one of the PRs |
Hi, with this issue now resolved: flutter/flutter#3671, does that unblock the work for this issue? If so, what are you currently thinking for ETA? This functionality is critical for a project I'm currently working on, I appreciate your work on this! |
It does unblock from looking at it but it's not rolled over to beta channel and that's where a lot of devs will likely to be on. So if it were done it would be a blocker for release until the changes rolled into beta. Last i checked dev was on 0.5.8 (I believe the changes have rolled into there) and there are reports of hot reloading or restarting being broken (think I ran into an issue too) and devs rolling back to the previous dev build (0.5.7) so there are concerns around stability there. From what I read the reason why beta hasn't been updated for a long time is so the Flutter team can do more rigorous testing as well. I'm in the midst of the working on an app (along with some other work) so hard to give an ETA at the moment though I'll likely get to test the headless execution in the next few days e.g. see if #21 would work on the Dev channel. If you or someone from the community would be able to assist then that would be great. @pinkfish submitted a PR a while back but that was before headless execution work was done and would be a good starting point |
Thanks for the feedback @adambridge and keep me posted. I've not seen more feedback from others though so not sure if that's something to be concerned about... |
@AdamBridges have you had a chance to test to on iOS? From the scenarios covered in the example, I believe this should work for your scenarios too |
Not yet. Hopefully before the end of the August I'll have a chance and will update then. |
@MaikuB we're using this package in our projects and want to start using notification actions. Actions work on Android but crash on iOS. On a real iPhone Xs running iOS 15.5 we immediately hard crash (i.e. app is terminated) when clicking on the notification action. The Flutter console shows:
We do see the notification and the action choice. We can successfully click on the notification itself and receive the appropriate callback. It's possible that we are missing something in our initialization. To do further testing, we created a new Flutter demo app using main.dartimport 'package:flutter/material.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
_initNotification();
runApp(const MyApp());
}
Future<void> _initNotification() async {
final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
final AndroidInitializationSettings initializationSettingsAndroid =
AndroidInitializationSettings('@mipmap/ic_launcher');
final DarwinInitializationSettings initializationSettingsIOS = DarwinInitializationSettings(
onDidReceiveLocalNotification: didReceiveLocalNotification, // Only needed for iOS <10.0 (our minimum)
notificationCategories: [
DarwinNotificationCategory("iosActions",
actions: [DarwinNotificationAction.plain("theAction", "Do the action")])
]);
final InitializationSettings initializationSettings =
InitializationSettings(android: initializationSettingsAndroid, iOS: initializationSettingsIOS);
await flutterLocalNotificationsPlugin.initialize(
initializationSettings,
onDidReceiveNotificationResponse: didReceiveNotificationResponse,
onDidReceiveBackgroundNotificationResponse: didReceiveBackgroundNotificationResponse,
);
const AndroidNotificationDetails androidPlatformChannelSpecifics = AndroidNotificationDetails(
'test_channel_id',
'Test Channel Name',
channelDescription: 'Notification test channel',
importance: Importance.max,
priority: Priority.high,
actions: [AndroidNotificationAction("theAction", "Do the action")],
);
const DarwinNotificationDetails iosPlatformChannelSpecifics = DarwinNotificationDetails(
subtitle: "subTitleText",
threadIdentifier: "threadId",
categoryIdentifier: "iosActions",
);
const NotificationDetails platformChannelSpecifics =
NotificationDetails(android: androidPlatformChannelSpecifics, iOS: iosPlatformChannelSpecifics);
await flutterLocalNotificationsPlugin
.show(1, "Action Test", "Notification with action", platformChannelSpecifics, payload: "Payload stuff")
.catchError((error) => print("Notification failed $error"));
}
//----------
void didReceiveNotificationResponse(NotificationResponse details) =>
print("Received DidReceiveNotificationResponse ${details.id}, ${details.actionId}, "
"${details.notificationResponseType}, ${details.payload}");
void didReceiveBackgroundNotificationResponse(NotificationResponse details) =>
print("Received DidReceiveBackgroundNotificationResponse ${details.id}, ${details.actionId}, "
"${details.notificationResponseType}, ${details.payload}");
// This callback is only for iOS <10 so we shouldn't need it
void didReceiveLocalNotification(int id, String? title, String? body, String? payload) =>
print("Received DidReceiveLocalNotification $id, $title");
//----------
class MyApp extends StatelessWidget { ... the rest of the Flutter Demo ... AppDelegate.swift addition if #available(iOS 10.0, *) {
UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate
} When we run the above code on Android we're given a notification with action. We can click on the action and receive this in the console:
We can also click on the notification itself (i.e. not the action) and receive:
On iOS we can click on the notification and receive (so the notification itself is working):
When we click on the iOS notification action we crash as shown above. We can also run on the iOS simulator which gives us a detailed crash report (over 500 lines) - here's the relevant stack traces: Crash stack trace synopsis
Please let us know if we're missing any configuration or other processing. |
@rich-j take a look at the readme on the 10.0.0 branch:
|
@Kavantix thank you for the pointer to the correct
Here's my updated import UIKit
import Flutter
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
// This is required to make any communication available in the action isolate.
FlutterLocalNotificationsPlugin.setPluginRegistrantCallback { (registry) in
GeneratedPluginRegistrant.register(with: registry)
}
GeneratedPluginRegistrant.register(with: self)
// Following line is support for FlutterLocalNotifications
if #available(iOS 10.0, *) {
UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate
}
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
} Is there an import needed? |
@rich-j you can import |
@Kavantix thanks, that works |
Hi guys, lately I am receiving the following crash notification via Crashlytics: Non-fatal Exception: io.flutter.plugins.firebase.crashlytics.FlutterError: type 'Null' is not a subtype of type 'int'. Error thrown null.
at MethodChannelFlutterLocalNotificationsPlugin.getNotificationAppLaunchDetails(platform_flutter_local_notifications.dart:65)
at FlutterLocalNotificationsPlugin.getNotificationAppLaunchDetails(flutter_local_notifications_plugin.dart:199)
at NotificationManager.init(notification_manager.dart:111) Here's whats happening in NotificationsManager.init(): notificationManager = FlutterLocalNotificationsPlugin();
await notificationManager!.initialize(
InitializationSettings(
android: const AndroidInitializationSettings(
'@mipmap/ic_launcher_foreground',
),
iOS: DarwinInitializationSettings(
requestSoundPermission: false,
requestBadgePermission: false,
requestAlertPermission: false,
notificationCategories: [
DarwinNotificationCategory(
await darwinNotificationCategoryId(NotificationType.rem),
actions: <DarwinNotificationAction>[
DarwinNotificationAction.plain(
NotificationAction.snooze.toString(),
localizations.notificationActionSnooze(snoozeDuration)),
],
options: <DarwinNotificationCategoryOption>{
DarwinNotificationCategoryOption.allowAnnouncement,
},
),
DarwinNotificationCategory(
await darwinNotificationCategoryId(NotificationType.med),
actions: <DarwinNotificationAction>[
DarwinNotificationAction.plain(
NotificationAction.take.toString(),
localizations.notificationActionTake),
DarwinNotificationAction.plain(
NotificationAction.snooze.toString(),
localizations.notificationActionSnooze(snoozeDuration)),
],
options: <DarwinNotificationCategoryOption>{
DarwinNotificationCategoryOption.allowAnnouncement,
},
),
],
),
),
onDidReceiveNotificationResponse: (response) async {
if (response.payload != null) {
handleLocalNotification(response.payload!);
}
},
onDidReceiveBackgroundNotificationResponse:
onDidReceiveBackgroundNotificationResponse,
);
NotificationAppLaunchDetails? launchDetails =
await notificationManager!.getNotificationAppLaunchDetails(); // <--- This is where the error occurs
if (launchDetails != null && launchDetails.didNotificationLaunchApp) {
if (launchDetails.notificationResponse?.payload != null) {
handleLocalNotification(launchDetails.notificationResponse!.payload!);
}
} The method behind that on Android is the following: @override
Future<NotificationAppLaunchDetails?>
getNotificationAppLaunchDetails() async {
final Map<dynamic, dynamic>? result =
await _channel.invokeMethod('getNotificationAppLaunchDetails');
final Map<dynamic, dynamic>? notificationResponse =
result != null && result.containsKey('notificationResponse')
? result['notificationResponse']
: null;
return result != null
? NotificationAppLaunchDetails(
result['notificationLaunchedApp'],
notificationResponse: notificationResponse == null
? null
: NotificationResponse(
id: notificationResponse['notificationId'],
actionId: notificationResponse['actionId'],
input: notificationResponse['input'],
notificationResponseType: NotificationResponseType.values[ // <--- Error seems to be on this line
notificationResponse['notificationResponseType']],
payload: notificationResponse.containsKey('payload')
? notificationResponse['payload']
: null,
),
)
: null;
} I yet have to find a way to replicate that error myself, however this crash started occurring when I bumped Is this happening to anyone else? Any ideas? |
I think the problem may have to do with that line and your use of the Bang (!) operator. Try assigning launchDetails.notificationResponse to a variable and using that to check for null first such as this:
|
@mk-dev-1 is this something you've been able to reproduce yourself? Looks odd that it's happened as the plugin should be returning a |
@AdamBridges Thank you for looking at it, although I can't see that this should resolve the issue as the error log clearly points to the line before... @MaikuB Unfortunately I still haven't found a way to reproduce the issue, but it keeps happening in production. From the native Android code in the plugin I also can't really see why that error occurs in the first place. What are you thinking? Maybe I can just incorporate whatever you have in mind in a test version that I distribute to a couple of test users that are affected.... |
@mk-dev-1 whilst this should be harmless change, I've done it on a separate branch for now https://github.com/MaikuB/flutter_local_notifications/tree/10.0.0_launchIntent. You should be able to see the commit with the differences. I suspect at some point the |
Thank you so much. I will give it a go with your changes and report back. It will take a couple of days for sure though. |
@mk-dev-1 Yeah, I wasn't certain. My logic was that because your logic insists that the UPDATE: I was just testing and was able to reproduce the issue with a Pixel 2 Emulator running API 31: Terminate app; launch app manually; send app to background; and then use a notification action that calls |
I can confirm I no longer see any crashes with the latest changes. Thank you once again for your great work! |
@mk-dev-1 this is now incorporated in the 10.0.0-dev.19 prerelease. Not sure how you referenced the fix in your app but the branch I had mentioned will be deleted soon. As you're using the pre-release and from what I recall, have been using the plugin for quite a while, are you able to share your experience in using the pre-release? I assume you've used it to make use of notification actions. Trying to figure out when to promote it to a stable release though the issue you found would've been one that's existed for quite a while now... |
Hi, I'm finally giving this a try, works great so far! Until now I was using my own fork that handled notification actions in my app. I already have one question: |
After testing it and using it in the beta channel of my app, I have been using the 10.0 prereleases in production for quite a while without any major issues or negative feedback. I don't see any reason to not promote it to a stable release ;-) |
@noinskit you'd need to try to confirm but the underlying native Apple API call is one that sets the categories so would think it should work. This might call for a platform-specific API exposed via the plugin so you don't have to call |
@MaikuB a new platform-specific API for (re)initializing iOS categories sounds reasonable to me. I agree that it should not block 1.0.0. |
Soon. Probably sometime in September so I'll follow up then. |
Ok thanks. I'll probably move it to stable before then. Was looking to see if others could give feedback before then but hopefully this has given enough time for feedback... |
Has anyone run into issue #1694? I was going to do a stable release but then saw this reported. It looks like a pub cache issue than a plugin issue though |
I believe you are right. Can't say I have seen this issue... |
Forgot to close this out since 10.0 was moved to stable and there's been more updates since then so will do so now. Thanks all for the contribution :) |
The plugin should provide the ability to specify custom notification actions. However, this will depends on the Flutter engine being able to support headless-Dart code as per flutter/flutter#6192 and flutter/flutter#3671
It appears Android support is there but will wait to see on if the engine can support it for iOS applications, and for Flutter to provide abstractions to access the functionality. Without the support being added in, then this won't work for scenarios like when the application has been terminated as the logic associated with the action would've been defined in Dart.
Update with remaining work (IMO)
Note that last two perhaps could be omitted given market share and those require using deprecated APIs
Edit:
The text was updated successfully, but these errors were encountered: