flutter_foreground_task icon indicating copy to clipboard operation
flutter_foreground_task copied to clipboard

Using flutter_foreground_task together with flutter_blue

Open leeflix opened this issue 2 years ago • 11 comments

I am trying to make an app that uses flutter_foreground_task to scan for bluetooth devices while it is in the background with the help of flutter_blue. Is this possible? When I am taking the example of flutter_foreground_task and insert the example of flutter_blue into it I get the following error:

I/flutter (16681): Error starting scan.
E/flutter (16681): [ERROR:flutter/lib/ui/ui_dart_state.cc(209)] Unhandled Exception: MissingPluginException(No implementation found for method startScan on channel plugins.pauldemarco.com/flutter_blue/methods)
E/flutter (16681): #0      FlutterBlue.scan (package:flutter_blue/src/flutter_blue.dart:123:7)
E/flutter (16681): <asynchronous suspension>
E/flutter (16681): 

When I insert the code in the initState method for example it works. It seems that we cannot use plugins that use platform channels in the background or is there a workaround or am I overseeing something?

Code:

import 'dart:isolate';

import 'package:flutter/material.dart';
import 'package:flutter_blue/flutter_blue.dart';
import 'package:flutter_foreground_task/flutter_foreground_task.dart';

void main() => runApp(const ExampleApp());

// The callback function should always be a top-level function.
void startCallback() {
  // The setTaskHandler function must be called to handle the task in the background.
  FlutterForegroundTask.setTaskHandler(MyTaskHandler());
}

class MyTaskHandler extends TaskHandler {
  SendPort? _sendPort;
  int _eventCount = 0;

  @override
  Future<void> onStart(DateTime timestamp, SendPort? sendPort) async {
    _sendPort = sendPort;

    // You can use the getData function to get the stored data.
    final customData =
        await FlutterForegroundTask.getData<String>(key: 'customData');
    print('customData: $customData');
  }

  @override
  Future<void> onEvent(DateTime timestamp, SendPort? sendPort) async {
    FlutterForegroundTask.updateService(
        notificationTitle: 'MyTaskHandler',
        notificationText: 'eventCount: $_eventCount');

    // Send data to the main isolate.
    sendPort?.send(_eventCount);

    _eventCount++;

    FlutterBlue flutterBlue = FlutterBlue.instance;
    // Start scanning
    flutterBlue.startScan(timeout: const Duration(seconds: 4));

    // Listen to scan results
    var subscription = flutterBlue.scanResults.listen((results) {
      // do something with scan results
      for (ScanResult r in results) {
        print('${r.device.name} found! rssi: ${r.rssi}');
      }
    });

    // // Stop scanning
    // flutterBlue.stopScan();
  }

  @override
  Future<void> onDestroy(DateTime timestamp, SendPort? sendPort) async {
    // You can use the clearAllData function to clear all the stored data.
    await FlutterForegroundTask.clearAllData();
  }

  @override
  void onButtonPressed(String id) {
    // Called when the notification button on the Android platform is pressed.
    print('onButtonPressed >> $id');
  }

  @override
  void onNotificationPressed() {
    // Called when the notification itself on the Android platform is pressed.
    //
    // "android.permission.SYSTEM_ALERT_WINDOW" permission must be granted for
    // this function to be called.

    // Note that the app will only route to "/resume-route" when it is exited so
    // it will usually be necessary to send a message through the send port to
    // signal it to restore state when the app is already started.
    FlutterForegroundTask.launchApp("/resume-route");
    _sendPort?.send('onNotificationPressed');
  }
}

class ExampleApp extends StatelessWidget {
  const ExampleApp({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      initialRoute: '/',
      routes: {
        '/': (context) => const ExamplePage(),
        '/resume-route': (context) => const ResumeRoutePage(),
      },
    );
  }
}

class ExamplePage extends StatefulWidget {
  const ExamplePage({Key? key}) : super(key: key);

  @override
  State<StatefulWidget> createState() => _ExamplePageState();
}

class _ExamplePageState extends State<ExamplePage> {
  ReceivePort? _receivePort;

  Future<void> _initForegroundTask() async {
    await FlutterForegroundTask.init(
      androidNotificationOptions: AndroidNotificationOptions(
        channelId: 'notification_channel_id',
        channelName: 'Foreground Notification',
        channelDescription:
            'This notification appears when the foreground service is running.',
        channelImportance: NotificationChannelImportance.LOW,
        priority: NotificationPriority.LOW,
        iconData: const NotificationIconData(
          resType: ResourceType.mipmap,
          resPrefix: ResourcePrefix.ic,
          name: 'launcher',
          backgroundColor: Colors.orange,
        ),
        buttons: [
          const NotificationButton(id: 'sendButton', text: 'Send'),
          const NotificationButton(id: 'testButton', text: 'Test'),
        ],
      ),
      iosNotificationOptions: const IOSNotificationOptions(
        showNotification: true,
        playSound: false,
      ),
      foregroundTaskOptions: const ForegroundTaskOptions(
        interval: 5000,
        autoRunOnBoot: true,
        allowWifiLock: true,
      ),
      printDevLog: true,
    );
  }

  Future<bool> _startForegroundTask() async {
    // "android.permission.SYSTEM_ALERT_WINDOW" permission must be granted for
    // onNotificationPressed function to be called.
    //
    // When the notification is pressed while permission is denied,
    // the onNotificationPressed function is not called and the app opens.
    //
    // If you do not use the onNotificationPressed or launchApp function,
    // you do not need to write this code.
    if (!await FlutterForegroundTask.canDrawOverlays) {
      final isGranted =
          await FlutterForegroundTask.openSystemAlertWindowSettings();
      if (!isGranted) {
        print('SYSTEM_ALERT_WINDOW permission denied!');
        return false;
      }
    }

    // You can save data using the saveData function.
    await FlutterForegroundTask.saveData(key: 'customData', value: 'hello');

    ReceivePort? receivePort;
    if (await FlutterForegroundTask.isRunningService) {
      receivePort = await FlutterForegroundTask.restartService();
    } else {
      receivePort = await FlutterForegroundTask.startService(
        notificationTitle: 'Foreground Service is running',
        notificationText: 'Tap to return to the app',
        callback: startCallback,
      );
    }

    return _registerReceivePort(receivePort);
  }

  Future<bool> _stopForegroundTask() async {
    return await FlutterForegroundTask.stopService();
  }

  bool _registerReceivePort(ReceivePort? receivePort) {
    _closeReceivePort();

    if (receivePort != null) {
      _receivePort = receivePort;
      _receivePort?.listen((message) {
        if (message is int) {
          print('eventCount: $message');
        } else if (message is String) {
          if (message == 'onNotificationPressed') {
            Navigator.of(context).pushNamed('/resume-route');
          }
        } else if (message is DateTime) {
          print('timestamp: ${message.toString()}');
        }
      });

      return true;
    }

    return false;
  }

  void _closeReceivePort() {
    _receivePort?.close();
    _receivePort = null;
  }

  T? _ambiguate<T>(T? value) => value;

  @override
  void initState() {
    super.initState();
    _initForegroundTask();
    _ambiguate(WidgetsBinding.instance)?.addPostFrameCallback((_) async {
      // You can get the previous ReceivePort without restarting the service.
      if (await FlutterForegroundTask.isRunningService) {
        final newReceivePort = await FlutterForegroundTask.receivePort;
        _registerReceivePort(newReceivePort);
      }
    });
  }

  @override
  void dispose() {
    _closeReceivePort();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    // A widget that prevents the app from closing when the foreground service is running.
    // This widget must be declared above the [Scaffold] widget.
    return WithForegroundTask(
      child: Scaffold(
        appBar: AppBar(
          title: const Text('Flutter Foreground Task'),
          centerTitle: true,
        ),
        body: _buildContentView(),
      ),
    );
  }

  Widget _buildContentView() {
    buttonBuilder(String text, {VoidCallback? onPressed}) {
      return ElevatedButton(
        child: Text(text),
        onPressed: onPressed,
      );
    }

    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          buttonBuilder('start', onPressed: _startForegroundTask),
          buttonBuilder('stop', onPressed: _stopForegroundTask),
        ],
      ),
    );
  }
}

class ResumeRoutePage extends StatelessWidget {
  const ResumeRoutePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Resume Route'),
        centerTitle: true,
      ),
      body: Center(
        child: ElevatedButton(
          onPressed: () {
            // Navigate back to first route when tapped.
            Navigator.of(context).pop();
          },
          child: const Text('Go back!'),
        ),
      ),
    );
  }
}

leeflix avatar May 21 '22 18:05 leeflix

Same problem TT

Kimsangwon0509 avatar May 23 '22 00:05 Kimsangwon0509

I have the similar problem with the different plugin that is flutter_beacon. I can not initiate the plugin inside of task handlers onStart function.

kumbulali avatar Jun 04 '22 23:06 kumbulali

Having similar issues here. Different plugins that have this issue with the isolate at different times. I'm googling for MissingPluginException all over the internet, hoping to gain some insight. No workarounds yet.

irjayjay avatar Jul 26 '22 14:07 irjayjay

@kumbulali , @irjayjay

I have a working proof of concept flutter_beacon and flutter_foregound_task. I've refactored flutter_beacon though. https://github.com/futureware-tech/flutter_foreground_service

Please add your beacons in the lib/fs/beacon_example.dart file.

I hope it will be helpful.

ksheremet avatar Aug 06 '22 06:08 ksheremet

I found the issue with running plugins in isolates such as this library. Please see my reply on a similar issue in this repo: https://github.com/Dev-hwang/flutter_foreground_task/issues/77#issuecomment-1203775491

Hope this helps. Check the plugin's repo issues to find how they solve MissingPluginException when using pre-Flutter 3.0.0, otherwise simply upgrade to Flutter 3.0.0 (I wouldn't recommend it, there are various bugs).

irjayjay avatar Aug 08 '22 10:08 irjayjay

@ksheremet many thanks for the poc. I compared your refactored flutter_beacon project and the original one and unfortunately didn't really spot a difference. Would it be possible for you to give me a hint what exactly you refactored so that I can be able to do it, too? Any help is highly appreciated @ksheremet Many thanks in advance

typexy avatar Oct 27 '22 22:10 typexy

@typexy

Sure, I've done changes in the android folder of flutter_beacon.

Please have a look at changes here: https://github.com/alann-maulana/flutter_beacon/compare/master...futureware-tech:flutter_beacon:background

ksheremet avatar Nov 06 '22 09:11 ksheremet

As of flutter 3.7 you can use plugins in any isolate :eyes: maybe that will help you guys

https://medium.com/flutter/whats-new-in-flutter-3-7-38cbea71133c

TheLastGimbus avatar Jan 27 '23 10:01 TheLastGimbus

Just before starting the foreground service make sure all permissions are enabled for bluetooth

ali-almas avatar Feb 23 '23 21:02 ali-almas

I am also using flutter_foreground_task and flutter_blue_plus. In my case, it works fine on Android, but when I run it on iOS, the following issues occur.

*** Terminating app due to uncaught exception of class 'FlutterError' libc++abi: terminating with uncaught exception of type FlutterError

  • thread '#'1, queue = 'com.apple.main-thread', stop reason = signal SIGABRT frame #0: 0x00000001ff0d7160 libsystem_kernel.dylib__pthread_kill + 8 libsystem_kernel.dylib: -> 0x1ff0d7160 <+8>: b.lo 0x1ff0d7180 ; <+40> 0x1ff0d7164 <+12>: pacibsp 0x1ff0d7168 <+16>: stp x29, x30, [sp, #-0x10]! 0x1ff0d716c <+20>: mov x29, sp Target 0: (Runner) stopped.

Advice please.

KyungRyul avatar Jun 12 '23 16:06 KyungRyul

My use of flutter_foreground_task and flutter_blue_plus (version 1.29.11) is functioning as intended.

Note: Ensure that all necessary permissions are requested within the app itself. Please be aware that foreground service does not support permissions.

rajan-nonstopio avatar Dec 04 '23 06:12 rajan-nonstopio