From bd8def2a86b0d1c19afd78ff90ebc848febdc27c Mon Sep 17 00:00:00 2001 From: Joan Cabezas Date: Wed, 12 Jun 2024 01:18:01 -0700 Subject: [PATCH] android setup foreground service --- apps/AppWithWearable/android/app/build.gradle | 2 +- .../android/app/src/main/AndroidManifest.xml | 18 +- apps/AppWithWearable/lib/pages/home/page.dart | 19 +- .../pages/onboarding/find_device/page.dart | 16 +- .../AppWithWearable/lib/utils/foreground.dart | 315 ++++++++++-------- apps/AppWithWearable/lib/utils/memories.dart | 3 + apps/AppWithWearable/pubspec.yaml | 1 + 7 files changed, 205 insertions(+), 169 deletions(-) diff --git a/apps/AppWithWearable/android/app/build.gradle b/apps/AppWithWearable/android/app/build.gradle index 26f78ab72..0b48b2a7e 100644 --- a/apps/AppWithWearable/android/app/build.gradle +++ b/apps/AppWithWearable/android/app/build.gradle @@ -46,7 +46,7 @@ android { defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "com.friend.ios" - minSdkVersion 21 + minSdkVersion 23 targetSdkVersion 34 versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/apps/AppWithWearable/android/app/src/main/AndroidManifest.xml b/apps/AppWithWearable/android/app/src/main/AndroidManifest.xml index f010353b3..ba43c368b 100644 --- a/apps/AppWithWearable/android/app/src/main/AndroidManifest.xml +++ b/apps/AppWithWearable/android/app/src/main/AndroidManifest.xml @@ -52,21 +52,21 @@ - - + + - - - - - + android:usesCleartextTraffic="true" + tools:replace="android:label"> + with WidgetsBindingObserver, TickerProviderStateMixin { + ForegroundUtil foregroundUtil = ForegroundUtil(); TabController? _controller; List screens = [Container(), const SizedBox(), const SizedBox()]; @@ -92,6 +96,7 @@ class _HomePageWrapperState extends State with WidgetsBindingOb WidgetsBinding.instance.addObserver(this); WidgetsBinding.instance.addPostFrameCallback((_) async { requestNotificationPermissions(); + foregroundUtil.requestPermissionForAndroid(); }); _initiateMemories(); @@ -118,10 +123,18 @@ class _HomePageWrapperState extends State with WidgetsBindingOb title: 'Friend Device Disconnected', body: 'Please reconnect to continue using your Friend.'); } MixpanelManager().deviceDisconnected(); + foregroundUtil.stopForegroundTask(); }, onConnected: ((d) => _onConnected(d, initiateConnectionListener: false))); } + _startForeground() async { + if (!Platform.isAndroid) return; + await foregroundUtil.initForegroundTask(); + var result = await foregroundUtil.startForegroundTask(); + debugPrint('_startForeground: $result'); + } + _onConnected(BTDeviceStruct? connectedDevice, {bool initiateConnectionListener = true}) { if (connectedDevice == null) return; clearNotification(1); @@ -133,6 +146,7 @@ class _HomePageWrapperState extends State with WidgetsBindingOb transcriptChildWidgetKey.currentState?.resetState(restartBytesProcessing: true, btDevice: connectedDevice); MixpanelManager().deviceConnected(); SharedPreferencesUtil().deviceId = _device!.id; + _startForeground(); } _initiateBleBatteryListener() async { @@ -154,7 +168,8 @@ class _HomePageWrapperState extends State with WidgetsBindingOb @override Widget build(BuildContext context) { - return Scaffold( + return WithForegroundTask( + child: Scaffold( backgroundColor: Theme.of(context).colorScheme.surface, body: GestureDetector( onTap: () { @@ -327,7 +342,7 @@ class _HomePageWrapperState extends State with WidgetsBindingOb elevation: 0, centerTitle: true, ), - ); + )); } @override diff --git a/apps/AppWithWearable/lib/pages/onboarding/find_device/page.dart b/apps/AppWithWearable/lib/pages/onboarding/find_device/page.dart index 0b7e1e1a5..022e2be73 100644 --- a/apps/AppWithWearable/lib/pages/onboarding/find_device/page.dart +++ b/apps/AppWithWearable/lib/pages/onboarding/find_device/page.dart @@ -2,9 +2,10 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; -import 'package:url_launcher/url_launcher.dart'; import 'package:friend_private/backend/schema/bt_device.dart'; import 'package:friend_private/utils/ble/scan.dart'; +import 'package:url_launcher/url_launcher.dart'; + import 'found_devices.dart'; class FindDevicesPage extends StatefulWidget { @@ -42,8 +43,8 @@ class _FindDevicesPageState extends State with SingleTickerProv enableInstructions = true; }); }); - // Update foundDevicesMap with new devices and remove the ones not found anymore - Map foundDevicesMap = {}; + // Update foundDevicesMap with new devices and remove the ones not found anymore + Map foundDevicesMap = {}; _findDevicesTimer = Timer.periodic(const Duration(seconds: 2), (timer) async { List foundDevices = await scanDevices(); @@ -57,15 +58,12 @@ class _FindDevicesPageState extends State with SingleTickerProv } } // Remove devices that are no longer found - foundDevicesMap.keys - .where((id) => !updatedDevicesMap.containsKey(id)) - .toList() - .forEach(foundDevicesMap.remove); + foundDevicesMap.keys.where((id) => !updatedDevicesMap.containsKey(id)).toList().forEach(foundDevicesMap.remove); // Merge the new devices into the current map to maintain order foundDevicesMap.addAll(updatedDevicesMap); - - // Convert the values of the map back to a list + + // Convert the values of the map back to a list List orderedDevices = foundDevicesMap.values.toList(); if (orderedDevices.isNotEmpty) { diff --git a/apps/AppWithWearable/lib/utils/foreground.dart b/apps/AppWithWearable/lib/utils/foreground.dart index e6b81fce7..e7a8aeec2 100644 --- a/apps/AppWithWearable/lib/utils/foreground.dart +++ b/apps/AppWithWearable/lib/utils/foreground.dart @@ -1,148 +1,167 @@ -// import 'dart:io'; -// import 'dart:isolate'; -// -// import 'package:flutter/material.dart'; -// import 'package:flutter_foreground_task/flutter_foreground_task.dart'; -// -// // The callback function should always be a top-level function. -// @pragma('vm:entry-point') -// void startCallback() { -// print('startCallback'); -// // The setTaskHandler function must be called to handle the task in the background. -// FlutterForegroundTask.setTaskHandler(FirstTaskHandler()); -// } -// -// class FirstTaskHandler extends TaskHandler { -// int _eventCount = 0; -// -// @override -// void onStart(DateTime timestamp, SendPort? sendPort) async {} -// -// @override -// void onRepeatEvent(DateTime timestamp, SendPort? sendPort) async { -// debugPrint('FirstTaskHandler ~ onRepeatEvent: $_eventCount'); -// sendPort?.send(_eventCount); // send data to main isolate -// _eventCount++; -// } -// -// // } -// -// @override -// void onDestroy(DateTime timestamp, SendPort? sendPort) async {} -// } -// -// class ForegroundUtil { -// ReceivePort? _receivePort; -// -// Future requestPermissionForAndroid() async { -// if (!Platform.isAndroid) { -// return; -// } -// if (!await FlutterForegroundTask.canDrawOverlays) { -// await FlutterForegroundTask.openSystemAlertWindowSettings(); -// } -// if (!await FlutterForegroundTask.isIgnoringBatteryOptimizations) { -// await FlutterForegroundTask.requestIgnoreBatteryOptimization(); -// } -// final NotificationPermission notificationPermissionStatus = -// await FlutterForegroundTask.checkNotificationPermission(); -// if (notificationPermissionStatus != NotificationPermission.granted) { -// await FlutterForegroundTask.requestNotificationPermission(); -// } -// } -// -// void initForegroundTask() { -// // if (await FlutterForegroundTask.isRunningService) return; -// print('initForegroundTask'); -// FlutterForegroundTask.init( -// androidNotificationOptions: AndroidNotificationOptions( -// foregroundServiceType: AndroidForegroundServiceType.CONNECTED_DEVICE, -// channelId: 'foreground_service', -// channelName: 'Foreground Service Notification', -// channelDescription: 'This notification appears when the foreground service is running.', -// channelImportance: NotificationChannelImportance.LOW, -// priority: NotificationPriority.HIGH, -// iconData: const NotificationIconData( -// resType: ResourceType.mipmap, -// resPrefix: ResourcePrefix.ic, -// name: 'launcher', -// ), -// ), -// iosNotificationOptions: const IOSNotificationOptions( -// showNotification: true, -// playSound: false, -// ), -// foregroundTaskOptions: const ForegroundTaskOptions( -// interval: 5000, -// isOnceEvent: false, -// autoRunOnBoot: false, -// allowWakeLock: false, -// allowWifiLock: false, -// ), -// ); -// } -// -// Future startForegroundTask() async { -// print('startForegroundTask'); -// // You can save data using the saveData function. -// // await FlutterForegroundTask.saveData(key: 'customData', value: 'hello'); -// -// // Register the receivePort before starting the service. -// final ReceivePort? receivePort = FlutterForegroundTask.receivePort; -// final bool isRegistered = _registerReceivePort(receivePort); -// if (!isRegistered) { -// print('Failed to register receivePort!'); -// return false; -// } -// -// if (await FlutterForegroundTask.isRunningService) { -// return FlutterForegroundTask.restartService(); -// } else { -// return FlutterForegroundTask.startService( -// notificationTitle: 'Your Friend Device is active', -// notificationText: 'Tap to open the app', -// callback: startCallback, -// ); -// } -// } -// -// void stopForegroundTask() { -// print('stopForegroundTask'); -// FlutterForegroundTask.stopService(); -// } -// -// bool _registerReceivePort(ReceivePort? newReceivePort) { -// if (newReceivePort == null) { -// return false; -// } -// -// _closeReceivePort(); -// -// _receivePort = newReceivePort; -// _receivePort?.listen((data) { -// if (data is int) { -// print('eventCount: $data'); -// } else if (data is String) { -// // if (data == 'onNotificationPressed') { -// // Navigator.of(context).pushNamed('/resume-route'); -// // } -// } else if (data is DateTime) { -// print('timestamp: ${data.toString()}'); -// } -// }); -// -// return _receivePort != null; -// } -// -// void _closeReceivePort() { -// _receivePort?.close(); -// _receivePort = null; -// } -// -// _handleReceivePort() async { -// if (await FlutterForegroundTask.isRunningService) { -// final newReceivePort = FlutterForegroundTask.receivePort; -// _registerReceivePort(newReceivePort); -// } -// } -// } +import 'dart:io'; +import 'dart:isolate'; + +import 'package:flutter/material.dart'; +import 'package:flutter_foreground_task/flutter_foreground_task.dart'; + +// The callback function should always be a top-level function. +@pragma('vm:entry-point') +void startCallback() { + print('startCallback'); + // The setTaskHandler function must be called to handle the task in the background. + FlutterForegroundTask.setTaskHandler(FirstTaskHandler()); +} + +class FirstTaskHandler extends TaskHandler { + int _eventCount = 0; + + @override + void onStart(DateTime timestamp, SendPort? sendPort) async { + debugPrint('onStart'); + } + + @override + void onRepeatEvent(DateTime timestamp, SendPort? sendPort) async { + // debugPrint('FirstTaskHandler ~ onRepeatEvent: $_eventCount'); + sendPort?.send(_eventCount); // send data to main isolate + await Future.delayed(const Duration(seconds: 1)); + _eventCount++; + } + + @override + void onDestroy(DateTime timestamp, SendPort? sendPort) async {} + + // Called when the notification button on the Android platform is pressed. + @override + void onNotificationButtonPressed(String id) { + print('onNotificationButtonPressed >> $id'); + } + + // 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. + @override + void onNotificationPressed() { + // 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"); + } +} + +class ForegroundUtil { + ReceivePort? _receivePort; + + Future requestPermissionForAndroid() async { + if (!Platform.isAndroid) { + return; + } + // if (!await FlutterForegroundTask.canDrawOverlays) { + // await FlutterForegroundTask.openSystemAlertWindowSettings(); + // } + debugPrint('requestPermissionForAndroid: ${!await FlutterForegroundTask.isIgnoringBatteryOptimizations}'); + if (!await FlutterForegroundTask.isIgnoringBatteryOptimizations) { + await FlutterForegroundTask.requestIgnoreBatteryOptimization(); + } + final NotificationPermission notificationPermissionStatus = + await FlutterForegroundTask.checkNotificationPermission(); + debugPrint('notificationPermissionStatus: $notificationPermissionStatus'); + if (notificationPermissionStatus != NotificationPermission.granted) { + await FlutterForegroundTask.requestNotificationPermission(); + } + } + + Future initForegroundTask() async { + if (await FlutterForegroundTask.isRunningService) return; + print('initForegroundTask'); + FlutterForegroundTask.init( + androidNotificationOptions: AndroidNotificationOptions( + // foregroundServiceType: AndroidForegroundServiceType.CONNECTED_DEVICE, + channelId: 'foreground_service', + channelName: 'Foreground Service Notification', + channelDescription: 'Your Friend Device is connected', + channelImportance: NotificationChannelImportance.LOW, + priority: NotificationPriority.HIGH, + iconData: const NotificationIconData( + resType: ResourceType.mipmap, + resPrefix: ResourcePrefix.ic, + name: 'launcher', + ), + ), + iosNotificationOptions: const IOSNotificationOptions( + showNotification: true, + playSound: false, + ), + foregroundTaskOptions: const ForegroundTaskOptions( + interval: 5000, + isOnceEvent: false, + autoRunOnBoot: false, + allowWakeLock: false, + allowWifiLock: false, + ), + ); + } + + Future startForegroundTask() async { + print('startForegroundTask'); + final ReceivePort? receivePort = FlutterForegroundTask.receivePort; + final bool isRegistered = _registerReceivePort(receivePort); + if (!isRegistered) { + print('Failed to register receivePort!'); + return false; + } + + if (await FlutterForegroundTask.isRunningService) { + return FlutterForegroundTask.restartService(); + } else { + print('starting service'); + return FlutterForegroundTask.startService( + notificationTitle: 'Your Friend Device is active', + notificationText: 'Tap to open the app', + callback: startCallback, + ); + } + } + + void stopForegroundTask() { + if (!Platform.isAndroid) return; + print('stopForegroundTask'); + FlutterForegroundTask.stopService(); + } + + bool _registerReceivePort(ReceivePort? newReceivePort) { + if (newReceivePort == null) { + return false; + } + + _closeReceivePort(); + + _receivePort = newReceivePort; + _receivePort?.listen((data) { + if (data is int) { + // print('eventCount: $data'); + } else if (data is String) { + // if (data == 'onNotificationPressed') { + // Navigator.of(context).pushNamed('/resume-route'); + // } + } else if (data is DateTime) { + print('timestamp: ${data.toString()}'); + } + }); + + return _receivePort != null; + } + + void _closeReceivePort() { + _receivePort?.close(); + _receivePort = null; + } + + _handleReceivePort() async { + if (await FlutterForegroundTask.isRunningService) { + final newReceivePort = FlutterForegroundTask.receivePort; + _registerReceivePort(newReceivePort); + } + } +} diff --git a/apps/AppWithWearable/lib/utils/memories.dart b/apps/AppWithWearable/lib/utils/memories.dart index 00c8c352e..0b00266b7 100644 --- a/apps/AppWithWearable/lib/utils/memories.dart +++ b/apps/AppWithWearable/lib/utils/memories.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; import 'package:friend_private/backend/mixpanel.dart'; import 'package:friend_private/backend/storage/memories.dart'; +import 'package:instabug_flutter/instabug_flutter.dart'; import 'package:uuid/uuid.dart'; + import '/backend/api_requests/api_calls.dart'; // Perform actions periodically @@ -32,6 +34,7 @@ Future memoryCreationBlock( structuredMemory = await generateTitleAndSummaryForMemory(transcript, recentMemories); } catch (e) { debugPrint('Error: $e'); + InstabugLog.logError(e.toString()); if (!retrievedFromCache) { ScaffoldMessenger.of(context).removeCurrentSnackBar(); ScaffoldMessenger.of(context).showSnackBar(const SnackBar( diff --git a/apps/AppWithWearable/pubspec.yaml b/apps/AppWithWearable/pubspec.yaml index 47f457f58..196d14bf0 100644 --- a/apps/AppWithWearable/pubspec.yaml +++ b/apps/AppWithWearable/pubspec.yaml @@ -73,6 +73,7 @@ dependencies: ml_linalg: any objectbox: ^4.0.1 objectbox_flutter_libs: any + flutter_foreground_task: ^6.5.0 dependency_overrides: http: ^1.2.1