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