Flutter Android combine alarmmanager with notifications

4,576

After a couple of hours of debugging the android_alarm_manager package, the reason for the callback no longer being triggered after closing the app by tapping back and then opening it again seems to be this static boolean on the native side:

// TODO(mattcarroll): make sIsIsolateRunning per-instance, not static.
private static AtomicBoolean sIsIsolateRunning = new AtomicBoolean(false);

This boolean keeps track of whether your Flutter callback has been registered with the alarm service. With the way Android works, when the app is closed by tapping back, the activity is destroyed, but the app memory is not cleared down, so static variables such as the boolean mentioned above keep their values if you open the app again. The value of this boolean is never set back to false, and because of that, even though the plugin registers itself with the application again, and you initialize it again, it doesn't run the part of the code that starts a Flutter isolate that would run your alarm callback:

sBackgroundFlutterView = new FlutterNativeView(context, true);
if (mAppBundlePath != null && !sIsIsolateRunning.get()) { // HERE sIsIsolateRunning will already be true when you open the app the 2nd time
  if (sPluginRegistrantCallback == null) {
    throw new PluginRegistrantException();
  }
  Log.i(TAG, "Starting AlarmService...");
  FlutterRunArguments args = new FlutterRunArguments();
  args.bundlePath = mAppBundlePath;
  args.entrypoint = flutterCallback.callbackName;
  args.libraryPath = flutterCallback.callbackLibraryPath;
  sBackgroundFlutterView.runFromBundle(args);
  sPluginRegistrantCallback.registerWith(sBackgroundFlutterView.getPluginRegistry());
}

Given that the boolean is private, I don't think you can do anything about it and you need to wait for it to be fixed - the TODO above the boolean's declaration indicates that the package's developers might already be aware of potential issues caused by it being static.

As for navigating to a specific page when the notification is tapped:

Create a GlobalKey for the navigator within _MyAppState:

final GlobalKey<NavigatorState> navigatorKey = GlobalKey();

Add it to your MaterialApp:

return MaterialApp(
  navigatorKey: navigatorKey,

And initialize the flutter_local_notifications plugin within initState() of _MyAppState. This way, the onSelectNotification function you pass to flutterLocalNotificationsPlugin.initialize can reference your navigatorKey:

flutterLocalNotificationsPlugin.initialize(initializationSettings, onSelectNotification: (String payload) {
  navigatorKey.currentState.pushNamed("your_route");
});
Share:
4,576
Robin Dijkhof
Author by

Robin Dijkhof

me

Updated on December 13, 2022

Comments

  • Robin Dijkhof
    Robin Dijkhof over 1 year

    I'm trying to use the Android alarmmanager with notifications, but I am encounter difficulties. Basically, this is the behaviour I am trying to achieve:

    1. Fetch the point in time when the alarmmanager has to fire from sharedpreferences and/or firebase. Use this to schedule the alarmmanager.
    2. When the alarmmanager fires, fetch some data from sharedpreferences and/or firebase and use it to create a notification. Also perform step 1 to schedule the next alarm.
    3. When the notification is pressed, a specific page has to open.

    I created some basic example which is available here: https://github.com/robindijkhof/flutter_noti I will include a snippet below for when the repo is deleted.

    Problems I am encountering:

    • I can't open a specific page when the notification is clicked.
    • When the app is closed using the back button, the alarmmanager keep firing which is as expected. However, when the notification is clicked, the app opens and the alarmmanager does not fire anymore. Probably because it runs on another Isolate?

    I have no clue how to solve this. Also, I have no idea if I am on the right track. I'd appreciate some help.

    HOW TO FIX In addition to the accepted answer, I'm using reflection to update AtomicBoolean

        new MethodChannel(getFlutterView(), CHANNEL).setMethodCallHandler(
                (call, result) -> {
    
                    if (call.method.equals("resetAlarmManager")) {
                        try {
                            // Get field instance
                            Field field = io.flutter.plugins.androidalarmmanager.AlarmService.class.getDeclaredField("sStarted"); // NOTE: this field may change!!!
                            field.setAccessible(true); // Suppress Java language access checking
    
                            // Remove "final" modifier
                            Field modifiersField = Field.class.getDeclaredField("accessFlags");
                            modifiersField.setAccessible(true);
                            modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
    
                            // Set value
                            field.set(null, new AtomicBoolean(false));
                        } catch (Exception ignored) {
                            Log.d("urenapp", "urenapp:reflection");
    
                            if (BuildConfig.DEBUG) {
                                throw new RuntimeException("REFLECTION ERROR, FIX DIT.");
                            }
                        }
                    }
                });
    

    I'm calling this native function in my notification click callback and when to app starts. This requires me to reschedule all alarms.

    SNIPPET

    import 'package:android_alarm_manager/android_alarm_manager.dart';
    import 'package:flutter/cupertino.dart';
    import 'package:flutter/material.dart';
    import 'package:flutter_local_notifications/flutter_local_notifications.dart';
    
    class AlarmHelper {
      static final int _REQUEST_CODE = 12377;
      static final int _REQUEST_CODE_OVERTIME = 12376;
    
      static void scheduleAlarm() async {
        print('schedule');
        //Read the desired time from sharedpreferences and/or firebase.
    
        AndroidAlarmManager.cancel(_REQUEST_CODE);
        AndroidAlarmManager.oneShot(Duration(seconds: 10), _REQUEST_CODE, clockIn, exact: true, wakeup: true);
      }
    }
    
    void clockIn() async {
      //Do Stuff
      print('trigger');
    
      //Read some stuff from sharedpreference and/or firebase.
    
      setNotification();
    
      //Schedule the next alarm.
      AlarmHelper.scheduleAlarm();
    }
    
    void setNotification() async {
      print('notification set');
    
      //Read some more stuff from sharedpreference and/or firebase. Use that information for the notification text.
    
    
    
    
      FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = new FlutterLocalNotificationsPlugin();
    
      var androidPlatformChannelSpecifics = new AndroidNotificationDetails('test', 'test', 'test',
          importance: Importance.Max, priority: Priority.Max, ongoing: true, color: Colors.blue[500]);
      var iOSPlatformChannelSpecifics = new IOSNotificationDetails();
      var platformChannelSpecifics = new NotificationDetails(androidPlatformChannelSpecifics, iOSPlatformChannelSpecifics);
      await flutterLocalNotificationsPlugin
          .show(0, 'test', 'time ' + DateTime.now().toIso8601String(), platformChannelSpecifics, payload: 'item id 2');
    
    }