From 28fe3fc3c09cc4de62194585330c36004194f2d5 Mon Sep 17 00:00:00 2001 From: Nutcake Date: Thu, 4 May 2023 13:13:24 +0200 Subject: [PATCH] Fix notification toggle and add check interval selector --- lib/clients/api_client.dart | 1 + lib/clients/neos_hub.dart | 22 +++++++++++-- lib/clients/settings_client.dart | 1 + lib/main.dart | 18 +++++++++- lib/models/authentication_data.dart | 8 +++++ lib/models/settings.dart | 4 ++- lib/widgets/friends_list.dart | 1 - lib/widgets/login_screen.dart | 2 +- lib/widgets/settings_page.dart | 51 +++++++++++++++-------------- 9 files changed, 77 insertions(+), 31 deletions(-) diff --git a/lib/clients/api_client.dart b/lib/clients/api_client.dart index daededb..fe4df9f 100644 --- a/lib/clients/api_client.dart +++ b/lib/clients/api_client.dart @@ -22,6 +22,7 @@ class ApiClient { final AuthenticationData _authenticationData; + AuthenticationData get authenticationData => _authenticationData; String get userId => _authenticationData.userId; bool get isAuthenticated => _authenticationData.isAuthenticated; diff --git a/lib/clients/neos_hub.dart b/lib/clients/neos_hub.dart index 4464d7c..ac887ee 100644 --- a/lib/clients/neos_hub.dart +++ b/lib/clients/neos_hub.dart @@ -1,12 +1,14 @@ import 'dart:convert'; import 'dart:io'; import 'package:contacts_plus_plus/apis/message_api.dart'; +import 'package:contacts_plus_plus/models/authentication_data.dart'; import 'package:http/http.dart' as http; import 'package:contacts_plus_plus/clients/api_client.dart'; import 'package:contacts_plus_plus/config.dart'; import 'package:contacts_plus_plus/models/message.dart'; import 'package:logging/logging.dart'; +import 'package:workmanager/workmanager.dart'; enum EventType { unknown, @@ -31,10 +33,12 @@ class NeosHub { static const String eofChar = ""; static const String _negotiationPacket = "{\"protocol\":\"json\", \"version\":1}$eofChar"; static const List _reconnectTimeoutsSeconds = [0, 5, 10, 20, 60]; + static const String taskName = "periodic-unread-check"; final ApiClient _apiClient; final Map _messageCache = {}; final Map _updateListeners = {}; final Logger _logger = Logger("NeosHub"); + final Workmanager _workmanager = Workmanager(); WebSocket? _wsChannel; bool _isConnecting = false; @@ -58,13 +62,27 @@ class NeosHub { return cache; } - Future checkUnreads() async { - final unreads = await MessageApi.getUserMessages(_apiClient, unreadOnly: true); + static Future backgroundCheckUnreads(Map? inputData) async { + if (inputData == null) return; + final auth = AuthenticationData.fromMap(inputData); + final unreads = await MessageApi.getUserMessages(ApiClient(authenticationData: auth), unreadOnly: true); for (var message in unreads) { throw UnimplementedError(); } } + Future _updateNotificationTask(int minuteInterval) async { + final auth = _apiClient.authenticationData; + if (!auth.isAuthenticated) throw "Unauthenticated"; + await _workmanager.cancelByUniqueName(taskName); + _workmanager.registerPeriodicTask( + taskName, + taskName, + frequency: Duration(minutes: minuteInterval), + inputData: auth.toMap(), + ); + } + void _onDisconnected(error) { _logger.warning("Neos Hub connection died with error '$error', reconnecting..."); start(); diff --git a/lib/clients/settings_client.dart b/lib/clients/settings_client.dart index fe8892f..8ba4277 100644 --- a/lib/clients/settings_client.dart +++ b/lib/clients/settings_client.dart @@ -18,6 +18,7 @@ class SettingsClient { _currentSettings = Settings.fromMap(jsonDecode(data)); } catch (_) { _storage.delete(key: _settingsKey); + rethrow; } } diff --git a/lib/main.dart b/lib/main.dart index fa96542..53b589c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,16 +1,21 @@ import 'dart:developer'; +import 'package:contacts_plus_plus/clients/neos_hub.dart'; import 'package:contacts_plus_plus/clients/settings_client.dart'; import 'package:contacts_plus_plus/widgets/friends_list.dart'; import 'package:contacts_plus_plus/widgets/login_screen.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_phoenix/flutter_phoenix.dart'; import 'package:logging/logging.dart'; +import 'package:workmanager/workmanager.dart'; import 'clients/api_client.dart'; import 'models/authentication_data.dart'; void main() async { + await Workmanager().initialize( + callbackDispatcher, // The top level function, aka callbackDispatcher + isInDebugMode: true // If enabled it will post a notification whenever the task is running. Handy for debugging tasks + ); Logger.root.onRecord.listen((event) => log(event.message, name: event.loggerName)); WidgetsFlutterBinding.ensureInitialized(); final settingsClient = SettingsClient(); @@ -18,6 +23,17 @@ void main() async { runApp(Phoenix(child: ContactsPlusPlus(settingsClient: settingsClient,))); } +@pragma('vm:entry-point') // Mandatory if the App is obfuscated or using Flutter 3.1+ +void callbackDispatcher() { + Workmanager().executeTask((String task, Map? inputData) async { + debugPrint("Native called background task: $task"); //simpleTask will be emitted here. + if (task == NeosHub.taskName) { + final unreads = NeosHub.backgroundCheckUnreads(inputData); + } + return Future.value(true); + }); +} + class ContactsPlusPlus extends StatefulWidget { const ContactsPlusPlus({required this.settingsClient, super.key}); diff --git a/lib/models/authentication_data.dart b/lib/models/authentication_data.dart index 8891daa..af3e033 100644 --- a/lib/models/authentication_data.dart +++ b/lib/models/authentication_data.dart @@ -24,4 +24,12 @@ class AuthenticationData { Map get authorizationHeader => { "Authorization": "neos $userId:$token" }; + + Map toMap() { + return { + "userId": userId, + "token": token, + "secretMachineId": secretMachineId, + }; + } } \ No newline at end of file diff --git a/lib/models/settings.dart b/lib/models/settings.dart index 7e35257..979497a 100644 --- a/lib/models/settings.dart +++ b/lib/models/settings.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + class SettingsEntry { final T? value; final T deflt; @@ -6,7 +8,7 @@ class SettingsEntry { factory SettingsEntry.fromMap(Map map) { return SettingsEntry( - value: map["value"] as T, + value: jsonDecode(map["value"]) as T?, deflt: map["default"], ); } diff --git a/lib/widgets/friends_list.dart b/lib/widgets/friends_list.dart index 331a3e1..448b686 100644 --- a/lib/widgets/friends_list.dart +++ b/lib/widgets/friends_list.dart @@ -10,7 +10,6 @@ import 'package:contacts_plus_plus/widgets/expanding_input_fab.dart'; import 'package:contacts_plus_plus/widgets/friend_list_tile.dart'; import 'package:contacts_plus_plus/widgets/settings_page.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_local_notifications/flutter_local_notifications.dart' as fln; class FriendsList extends StatefulWidget { const FriendsList({super.key}); diff --git a/lib/widgets/login_screen.dart b/lib/widgets/login_screen.dart index d32e7c8..37d564b 100644 --- a/lib/widgets/login_screen.dart +++ b/lib/widgets/login_screen.dart @@ -104,7 +104,7 @@ class _LoginScreenState extends State { final requestResult = await notificationManager.resolvePlatformSpecificImplementation< AndroidFlutterLocalNotificationsPlugin>() ?.requestPermission(); - await settingsClient.changeSettings(settingsClient.currentSettings.copyWith(notificationsDenied: requestResult)); + await settingsClient.changeSettings(settingsClient.currentSettings.copyWith(notificationsDenied: requestResult == null ? null : !requestResult)); }, child: const Text("Yes"), ) diff --git a/lib/widgets/settings_page.dart b/lib/widgets/settings_page.dart index 7bfdb52..2b958f4 100644 --- a/lib/widgets/settings_page.dart +++ b/lib/widgets/settings_page.dart @@ -1,10 +1,19 @@ import 'package:contacts_plus_plus/clients/api_client.dart'; import 'package:flutter/material.dart'; import 'package:url_launcher/url_launcher.dart'; +import 'package:workmanager/workmanager.dart'; class SettingsPage extends StatelessWidget { const SettingsPage({super.key}); - + static const Map _intervalSelections = { + 5: "5 Minutes", + 15: "15 Minutes", + 30: "30 Minutes", + 60: "1 Hour", + 120: "2 Hours", + 300: "6 Hours", + 600: "12 Hours", + }; @override Widget build(BuildContext context) { @@ -23,33 +32,25 @@ class SettingsPage extends StatelessWidget { children: [ const ListSectionHeader(name: "Notifications"), BooleanSettingsTile( - title: "Send Notifications", - initialState: sClient.currentSettings.notificationsDenied.valueOrDefault, - onChanged: (value) async => await sClient.changeSettings(sClient.currentSettings.copyWith(notificationsDenied: value)), + title: "Enable Notifications", + initialState: !sClient.currentSettings.notificationsDenied.valueOrDefault, + onChanged: (value) async => await sClient.changeSettings(sClient.currentSettings.copyWith(notificationsDenied: !value)), ), ListTile( - trailing: const Icon(Icons.logout), - title: const Text("Sign out"), + trailing: StatefulBuilder( + builder: (context, setState) { + return DropdownButton( + items: _intervalSelections.keys.map((e) => DropdownMenuItem(value: e, child: Text("${_intervalSelections[e]}"))).toList(), + value: sClient.currentSettings.unreadCheckIntervalMinutes.valueOrDefault, + onChanged: (int? value) async { + await sClient.changeSettings(sClient.currentSettings.copyWith(unreadCheckIntervalMinutes: value)); + setState(() {}); + }, + ); + } + ), + title: const Text("Check Interval"), onTap: () { - showDialog( - context: context, - builder: (context) => - AlertDialog( - title: Text("Are you sure you want to sign out?", style: Theme - .of(context) - .textTheme - .titleLarge,), - actions: [ - TextButton(onPressed: () => Navigator.of(context).pop(), child: const Text("No")), - TextButton( - onPressed: () async { - await ClientHolder.of(context).apiClient.logout(context); - }, - child: const Text("Yes"), - ), - ], - ), - ); }, ), const ListSectionHeader(name: "Other"),