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
140 stars 105 forks source link

App in foreground but no notification #236

Closed SyedKaz00 closed 1 month ago

SyedKaz00 commented 1 month ago

Ive tested my code on an android emulator and it works, it runs the code that I am calling "HandleBLECommunication" but there is no accompanying notification, could i get some help? I am also using awesomenotifications if that helps. Here is my main.dart

import 'dart:async';
import 'dart:io';
import 'dart:isolate';
import 'dart:math';
import 'dart:typed_data';

import 'package:flutter/material.dart';
import 'package:flutter_foreground_task/flutter_foreground_task.dart'
    as foreground_task;
import 'package:tactics/pages/bleCentralDebug.dart';
import 'package:tactics/pages/bleCentralScreen.dart';
import 'package:tactics/util/ble_manager.dart';
import 'package:tactics/util/data_manager.dart';
import 'package:tactics/util/flow_engine_debug.dart';
import 'package:tactics/util/packet_manager.dart';
import 'pages/chat.dart';
import 'pages/feed.dart';
import 'pages/contacts.dart';
import 'pages/profile.dart';
import 'package:permission_handler/permission_handler.dart';

import 'pages/qrcode_scanner.dart';

import 'util/ble_peripheral.dart';

import 'package:awesome_notifications/awesome_notifications.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  AwesomeNotifications().initialize(
      // set the icon to null if you want to use the default app icon
      null, //'resource://drawable/res_app_icon',
      [
        NotificationChannel(
            channelGroupKey: 'basic_channel_group',
            channelKey: 'basic_channel',
            channelName: 'Basic notifications',
            channelDescription: 'Notification channel for basic tests',
            defaultColor: Color(0xFF9D50DD),
            ledColor: Colors.white)
      ],
      // Channel groups are only visual and are not required
      channelGroups: [
        NotificationChannelGroup(
            channelGroupKey: 'basic_channel_group',
            channelGroupName: 'Basic group')
      ],
      debug: true);
  AwesomeNotifications().isNotificationAllowed().then((isAllowed) {
    if (!isAllowed) {
      // This is just a basic example. For real apps, you must show some
      // friendly dialog box before call the request method.
      // This is very important to not harm the user experience
      AwesomeNotifications().requestPermissionToSendNotifications();
    }
  });

  requestBluetoothPermissions();
  _initForegroundTask();

  runApp(const MyApp());
  //startForegroundService();
}

void requestBluetoothPermissions() async {
  // Request Bluetooth permissions
  var status = await Permission.bluetooth.request();
  var status1 = await Permission.bluetoothAdvertise.request();
  var status2 = await Permission.bluetoothScan.request();
  var status3 = await Permission.bluetoothConnect.request();
  var status4 = await Permission.locationAlways.request();

  if (status.isGranted &&
      status1.isGranted &&
      status2.isGranted &&
      status3.isGranted &&
      status4.isGranted) {
    print("Bluetooth permissions granted");
  } else {
    openAppSettings();
  }
}

void _initForegroundTask() {
  foreground_task.FlutterForegroundTask.init(
    androidNotificationOptions: foreground_task.AndroidNotificationOptions(
      channelId: 'foreground_service',
      channelName: 'Foreground Service Notification',
      channelDescription:
          'This notification appears when the foreground service is running.',
      channelImportance: foreground_task.NotificationChannelImportance.DEFAULT,
      priority: foreground_task.NotificationPriority.LOW,
    ),
    iosNotificationOptions: const foreground_task.IOSNotificationOptions(),
    foregroundTaskOptions: const foreground_task.ForegroundTaskOptions(
      interval: 60000,
      isOnceEvent: false,
      autoRunOnBoot: true,
      allowWakeLock: true,
      allowWifiLock: true,
    ),
  );
}

// The callback function should always be a top-level function.
@pragma('vm:entry-point')
void startCallback() {
  // The setTaskHandler function must be called to handle the task in the background.
  foreground_task.FlutterForegroundTask.setTaskHandler(FirstTaskHandler());
}

// Define the routes.
final routes = {
  '/home': (context) => const HomePage(),
  '/profile': (context) => const ProfilePage(),
  '/feed': (context) => const FeedPage(),
  '/contacts': (context) => const ContactsPage(),
  '/chat': (context) => const ChatPage(),
  '/qrcode_scanner': (context) => const QRCodeScannerPage(),
};

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);
  static final GlobalKey<NavigatorState> navigatorKey =
      GlobalKey<NavigatorState>();

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      navigatorKey: MyApp.navigatorKey,
      routes: routes,
      home: const MyHomePage(),
    );
  }
}

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

  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class NotificationController {
  /// Use this method to detect when a new notification or a schedule is created
  @pragma("vm:entry-point")
  static Future<void> onNotificationCreatedMethod(
      ReceivedNotification receivedNotification) async {
    // Your code goes here
  }

  /// Use this method to detect every time that a new notification is displayed
  @pragma("vm:entry-point")
  static Future<void> onNotificationDisplayedMethod(
      ReceivedNotification receivedNotification) async {
    // Your code goes here
  }

  /// Use this method to detect if the user dismissed a notification
  @pragma("vm:entry-point")
  static Future<void> onDismissActionReceivedMethod(
      ReceivedAction receivedAction) async {
    // Your code goes here
  }

  /// Use this method to detect when the user taps on a notification or action button
  @pragma("vm:entry-point")
  static Future<void> onActionReceivedMethod(
      ReceivedAction receivedAction) async {
    // Your code goes here

    // Navigate into pages, avoiding to open the notification details page over another details page already opened
    MyApp.navigatorKey.currentState?.pushNamedAndRemoveUntil(
        '/notification-page',
        (route) =>
            (route.settings.name != '/notification-page') || route.isFirst,
        arguments: receivedAction);
  }
}

class _MyHomePageState extends State<MyHomePage> {
  int _selectedIndex = 1;

  Future<bool> _startForegroundTask() async {
    // You can save data using the saveData function.
    await foreground_task.FlutterForegroundTask.saveData(
        key: 'customData', value: 'hello');

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

  Future<void> _requestPermissionForAndroid() async {
    if (!Platform.isAndroid) {
      return;
    }

    // "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 foreground_task.FlutterForegroundTask.canDrawOverlays) {
      // This function requires `android.permission.SYSTEM_ALERT_WINDOW` permission.
      await foreground_task.FlutterForegroundTask
          .openSystemAlertWindowSettings();
    }

    // Android 12 or higher, there are restrictions on starting a foreground service.
    //
    // To restart the service on device reboot or unexpected problem, you need to allow below permission.
    if (!await foreground_task
        .FlutterForegroundTask.isIgnoringBatteryOptimizations) {
      // This function requires `android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS` permission.
      await foreground_task.FlutterForegroundTask
          .requestIgnoreBatteryOptimization();
    }

    // Android 13 and higher, you need to allow notification permission to expose foreground service notification.
    final foreground_task.NotificationPermission notificationPermissionStatus =
        await foreground_task.FlutterForegroundTask
            .checkNotificationPermission();
    if (notificationPermissionStatus !=
        foreground_task.NotificationPermission.granted) {
      await foreground_task.FlutterForegroundTask
          .requestNotificationPermission();
    }
  }

  @override
  void initState() {
    super.initState();
    // Only after at least the action method is set, the notification events are delivered
    AwesomeNotifications().setListeners(
        onActionReceivedMethod: NotificationController.onActionReceivedMethod,
        onNotificationCreatedMethod:
            NotificationController.onNotificationCreatedMethod,
        onNotificationDisplayedMethod:
            NotificationController.onNotificationDisplayedMethod,
        onDismissActionReceivedMethod:
            NotificationController.onDismissActionReceivedMethod);
    //_initForegroundTask();
    _requestPermissionForAndroid();
    _startForegroundTask();
  }

  void _onItemTapped(int index) {
    setState(() {
      _selectedIndex = index;
    });
  }

  @override
  void dispose() {
    //ForegroundService().stop();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return foreground_task.WithForegroundTask(
        child: Scaffold(
      appBar: AppBar(
        title: const Text('T.A.C.T.I.C.S Demo'),
      ),
      body: IndexedStack(
        index: _selectedIndex,
        children: const [
          // The Contacts page
          ContactsPage(),
          //This is the main feed page
          FeedPage(),
          // The profile page.
          ProfilePage(),
          //Flow engine debug page
          FlowEngineDebugPage(),
          // This is the Network down Dialog
          // This is the network up Dialog
          BleCentralScreen(),
          //const BLEDebug(),
          FlutterBlueApp(),
        ],
      ),
      bottomNavigationBar: BottomNavigationBar(
        type: BottomNavigationBarType.fixed,
        selectedItemColor: Colors.black,
        backgroundColor: Colors.blue,
        unselectedItemColor: Colors.white,
        //fixedColor: Colors.red,
        onTap: _onItemTapped,
        currentIndex: _selectedIndex,
        items: const [
          BottomNavigationBarItem(
            icon: Icon(Icons.contact_mail),
            label: 'Contacts',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.message),
            label: 'Chats',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.remember_me),
            label: 'Profile',
          ),
          BottomNavigationBarItem(icon: Icon(Icons.water), label: "Flow"),
          // BottomNavigationBarItem(
          //   icon: Icon(Icons.bluetooth),
          //   label: 'BLE Peripheral Debug',
          // ),
          // BottomNavigationBarItem(
          //     icon: Icon(Icons.bluetooth_drive), label: 'DIY Scan'),
          // BottomNavigationBarItem(
          //     icon: Icon(Icons.bluetooth_connected_sharp),
          //     label: 'BLE Central Debug')
        ],
      ),
    ));
  }
}

// The home page.
class HomePage extends StatelessWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return const Center(
      child: Text('Welcome to my app!'),
    );
  }
}

List<List<int>> centralQueue = [];

void handleBLECommunication() async {
  print(DateTime.now());
//Rest of BLE Code...
  }

  final random = Random();
  final randomNumber = random.nextInt(21) + 10;
  print("Wait $randomNumber seconds before next iteration");
  await Future.delayed(Duration(seconds: randomNumber));
  print("$randomNumber seconds have passed, now running the next iteration");
}

class FirstTaskHandler extends foreground_task.TaskHandler {
  // Called when the task is started.
  @override
  void onStart(DateTime timestamp, SendPort? sendPort) async {
    // You can use the getData function to get the stored data.
    print('Onstart called initialising the task');
    //Just ask for the notification and other permissions here
  }

  // Called every [interval] milliseconds in [ForegroundTaskOptions].
  @override
  void onRepeatEvent(DateTime timestamp, SendPort? sendPort) async {
    // Send data to the main isolate.
    sendPort?.send(timestamp);
    //call the bluetooth code here
    print("onRepeatEvent called");
    handleBLECommunication();
  }

  // Called when the foreground service is closed.
  @override
  void onDestroy(DateTime timestamp, SendPort? sendPort) async {
    print('onDestroy called for foreground service');
  }
}

and this is my androidManifest

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.Whispr.Whispr.whispr_mvp">
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC"/>
    <!-- Tell Google Play Store that your app uses Bluetooth LE
     Set android:required="true" if bluetooth is necessary -->
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true" />
<!-- New Permissions to get it to run on real devices -->
<uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
<uses-permission android:name="android.permission.INTERNET"/>
<!-- New Bluetooth permissions in Android 12
https://developer.android.com/about/versions/12/features/bluetooth-permissions -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" android:usesPermissionFlags="neverForLocation" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />

<!-- legacy for Android 11 or lower -->
<uses-permission android:name="android.permission.BLUETOOTH"  />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"  />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <!-- Required only when requesting background location access on
           Android 10 (API level 29) and higher. -->
    <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />

<!-- legacy for Android 9 or lower -->
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"  />
<!-- for notifcation-->
<uses-permission android:name="android.permission.VIBRATE"/>
    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
   <application
        android:label="TACTICS"
        android:name="${applicationName}"
        android:icon="@mipmap/ic_launcher">
        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:launchMode="singleTop"
            android:theme="@style/LaunchTheme"
            android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
            android:hardwareAccelerated="true"
            android:windowSoftInputMode="adjustResize">
            <!-- Specifies an Android theme to apply to this Activity as soon as
                 the Android process has started. This theme is visible to the user
                 while the Flutter UI initializes. After that, this theme continues
                 to determine the Window background behind the Flutter UI. -->
            <meta-data
              android:name="io.flutter.embedding.android.NormalTheme"
              android:resource="@style/NormalTheme"
              />
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
        <!-- Don't delete the meta-data below.
             This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
        <meta-data
            android:name="flutterEmbedding"
            android:value="2" />
       <service
           android:name="FlowEngineDataSyncService"
           android:foregroundServiceType="dataSync"
       android:exported="false" />
    </application>
</manifest>
Dev-hwang commented 1 month ago

@Jgeyen

The service tag was incorrectly declared in AndroidManifest.

<!-- current -->
<service
     android:name="FlowEngineDataSyncService"
     android:foregroundServiceType="dataSync"
     android:exported="false" />

<!-- correct -->
<service 
    android:name="com.pravera.flutter_foreground_task.service.ForegroundService"
    android:foregroundServiceType="dataSync"
    android:exported="false" />