import 'dart:math'; import 'package:feet/api/api.dart'; import 'package:feet/main.dart'; import 'package:feet/pages/article.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:workmanager/workmanager.dart'; @pragma('vm:entry-point') void callbackDispatcher() { Workmanager().executeTask((taskName, inputData) async { print("Native called background task: $taskName"); if (taskName == 'fetchNotifications') { final sharedPreferences = await SharedPreferences.getInstance(); try { var initialized = await FlutterLocalNotificationsPlugin().initialize( const InitializationSettings( android: AndroidInitializationSettings('notification_icon'), ), ); if (initialized == false) { print("Notification plugin did not initialize; cancelling task"); return true; } var apiUrl = sharedPreferences.getString('apiUrl'), apiKey = sharedPreferences.getString('apiKey'), latest = sharedPreferences.getInt('newestKnownPost'); if (apiUrl == null || apiKey == null) { print("API URL or API key not set; cancelling task"); return true; } if (latest == null || latest == 0) { print("newestKnownPost is not set; cancelling task"); return true; } var api = FeverAPI( apiKey: apiKey, apiUrl: apiUrl, sharedPrefs: sharedPreferences, ); var response = await api.request().withItems().sinceId(latest).execute(); var items = api.cache.items.getAll().values.where((post) => !post.isRead); print("Found ${items.length} new unread posts (out of " "${api.cache.items.size} total)"); print(response); // Only fetch feeds and favicons if necessary to avoid wasting bandwidth if (items.isNotEmpty) { print("New posts found, fetching feeds and favicons"); await api.request().withFeeds().withFavicons().execute(); } for (var item in items) { var feed = api.cache.feeds.get(item.feedId); print("Notifying for post: ${item.id}"); AndroidBitmap? iconData; var favicon = api.cache.favicons.get(feed?.faviconId ?? -1); if (favicon != null) { try { iconData = ByteArrayAndroidBitmap.fromBase64String( favicon.data.split(',')[1]); } catch (e) { print("Failed to decode favicon: $e - Continuing"); } } await FlutterLocalNotificationsPlugin().show( Random().nextInt( 10000000), // Can't use the post ID here because FreshRSS's IDs are too large feed != null ? 'New post - ${feed.title}' : 'New post', item.title, payload: 'item/${item.id}', NotificationDetails( android: AndroidNotificationDetails( 'new_posts', 'New posts', autoCancel: true, channelShowBadge: true, importance: Importance.low, largeIcon: iconData, when: item.createdOnTime.millisecondsSinceEpoch, showWhen: true, ), ), ); } } catch (e) { print(e); return false; } return true; } return true; }); } void setupBackgroundTasks() async { WidgetsFlutterBinding.ensureInitialized(); await Workmanager().initialize( callbackDispatcher, isInDebugMode: kDebugMode, ); await Permission.notification.request(); await FlutterLocalNotificationsPlugin().initialize( const InitializationSettings( android: AndroidInitializationSettings('notification_icon'), ), onSelectNotification: handleNotificationClick, ); var launchDetails = await FlutterLocalNotificationsPlugin().getNotificationAppLaunchDetails(); if (launchDetails?.payload != null && launchDetails!.payload!.isNotEmpty) { handleNotificationClick(launchDetails.payload); } // Test notification //if (kDebugMode) { // await FlutterLocalNotificationsPlugin().show( // Random().nextInt(1000000), // "Test Notification", // "Test Notification Body", // const NotificationDetails( // android: AndroidNotificationDetails( // 'new_posts', // 'New posts', // ), // ), // payload: "item/1675609277381551"); //} // Runs every 15 minutes await Workmanager().registerPeriodicTask( 'fetch_notifications', 'fetchNotifications', constraints: Constraints(networkType: NetworkType.connected), initialDelay: const Duration(seconds: 60), ); } Future handleNotificationClick(String? payload) async { print("Notification click: $payload"); if (globalContext == null) { print("Global context not available."); return; } if (payload == null) { print("Didn't receive a payload"); return; } try { if (payload.startsWith('item/')) { if (!api.loggedIn()) { print("API client is not logged in"); return; } var id = toInt(payload.split('/')[1]); var item = api.cache.items.get(id); if (item == null) { var res = await api.request().withItems().withIds([id]).execute(); item = api.cache.items.get(id); if (item == null) throw 'Didn\'t receive item $id from API: $res'; } Navigator.of(globalContext!).push( MaterialPageRoute( builder: (context) => ArticlePage(article: item!), ), ); if (!item.isRead) { await api .request() .markItem(item, ItemMarkType.read) .execute(); } } } catch (e) { print("Error while handling notification click: $e"); } }