From 8189adab68501be1c47c04796b5cb1fb4277f34d Mon Sep 17 00:00:00 2001 From: kmac <135567+kmac@users.noreply.github.com> Date: Sat, 13 Nov 2021 21:02:31 -0500 Subject: [PATCH] Add nextAlarm property to Hive - The nextAlarm property is used with the enabled property to track service state over restarts --- lib/components/alarmservice.dart | 4 +- lib/components/datastore.dart | 68 +++++++++++++++++--------------- lib/components/scheduler.dart | 56 ++++++++++++++++---------- lib/main.dart | 2 +- lib/screens/mindfulnotifier.dart | 25 ++++++------ lib/screens/schedulesview.dart | 12 +++--- 6 files changed, 95 insertions(+), 72 deletions(-) diff --git a/lib/components/alarmservice.dart b/lib/components/alarmservice.dart index 618f582..03d036a 100644 --- a/lib/components/alarmservice.dart +++ b/lib/components/alarmservice.dart @@ -33,9 +33,9 @@ Future initializeAlarmService({bool bootstrap: false}) async { return alarmServiceAlreadyRunning; } - // TODO the underlying issue is that we can't query android alarm manager + // The underlying issue is that we can't query android alarm manager // to see if we have outstanding alarms. - // We could store next alarm time in hive db and compare + // We store the next alarm time in hive db and compare // with current time to see if we should have an alarm scheduled try { diff --git a/lib/components/datastore.dart b/lib/components/datastore.dart index 754190d..a65d764 100644 --- a/lib/components/datastore.dart +++ b/lib/components/datastore.dart @@ -13,6 +13,9 @@ var logger = createLogger('datastore'); final Random random = Random(); +bool testMigrateApp = false; +bool testMigrateSched = false; + // A list for the initial json string. Each entry has keys: text, enabled, tag, weight // // Idea: add optional weight to support weighing reminders differently @@ -94,18 +97,6 @@ const List> defaultJsonReminderMap = [ "enabled": true, "tag": "${Reminder.defaultTagName}" }, - // { - // "text": - // "Two is very two, two is very too. This is very long. This is very long. This is very long. This is very long. This is very long. This is very long. This is very long. This is very long. This is very long. This is very long. This is very long. This is very long. This is very long. This is very long. This is very long. This is very long. This is very long. This is very long. This is very long. This is very long. This is very long. This is very long. This is very long.", - // "enabled": true, - // "tag": "${Reminder.defaultTagName}" - // }, - // { - // "text": - // "This is very long. This is very long. This is very long. This is very long. This is very long. This is very long. This is very long. This is very long. This is very long. This is very long. This is very long. This is very long. This is very long. This is very long. This is very long. This is very long. This is very long. This is very long. This is very long. This is very long. This is very long. This is very long. This is very long.", - // "enabled": true, - // "tag": "${Reminder.defaultTagName}" - // }, ]; Future checkMigrateSharedPreferences(var box, @@ -116,7 +107,7 @@ Future checkMigrateSharedPreferences(var box, // Check if we need to convert from SharedPreferences SharedPreferences prefs = await SharedPreferences.getInstance(); if (prefs.getKeys().length > 0) { - logger.i("Migrating SharedPreferences to Hive"); + logger.i("Migrating SharedPreferences to Hive box ${box.name}"); for (String key in prefs.getKeys()) { bool ignoreKey = false; @@ -126,12 +117,11 @@ Future checkMigrateSharedPreferences(var box, ignoreKey = true; } if (ignoreKey) { - logger.i("Ignoring $key"); + logger.i("${box.name}: Ignoring $key"); } else { var value = prefs.get(key); - logger.i("Converting $key = $value"); + logger.i("${box.name}: Converting $key = $value"); box.put(key, value); - // TODO uncomment this when ready: // logger.i("Removing key: $key from SharedPreferences"); // prefs.remove(key); } @@ -154,7 +144,7 @@ class AppDataStore { static AppDataStore _instance; - var _box; + Box _box; /// Public factory static Future getInstance() async { @@ -175,10 +165,9 @@ class AppDataStore { await Hive.initFlutter(); _box = await Hive.openBox('appdata'); - bool testMigrate = false; - if (testMigrate) { + if (testMigrateApp) { await _box.clear(); - testMigrate = false; + testMigrateApp = false; } await checkMigrateSharedPreferences(_box, includeKeys: [ ScheduleDataStoreBase.themeKey, @@ -233,6 +222,7 @@ abstract class ScheduleDataStoreBase { static const String quietHoursEndMinuteKey = 'quietHoursEndMinute'; static const String notifyQuietHoursKey = 'notifyQuietHours'; static const String reminderMessageKey = 'reminderMessage'; + static const String nextAlarmKey = 'nextAlarm'; // replaced by jsonReminders : static const String remindersKeyDeprecated = 'reminders'; @@ -258,7 +248,7 @@ abstract class ScheduleDataStoreBase { static const int defaultQuietHoursEndMinute = 0; static const bool defaultNotifyQuietHours = false; static const String defaultReminderMessage = 'Not Enabled'; - static const String defaultInfoMessage = 'Uninitialized'; + static const String defaultInfoMessage = 'Disabled'; static const String defaultControlMessage = ''; static const String defaultTheme = 'Default'; static const String defaultBellId = 'bell1'; @@ -287,6 +277,7 @@ abstract class ScheduleDataStoreBase { String get controlMessage; String get bellId; String get customBellPath; + String get nextAlarm; // this is only here to support old reminders format bool reminderExists(String reminderText, {List jsonReminderList}) { @@ -298,6 +289,11 @@ abstract class ScheduleDataStoreBase { } return false; } + + String randomReminder({String tag}) { + Reminders reminders = Reminders.fromJson(jsonReminders); + return reminders.randomReminder(tag: tag); + } } /// In-memory data store. Created from the scheduler/alarm service and passed @@ -327,6 +323,7 @@ class InMemoryScheduleDataStore extends ScheduleDataStoreBase { String controlMessage; String bellId; String customBellPath; + String nextAlarm; InMemoryScheduleDataStore.fromDS(ScheduleDataStore ds) : this.enabled = ds.enabled, @@ -351,7 +348,8 @@ class InMemoryScheduleDataStore extends ScheduleDataStoreBase { this.infoMessage = ds.infoMessage, this.controlMessage = ds.controlMessage, this.bellId = ds.bellId, - this.customBellPath = ds.customBellPath; + this.customBellPath = ds.customBellPath, + this.nextAlarm = ds.nextAlarm; } /// Data store for the scheduler/alarm service. This data store is accessed @@ -378,17 +376,16 @@ class ScheduleDataStore extends ScheduleDataStoreBase { return InMemoryScheduleDataStore.fromDS(this); } - var _box; + Box _box; Future _init() async { logger.i("Initializing ScheduleDataStore (hive"); await Hive.initFlutter(); - _box = await Hive.openBox('mindfulnotifier'); - bool testMigrate = false; - if (testMigrate) { + _box = await Hive.openBox('scheduledata'); + if (testMigrateSched) { await _box.clear(); - testMigrate = false; + testMigrateSched = false; } await checkMigrateSharedPreferences(_box, excludeKeys: [ ScheduleDataStoreBase.themeKey, @@ -449,6 +446,7 @@ class ScheduleDataStore extends ScheduleDataStoreBase { _mergeVal(ScheduleDataStoreBase.controlMessageKey, mds.controlMessage); _mergeVal(ScheduleDataStoreBase.bellIdKey, mds.bellId); _mergeVal(ScheduleDataStoreBase.customBellPathKey, mds.customBellPath); + _mergeVal(ScheduleDataStoreBase.nextAlarmKey, mds.nextAlarm); } bool get enabled { @@ -687,6 +685,17 @@ class ScheduleDataStore extends ScheduleDataStoreBase { setSync(ScheduleDataStoreBase.customBellPathKey, value); } + String get nextAlarm { + if (_box.get(ScheduleDataStoreBase.nextAlarmKey) == null) { + nextAlarm = ''; + } + return _box.get(ScheduleDataStoreBase.nextAlarmKey); + } + + set nextAlarm(String value) { + setSync(ScheduleDataStoreBase.nextAlarmKey, value); + } + String get jsonReminders { // Check for migration to new format: if (_box.get(ScheduleDataStoreBase.remindersKeyDeprecated) != null) { @@ -711,11 +720,6 @@ class ScheduleDataStore extends ScheduleDataStoreBase { setSync(ScheduleDataStoreBase.jsonRemindersKey, jsonString); } - - String randomReminder({String tag}) { - Reminders reminders = Reminders.fromJson(jsonReminders); - return reminders.randomReminder(tag: tag); - } } class Reminder extends Equatable { diff --git a/lib/components/scheduler.dart b/lib/components/scheduler.dart index 5cadf28..907d7d1 100644 --- a/lib/components/scheduler.dart +++ b/lib/components/scheduler.dart @@ -7,7 +7,6 @@ import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:package_info_plus/package_info_plus.dart'; -import 'package:shared_preferences/shared_preferences.dart'; import 'package:mindfulnotifier/components/alarmservice.dart'; import 'package:mindfulnotifier/components/constants.dart' as constants; @@ -26,14 +25,13 @@ const bool rescheduleAfterQuietHours = true; const int scheduleAlarmID = 10; bool initialNotificationTriggered = false; -/// Newest changes: -/// scheduler owns the data -/// is reinitialized upon every alarm -/// reschedules next alarm upon every alarm -/// always refreshes from the datastore -/// - no need to reinitialize when UI changes config - // Design notes: +/// - scheduler owns the data +/// - is reinitialized upon every alarm +/// - reschedules next alarm upon every alarm +/// - always refreshes from the datastore +/// - no need to reinitialize when UI changes config + // - the UI only makes config changes and enable/disable // - everything else is controlled by the scheduler @@ -88,10 +86,6 @@ class Scheduler { // throws during testing } - SharedPreferences prefs = await SharedPreferences.getInstance(); - Get.delete(); - Get.put(prefs, permanent: true); - ds = await ScheduleDataStore.getInstance(); Get.delete(); Get.put(ds, permanent: true); @@ -118,7 +112,7 @@ class Scheduler { } } - void enable() { + void enable({bool restart = false}) { logger.i("enable"); ds.enabled = true; @@ -129,7 +123,7 @@ class Scheduler { // 1) reboot // 2) first enabled by user // 3) re-enable after config changes by user - delegate.scheduleNext(); + delegate.scheduleNext(restart: restart); // sendInfoMessage( // 'Next reminder at ${formatHHMM(delegate.queryNext())}'); String enabledReminderText = '${constants.appName} is enabled'; @@ -155,13 +149,16 @@ class Scheduler { // return false; // } - void disable({bool sendUpdate = true}) async { + void disable({bool sendUpdate = true, bool restart = false}) async { logger.i("disable"); delegate?.cancel(); Notifier().shutdown(); running = false; ds.enabled = false; - ds.reminderMessage = "Disabled"; + ds.reminderMessage = ScheduleDataStoreBase.defaultReminderMessage; + if (!restart) { + ds.nextAlarm = ''; + } if (sendUpdate) { sendDataStoreUpdate(); } @@ -169,9 +166,9 @@ class Scheduler { void restart() { logger.i("restart"); - disable(sendUpdate: false); - sleep(Duration(seconds: 1)); - enable(); + disable(sendUpdate: false, restart: true); + sleep(Duration(milliseconds: 500)); + enable(restart: true); } void playSound(var fileOrPath) { @@ -288,11 +285,25 @@ abstract class DelegatedScheduler { DateTime getNextFireTime({DateTime fromTime, bool adjustFromQuiet}); - void scheduleNext() async { + void scheduleNext({bool restart = false}) async { logger.d( "Scheduling next notification, type=$scheduleType ${getCurrentIsolate()}"); - _nextDate = getNextFireTime(); + _nextDate = null; + if (restart) { + // use nextAlarm if possible; otherwise it gets left null + String nextAlarmStr = scheduler.ds.nextAlarm; + if (nextAlarmStr != null && nextAlarmStr != '') { + DateTime nextAlarm = DateTime.parse(scheduler.ds.nextAlarm); + if (nextAlarm.isAfter(DateTime.now().add(Duration(seconds: 10)))) { + logger.i("Re-scheduling based on nextAlarm $nextAlarmStr"); + _nextDate = nextAlarm; + } + } + } + if (_nextDate == null) { + _nextDate = getNextFireTime(); + } if (rescheduleAfterQuietHours && quietHours.isInQuietHours(_nextDate)) { _nextDate = getNextFireTime( @@ -307,6 +318,9 @@ abstract class DelegatedScheduler { TimerService timerService = await getAlarmManagerTimerService(); timerService.oneShotAt(_nextDate, scheduleAlarmID, scheduleCallback); + scheduler.updateDS( + ScheduleDataStoreBase.nextAlarmKey, _nextDate.toIso8601String(), + sendUpdate: true); if (!scheduled) { initialScheduleComplete(); diff --git a/lib/main.dart b/lib/main.dart index e85e590..afbe1ea 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -41,7 +41,7 @@ void main() async { } Get.put(await initializeAlarmService(bootstrap: true), - permanent: true, tag: constants.tagAlarmServiceAlreadyRunning); + tag: constants.tagAlarmServiceAlreadyRunning); runApp( GetMaterialApp( diff --git a/lib/screens/mindfulnotifier.dart b/lib/screens/mindfulnotifier.dart index 0dc830e..23a3ad4 100644 --- a/lib/screens/mindfulnotifier.dart +++ b/lib/screens/mindfulnotifier.dart @@ -55,7 +55,8 @@ class MindfulNotifierWidgetController extends GetxController { // after the above onInit finishes, we could see odd things? initFinished = false; - // Now send a sync message which will reinit the data store from the alarm/scheduler isolate + // Now send a sync message which will reinit the data store + // from the alarm/scheduler isolate sendToAlarmService({'syncDataStore': 1}); super.onInit(); @@ -77,8 +78,7 @@ class MindfulNotifierWidgetController extends GetxController { initFinished = true; if (!alarmServiceAlreadyRunning && mds.enabled) { logger.i("initFinish: re-enabling"); - // handleEnabled(true); - triggerSchedulerRestart(reason: "Restarting alarm service"); + triggerSchedulerRestart(); } } @@ -94,7 +94,7 @@ class MindfulNotifierWidgetController extends GetxController { // Replace the main/app InMemoryScheduleDataStore instance with the // newer one from alarm service Get.delete(); - Get.put(mds, permanent: true); + Get.put(mds); // set all the local UI-visible values _enabled.value = mds.enabled; @@ -152,10 +152,11 @@ class MindfulNotifierWidgetController extends GetxController { controlMessage.value = value; if (!initFinished) { sendToAlarmService({'syncDataStore': 1}); + // Note: response comes in and handled in next case } break; case 'syncDataStore': - logger.i("Received syncDataStore"); + logger.i("Received syncDataStore from alarm service"); // Receives a complete InMemoryScheduleDataStore update InMemoryScheduleDataStore mds = value; initFromDS(mds); @@ -180,16 +181,17 @@ class MindfulNotifierWidgetController extends GetxController { sendToAlarmService({'restore': mds}); } - void triggerSchedulerRestart( - {InMemoryScheduleDataStore mds, - String reason = "Configuration changed, restarting the notifier."}) { + void triggerSchedulerRestart({InMemoryScheduleDataStore mds, String reason}) { if (_enabled.value) { + // logger.i("sending restart to scheduler"); // Send to the alarm isolate sendToAlarmService({'restart': mds}); // alert user - Get.snackbar("Restarting", reason, - snackPosition: SnackPosition.BOTTOM, instantInit: false); + if (reason != null) { + Get.snackbar("Restarting", reason, + snackPosition: SnackPosition.BOTTOM, instantInit: false); + } } else { if (mds != null) { // we need to update the datastore @@ -228,7 +230,8 @@ class MindfulNotifierWidgetController extends GetxController { return; } if (enabled) { - if (_reminderMessage.value == 'Not Enabled' || + if (_reminderMessage.value == + ScheduleDataStoreBase.defaultReminderMessage || _reminderMessage.value == 'In quiet hours') { _reminderMessage.value = 'Enabled. Waiting for notification...'; } diff --git a/lib/screens/schedulesview.dart b/lib/screens/schedulesview.dart index f67b2ac..fe91fd2 100644 --- a/lib/screens/schedulesview.dart +++ b/lib/screens/schedulesview.dart @@ -159,6 +159,10 @@ class SchedulesWidgetController extends GetxController { void handleScheduleDirty() { logger.d("handleScheduleDirty"); InMemoryScheduleDataStore mds = Get.find(); + + // Set nextAlarm to '' in order to reset the scheduler on restart + mds.nextAlarm = ''; + Get.find().triggerSchedulerRestart( mds: mds, reason: "Configuration changed, restarting the notifier."); scheduleDirty.value = false; @@ -370,8 +374,7 @@ class SchedulesWidget extends StatelessWidget { Future _selectQuietHoursStartTime(BuildContext context) async { InMemoryScheduleDataStore mds = Get.find(); var selectedTime = TimeOfDay( - hour: mds.quietHoursStartHour, - minute: mds.quietHoursStartMinute); + hour: mds.quietHoursStartHour, minute: mds.quietHoursStartMinute); final TimeOfDay picked = await showTimePicker( context: context, initialTime: selectedTime, @@ -384,9 +387,8 @@ class SchedulesWidget extends StatelessWidget { Future _selectQuietHoursEndTime(BuildContext context) async { InMemoryScheduleDataStore mds = Get.find(); - var selectedTime = TimeOfDay( - hour: mds.quietHoursEndHour, - minute: mds.quietHoursEndMinute); + var selectedTime = + TimeOfDay(hour: mds.quietHoursEndHour, minute: mds.quietHoursEndMinute); final TimeOfDay picked = await showTimePicker( context: context, initialTime: selectedTime,