diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 6ac669c..51fdbb0 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -1,6 +1,7 @@
+
@@ -41,5 +42,12 @@
+
+
+
+
+
+
+
diff --git a/lib/api/api.dart b/lib/api/api.dart
index 39264aa..bfb73c3 100644
--- a/lib/api/api.dart
+++ b/lib/api/api.dart
@@ -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 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- items = (data['items'] as List)
.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);
}
}
diff --git a/lib/main.dart b/lib/main.dart
index ea60b0e..d24a241 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -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 _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) {
diff --git a/lib/notifications/tasks.dart b/lib/notifications/tasks.dart
new file mode 100644
index 0000000..e948291
--- /dev/null
+++ b/lib/notifications/tasks.dart
@@ -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