1/*
2 * Copyright (c) 2013-2016 gnome-pomodoro contributors
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
16 *
17 * Authors: Kamil Prusko <kamilprusko@gmail.com>
18 *
19 */
20
21using GLib;
22
23
24namespace Pomodoro
25{
26    public interface ApplicationExtension : Peas.ExtensionBase
27    {
28    }
29
30    internal Gom.Repository get_repository ()
31    {
32        var application = Pomodoro.Application.get_default ();
33
34        return (Gom.Repository) application.get_repository ();
35    }
36
37    public class Application : Gtk.Application
38    {
39        private const uint REPOSITORY_VERSION = 1;
40        private const uint SETUP_PLUGINS_TIMEOUT = 3000;
41
42        public Pomodoro.Service service;
43        public Pomodoro.Timer timer;
44        public Pomodoro.CapabilityManager capabilities;
45
46        private Gom.Repository repository;
47        private Gom.Adapter adapter;
48
49        public GLib.Object get_repository ()
50        {
51            return (GLib.Object) this.repository;
52        }
53
54        private Pomodoro.PreferencesDialog preferences_dialog;
55        private Pomodoro.Window window;
56        private Pomodoro.DesktopExtension desktop_extension;
57        private Gtk.Window about_dialog;
58        private Peas.ExtensionSet extensions;
59        private GLib.Settings settings;
60        private bool was_activated = false;
61
62        private enum ExitStatus
63        {
64            UNDEFINED = -1,
65            SUCCESS   =  0,
66            FAILURE   =  1
67        }
68
69        private class Options
70        {
71            public static bool no_default_window = false;
72            public static bool preferences = false;
73            public static bool quit = false;
74            public static bool start_stop = false;
75            public static bool start = false;
76            public static bool stop = false;
77            public static bool pause_resume = false;
78            public static bool pause = false;
79            public static bool resume = false;
80
81            public static ExitStatus exit_status = ExitStatus.UNDEFINED;
82
83            public const GLib.OptionEntry[] ENTRIES = {
84                { "start-stop", 0, 0, GLib.OptionArg.NONE,
85                  ref start_stop, N_("Start/Stop"), null },
86
87                { "start", 0, 0, GLib.OptionArg.NONE,
88                  ref start, N_("Start"), null },
89
90                { "stop", 0, 0, GLib.OptionArg.NONE,
91                  ref stop, N_("Stop"), null },
92
93                { "pause-resume", 0, 0, GLib.OptionArg.NONE,
94                  ref pause_resume, N_("Pause/Resume"), null },
95
96                { "pause", 0, 0, GLib.OptionArg.NONE,
97                  ref pause, N_("Pause"), null },
98
99                { "resume", 0, 0, GLib.OptionArg.NONE,
100                  ref resume, N_("Resume"), null },
101
102                { "no-default-window", 0, 0, GLib.OptionArg.NONE,
103                  ref no_default_window, N_("Run as background service"), null },
104
105                { "preferences", 0, 0, GLib.OptionArg.NONE,
106                  ref preferences, N_("Show preferences"), null },
107
108                { "quit", 0, 0, GLib.OptionArg.NONE,
109                  ref quit, N_("Quit application"), null },
110
111                { "version", 0, GLib.OptionFlags.NO_ARG, GLib.OptionArg.CALLBACK,
112                  (void *) command_line_version_callback, N_("Print version information and exit"), null },
113
114                { null }
115            };
116
117            public static void reset ()
118            {
119                Options.no_default_window = false;
120                Options.preferences = false;
121                Options.quit = false;
122                Options.start_stop = false;
123                Options.start = false;
124                Options.stop = false;
125                Options.pause_resume = false;
126                Options.pause = false;
127                Options.resume = false;
128            }
129        }
130
131        public Application ()
132        {
133            GLib.Object (
134                application_id: "org.gnome.Pomodoro",
135                flags: GLib.ApplicationFlags.HANDLES_COMMAND_LINE
136            );
137
138            this.timer = null;
139            this.service = null;
140        }
141
142        public new static unowned Application get_default ()
143        {
144            return GLib.Application.get_default () as Pomodoro.Application;
145        }
146
147        public unowned Gtk.Window get_last_focused_window ()
148        {
149            unowned List<Gtk.Window> windows = this.get_windows ();
150
151            return windows != null
152                    ? windows.first ().data
153                    : null;
154        }
155
156        private void setup_resources ()
157        {
158            var css_provider = new Gtk.CssProvider ();
159            css_provider.load_from_resource ("/org/gnome/pomodoro/style.css");
160
161            Gtk.StyleContext.add_provider_for_screen (
162                                         Gdk.Screen.get_default (),
163                                         css_provider,
164                                         Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION);
165        }
166
167        private void setup_desktop_extension ()
168        {
169            try {
170                this.desktop_extension = new Pomodoro.DesktopExtension ();
171
172                this.capabilities.add_group (this.desktop_extension.capabilities, Pomodoro.Priority.HIGH);
173            }
174            catch (GLib.Error error) {
175                GLib.warning ("Error while initializing desktop extension: %s",
176                              error.message);
177            }
178        }
179
180        private async void setup_plugins ()
181        {
182            var engine = Peas.Engine.get_default ();
183            engine.add_search_path (Config.PLUGIN_LIB_DIR, Config.PLUGIN_DATA_DIR);
184
185            var timeout_cancellable = new GLib.Cancellable ();
186            var timeout_source = (uint) 0;
187            var wait_count = 0;
188
189            timeout_source = GLib.Timeout.add (SETUP_PLUGINS_TIMEOUT, () => {
190                GLib.debug ("Timeout reached while setting up plugins");
191
192                timeout_source = 0;
193                timeout_cancellable.cancel ();
194
195                return GLib.Source.REMOVE;
196            });
197
198            this.extensions = new Peas.ExtensionSet (engine, typeof (Pomodoro.ApplicationExtension));
199            this.extensions.extension_added.connect ((extension_set,
200                                                      info,
201                                                      extension_object) => {
202                var extension = extension_object as GLib.AsyncInitable;
203
204                if (extension != null)
205                {
206                    extension.init_async.begin (GLib.Priority.DEFAULT, timeout_cancellable, (obj, res) => {
207                        try {
208                            extension.init_async.end (res);
209                        }
210                        catch (GLib.Error error) {
211                            GLib.warning ("Failed to initialize plugin \"%s\": %s",
212                                          info.get_module_name (),
213                                          error.message);
214                        }
215
216                        wait_count--;
217
218                        this.setup_plugins.callback ();
219                    });
220
221                    wait_count++;
222                }
223            });
224
225            this.load_plugins ();
226
227            while (wait_count > 0) {
228                yield;
229            }
230
231            timeout_cancellable = null;
232
233            if (timeout_source != 0) {
234                GLib.Source.remove (timeout_source);
235            }
236        }
237
238        private void setup_capabilities ()
239        {
240            var default_capabilities = new Pomodoro.CapabilityGroup ("default");
241
242            default_capabilities.add (new Pomodoro.NotificationsCapability ("notifications"));
243
244            this.capabilities = new Pomodoro.CapabilityManager ();
245            this.capabilities.add_group (default_capabilities, Pomodoro.Priority.LOW);
246        }
247
248        private static bool migrate_repository (Gom.Repository repository,
249                                                Gom.Adapter    adapter,
250                                                uint           version)
251                                                throws GLib.Error
252        {
253            uint8[] file_contents;
254            string error_message;
255
256            GLib.debug ("Migrating database to version %u", version);
257
258            var file = File.new_for_uri ("resource:///org/gnome/pomodoro/database/version-%u.sql".printf (version));
259            file.load_contents (null, out file_contents, null);
260
261            /* Gom.Adapter.execute_sql is limited to single line queries,
262             * so we need to use Sqlite API directly
263             */
264            unowned Sqlite.Database database = adapter.get_handle ();
265
266            if (database.exec ((string) file_contents, null, out error_message) != Sqlite.OK)
267            {
268                throw new Gom.Error.COMMAND_SQLITE (error_message);
269            }
270
271            return true;
272        }
273
274        private void setup_repository ()
275        {
276            this.hold ();
277            this.mark_busy ();
278
279            var path = GLib.Path.build_filename (GLib.Environment.get_user_data_dir (),
280                                                 Config.PACKAGE_NAME,
281                                                 "database.sqlite");
282            var file = GLib.File.new_for_path (path);
283            var directory = file.get_parent ();
284
285            if (!directory.query_exists ()) {
286                try {
287                    directory.make_directory_with_parents ();
288                }
289                catch (GLib.Error error) {
290                    GLib.warning ("Failed to create directory: %s", error.message);
291                }
292            }
293
294            try {
295                /* Open database handle */
296                var adapter = new Gom.Adapter ();
297                adapter.open_sync (file.get_uri ());
298                this.adapter = adapter;
299
300                /* Migrate database if needed */
301                var repository = new Gom.Repository (adapter);
302                repository.migrate_sync (Pomodoro.Application.REPOSITORY_VERSION,
303                                         Pomodoro.Application.migrate_repository);
304
305                this.repository = repository;
306            }
307            catch (GLib.Error error) {
308                GLib.critical ("Failed to migrate database: %s", error.message);
309            }
310
311            this.unmark_busy ();
312            this.release ();
313        }
314
315        private void load_plugins ()
316        {
317            var engine          = Peas.Engine.get_default ();
318            var enabled_plugins = this.settings.get_strv ("enabled-plugins");
319            var enabled_hash    = new GLib.HashTable<string, bool> (str_hash, str_equal);
320
321            foreach (var name in enabled_plugins)
322            {
323                enabled_hash.insert (name, true);
324            }
325
326            foreach (var plugin_info in engine.get_plugin_list ())
327            {
328                if (plugin_info.is_hidden () || enabled_hash.contains (plugin_info.get_module_name ())) {
329                    engine.try_load_plugin (plugin_info);
330                }
331                else {
332                    engine.try_unload_plugin (plugin_info);
333                }
334            }
335        }
336
337        public void show_window (string mode,
338                                 uint32 timestamp = 0)
339        {
340            if (this.window == null) {
341                this.window = new Pomodoro.Window ();
342                this.window.application = this;
343                this.window.destroy.connect (() => {
344                    this.remove_window (this.window);
345                    this.window = null;
346                });
347
348                this.add_window (this.window);
349            }
350
351            this.window.mode = mode != null && mode != "default"
352                ? mode : this.window.default_mode;
353
354            if (timestamp > 0) {
355                this.window.present_with_time (timestamp);
356            }
357            else {
358                this.window.present ();
359            }
360        }
361
362        public void show_preferences (uint32 timestamp = 0)
363        {
364            if (this.preferences_dialog == null) {
365                this.preferences_dialog = new Pomodoro.PreferencesDialog ();
366                this.preferences_dialog.destroy.connect (() => {
367                    this.remove_window (this.preferences_dialog);
368                    this.preferences_dialog = null;
369                });
370                this.add_window (this.preferences_dialog);
371            }
372
373            if (this.preferences_dialog != null) {
374                if (timestamp > 0) {
375                    this.preferences_dialog.present_with_time (timestamp);
376                }
377                else {
378                    this.preferences_dialog.present ();
379                }
380            }
381        }
382
383        private void activate_timer (GLib.SimpleAction action,
384                                     GLib.Variant?     parameter)
385        {
386            this.show_window ("timer");
387        }
388
389        private void activate_stats (GLib.SimpleAction action,
390                                     GLib.Variant?     parameter)
391        {
392            this.show_window ("stats");
393        }
394
395        private void activate_preferences (GLib.SimpleAction action,
396                                           GLib.Variant?     parameter)
397        {
398            this.show_preferences ();
399        }
400
401        private void activate_visit_website (GLib.SimpleAction action,
402                                             GLib.Variant?     parameter)
403        {
404            try {
405                string[] spawn_args = { "xdg-open", Config.PACKAGE_URL };
406                string[] spawn_env = GLib.Environ.get ();
407
408                GLib.Process.spawn_async (null,
409                                          spawn_args,
410                                          spawn_env,
411                                          GLib.SpawnFlags.SEARCH_PATH,
412                                          null,
413                                          null);
414            }
415            catch (GLib.SpawnError error) {
416                GLib.warning ("Failed to spawn process: %s", error.message);
417            }
418        }
419
420        private void activate_report_issue (GLib.SimpleAction action,
421                                            GLib.Variant?     parameter)
422        {
423            try {
424                string[] spawn_args = { "xdg-open", Config.PACKAGE_BUGREPORT };
425                string[] spawn_env = GLib.Environ.get ();
426
427                GLib.Process.spawn_async (null,
428                                          spawn_args,
429                                          spawn_env,
430                                          GLib.SpawnFlags.SEARCH_PATH,
431                                          null,
432                                          null);
433            }
434            catch (GLib.SpawnError error) {
435                GLib.warning ("Failed to spawn process: %s", error.message);
436            }
437        }
438
439        private void activate_about (GLib.SimpleAction action,
440                                     GLib.Variant?     parameter)
441        {
442            if (this.about_dialog == null)
443            {
444                var window = this.get_last_focused_window ();
445
446                this.about_dialog = new Pomodoro.AboutDialog ();
447                this.about_dialog.destroy.connect (() => {
448                    this.remove_window (this.about_dialog);
449                    this.about_dialog = null;
450                });
451
452                if (window != null) {
453                    this.about_dialog.set_transient_for (window);
454                }
455
456                this.add_window (this.about_dialog);
457            }
458
459            this.about_dialog.present ();
460        }
461
462        private void activate_quit (GLib.SimpleAction action,
463                                    GLib.Variant?     parameter)
464        {
465            this.quit ();
466        }
467
468        private void activate_timer_skip (GLib.SimpleAction action,
469                                          GLib.Variant?     parameter)
470        {
471            try {
472                this.service.skip ();
473            }
474            catch (GLib.Error error) {
475            }
476        }
477
478        private void activate_timer_set_state (GLib.SimpleAction action,
479                                               GLib.Variant?     parameter)
480        {
481            try {
482                this.service.set_state (parameter.get_string (), 0.0);
483            }
484            catch (GLib.Error error) {
485            }
486        }
487
488        private void activate_timer_switch_state (GLib.SimpleAction action,
489                                                  GLib.Variant? parameter)
490        {
491            try {
492                this.service.set_state (parameter.get_string (),
493                                        this.timer.state.timestamp);
494            }
495            catch (GLib.Error error) {
496            }
497        }
498
499        private void setup_actions ()
500        {
501            GLib.SimpleAction action;
502
503            action = new GLib.SimpleAction ("timer", null);
504            action.activate.connect (this.activate_timer);
505            this.add_action (action);
506
507            action = new GLib.SimpleAction ("stats", null);
508            action.activate.connect (this.activate_stats);
509            this.add_action (action);
510
511            action = new GLib.SimpleAction ("preferences", null);
512            action.activate.connect (this.activate_preferences);
513            this.add_action (action);
514
515            action = new GLib.SimpleAction ("visit-website", null);
516            action.activate.connect (this.activate_visit_website);
517            this.add_action (action);
518
519            action = new GLib.SimpleAction ("report-issue", null);
520            action.activate.connect (this.activate_report_issue);
521            this.add_action (action);
522
523            action = new GLib.SimpleAction ("about", null);
524            action.activate.connect (this.activate_about);
525            this.add_action (action);
526
527            action = new GLib.SimpleAction ("quit", null);
528            action.activate.connect (this.activate_quit);
529            this.add_action (action);
530
531            action = new GLib.SimpleAction ("timer-skip", null);
532            action.activate.connect (this.activate_timer_skip);
533            this.add_action (action);
534
535            action = new GLib.SimpleAction ("timer-set-state", GLib.VariantType.STRING);
536            action.activate.connect (this.activate_timer_set_state);
537            this.add_action (action);
538
539            action = new GLib.SimpleAction ("timer-switch-state", GLib.VariantType.STRING);
540            action.activate.connect (this.activate_timer_switch_state);
541            this.add_action (action);
542
543            this.set_accels_for_action ("stats.previous", {"<Alt>Left", "Back"});
544            this.set_accels_for_action ("stats.next", {"<Alt>Right", "Forward"});
545            this.set_accels_for_action ("app.quit", {"<Primary>q"});
546        }
547
548        private static bool command_line_version_callback ()
549        {
550            stdout.printf ("%s %s\n",
551                           GLib.Environment.get_application_name (),
552                           Config.PACKAGE_VERSION);
553
554            Options.exit_status = ExitStatus.SUCCESS;
555
556            return true;
557        }
558
559        /**
560         * Emitted on the primary instance immediately after registration.
561         */
562        public override void startup ()
563        {
564            this.hold ();
565
566            base.startup ();
567
568            this.restore_timer ();
569
570            this.setup_resources ();
571            this.setup_actions ();
572            this.setup_repository ();
573            this.setup_capabilities ();
574            this.setup_desktop_extension ();
575            this.setup_plugins.begin ((obj, res) => {
576                this.setup_plugins.end (res);
577
578                GLib.Idle.add (() => {
579                    // TODO: shouldn't these be enabled by settings?!
580                    this.capabilities.enable ("notifications");
581                    this.capabilities.enable ("indicator");
582                    this.capabilities.enable ("accelerator");
583                    this.capabilities.enable ("hide-system-notifications");
584                    this.capabilities.enable ("idle-monitor");
585
586                    this.release ();
587
588                    return GLib.Source.REMOVE;
589                });
590            });
591        }
592
593        /**
594         * This is just for local things, like showing help
595         */
596        private void parse_command_line (ref unowned string[] arguments) throws GLib.OptionError
597        {
598            var option_context = new GLib.OptionContext ();
599            option_context.add_main_entries (Options.ENTRIES, Config.GETTEXT_PACKAGE);
600            option_context.add_group (Gtk.get_option_group (true));
601
602            // TODO: add options from plugins
603
604            option_context.parse (ref arguments);
605        }
606
607        protected override bool local_command_line ([CCode (array_length = false, array_null_terminated = true)]
608                                                    ref unowned string[] arguments,
609                                                    out int              exit_status)
610        {
611            string[] tmp = arguments;
612            unowned string[] arguments_copy = tmp;
613
614            try
615            {
616                // This is just for local things, like showing help
617                this.parse_command_line (ref arguments_copy);
618            }
619            catch (GLib.Error error)
620            {
621                stderr.printf ("Failed to parse options: %s\n", error.message);
622                exit_status = ExitStatus.FAILURE;
623
624                return true;
625            }
626
627            if (Options.exit_status != ExitStatus.UNDEFINED)
628            {
629                exit_status = Options.exit_status;
630
631                return true;
632            }
633
634            return base.local_command_line (ref arguments, out exit_status);
635        }
636
637        public override int command_line (GLib.ApplicationCommandLine command_line)
638        {
639            string[] tmp = command_line.get_arguments ();
640            unowned string[] arguments_copy = tmp;
641
642            var exit_status = ExitStatus.SUCCESS;
643
644            do {
645                try
646                {
647                    this.parse_command_line (ref arguments_copy);
648                }
649                catch (GLib.Error error)
650                {
651                    stderr.printf ("Failed to parse options: %s\n", error.message);
652
653                    exit_status = ExitStatus.FAILURE;
654                    break;
655                }
656
657                if (Options.exit_status != ExitStatus.UNDEFINED)
658                {
659                    exit_status = Options.exit_status;
660                    break;
661                }
662
663                this.activate ();
664            }
665            while (false);
666
667            return exit_status;
668        }
669
670        /* Save the state before exit.
671         *
672         * Emitted only on the registered primary instance immediately after
673         * the main loop terminates.
674         */
675        public override void shutdown ()
676        {
677            this.hold ();
678
679            this.save_timer ();
680
681            foreach (var window in this.get_windows ()) {
682                this.remove_window (window);
683            }
684
685            this.capabilities.disable_all ();
686
687            var engine = Peas.Engine.get_default ();
688
689            foreach (var plugin_info in engine.get_plugin_list ()) {
690                engine.try_unload_plugin (plugin_info);
691            }
692
693            try {
694                if (this.adapter != null) {
695                    this.adapter.close_sync ();
696                }
697            }
698            catch (GLib.Error error) {
699            }
700
701            base.shutdown ();
702
703            this.release ();
704        }
705
706        /* Emitted on the primary instance when an activation occurs.
707         * The application must be registered before calling this function.
708         */
709        public override void activate ()
710        {
711            this.hold ();
712
713            if (this.was_activated) {
714                Options.no_default_window |= Options.start_stop |
715                                             Options.start |
716                                             Options.stop |
717                                             Options.pause_resume |
718                                             Options.pause |
719                                             Options.resume;
720            }
721
722            if (Options.quit) {
723                this.quit ();
724            }
725            else {
726                if (Options.start_stop) {
727                    this.timer.toggle ();
728                }
729                else if (Options.start) {
730                    this.timer.start ();
731                }
732                else if (Options.stop) {
733                    this.timer.stop ();
734                }
735
736                if (Options.pause_resume) {
737                    if (this.timer.is_paused) {
738                        this.timer.resume ();
739                    }
740                    else {
741                        this.timer.pause ();
742                    }
743                }
744                else if (Options.pause) {
745                    this.timer.pause ();
746                }
747                else if (Options.resume) {
748                    this.timer.resume ();
749                }
750
751                if (Options.preferences) {
752                    this.show_preferences ();
753                }
754                else if (!Options.no_default_window) {
755                    this.show_window ("default");
756                }
757
758                Options.reset ();
759            }
760
761            this.was_activated = true;
762
763            this.release ();
764        }
765
766        public override bool dbus_register (GLib.DBusConnection connection,
767                                            string              object_path) throws GLib.Error
768        {
769            if (!base.dbus_register (connection, object_path)) {
770                return false;
771            }
772
773            if (this.timer == null) {
774                this.timer = Pomodoro.Timer.get_default ();
775                this.timer.notify["is-paused"].connect (this.on_timer_is_paused_notify);
776                this.timer.state_changed.connect_after (this.on_timer_state_changed);
777            }
778
779            if (this.settings == null) {
780                this.settings = Pomodoro.get_settings ()
781                                        .get_child ("preferences");
782                this.settings.changed.connect (this.on_settings_changed);
783            }
784
785            if (this.service == null) {
786                this.hold ();
787                this.service = new Pomodoro.Service (connection, this.timer);
788
789                try {
790                    connection.register_object ("/org/gnome/Pomodoro", this.service);
791                }
792                catch (GLib.IOError error) {
793                    GLib.warning ("%s", error.message);
794                    return false;
795                }
796            }
797
798            return true;
799        }
800
801        public override void dbus_unregister (GLib.DBusConnection connection,
802                                              string              object_path)
803        {
804            base.dbus_unregister (connection, object_path);
805
806            if (this.timer != null) {
807                this.timer.destroy ();
808                this.timer = null;
809            }
810
811            if (this.service != null) {
812                this.service = null;
813
814                this.release ();
815            }
816        }
817
818        private void save_timer ()
819        {
820            var state_settings = Pomodoro.get_settings ()
821                                         .get_child ("state");
822
823            this.timer.save (state_settings);
824        }
825
826        private void restore_timer ()
827        {
828            var state_settings = Pomodoro.get_settings ()
829                                         .get_child ("state");
830
831            this.timer.restore (state_settings);
832        }
833
834        private void on_settings_changed (GLib.Settings settings,
835                                          string        key)
836        {
837            var state_duration = this.timer.state_duration;
838
839            switch (key)
840            {
841                case "pomodoro-duration":
842                    if (this.timer.state is Pomodoro.PomodoroState) {
843                        state_duration = settings.get_double (key);
844                    }
845                    break;
846
847                case "short-break-duration":
848                    if (this.timer.state is Pomodoro.ShortBreakState) {
849                        state_duration = settings.get_double (key);
850                    }
851                    break;
852
853                case "long-break-duration":
854                    if (this.timer.state is Pomodoro.LongBreakState) {
855                        state_duration = settings.get_double (key);
856                    }
857                    break;
858
859                case "enabled-plugins":
860                    this.load_plugins ();
861
862                    break;
863            }
864
865            if (state_duration != this.timer.state_duration)
866            {
867                this.timer.state_duration = double.max (state_duration, this.timer.elapsed);
868            }
869        }
870
871        private void on_timer_is_paused_notify ()
872        {
873            this.save_timer ();
874        }
875
876        /**
877         * Save timer state, assume user is idle when break is completed.
878         */
879        private void on_timer_state_changed (Pomodoro.Timer      timer,
880                                             Pomodoro.TimerState state,
881                                             Pomodoro.TimerState previous_state)
882        {
883            this.hold ();
884            this.save_timer ();
885
886            if (this.timer.is_paused)
887            {
888                this.timer.resume ();
889            }
890
891            if (!(previous_state is Pomodoro.DisabledState))
892            {
893                var entry = new Pomodoro.Entry.from_state (previous_state);
894                entry.repository = this.repository;
895                entry.save_async.begin ((obj, res) => {
896                    try {
897                        entry.save_async.end (res);
898                    }
899                    catch (GLib.Error error) {
900                        GLib.warning ("Error while saving entry: %s", error.message);
901                    }
902
903                    this.release ();
904                });
905            }
906        }
907    }
908}
909