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"
package="xyz.janderedev.feet">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<queries>
<intent>
<action android:name="android.intent.action.VIEW" />
@ -41,5 +42,12 @@
<meta-data
android:name="flutterEmbedding"
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>
</manifest>

View file

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

View file

@ -1,5 +1,6 @@
import 'dart:async';
import 'package:feet/notifications/tasks.dart';
import 'package:feet/widgets/centered_page_hint.dart';
import 'package:feet/widgets/filter_menu.dart';
import 'package:feet/widgets/homepage_post.dart';
@ -15,6 +16,7 @@ import 'api/api.dart';
const FETCH_MAX_POSTS = 1000;
void main() {
setupBackgroundTasks();
runApp(MyApp());
}
@ -27,6 +29,7 @@ class MyApp extends StatelessWidget {
Future<bool> _loadPrefs() async {
var prefs = await SharedPreferences.getInstance();
this.prefs = prefs;
api.sharedPrefs = prefs;
var apiKey = prefs.getString('apiKey'), apiUrl = prefs.getString('apiUrl');
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 dynamic_color
import flutter_local_notifications
import path_provider_macos
import share_plus
import shared_preferences_macos
@ -14,6 +15,7 @@ import wakelock_macos
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin"))
FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))

View file

@ -1,6 +1,14 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
args:
dependency: transitive
description:
name: args
sha256: "139d809800a412ebb26a3892da228b2d0ba36f0ef5d9a82166e5e52ec8d61611"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
async:
dependency: transitive
description:
@ -89,6 +97,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.5"
dbus:
dependency: transitive
description:
name: dbus
sha256: "3350efa144252eaa4264055dded4404a94b770cfe914f1d08c20953aee55cac2"
url: "https://pub.dev"
source: hosted
version: "0.5.4"
dynamic_color:
dependency: "direct main"
description:
@ -150,6 +166,30 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: transitive
description:
@ -344,6 +384,54 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: transitive
description:
@ -525,6 +613,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.4.16"
timezone:
dependency: transitive
description:
name: timezone
sha256: "57b35f6e8ef731f18529695bffc62f92c6189fac2e52c12d478dec1931afb66e"
url: "https://pub.dev"
source: hosted
version: "0.8.0"
tuple:
dependency: transitive
description:
@ -741,6 +837,14 @@ packages:
url: "https://pub.dev"
source: hosted
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:
dependency: transitive
description:

View file

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

View file

@ -7,12 +7,15 @@
#include "generated_plugin_registrant.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 <url_launcher_windows/url_launcher_windows.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
DynamicColorPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("DynamicColorPluginCApi"));
PermissionHandlerWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
SharePlusWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
UrlLauncherWindowsRegisterWithRegistrar(

View file

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