Dev-hwang / flutter_foreground_task

This plugin is used to implement a foreground service on the Android platform.
https://pub.dev/packages/flutter_foreground_task
MIT License
147 stars 107 forks source link

Using flutter_foreground_task together with flutter_blue #84

Closed leeflix closed 2 months ago

leeflix commented 2 years ago

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!'),
        ),
      ),
    );
  }
}
Kimsangwon0509 commented 2 years ago

Same problem TT

kumbulali commented 2 years ago

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.

irjayjay commented 2 years ago

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.

ksheremet commented 2 years ago

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

irjayjay commented 2 years ago

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

typexy commented 1 year ago

@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

ksheremet commented 1 year ago

@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

TheLastGimbus commented 1 year ago

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

ali-almas commented 1 year ago

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

KyungRyul commented 1 year ago

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

Advice please.

rajan-nonstopio commented 10 months ago

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.

ConnorDykes commented 1 month ago

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.

how are you doing this? I need to listen to a a Characteristic Stream and reform calculation with the value?