1
0
mirror of https://github.com/go-vikunja/app synced 2024-06-02 18:49:47 +00:00

added workmanager for background sync

moved all notification stuff to own class for a cleaner flow
This commit is contained in:
Benimautner 2023-01-07 22:50:32 +01:00
parent f84ad663c5
commit debab1d689
5 changed files with 173 additions and 120 deletions

View File

@ -47,6 +47,7 @@ class VikunjaGlobalState extends State<VikunjaGlobal> {
bool expired = false;
late Client _client;
UserService? _newUserService;
NotificationClass _notificationClass = NotificationClass();
User? get currentUser => _currentUser;
@ -74,13 +75,10 @@ class VikunjaGlobalState extends State<VikunjaGlobal> {
ListService get listService => new ListAPIService(client, _storage);
notifs.FlutterLocalNotificationsPlugin get notificationsPlugin => new notifs.FlutterLocalNotificationsPlugin();
TaskServiceOptions get taskServiceOptions => new TaskServiceOptions();
NotificationClass get notifications => new NotificationClass();
NotificationClass get notifications => _notificationClass;
notifs.NotificationAppLaunchDetails? notifLaunch;
LabelService get labelService => new LabelAPIService(client);
@ -89,23 +87,6 @@ class VikunjaGlobalState extends State<VikunjaGlobal> {
LabelTaskBulkAPIService get labelTaskBulkService =>
new LabelTaskBulkAPIService(client);
var androidSpecificsDueDate = notifs.AndroidNotificationDetails(
"Vikunja1",
"Due Date Notifications",
channelDescription: "description",
icon: 'vikunja_notification_logo',
importance: notifs.Importance.high
);
var androidSpecificsReminders = notifs.AndroidNotificationDetails(
"Vikunja2",
"Reminder Notifications",
channelDescription: "description",
icon: 'vikunja_notification_logo',
importance: notifs.Importance.high
);
late notifs.IOSNotificationDetails iOSSpecifics;
late notifs.NotificationDetails platformChannelSpecificsDueDate;
late notifs.NotificationDetails platformChannelSpecificsReminders;
late String currentTimeZone;
@ -115,8 +96,8 @@ class VikunjaGlobalState extends State<VikunjaGlobal> {
{
if(duration.inMinutes > 0) {
Workmanager().registerPeriodicTask(
"update-tasks", "update-tasks", frequency: duration,
initialDelay: Duration.zero, inputData: {})
"update-tasks", "update-tasks", frequency: duration, constraints: Constraints(networkType: NetworkType.connected),
initialDelay: Duration(seconds: 15), inputData: {"client_token": client.token, "client_base": client.base})
}
});
});
@ -130,19 +111,12 @@ class VikunjaGlobalState extends State<VikunjaGlobal> {
_newUserService = UserAPIService(client);
_loadCurrentUser();
tz.initializeTimeZones();
iOSSpecifics = notifs.IOSNotificationDetails();
platformChannelSpecificsDueDate = notifs.NotificationDetails(
android: androidSpecificsDueDate, iOS: iOSSpecifics);
platformChannelSpecificsReminders = notifs.NotificationDetails(
android: androidSpecificsReminders, iOS: iOSSpecifics);
notificationInitializer();
notifications.notificationInitializer();
settingsManager.getVersionNotifications().then((value) {
if(value == "1") {
versionChecker.postVersionCheckSnackbar();
}
});
updateWorkmanagerDuration();
}
void changeUser(User newUser, {String? token, String? base}) async {
@ -164,53 +138,14 @@ class VikunjaGlobalState extends State<VikunjaGlobal> {
// Set current user in storage
await _storage.write(key: 'currentUser', value: newUser.id.toString());
client.configure(token: token, base: base, authenticated: true);
updateWorkmanagerDuration();
setState(() {
_currentUser = newUser;
_loading = false;
});
}
void notificationInitializer() async {
currentTimeZone = await FlutterTimezone.getLocalTimezone();
notifLaunch = await notificationsPlugin.getNotificationAppLaunchDetails();
await notifications.initNotifications(notificationsPlugin);
requestIOSPermissions(notificationsPlugin);
}
Future<void> scheduleDueNotifications() async {
final tasks = await taskService.getAll();
if(tasks == null) {
dev.log("did not receive tasks on notification update");
return;
}
await notificationsPlugin.cancelAll();
for (final task in tasks) {
for (final reminder in task.reminderDates) {
scheduleNotification(
"Reminder",
"This is your reminder for '" + task.title + "'",
notificationsPlugin,
reminder,
currentTimeZone,
platformChannelSpecificsReminders,
id: (reminder.millisecondsSinceEpoch / 1000).floor(),
);
}
if (task.hasDueDate) {
scheduleNotification(
"Due Reminder",
"The task '" + task.title + "' is due.",
notificationsPlugin,
task.dueDate!,
currentTimeZone,
platformChannelSpecificsDueDate,
id: task.id,
);
}
}
print("notifications scheduled successfully");
}
void logoutUser(BuildContext context) {
_storage.deleteAll().then((_) {
@ -265,6 +200,7 @@ class VikunjaGlobalState extends State<VikunjaGlobal> {
} catch (otherExceptions) {
loadedCurrentUser = User(id: int.parse(currentUser), username: '');
}
updateWorkmanagerDuration();
setState(() {
_currentUser = loadedCurrentUser;
_loading = false;
@ -277,7 +213,7 @@ class VikunjaGlobalState extends State<VikunjaGlobal> {
return new Center(child: new CircularProgressIndicator());
}
if(client.authenticated) {
scheduleDueNotifications();
notifications.scheduleDueNotifications(taskService);
}
return new _VikunjaGlobalInherited(
data: this,

View File

@ -1,16 +1,28 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:vikunja_app/api/task_implementation.dart';
import 'package:vikunja_app/api/client.dart';
import 'package:vikunja_app/service/services.dart';
import 'package:workmanager/workmanager.dart';
import 'package:vikunja_app/global.dart';
import 'package:vikunja_app/pages/home.dart';
import 'package:vikunja_app/pages/user/login.dart';
import 'package:vikunja_app/theme/theme.dart';
import 'package:http/http.dart';
import 'package:timezone/data/latest_all.dart' as tz;
import 'package:http/http.dart' as httpl;
import 'managers/notifications.dart';
class IgnoreCertHttpOverrides extends HttpOverrides {
bool ignoreCerts = false;
IgnoreCertHttpOverrides(bool _ignore) {ignoreCerts = _ignore;}
IgnoreCertHttpOverrides(bool _ignore) {
ignoreCerts = _ignore;
}
@override
HttpClient createHttpClient(SecurityContext? context) {
return super.createHttpClient(context)
@ -22,35 +34,59 @@ class IgnoreCertHttpOverrides extends HttpOverrides {
void callbackDispatcher() {
Workmanager().executeTask((task, inputData) {
print("Native called background task: $task"); //simpleTask will be emitted here.
if(task == "update-tasks") {
//TODO
if (task == "update-tasks" && inputData != null) {
Client client = Client(null,
token: inputData["client_token"],
base: inputData["client_base"],
authenticated: true);
tz.initializeTimeZones();
return SettingsManager(new FlutterSecureStorage())
.getIgnoreCertificates()
.then((value) async {
print("ignoring: $value");
client.reload_ignore_certs(value == "1");
TaskAPIService taskService = TaskAPIService(client);
NotificationClass nc = NotificationClass();
await nc.notificationInitializer();
return nc
.scheduleDueNotifications(taskService)
.then((value) => Future.value(true));
});
} else {
return Future.value(true);
}
return get(Uri.parse("https://webhook.site/"), headers: {"task":"$task", "data":"$inputData"}).then((value) => Future.value(true));
});
}
void main() {
WidgetsFlutterBinding.ensureInitialized();
Workmanager().initialize(callbackDispatcher, isInDebugMode: true);
runApp(VikunjaGlobal(
child: new VikunjaApp(home: HomePage(), key: UniqueKey(),),
login: new VikunjaApp(home: LoginPage(), key: UniqueKey(),)));
runApp(VikunjaGlobal(
child: new VikunjaApp(
home: HomePage(),
key: UniqueKey(),
),
login: new VikunjaApp(
home: LoginPage(),
key: UniqueKey(),
)));
}
class VikunjaApp extends StatelessWidget {
final Widget home;
const VikunjaApp({Key? key, required this.home}) : super(key: key);
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Vikunja',
theme: buildVikunjaTheme(),
darkTheme: buildVikunjaDarkTheme(),
scaffoldMessengerKey: VikunjaGlobal.of(context).snackbarKey, // <= this
scaffoldMessengerKey: VikunjaGlobal.of(context).snackbarKey,
// <= this
home: this.home,
);
}

View File

@ -2,24 +2,51 @@
import 'dart:math';
import 'package:flutter_timezone/flutter_timezone.dart';
import 'package:timezone/timezone.dart' as tz;
import 'package:flutter_local_notifications/flutter_local_notifications.dart'as notifs;
import 'package:rxdart/subjects.dart' as rxSub;
import 'package:vikunja_app/service/services.dart';
class NotificationClass{
class NotificationClass {
final int? id;
final String? title;
final String? body;
final String? payload;
late String currentTimeZone;
notifs.NotificationAppLaunchDetails? notifLaunch;
notifs.FlutterLocalNotificationsPlugin get notificationsPlugin => new notifs.FlutterLocalNotificationsPlugin();
var androidSpecificsDueDate = notifs.AndroidNotificationDetails(
"Vikunja1",
"Due Date Notifications",
channelDescription: "description",
icon: 'vikunja_notification_logo',
importance: notifs.Importance.high
);
var androidSpecificsReminders = notifs.AndroidNotificationDetails(
"Vikunja2",
"Reminder Notifications",
channelDescription: "description",
icon: 'vikunja_notification_logo',
importance: notifs.Importance.high
);
late notifs.IOSNotificationDetails iOSSpecifics;
late notifs.NotificationDetails platformChannelSpecificsDueDate;
late notifs.NotificationDetails platformChannelSpecificsReminders;
NotificationClass({this.id, this.body, this.payload, this.title});
final rxSub.BehaviorSubject<NotificationClass> didReceiveLocalNotificationSubject =
final rxSub.BehaviorSubject<
NotificationClass> didReceiveLocalNotificationSubject =
rxSub.BehaviorSubject<NotificationClass>();
final rxSub.BehaviorSubject<String> selectNotificationSubject =
rxSub.BehaviorSubject<String>();
Future<void> initNotifications(notifs.FlutterLocalNotificationsPlugin notifsPlugin) async {
Future<void> _initNotifications() async {
var initializationSettingsAndroid =
notifs.AndroidInitializationSettings('vikunja_logo');
var initializationSettingsIOS = notifs.IOSInitializationSettings(
@ -29,11 +56,12 @@ class NotificationClass{
onDidReceiveLocalNotification:
(int? id, String? title, String? body, String? payload) async {
didReceiveLocalNotificationSubject
.add(NotificationClass(id: id, title: title, body: body, payload: payload));
.add(NotificationClass(
id: id, title: title, body: body, payload: payload));
});
var initializationSettings = notifs.InitializationSettings(
android: initializationSettingsAndroid, iOS: initializationSettingsIOS);
await notifsPlugin.initialize(initializationSettings,
await notificationsPlugin.initialize(initializationSettings,
onSelectNotification: (String? payload) async {
if (payload != null) {
print('notification payload: ' + payload);
@ -42,32 +70,88 @@ class NotificationClass{
});
print("Notifications initialised successfully");
}
}
Future<void> scheduleNotification(String title, String description,
notifs.FlutterLocalNotificationsPlugin notifsPlugin,
DateTime scheduledTime, String currentTimeZone, notifs.NotificationDetails platformChannelSpecifics, {int? id}) async {
if(id == null)
id = Random().nextInt(1000000);
// TODO: move to setup
tz.TZDateTime time = tz.TZDateTime.from(scheduledTime,tz.getLocation(currentTimeZone));
if(time.difference(tz.TZDateTime.now(tz.getLocation(currentTimeZone))) < Duration.zero)
return;
await notifsPlugin.zonedSchedule(id, title, description,
time, platformChannelSpecifics, androidAllowWhileIdle: true, uiLocalNotificationDateInterpretation: notifs.UILocalNotificationDateInterpretation.wallClockTime); // This literally schedules the notification
}
Future<void> notificationInitializer() async {
iOSSpecifics = notifs.IOSNotificationDetails();
platformChannelSpecificsDueDate = notifs.NotificationDetails(
android: androidSpecificsDueDate, iOS: iOSSpecifics);
platformChannelSpecificsReminders = notifs.NotificationDetails(
android: androidSpecificsReminders, iOS: iOSSpecifics);
currentTimeZone = await FlutterTimezone.getLocalTimezone();
notifLaunch = await notificationsPlugin.getNotificationAppLaunchDetails();
await _initNotifications();
requestIOSPermissions();
return Future.value();
}
void sendTestNotification(notifs.FlutterLocalNotificationsPlugin notifsPlugin, notifs.NotificationDetails platformChannelSpecifics) {
notifsPlugin.show(Random().nextInt(10000000), "Test Notification", "This is a test notification", platformChannelSpecifics);
}
Future<void> scheduleNotification(String title, String description,
notifs.FlutterLocalNotificationsPlugin notifsPlugin,
DateTime scheduledTime, String currentTimeZone,
notifs.NotificationDetails platformChannelSpecifics, {int? id}) async {
if (id == null)
id = Random().nextInt(1000000);
// TODO: move to setup
tz.TZDateTime time = tz.TZDateTime.from(
scheduledTime, tz.getLocation(currentTimeZone));
if (time.difference(tz.TZDateTime.now(tz.getLocation(currentTimeZone))) <
Duration.zero)
return;
await notifsPlugin.zonedSchedule(id, title, description,
time, platformChannelSpecifics, androidAllowWhileIdle: true,
uiLocalNotificationDateInterpretation: notifs
.UILocalNotificationDateInterpretation
.wallClockTime); // This literally schedules the notification
}
void sendTestNotification() {
notificationsPlugin.show(Random().nextInt(10000000), "Test Notification",
"This is a test notification", platformChannelSpecificsReminders);
}
void requestIOSPermissions(
notifs.FlutterLocalNotificationsPlugin notifsPlugin) {
notifsPlugin.resolvePlatformSpecificImplementation<notifs.IOSFlutterLocalNotificationsPlugin>()
?.requestPermissions(
alert: true,
badge: true,
sound: true,
);
}
void requestIOSPermissions() {
notificationsPlugin.resolvePlatformSpecificImplementation<
notifs.IOSFlutterLocalNotificationsPlugin>()
?.requestPermissions(
alert: true,
badge: true,
sound: true,
);
}
Future<void> scheduleDueNotifications(TaskService taskService) async {
final tasks = await taskService.getAll();
if (tasks == null) {
print("did not receive tasks on notification update");
return;
}
await notificationsPlugin.cancelAll();
for (final task in tasks) {
for (final reminder in task.reminderDates) {
scheduleNotification(
"Reminder",
"This is your reminder for '" + task.title + "'",
notificationsPlugin,
reminder,
await FlutterTimezone.getLocalTimezone(),
platformChannelSpecificsReminders,
id: (reminder.millisecondsSinceEpoch / 1000).floor(),
);
}
if (task.hasDueDate) {
scheduleNotification(
"Due Reminder",
"The task '" + task.title + "' is due.",
notificationsPlugin,
task.dueDate!,
currentTimeZone,
platformChannelSpecificsDueDate,
id: task.id,
);
}
}
print("notifications scheduled successfully");
}
}

View File

@ -173,7 +173,7 @@ class LandingPageState extends State<LandingPage>
_list = [];
landingPageStatus = PageStatus.loading;
// FIXME: loads and reschedules tasks each time list is updated
VikunjaGlobal.of(context).scheduleDueNotifications();
VikunjaGlobal.of(context).notifications.scheduleDueNotifications(VikunjaGlobal.of(context).taskService);
return VikunjaGlobal.of(context)
.taskService
.getByOptions(VikunjaGlobal.of(context).taskServiceOptions)

View File

@ -127,10 +127,7 @@ class SettingsPageState extends State<SettingsPage> {
: ListTile(title: Text("...")),
TextButton(
onPressed: () {
sendTestNotification(
VikunjaGlobal.of(context).notificationsPlugin,
VikunjaGlobal.of(context)
.platformChannelSpecificsReminders);
VikunjaGlobal.of(context).notifications.sendTestNotification();
},
child: Text("Send test notification")),
TextButton(