[gnome-clocks/systemd-timers: 3/5] alarms: Make alarms persisted and add missed alarms notation




commit 559d0a12d73ced08bd74be079fa7063f1c28d6ab
Author: Julian Sparber <julian sparber net>
Date:   Fri Nov 13 17:59:06 2020 +0100

    alarms: Make alarms persisted and add missed alarms notation
    
    This stores the alarm state as well as the alarm and snooze time
    to gsettings. This alllows us to tell the user about missed alarms.
    A user could miss alarms for two reasons:
     - Clocks wasn't running when the alarm went of.
     - The user didn't stop the alarm within "ring-time"
    
    This also adds the baseline for starting Clocks via systemd user .timer
    units by making Clocks checks at startup if any alarms need to go off.
    
    As a side effect: closing Clocks doesn't stop an alarm and when Clocks
    is reopend within "ring-time" after an alarm or snooze the alarm starts
    ringing again.

 src/alarm-face.vala         |   5 +-
 src/alarm-item.vala         | 198 +++++++++++++++++++++++++++-----------------
 src/alarm-row.vala          |  16 ++++
 src/alarm-setup-dialog.vala |  12 +--
 src/window.vala             |   3 +
 5 files changed, 148 insertions(+), 86 deletions(-)
---
diff --git a/src/alarm-face.vala b/src/alarm-face.vala
index 2552149..2caa602 100644
--- a/src/alarm-face.vala
+++ b/src/alarm-face.vala
@@ -66,7 +66,7 @@ public class Face : Gtk.Stack, Clocks.Clock {
         });
 
         listbox.bind_model (alarms, (item) => {
-            item.notify["active"].connect (save);
+            item.notify["state"].connect (save);
             return new Row ((Item) item, this);
         });
 
@@ -134,12 +134,13 @@ public class Face : Gtk.Stack, Clocks.Clock {
 
     public void activate_new () {
         var wc = Utils.WallClock.get_default ();
-        var alarm = new Item ({ wc.date_time.get_hour (), wc.date_time.get_minute () }, false);
+        var alarm = new Item (wc.date_time.get_hour (), wc.date_time.get_minute ());
         var dialog = new SetupDialog ((Gtk.Window) get_toplevel (), alarm, alarms);
 
         dialog.response.connect ((dialog, response) => {
           // Enable the newly created alarm
           alarm.active = true;
+
             if (response == Gtk.ResponseType.OK) {
                 ((SetupDialog) dialog).apply_to_alarm ();
                 alarms.add (alarm);
diff --git a/src/alarm-item.vala b/src/alarm-item.vala
index dd44b15..bed3067 100644
--- a/src/alarm-item.vala
+++ b/src/alarm-item.vala
@@ -20,15 +20,9 @@
 namespace Clocks {
 namespace Alarm {
 
-private struct AlarmTime {
-    public int hour;
-    public int minute;
-}
-
 private class Item : Object, ContentItem {
-    // FIXME: should we add a "MISSED" state where the alarm stopped
-    // ringing but we keep showing the ringing panel?
     public enum State {
+        DISABLED,
         READY,
         RINGING,
         SNOOZING
@@ -43,6 +37,12 @@ private class Item : Object, ContentItem {
 
     public int ring_minutes { get; set; default = 5; }
 
+    public bool recurring {
+        get {
+            return days != null && !((!) days).empty;
+        }
+    }
+
     public string? name {
         get {
             return _name;
@@ -54,21 +54,34 @@ private class Item : Object, ContentItem {
         }
     }
 
-    public AlarmTime time { get; set; }
-
     public Utils.Weekdays? days { get; set; }
 
-    public State state { get; private set; }
+    private State _state = State.DISABLED;
+    public State state {
+        get {
+            return _state;
+        }
+        private set {
+            if (_state == value)
+                return;
+
+            _state = value;
+            notify_property ("active");
+        }
+    }
 
     public string time_label {
          owned get {
-            return Utils.WallClock.get_default ().format_time (alarm_time);
+            return Utils.WallClock.get_default ().format_time (time);
          }
     }
 
     public string snooze_time_label {
          owned get {
-            return Utils.WallClock.get_default ().format_time (snooze_time);
+            if (snooze_time == null)
+                return Utils.WallClock.get_default ().format_time (time.add_minutes (snooze_minutes));
+            else
+                return Utils.WallClock.get_default ().format_time ((!) snooze_time);
          }
     }
 
@@ -78,37 +91,43 @@ private class Item : Object, ContentItem {
          }
     }
 
+    public GLib.DateTime time { get; set; }
+
     [CCode (notify = false)]
     public bool active {
         get {
-            return _active && !this.editing;
+            return this.state > State.DISABLED;
         }
-
         set {
-            if (value != _active) {
-                _active = value;
-                if (_active) {
-                    reset ();
-                } else if (state == State.RINGING) {
-                    stop ();
-                }
+            if (this.state != State.DISABLED && !value) {
+                stop ();
+                this.state = State.DISABLED;
+                notify_property ("active");
+            } else if (this.state == State.DISABLED && value) {
+                this.missed = false;
+                this.time = get_next_alarm_time (time.get_hour (), time.get_minute (), days);
+                this.state = State.READY;
                 notify_property ("active");
             }
         }
     }
 
     private string _name;
-    private bool _active = true;
-    private GLib.DateTime alarm_time;
-    private GLib.DateTime snooze_time;
-    private GLib.DateTime ring_end_time;
+    private GLib.DateTime? snooze_time;
     private Utils.Bell bell;
     private GLib.Notification notification;
 
-    public Item (AlarmTime time, bool active = true, Utils.Weekdays? days = null, string? id = null) {
+    public Item (int hour, int minute, Utils.Weekdays? days = null, string? id = null) {
+        var guid = id != null ? (string) id : GLib.DBus.generate_guid ();
+        var time = get_next_alarm_time (hour, minute, days);
+        Object (id: guid,
+                time: time,
+                days: days);
+    }
+
+    public Item.for_specific_time (GLib.DateTime time, Utils.Weekdays? days = null, string? id = null) {
         var guid = id != null ? (string) id : GLib.DBus.generate_guid ();
         Object (id: guid,
-                active: active,
                 time: time,
                 days: days);
     }
@@ -121,24 +140,23 @@ private class Item : Object, ContentItem {
         notification.add_button (_("Snooze"), "app.snooze-alarm::".concat (id));
     }
 
-    public void reset () {
-        update_alarm_time ();
-        update_snooze_time (alarm_time);
-        state = State.READY;
+    public void set_alarm_time (int hour, int minute, Utils.Weekdays? days) {
+      this.days = days;
+      this.time = get_next_alarm_time (hour, minute, days);
     }
 
-    private void update_alarm_time () {
+    private static GLib.DateTime get_next_alarm_time (int hour, int minute, Utils.Weekdays? days) {
         var wallclock = Utils.WallClock.get_default ();
         var now = wallclock.date_time;
         var dt = new GLib.DateTime (wallclock.timezone,
                                     now.get_year (),
                                     now.get_month (),
                                     now.get_day_of_month (),
-                                    time.hour,
-                                    time.minute,
+                                    hour,
+                                    minute,
                                     0);
 
-        if (days == null || ((Utils.Weekdays) days).empty) {
+        if (days == null || ((!) days).empty) {
             // Alarm without days.
             if (dt.compare (now) <= 0) {
                 // Time already passed, ring tomorrow.
@@ -147,16 +165,13 @@ private class Item : Object, ContentItem {
         } else {
             // Alarm with at least one day set.
             // Find the next possible day for ringing
-            while (dt.compare (now) <= 0 || ! ((Utils.Weekdays) days).get ((Utils.Weekdays.Day) 
(dt.get_day_of_week () - 1))) {
+            while (dt.compare (now) <= 0 ||
+                   ! ((Utils.Weekdays) days).get ((Utils.Weekdays.Day) (dt.get_day_of_week () - 1))) {
                 dt = dt.add_days (1);
             }
         }
 
-        alarm_time = dt;
-    }
-
-    private void update_snooze_time (GLib.DateTime start_time) {
-        snooze_time = start_time.add_minutes (snooze_minutes);
+        return dt;
     }
 
     public virtual signal void ring () {
@@ -166,30 +181,43 @@ private class Item : Object, ContentItem {
     }
 
     private void start_ringing (GLib.DateTime now) {
-        update_snooze_time (now);
-        ring_end_time = now.add_minutes (ring_minutes);
         state = State.RINGING;
         ring ();
     }
 
     public void snooze () {
         bell.stop ();
+        if (snooze_time == null)
+            snooze_time = time.add_minutes (snooze_minutes);
+        else
+            snooze_time = ((!) snooze_time).add_minutes (snooze_minutes);
+
         state = State.SNOOZING;
     }
 
     public void stop () {
         bell.stop ();
-        update_snooze_time (alarm_time);
-        state = State.READY;
+        snooze_time = null;
+
+        // scheduale the next alarm if recurring
+        if (recurring) {
+            time = get_next_alarm_time (time.get_hour (), time.get_minute (), days);
+            state = State.READY;
+            GLib.Timeout.add_seconds (120, () => {
+                missed = false;
+                return GLib.Source.REMOVE;
+            });
+        } else {
+            state = State.DISABLED;
+        }
     }
 
     private bool compare_with_item (Item i) {
-        return (this.time.compare (i.time) == 0);
+        return (this.time.get_hour () == i.time.get_hour () &&
+                this.time.get_minute () == i.time.get_minute ());
     }
 
     public bool check_duplicate_alarm (List<Item> alarms) {
-        update_alarm_time ();
-
         foreach (var item in alarms) {
             if (this.compare_with_item (item)) {
                 return true;
@@ -198,11 +226,20 @@ private class Item : Object, ContentItem {
         return false;
     }
 
+    private void start_ringing_or_missed (GLib.DateTime now, GLib.DateTime ring_end_time) {
+        if (now.compare (ring_end_time) > 0 ) {
+            missed = true;
+            stop ();
+        } else {
+            start_ringing (now);
+        }
+    }
+
     // Update the state and ringing time. Ring or stop
     // depending on the current time.
     // Returns true if the state changed, false otherwise.
     public bool tick () {
-        if (!active) {
+        if (state == State.DISABLED) {
             return false;
         }
 
@@ -211,17 +248,25 @@ private class Item : Object, ContentItem {
         var wallclock = Utils.WallClock.get_default ();
         var now = wallclock.date_time;
 
-        if (state == State.RINGING && now.compare (ring_end_time) > 0) {
-            stop ();
-        }
-
-        if (state == State.SNOOZING && now.compare (snooze_time) > 0) {
-            start_ringing (now);
-        }
-
-        if (state == State.READY && now.compare (alarm_time) > 0) {
-            start_ringing (now);
-            update_alarm_time (); // reschedule for the next repeat
+        GLib.DateTime ring_end_time = (snooze_time != null) ?
+            ( (!) snooze_time).add_minutes (ring_minutes) : time.add_minutes (ring_minutes);
+
+        switch (state) {
+            case State.DISABLED:
+                break;
+            case State.RINGING:
+                // make sure the state changes
+                last_state = State.READY;
+                start_ringing_or_missed (now, ring_end_time);
+                break;
+            case State.SNOOZING:
+                if (snooze_time != null && now.compare ((!) snooze_time) > 0)
+                    start_ringing_or_missed (now, ring_end_time);
+                break;
+            case State.READY:
+                if (now.compare (time) > 0)
+                    start_ringing_or_missed (now, ring_end_time);
+                break;
         }
 
         return state != last_state;
@@ -231,9 +276,10 @@ private class Item : Object, ContentItem {
         builder.open (new GLib.VariantType ("a{sv}"));
         builder.add ("{sv}", "name", new GLib.Variant.string ((string) name));
         builder.add ("{sv}", "id", new GLib.Variant.string (id));
-        builder.add ("{sv}", "active", new GLib.Variant.boolean (active));
-        builder.add ("{sv}", "hour", new GLib.Variant.int32 (time.hour));
-        builder.add ("{sv}", "minute", new GLib.Variant.int32 (time.minute));
+        builder.add ("{sv}", "state", new GLib.Variant.int32 (state));
+        builder.add ("{sv}", "time", new GLib.Variant.string (time.format_iso8601 ()));
+        if (snooze_time != null)
+            builder.add ("{sv}", "snooze_time", new GLib.Variant.string (((!) snooze_time).format_iso8601 
()));
         builder.add ("{sv}", "days", ((Utils.Weekdays) days).serialize ());
         builder.add ("{sv}", "snooze_minutes", new GLib.Variant.int32 (snooze_minutes));
         builder.add ("{sv}", "ring_minutes", new GLib.Variant.int32 (ring_minutes));
@@ -245,9 +291,9 @@ private class Item : Object, ContentItem {
         Variant val;
         string? name = null;
         string? id = null;
-        bool active = true;
-        int hour = -1;
-        int minute = -1;
+        State state = State.DISABLED;
+        GLib.DateTime? time = null;
+        GLib.DateTime? snooze_time = null;
         int snooze_minutes = 10;
         int ring_minutes = 5;
         Utils.Weekdays? days = null;
@@ -258,12 +304,12 @@ private class Item : Object, ContentItem {
                 name = (string) val;
             } else if (key == "id") {
                 id = (string) val;
-            } else if (key == "active") {
-                active = (bool) val;
-            } else if (key == "hour") {
-                hour = (int32) val;
-            } else if (key == "minute") {
-                minute = (int32) val;
+            } else if (key == "state") {
+                state = (State) val;
+            } else if (key == "time") {
+                time = new GLib.DateTime.from_iso8601 ((string) val, null);
+            } else if (key == "snooze_time") {
+                snooze_time = new GLib.DateTime.from_iso8601 ((string) val, null);
             } else if (key == "days") {
                 days = Utils.Weekdays.deserialize (val);
             } else if (key == "snooze_minutes") {
@@ -273,12 +319,14 @@ private class Item : Object, ContentItem {
             }
         }
 
-        if (hour >= 0 && minute >= 0) {
-            Item alarm = new Item ({ hour, minute }, active, days, id);
+        if (time != null) {
+            Item alarm = new Item.for_specific_time ((!) time, days, id);
+            alarm.state = state;
             alarm.name = name;
+            if (snooze_time != null)
+                alarm.snooze_time = (!) snooze_time;
             alarm.ring_minutes = ring_minutes;
             alarm.snooze_minutes = snooze_minutes;
-            alarm.reset ();
             return alarm;
         } else {
             warning ("Invalid alarm %s", name != null ? (string) name : "[unnamed]");
diff --git a/src/alarm-row.vala b/src/alarm-row.vala
index c30c6a5..e8003a6 100644
--- a/src/alarm-row.vala
+++ b/src/alarm-row.vala
@@ -74,6 +74,12 @@ private class Row : Gtk.ListBoxRow {
             time.label = alarm.time_label;
         }
 
+        if (alarm.missed) {
+            get_style_context ().add_class ("missed");
+        } else {
+            get_style_context ().add_class ("missed");
+        }
+
         var label = alarm.name;
 
         // Prior to 3.36 unamed alarms would just be called "Alarm",
@@ -93,6 +99,16 @@ private class Row : Gtk.ListBoxRow {
             }
         }
 
+        if (alarm.missed) {
+            if (label != null && ((string) label).length > 0) {
+                // Translators: The alarm for the time %s titled %s was "missed"
+                label = _("Alarm at %s was missed: %s").printf (alarm.time_label, (string) label);
+            } else {
+                // Translators: %s is a time
+                label = _("Alarm at %s was missed").printf (alarm.time_label);
+            }
+        }
+
         title_reveal.reveal_child = label != null && ((string) label).length > 0;
         title.label = (string) label;
     }
diff --git a/src/alarm-setup-dialog.vala b/src/alarm-setup-dialog.vala
index 1e9a696..7e8a883 100644
--- a/src/alarm-setup-dialog.vala
+++ b/src/alarm-setup-dialog.vala
@@ -165,8 +165,8 @@ private class SetupDialog : Gtk.Dialog {
 
     // Sets up the dialog to show the values of alarm.
     public void set_from_alarm () {
-      var hour = alarm.time.hour;
-      var minute = alarm.time.minute;
+      var hour = alarm.time.get_hour ();
+      var minute = alarm.time.get_minute ();
         // Set the time.
         if (format == Utils.WallClock.Format.TWELVE) {
             if (hour < 12) {
@@ -211,21 +211,15 @@ private class SetupDialog : Gtk.Dialog {
             }
         }
 
-        AlarmTime time = { hour, minute };
-
         var days = repeats.store ();
 
         alarm.freeze_notify ();
 
         alarm.name = name;
-        alarm.time = time;
-        alarm.days = days;
+        alarm.set_alarm_time (hour, minute, days);
         alarm.snooze_minutes = snooze_item.minutes;
         alarm.ring_minutes = ring_item.minutes;
 
-        // Force update of alarm_time before notifying the changes
-        alarm.reset ();
-
         alarm.thaw_notify ();
     }
 
diff --git a/src/window.vala b/src/window.vala
index fe954b7..a32186e 100644
--- a/src/window.vala
+++ b/src/window.vala
@@ -137,6 +137,9 @@ public class Window : Hdy.ApplicationWindow {
         if (Config.PROFILE == "Devel") {
             style.add_class ("devel");
         }
+
+        // Immidiatly check if we need to notifiy the user about alarms
+        Utils.WallClock.get_default ().tick ();
     }
 
     [Signal (action = true)]


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]