Implement background notifications, I think

This commit is contained in:
Lea 2023-02-01 22:35:16 +01:00
parent 3270f5d888
commit 4ec79e09e0
Signed by: Lea
GPG key ID: 1BAFFE8347019C42
9 changed files with 257 additions and 4 deletions

View file

@ -1,6 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="xyz.janderedev.feet"> package="xyz.janderedev.feet">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<queries> <queries>
<intent> <intent>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
@ -41,5 +42,12 @@
<meta-data <meta-data
android:name="flutterEmbedding" android:name="flutterEmbedding"
android:value="2" /> android:value="2" />
<receiver android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationBootReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"></action>
</intent-filter>
</receiver>
<receiver android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationReceiver" />
</application> </application>
</manifest> </manifest>

View file

@ -4,6 +4,7 @@ import 'dart:convert';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:crypto/crypto.dart'; import 'package:crypto/crypto.dart';
import 'package:shared_preferences/shared_preferences.dart';
part 'feed.dart'; part 'feed.dart';
part 'item.dart'; part 'item.dart';
@ -21,12 +22,16 @@ class FeverAPI {
String? apiKey; String? apiKey;
String? apiUrl; String? apiUrl;
late final APICache cache;
/// We use a shared HTTP client for all requests /// We use a shared HTTP client for all requests
final _httpClient = http.Client(); final _httpClient = http.Client();
late final APICache cache; /// If set, the `newestKnownPost` value will be updated automatically.
/// This is used for notifications.
SharedPreferences? sharedPrefs;
FeverAPI({this.apiKey, this.apiUrl}) { FeverAPI({this.apiKey, this.apiUrl, this.sharedPrefs}) {
cache = APICache(this); cache = APICache(this);
} }
@ -64,19 +69,20 @@ class FeverAPI {
} }
APIRequestBuilder request() { APIRequestBuilder request() {
return APIRequestBuilder(this, _httpClient); return APIRequestBuilder(this, _httpClient, sharedPrefs);
} }
} }
class APIRequestBuilder { class APIRequestBuilder {
final FeverAPI api; final FeverAPI api;
final SharedPreferences? _sharedPrefs;
final http.Client _httpClient; final http.Client _httpClient;
List<String> args = []; List<String> args = [];
Item? _markedItem; Item? _markedItem;
ItemMarkType? _markedItemAs; ItemMarkType? _markedItemAs;
APIRequestBuilder(this.api, this._httpClient); APIRequestBuilder(this.api, this._httpClient, this._sharedPrefs);
void _addArg(String arg) { void _addArg(String arg) {
if (!args.contains(arg)) args.add(arg); if (!args.contains(arg)) args.add(arg);
@ -173,12 +179,19 @@ class APIRequestBuilder {
} }
if (data.containsKey('items')) { if (data.containsKey('items')) {
var currentHighest = _sharedPrefs?.getInt('newestKnownPost') ?? 0;
List<Item> items = (data['items'] as List<dynamic>) List<Item> items = (data['items'] as List<dynamic>)
.map((json) => Item.fromJSON(api, json)) .map((json) => Item.fromJSON(api, json))
.toList(); .toList();
for (var item in items) { for (var item in items) {
if (item.id > currentHighest) {
currentHighest = item.id;
}
api.cache.items.set(item.id, item); api.cache.items.set(item.id, item);
_sharedPrefs?.setInt('newestKnownPost', currentHighest);
} }
} }

View file

@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:feet/notifications/tasks.dart';
import 'package:feet/widgets/centered_page_hint.dart'; import 'package:feet/widgets/centered_page_hint.dart';
import 'package:feet/widgets/filter_menu.dart'; import 'package:feet/widgets/filter_menu.dart';
import 'package:feet/widgets/homepage_post.dart'; import 'package:feet/widgets/homepage_post.dart';
@ -15,6 +16,7 @@ import 'api/api.dart';
const FETCH_MAX_POSTS = 1000; const FETCH_MAX_POSTS = 1000;
void main() { void main() {
setupBackgroundTasks();
runApp(MyApp()); runApp(MyApp());
} }
@ -27,6 +29,7 @@ class MyApp extends StatelessWidget {
Future<bool> _loadPrefs() async { Future<bool> _loadPrefs() async {
var prefs = await SharedPreferences.getInstance(); var prefs = await SharedPreferences.getInstance();
this.prefs = prefs; this.prefs = prefs;
api.sharedPrefs = prefs;
var apiKey = prefs.getString('apiKey'), apiUrl = prefs.getString('apiUrl'); var apiKey = prefs.getString('apiKey'), apiUrl = prefs.getString('apiUrl');
if (apiUrl != null && apiKey != null) { if (apiUrl != null && apiKey != null) {

View file

@ -0,0 +1,116 @@
import 'dart:convert';
import 'dart:math';
import 'package:feet/api/api.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('@mipmap/ic_launcher'),
),
);
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;
}
var api = FeverAPI(
apiKey: apiKey,
apiUrl: apiUrl,
sharedPrefs: sharedPreferences,
);
await api.request().withItems().sinceId(latest ?? 0).execute();
var items =
api.cache.items.getAll().values.where((post) => !post.isRead);
// 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<Object>? 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,
NotificationDetails(
android: AndroidNotificationDetails(
'new_posts',
'New posts',
autoCancel: true,
channelShowBadge: true,
importance: Importance.low,
largeIcon: iconData,
),
),
);
}
} catch (e) {
print(e);
return false;
}
return true;
}
return true;
});
}
void setupBackgroundTasks() async {
WidgetsFlutterBinding.ensureInitialized();
Workmanager().initialize(
callbackDispatcher,
isInDebugMode: kDebugMode,
);
await Permission.notification.request();
// Runs every 15 minutes
Workmanager().registerPeriodicTask(
'fetch-notifications',
'fetchNotifications',
initialDelay: const Duration(seconds: 30),
);
}

View file

@ -6,6 +6,7 @@ import FlutterMacOS
import Foundation import Foundation
import dynamic_color import dynamic_color
import flutter_local_notifications
import path_provider_macos import path_provider_macos
import share_plus import share_plus
import shared_preferences_macos import shared_preferences_macos
@ -14,6 +15,7 @@ import wakelock_macos
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin")) DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin"))
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))

View file

@ -1,6 +1,14 @@
# Generated by pub # Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile # See https://dart.dev/tools/pub/glossary#lockfile
packages: packages:
args:
dependency: transitive
description:
name: args
sha256: "139d809800a412ebb26a3892da228b2d0ba36f0ef5d9a82166e5e52ec8d61611"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
async: async:
dependency: transitive dependency: transitive
description: description:
@ -89,6 +97,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.5" version: "1.0.5"
dbus:
dependency: transitive
description:
name: dbus
sha256: "3350efa144252eaa4264055dded4404a94b770cfe914f1d08c20953aee55cac2"
url: "https://pub.dev"
source: hosted
version: "0.5.4"
dynamic_color: dynamic_color:
dependency: "direct main" dependency: "direct main"
description: description:
@ -150,6 +166,30 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.1" version: "2.0.1"
flutter_local_notifications:
dependency: "direct main"
description:
name: flutter_local_notifications
sha256: "11d12ac8c713d28a654036c6e61464ef039eb560296282b17b933682ede0b4b8"
url: "https://pub.dev"
source: hosted
version: "9.1.4"
flutter_local_notifications_linux:
dependency: transitive
description:
name: flutter_local_notifications_linux
sha256: "0824c36855ef1f159e1b957e4ff43dfd171372d6915424bc5004e277db4f1bc5"
url: "https://pub.dev"
source: hosted
version: "0.3.0"
flutter_local_notifications_platform_interface:
dependency: transitive
description:
name: flutter_local_notifications_platform_interface
sha256: "21bceee103a66a53b30ea9daf677f990e5b9e89b62f222e60dd241cd08d63d3a"
url: "https://pub.dev"
source: hosted
version: "5.0.0"
flutter_math_fork: flutter_math_fork:
dependency: transitive dependency: transitive
description: description:
@ -344,6 +384,54 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.3" version: "2.1.3"
pedantic:
dependency: transitive
description:
name: pedantic
sha256: "67fc27ed9639506c856c840ccce7594d0bdcd91bc8d53d6e52359449a1d50602"
url: "https://pub.dev"
source: hosted
version: "1.11.1"
permission_handler:
dependency: "direct main"
description:
name: permission_handler
sha256: "33c6a1253d1f95fd06fa74b65b7ba907ae9811f9d5c1d3150e51417d04b8d6a8"
url: "https://pub.dev"
source: hosted
version: "10.2.0"
permission_handler_android:
dependency: transitive
description:
name: permission_handler_android
sha256: "8028362b40c4a45298f1cbfccd227c8dd6caf0e27088a69f2ba2ab15464159e2"
url: "https://pub.dev"
source: hosted
version: "10.2.0"
permission_handler_apple:
dependency: transitive
description:
name: permission_handler_apple
sha256: "9c370ef6a18b1c4b2f7f35944d644a56aa23576f23abee654cf73968de93f163"
url: "https://pub.dev"
source: hosted
version: "9.0.7"
permission_handler_platform_interface:
dependency: transitive
description:
name: permission_handler_platform_interface
sha256: "68abbc472002b5e6dfce47fe9898c6b7d8328d58b5d2524f75e277c07a97eb84"
url: "https://pub.dev"
source: hosted
version: "3.9.0"
permission_handler_windows:
dependency: transitive
description:
name: permission_handler_windows
sha256: f67cab14b4328574938ecea2db3475dad7af7ead6afab6338772c5f88963e38b
url: "https://pub.dev"
source: hosted
version: "0.1.2"
petitparser: petitparser:
dependency: transitive dependency: transitive
description: description:
@ -525,6 +613,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.4.16" version: "0.4.16"
timezone:
dependency: transitive
description:
name: timezone
sha256: "57b35f6e8ef731f18529695bffc62f92c6189fac2e52c12d478dec1931afb66e"
url: "https://pub.dev"
source: hosted
version: "0.8.0"
tuple: tuple:
dependency: transitive dependency: transitive
description: description:
@ -741,6 +837,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.1.3" version: "3.1.3"
workmanager:
dependency: "direct main"
description:
name: workmanager
sha256: e0be7e35d644643f164ee45d2ce14414f0e0fdde19456aa66065f35a0b1d2ea1
url: "https://pub.dev"
source: hosted
version: "0.5.1"
xdg_directories: xdg_directories:
dependency: transitive dependency: transitive
description: description:

View file

@ -42,6 +42,9 @@ dependencies:
share_plus: ^6.3.0 share_plus: ^6.3.0
flutter_html: ^2.2.1 flutter_html: ^2.2.1
url_launcher: ^6.1.7 url_launcher: ^6.1.7
workmanager: ^0.5.1
flutter_local_notifications: ^9.1.4
permission_handler: ^10.2.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

View file

@ -7,12 +7,15 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <dynamic_color/dynamic_color_plugin_c_api.h> #include <dynamic_color/dynamic_color_plugin_c_api.h>
#include <permission_handler_windows/permission_handler_windows_plugin.h>
#include <share_plus/share_plus_windows_plugin_c_api.h> #include <share_plus/share_plus_windows_plugin_c_api.h>
#include <url_launcher_windows/url_launcher_windows.h> #include <url_launcher_windows/url_launcher_windows.h>
void RegisterPlugins(flutter::PluginRegistry* registry) { void RegisterPlugins(flutter::PluginRegistry* registry) {
DynamicColorPluginCApiRegisterWithRegistrar( DynamicColorPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("DynamicColorPluginCApi")); registry->GetRegistrarForPlugin("DynamicColorPluginCApi"));
PermissionHandlerWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
SharePlusWindowsPluginCApiRegisterWithRegistrar( SharePlusWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
UrlLauncherWindowsRegisterWithRegistrar( UrlLauncherWindowsRegisterWithRegistrar(

View file

@ -4,6 +4,7 @@
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
dynamic_color dynamic_color
permission_handler_windows
share_plus share_plus
url_launcher_windows url_launcher_windows
) )