diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index e2a2e3d..fe3e2c9 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -3,6 +3,7 @@ + @@ -84,6 +85,11 @@ android:screenOrientation="portrait" android:theme="@style/Theme.AppCompat.Light.NoActionBar" /> + + _initializeFirebase() async { }; } +Future _initializeBackgroundNotificationService() async { + final prefs = await SharedPreferences.getInstance(); + if (prefs.getBool('service_background_notification') != true) return; + + final service = FlutterBackgroundService(); + + await service.configure( + androidConfiguration: AndroidConfiguration( + onStart: onBackgroundNotificationServiceStart, + autoStart: true, + autoStartOnBoot: true, + isForegroundMode: false, + ), + // This feature won't be able to use on iOS + // We got APNs support covered + iosConfiguration: IosConfiguration( + autoStart: false, + ), + ); + + await service.startService(); +} + +@pragma('vm:entry-point') +void onBackgroundNotificationServiceStart(ServiceInstance service) async { + WidgetsFlutterBinding.ensureInitialized(); + DartPluginRegistrant.ensureInitialized(); + + Get.put(AuthProvider()); + Get.put(WebSocketProvider()); + + final auth = Get.find(); + await auth.refreshAuthorizeStatus(); + await auth.ensureCredentials(); + if (!auth.isAuthorized.value) { + debugPrint( + 'Background notification do nothing due to user didn\'t sign in.', + ); + return; + } + + const notificationChannelId = 'solian_notification_service'; + + final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = + FlutterLocalNotificationsPlugin(); + + final ws = Get.find(); + await ws.connect(); + debugPrint('Background notification has been started'); + ws.stream.stream.listen( + (event) { + debugPrint( + 'Background notification service incoming message: ${event.method} ${event.message}', + ); + + if (event.method == 'notifications.new' && event.payload != null) { + final data = Notification.fromJson(event.payload!); + debugPrint( + 'Background notification service got a notification id=${data.id}', + ); + flutterLocalNotificationsPlugin.show( + data.id, + data.title, + [data.subtitle, data.body].where((x) => x != null).join('\n'), + const NotificationDetails( + android: AndroidNotificationDetails( + notificationChannelId, + 'Solian Notification Service', + channelDescription: 'Notifications that sent via Solar Network', + importance: Importance.high, + icon: 'mipmap/ic_launcher', + ), + ), + ); + } + }, + ); +} + Future _initializePlatformComponents() async { if (!PlatformInfo.isWeb) { await protocolHandler.register('solink'); @@ -144,5 +228,7 @@ class SolianApp extends StatelessWidget { Get.lazyPut(() => LinkExpandProvider()); Get.lazyPut(() => DailySignProvider()); Get.lazyPut(() => LastReadProvider()); + + Get.find().requestPermissions(); } } diff --git a/lib/providers/websocket.dart b/lib/providers/websocket.dart index ff4a4ca..0c804da 100644 --- a/lib/providers/websocket.dart +++ b/lib/providers/websocket.dart @@ -5,7 +5,9 @@ import 'dart:io'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:get/get.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'package:solian/exceptions/request.dart'; import 'package:solian/models/notification.dart'; import 'package:solian/models/packet.dart'; @@ -29,20 +31,46 @@ class WebSocketProvider extends GetxController { @override onInit() { - FirebaseMessaging.instance - .requestPermission( - alert: true, - announcement: true, - carPlay: true, - badge: true, - sound: true) - .then((status) { - notifyPrefetch(); - }); + notifyPrefetch(); super.onInit(); } + void requestPermissions() { + try { + FirebaseMessaging.instance.requestPermission( + alert: true, + announcement: true, + carPlay: true, + badge: true, + sound: true); + } catch (_) { + // When firebase isn't initialized (background service) + FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = + FlutterLocalNotificationsPlugin(); + flutterLocalNotificationsPlugin + .resolvePlatformSpecificImplementation< + AndroidFlutterLocalNotificationsPlugin>() + ?.requestNotificationsPermission(); + flutterLocalNotificationsPlugin + .resolvePlatformSpecificImplementation< + IOSFlutterLocalNotificationsPlugin>() + ?.requestPermissions( + alert: true, + badge: true, + sound: true, + ); + flutterLocalNotificationsPlugin + .resolvePlatformSpecificImplementation< + MacOSFlutterLocalNotificationsPlugin>() + ?.requestPermissions( + alert: true, + badge: true, + sound: true, + ); + } + } + Future connect({noRetry = false}) async { if (isConnected.value) { return; @@ -120,6 +148,12 @@ class WebSocketProvider extends GetxController { } Future registerPushNotifications() async { + final prefs = await SharedPreferences.getInstance(); + if (prefs.getBool('service_background_notification') == true) { + log('Background notification service has been enabled, skip register push notifications'); + return; + } + final AuthProvider auth = Get.find(); if (auth.isAuthorized.isFalse) return; diff --git a/lib/screens/settings.dart b/lib/screens/settings.dart index b2bf35c..955730a 100644 --- a/lib/screens/settings.dart +++ b/lib/screens/settings.dart @@ -3,6 +3,7 @@ import 'package:get/get.dart'; import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:solian/exts.dart'; +import 'package:solian/platform.dart'; import 'package:solian/providers/database/database.dart'; import 'package:solian/providers/theme_switcher.dart'; import 'package:solian/router.dart'; @@ -16,7 +17,7 @@ class SettingScreen extends StatefulWidget { } class _SettingScreenState extends State { - late final SharedPreferences _prefs; + SharedPreferences? _prefs; Widget _buildCaptionHeader(String title) { return Container( @@ -42,7 +43,7 @@ class _SettingScreenState extends State { seedColor: color, ), ); - _prefs.setInt('global_theme_color', color.value); + _prefs?.setInt('global_theme_color', color.value); context.clearSnackbar(); context.showSnackbar('themeColorApplied'.tr); }, @@ -62,6 +63,9 @@ class _SettingScreenState extends State { super.initState(); SharedPreferences.getInstance().then((inst) { _prefs = inst; + if (mounted) { + setState(() {}); + } }); } @@ -81,6 +85,35 @@ class _SettingScreenState extends State { .toList(), ).paddingSymmetric(horizontal: 12, vertical: 8), ), + _buildCaptionHeader('notification'.tr), + Tooltip( + message: 'settingsNotificationBgServiceDesc'.tr, + child: CheckboxListTile( + contentPadding: const EdgeInsets.symmetric(horizontal: 22), + secondary: const Icon(Icons.system_security_update_warning), + enabled: PlatformInfo.isAndroid, + title: Text('settingsNotificationBgService'.tr), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('holdToSeeDetail'.tr), + Text( + 'needRestartToApply'.tr, + style: const TextStyle(fontWeight: FontWeight.bold), + ) + ], + ), + value: + _prefs?.getBool('service_background_notification') ?? false, + onChanged: (value) { + _prefs + ?.setBool('service_background_notification', value ?? false) + .then((_) { + setState(() {}); + }); + }, + ), + ), _buildCaptionHeader('more'.tr), ListTile( leading: const Icon(Icons.delete_sweep), diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 573eb29..9c19bdd 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -13,6 +13,7 @@ import firebase_analytics import firebase_core import firebase_crashlytics import firebase_messaging +import flutter_local_notifications import flutter_secure_storage_macos import flutter_webrtc import gal @@ -41,6 +42,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) FLTFirebaseCrashlyticsPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCrashlyticsPlugin")) FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin")) + FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin")) GalPlugin.register(with: registry.registrar(forPlugin: "GalPlugin")) diff --git a/pubspec.lock b/pubspec.lock index 5be538d..3a8d937 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -635,6 +635,38 @@ packages: url: "https://pub.dev" source: hosted version: "4.5.0" + flutter_background_service: + dependency: "direct main" + description: + name: flutter_background_service + sha256: d32f078ec57647c9cfd6e1a8da9297f7d8f021d4dcc204a35aaad2cdbfe255f0 + url: "https://pub.dev" + source: hosted + version: "5.0.10" + flutter_background_service_android: + dependency: transitive + description: + name: flutter_background_service_android + sha256: "39da42dddf877beeef82bc2583130d8bedb4d0765e99ca9e7b4a32e8c6abd239" + url: "https://pub.dev" + source: hosted + version: "6.2.7" + flutter_background_service_ios: + dependency: transitive + description: + name: flutter_background_service_ios + sha256: "6037ffd45c4d019dab0975c7feb1d31012dd697e25edc05505a4a9b0c7dc9fba" + url: "https://pub.dev" + source: hosted + version: "5.0.3" + flutter_background_service_platform_interface: + dependency: transitive + description: + name: flutter_background_service_platform_interface + sha256: ca74aa95789a8304f4d3f57f07ba404faa86bed6e415f83e8edea6ad8b904a41 + url: "https://pub.dev" + source: hosted + version: "5.1.2" flutter_cache_manager: dependency: "direct main" description: @@ -715,6 +747,30 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.0" + flutter_local_notifications: + dependency: "direct main" + description: + name: flutter_local_notifications + sha256: c500d5d9e7e553f06b61877ca6b9c8b92c570a4c8db371038702e8ce57f8a50f + url: "https://pub.dev" + source: hosted + version: "17.2.2" + flutter_local_notifications_linux: + dependency: transitive + description: + name: flutter_local_notifications_linux + sha256: c49bd06165cad9beeb79090b18cd1eb0296f4bf4b23b84426e37dd7c027fc3af + url: "https://pub.dev" + source: hosted + version: "4.0.1" + flutter_local_notifications_platform_interface: + dependency: transitive + description: + name: flutter_local_notifications_platform_interface + sha256: "85f8d07fe708c1bdcf45037f2c0109753b26ae077e9d9e899d55971711a4ea66" + url: "https://pub.dev" + source: hosted + version: "7.2.0" flutter_markdown: dependency: "direct main" description: @@ -1930,6 +1986,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.7.0" + timezone: + dependency: transitive + description: + name: timezone + sha256: "2236ec079a174ce07434e89fcd3fcda430025eb7692244139a9cf54fdcf1fc7d" + url: "https://pub.dev" + source: hosted + version: "0.9.4" timing: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 3626806..49f261d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -79,6 +79,8 @@ dependencies: drift_flutter: ^0.2.0 very_good_infinite_list: ^0.8.0 path_provider: ^2.1.4 + flutter_background_service: ^5.0.10 + flutter_local_notifications: ^17.2.2 dev_dependencies: flutter_test: