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

Support for custom notification actions #17

Closed
2 tasks done
MaikuB opened this issue Apr 21, 2018 · 163 comments
Closed
2 tasks done

Support for custom notification actions #17

MaikuB opened this issue Apr 21, 2018 · 163 comments
Labels
enhancement New feature or request help wanted Extra attention is needed

Comments

@MaikuB
Copy link
Owner

MaikuB commented Apr 21, 2018

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:

@MaikuB MaikuB added the enhancement New feature or request label Apr 21, 2018
@MaikuB MaikuB changed the title Support for custom notification actions [Blocked] Support for custom notification actions Apr 22, 2018
@mit-mit
Copy link

mit-mit commented Apr 23, 2018

Hi @MaikuB do you have an example use case where of using a custom notification action on iOS?

@MaikuB
Copy link
Owner Author

MaikuB commented Apr 23, 2018

@mit-mit

  • a social media app may notify users when someone has replied their post, in which case actions may be presented to allow the user to either reply or like the reply
  • similar to the clock app on Android, an app may present a reminder to a user and they can choose to have it rescheduled (think snooze)

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

@mitchhymel
Copy link

@MaikuB
I worked a bit on my own notification plugin and while it's not fully featured as yours, I did get custom actions working with headless Dart in Android (at least from my testing of using my app daily for the past month, it seems to be working).

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.

@MaikuB
Copy link
Owner Author

MaikuB commented Apr 24, 2018

@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

@mitchhymel
Copy link

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.

@MaikuB
Copy link
Owner Author

MaikuB commented Apr 24, 2018

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 :)

@MaikuB
Copy link
Owner Author

MaikuB commented Apr 30, 2018

@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 :)

@MaikuB
Copy link
Owner Author

MaikuB commented May 2, 2018

@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

@mit-mit
Copy link

mit-mit commented May 3, 2018

@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.

@MaikuB
Copy link
Owner Author

MaikuB commented May 3, 2018

Awesome, i understand so thanks for that. Looking forward to see what's on next week :)

@MaikuB
Copy link
Owner Author

MaikuB commented May 20, 2018

@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

@pinkfish
Copy link

Doesn't the firebase messaging plugin handle this? I am pretty sure it does processing without pulling the app to the foreground...

@MaikuB
Copy link
Owner Author

MaikuB commented May 20, 2018

Nope, take a look at the README file for it on how it handles when the app is terminated and you'll see this

Notification is delivered to system tray. When the user clicks on it to open app onLaunch fires if click_action: FLUTTER_NOTIFICATION_CLICK is set (see below).

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

@pinkfish
Copy link

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.

@pinkfish
Copy link

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.

@MaikuB
Copy link
Owner Author

MaikuB commented May 22, 2018

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

@pinkfish
Copy link

pinkfish commented May 22, 2018 via email

@honga
Copy link

honga commented May 22, 2018

Great example @MaikuB -a social media app may notify users when someone has replied their post, in which case actions may be presented to allow the user to either reply or like the reply
Looking forward to be able to do something like a direct reply/voice recording as well

@MaikuB
Copy link
Owner Author

MaikuB commented May 22, 2018

@pinkfish too right mate :)

@s-bauer
Copy link

s-bauer commented Jul 24, 2018

Any Update on this? There are quite a lot of use-cases that require custom actions!

@MaikuB
Copy link
Owner Author

MaikuB commented Jul 24, 2018

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

@figelwump
Copy link

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!

@MaikuB
Copy link
Owner Author

MaikuB commented Aug 22, 2018

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

@MaikuB
Copy link
Owner Author

MaikuB commented May 19, 2022

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...

@MaikuB
Copy link
Owner Author

MaikuB commented Jun 26, 2022

@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

@AdamBridges
Copy link

@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.

@rich-j
Copy link

rich-j commented Jun 27, 2022

@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:

*** Assertion failure in -[FlutterEngineManager startEngineIfNeeded:registerPlugins:], FlutterEngineManager.m:73
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'failed to set registerPlugins'
*** First throw call stack:
(0x1db3a9288 0x1f40a3744 0x1dcc36360 0x10077c674 0x1db00ee6c 0x1db010a30 0x1db01ef48 0x1db01eb98 0x1db361800 0x1db31b704 0x1db32ebc8 0x1f7462374 0x1ddc9e648 0x1dda1fd90 0x10052cf78 0x100925ce4)
libc++abi: terminating with uncaught exception of type NSException
* thread #1, queue = 'com.apple.main-thread', stop reason = signal SIGABRT
    frame #0: 0x0000000212de5b38 libsystem_kernel.dylib`__pthread_kill + 8
libsystem_kernel.dylib`__pthread_kill:
->  0x212de5b38 <+8>:  b.lo   0x212de5b58               ; <+40>
    0x212de5b3c <+12>: pacibsp 
    0x212de5b40 <+16>: stp    x29, x30, [sp, #-0x10]!
    0x212de5b44 <+20>: mov    x29, sp
Target 0: (Runner) stopped.
Lost connection to device.

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 flutter_local_notifications: ^10.0.0-dev.16 and added the following code to the app:

main.dart
import '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:

I/flutter (21426): Received DidReceiveBackgroundNotificationResponse 1, theAction, NotificationResponseType.selectedNotificationAction, Payload stuff

We can also click on the notification itself (i.e. not the action) and receive:

I/flutter (21426): Received DidReceiveNotificationResponse 1, null, NotificationResponseType.selectedNotification, Payload stuff

On iOS we can click on the notification and receive (so the notification itself is working):

flutter: Received DidReceiveNotificationResponse 1, null, NotificationResponseType.selectedNotification, Payload stuff

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
Exception Type:  EXC_CRASH (SIGABRT)
Exception Codes: 0x0000000000000000, 0x0000000000000000
Exception Note:  EXC_CORPSE_NOTIFY
Triggered by Thread:  0

Last Exception Backtrace:
0   CoreFoundation                	       0x10e2c75f4 __exceptionPreprocess + 226
1   libobjc.A.dylib               	       0x10e177a45 objc_exception_throw + 48
2   Foundation                    	       0x10ece3874 _userInfoForFileAndLine + 0
3   flutter_local_notifications   	       0x10e083d02 __60-[FlutterEngineManager startEngineIfNeeded:registerPlugins:]_block_invoke + 290 (FlutterEngineManager.m:73)
4   libdispatch.dylib             	       0x10f7228e4 _dispatch_call_block_and_release + 12
5   libdispatch.dylib             	       0x10f723b25 _dispatch_client_callout + 8
6   libdispatch.dylib             	       0x10f731043 _dispatch_main_queue_drain + 1050
7   libdispatch.dylib             	       0x10f730c1b _dispatch_main_queue_callback_4CF + 31
8   CoreFoundation                	       0x10e233ed5 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__ + 9
9   CoreFoundation                	       0x10e22e6ca __CFRunLoopRun + 2761
10  CoreFoundation                	       0x10e22d704 CFRunLoopRunSpecific + 562
11  GraphicsServices              	       0x114810c8e GSEventRunModal + 139
12  UIKitCore                     	       0x12469265a -[UIApplication _run] + 928
13  UIKitCore                     	       0x1246972b5 UIApplicationMain + 101
14  Runner                        	       0x10ddb222f main + 63 (AppDelegate.swift:5)
15  dyld_sim                      	       0x10dfd2f21 start_sim + 10
16  dyld                          	       0x11ac0051e start + 462

Thread 24:: FlutterLocalNotificationsIsolate.2.ui
0   Flutter                       	       0x1120c312f dart::Utf8::CodeUnitCount(unsigned char const*, long, dart::Utf8::Type*) + 63
1   Flutter                       	       0x1121bc1d3 dart::String::FromUTF8(unsigned char const*, long, dart::Heap::Space) + 35
2   Flutter                       	       0x1123c7f31 dart::kernel::KernelReaderHelper::GetSourceFor(long) + 289
3   Flutter                       	       0x11214d90f dart::kernel::KernelLoader::LoadScriptAt(long, dart::DirectChainedHashMap<dart::kernel::UriToSourceTableTrait>*) + 239
4   Flutter                       	       0x11214c8ce dart::kernel::KernelLoader::InitializeFields(dart::DirectChainedHashMap<dart::kernel::UriToSourceTableTrait>*) + 1758
5   Flutter                       	       0x11214c9ae dart::kernel::KernelLoader::LoadEntireProgram(dart::kernel::Program*, bool) + 94
6   Flutter                       	       0x11243a8df Dart_LoadLibraryFromKernel + 351
7   Flutter                       	       0x1120ac9b2 flutter::DartIsolate::LoadKernel(std::__1::shared_ptr<fml::Mapping const>, bool) + 442
8   Flutter                       	       0x1120acae1 flutter::DartIsolate::PrepareForRunningFromKernel(std::__1::shared_ptr<fml::Mapping const>, bool, bool) + 235
9   Flutter                       	       0x1120b49c4 flutter::KernelIsolateConfiguration::DoPrepareIsolate(flutter::DartIsolate&) + 122
10  Flutter                       	       0x1120aa25a flutter::DartIsolate::CreateRunningRootIsolate(flutter::Settings const&, fml::RefPtr<flutter::DartSnapshot const>, std::__1::unique_ptr<flutter::PlatformConfiguration, std::__1::default_delete<flutter::PlatformConfiguration> >, flutter::DartIsolate::Flags, std::__1::function<void ()>, std::__1::function<void ()> const&, std::__1::function<void ()> const&, std::__1::optional<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > >, std::__1::optional<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > >, std::__1::vector<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> >, std::__1::allocator<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > > > const&, std::__1::unique_ptr<flutter::IsolateConfiguration, std::__1::default_delete<flutter::IsolateConfiguration> >, flutter::UIDartState::Context const&, flutter::DartIsolate const*) + 752
11  Flutter                       	       0x1120b68aa flutter::RuntimeController::LaunchRootIsolate(flutter::Settings const&, std::__1::function<void ()>, std::__1::optional<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > >, std::__1::optional<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > >, std::__1::vector<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> >, std::__1::allocator<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> > > > const&, std::__1::unique_ptr<flutter::IsolateConfiguration, std::__1::default_delete<flutter::IsolateConfiguration> >) + 580
12  Flutter                       	       0x111f31c84 flutter::Engine::Run(flutter::RunConfiguration) + 400
13  Flutter                       	       0x111f4996f std::__1::__function::__func<fml::internal::CopyableLambda<flutter::Shell::RunEngine(flutter::RunConfiguration, std::__1::function<void (flutter::Engine::RunStatus)> const&)::$_7>, std::__1::allocator<fml::internal::CopyableLambda<flutter::Shell::RunEngine(flutter::RunConfiguration, std::__1::function<void (flutter::Engine::RunStatus)> const&)::$_7> >, void ()>::operator()() + 119
14  Flutter                       	       0x111e46a1c fml::MessageLoopImpl::FlushTasks(fml::FlushType) + 164
15  Flutter                       	       0x111e4cb98 fml::MessageLoopDarwin::OnTimerFire(__CFRunLoopTimer*, fml::MessageLoopDarwin*) + 26
16  CoreFoundation                	       0x10e234d6e __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__ + 20
17  CoreFoundation                	       0x10e23483c __CFRunLoopDoTimer + 915
18  CoreFoundation                	       0x10e233dfd __CFRunLoopDoTimers + 265
19  CoreFoundation                	       0x10e22e3e1 __CFRunLoopRun + 2016
20  CoreFoundation                	       0x10e22d704 CFRunLoopRunSpecific + 562
21  Flutter                       	       0x111e4ccd5 fml::MessageLoopDarwin::Run() + 65
22  Flutter                       	       0x111e4692c fml::MessageLoopImpl::DoRun() + 22
23  Flutter                       	       0x111e4baff void* std::__1::__thread_proxy<std::__1::tuple<std::__1::unique_ptr<std::__1::__thread_struct, std::__1::default_delete<std::__1::__thread_struct> >, fml::Thread::Thread(std::__1::function<void (fml::Thread::ThreadConfig const&)> const&, fml::Thread::ThreadConfig const&)::$_0> >(void*) + 187
24  libsystem_pthread.dylib       	    0x7fff701cb4e1 _pthread_start + 125
25  libsystem_pthread.dylib       	    0x7fff701c6f6b thread_start + 15

Please let us know if we're missing any configuration or other processing.

@Kavantix
Copy link
Contributor

@rich-j take a look at the readme on the 10.0.0 branch:

For Swift, open the `AppDelegate.swift` and update the `didFinishLaunchingWithOptions` as follows

@rich-j
Copy link

rich-j commented Jun 27, 2022

@Kavantix thank you for the pointer to the correct README.md. I have added the suggested code to my AppDelegate.swift and now receive a compile time error of

/IdeaProjects/notification_test/ios/Runner/AppDelegate.swift:11:5: error: cannot find 'FlutterLocalNotificationsPlugin' in scope
        FlutterLocalNotificationsPlugin.setPluginRegistrantCallback { (registry) in

Here's my updated AppDelegate.swift

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?

@Kavantix
Copy link
Contributor

@rich-j you can import flutter_local_notifications

@rich-j
Copy link

rich-j commented Jun 27, 2022

@Kavantix thanks, that works

@mk-dev-1
Copy link

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 flutter_local_notifications from 10.0.0-dev.11 to 10.0.0-dev.14, and I believe the error was essentially introduced with the changes in 10.0.0-dev.12.

Is this happening to anyone else? Any ideas?

@AdamBridges
Copy link

AdamBridges commented Jul 19, 2022

if (launchDetails.notificationResponse?.payload != null) {
        handleLocalNotification(launchDetails.notificationResponse!.payload!);
      }

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:

NotificationResponse? response = launchDetails.notificationResponse; 
if (response != null && response.payload != null) {
handleLocalNotification(response.payload);
}

@mk-dev-1

@MaikuB
Copy link
Owner Author

MaikuB commented Jul 23, 2022

@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 notificationResponseType from the Android side. There is something I can try out to see if it fixes the issue but be great if you're able to reproduce it so we can see if the change fixes it

@mk-dev-1
Copy link

@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....

@MaikuB
Copy link
Owner Author

MaikuB commented Jul 26, 2022

@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 launchIntent read in different from what gets read to actually get the launch details. If so, then this is something that should've been noticed sooner but I suppose your crash reports make it more noticeable. Let me know how it goes

@mk-dev-1
Copy link

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.

@AdamBridges
Copy link

AdamBridges commented Jul 31, 2022

@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...

@mk-dev-1 Yeah, I wasn't certain. My logic was that because your logic insists that the NotificationResponse will never be null, it's somehow being insisted in the Android getNotificationAppLaunchDetails() method and tries to return a NotificationResponse, but since the notificationResponseType parameter cannot return a Null value (whereas id, payload, actionId, and input can), it decides to crash.

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 getNotificationAppLaunchDetails(). The provided PR seems to fix the issue. @MaikuB

@mk-dev-1
Copy link

mk-dev-1 commented Aug 9, 2022

I can confirm I no longer see any crashes with the latest changes. Thank you once again for your great work!

@MaikuB
Copy link
Owner Author

MaikuB commented Aug 14, 2022

@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...

@noinskit
Copy link
Contributor

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:
Is it possible to reinitialize notification categories on iOS? They're now part of DarwinInitializationSettings passed to initialize. My problem is that in some languages user's gender tends to impact the notification action title.
Or is it safe to call initialize multiple times and expect the last configuration to be used?

@mk-dev-1
Copy link

@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...

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 ;-)

@MaikuB
Copy link
Owner Author

MaikuB commented Aug 22, 2022

@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 initalize() several times. Happy to take a PR for it though I'm thinking to make this a stable release soon given what @mk-dev-1 has said. Curious though if you managed to get around to testing iOS @AdamBridges?

@noinskit
Copy link
Contributor

@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.

@AdamBridges
Copy link

@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 initalize() several times. Happy to take a PR for it though I'm thinking to make this a stable release soon given what @mk-dev-1 has said. Curious though if you managed to get around to testing iOS @AdamBridges?

Soon. Probably sometime in September so I'll follow up then.

@MaikuB
Copy link
Owner Author

MaikuB commented Aug 22, 2022

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...

@MaikuB
Copy link
Owner Author

MaikuB commented Sep 12, 2022

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

@mk-dev-1
Copy link

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...

@MaikuB
Copy link
Owner Author

MaikuB commented Oct 6, 2022

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 :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request help wanted Extra attention is needed
Projects
None yet
Development

No branches or pull requests