Merge lp:~charlesk/indicator-datetime/alarms into lp:indicator-datetime/14.04

Proposed by Charles Kerr on 2014-02-03
Status: Merged
Approved by: Ted Gould on 2014-02-06
Approved revision: 405
Merged at revision: 305
Proposed branch: lp:~charlesk/indicator-datetime/alarms
Merge into: lp:indicator-datetime/14.04
Prerequisite: lp:~charlesk/indicator-datetime/lp-1271484
Diff against target: 1743 lines (+1011/-204)
25 files modified
CMakeLists.txt (+2/-2)
debian/control (+1/-1)
include/datetime/clock-watcher.h (+72/-0)
include/datetime/date-time.h (+3/-0)
include/datetime/planner-eds.h (+3/-2)
include/datetime/snap.h (+51/-0)
include/datetime/timezone-file.h (+2/-2)
po/POTFILES.in (+2/-0)
src/CMakeLists.txt (+2/-0)
src/actions-live.cpp (+1/-1)
src/clock-watcher.cpp (+71/-0)
src/date-time.cpp (+18/-0)
src/main.cpp (+22/-9)
src/menu.cpp (+37/-66)
src/planner-eds.cpp (+216/-105)
src/snap.cpp (+256/-0)
src/timezone-file.cpp (+4/-4)
tests/CMakeLists.txt (+5/-0)
tests/geoclue-fixture.h (+1/-1)
tests/manual-test-snap.cpp (+63/-0)
tests/test-clock-watcher.cpp (+166/-0)
tests/test-clock.cpp (+2/-2)
tests/test-planner.cpp (+8/-6)
tests/test-settings.cpp (+2/-2)
tests/test-utils.cpp (+1/-1)
To merge this branch: bzr merge lp:~charlesk/indicator-datetime/alarms
Reviewer Review Type Date Requested Status
Ted Gould (community) 2014-02-03 Approve on 2014-02-06
PS Jenkins bot (community) continuous-integration Approve on 2014-02-06
Review via email: mp+204420@code.launchpad.net

Commit message

support for ubuntu-clock-app's alarms

Description of the change

Alarm Support for indicator-datetime:

 * add ECalClient view to monitor changes in the client's components
 * watch both calendar and task-list because ubuntu-ui-tookit uses the latter
 * add ClockWatcher + unit tests to decide when to show a snap decision
 * restore Snap decision code + manual tests to display alarms on phone
 * play a looping sound while the snap decision is being shown
 * update the header state when alarms change s.t. we sync the icon
 * use the ubuntu-mobile icon 'alarm-clock' for alarm + header icon

To post a comment you must log in.
397. By Charles Kerr on 2014-02-05

bugfix: when closing the snap decision, ensure there's not a timeout waiting to loop the ringtone

398. By Charles Kerr on 2014-02-05

copyediting: make the Snap lambdas a little easier to read.

399. By Charles Kerr on 2014-02-05

add utils.c to POTFILES.in

400. By Charles Kerr on 2014-02-05

remove alarms from the menu once they've been shown in a snap decision.

401. By Charles Kerr on 2014-02-05

if an alarm doesn't have a URL associated with it, use 'appid://com.ubuntu.clock/clock/current-user-version' as a fallback url.

402. By Charles Kerr on 2014-02-05

revert r400; we can't block alarms by UID because that would hide recurring alarms

403. By Charles Kerr on 2014-02-05

remove upcoming events from the menu once they're no longer upcoming.

404. By Charles Kerr on 2014-02-05

when playing a sound in canberra, don't use CA_PROP_EVENT_ID if caching failed

405. By Charles Kerr on 2014-02-06

another pass at removing alarms from the menu once they're no longer upcoming. This version fixes the header's icon as well.

Ted Gould (ted) wrote :

Okay, looks good!

review: Approve
406. By Charles Kerr on 2014-02-19

sync with trunk

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'CMakeLists.txt'
2--- CMakeLists.txt 2014-02-10 22:59:02 +0000
3+++ CMakeLists.txt 2014-02-19 15:36:23 +0000
4@@ -38,10 +38,10 @@
5 libical>=0.48
6 libecal-1.2>=3.5
7 libedataserver-1.2>=3.5
8+ libcanberra>=0.12
9 libnotify>=0.7.6
10 url-dispatcher-1>=1
11- properties-cpp>=0.0.1
12- json-glib-1.0>=0.16.2)
13+ properties-cpp>=0.0.1)
14 include_directories (SYSTEM ${SERVICE_DEPS_INCLUDE_DIRS})
15
16 ##
17
18=== modified file 'debian/control'
19--- debian/control 2014-02-11 20:06:28 +0000
20+++ debian/control 2014-02-19 15:36:23 +0000
21@@ -16,13 +16,13 @@
22 libgtest-dev,
23 libglib2.0-dev (>= 2.35.4),
24 libnotify-dev (>= 0.7.6),
25+ libcanberra-dev,
26 libido3-0.1-dev (>= 0.2.90),
27 libgeoclue-dev (>= 0.12.0),
28 libecal1.2-dev (>= 3.5),
29 libical-dev (>= 1.0),
30 libgtk-3-dev (>= 3.1.4),
31 libcairo2-dev (>= 1.10),
32- libjson-glib-dev,
33 libpolkit-gobject-1-dev,
34 libedataserver1.2-dev (>= 3.5),
35 libgconf2-dev (>= 2.31),
36
37=== added file 'include/datetime/clock-watcher.h'
38--- include/datetime/clock-watcher.h 1970-01-01 00:00:00 +0000
39+++ include/datetime/clock-watcher.h 2014-02-19 15:36:23 +0000
40@@ -0,0 +1,72 @@
41+/*
42+ * Copyright 2014 Canonical Ltd.
43+ *
44+ * This program is free software: you can redistribute it and/or modify it
45+ * under the terms of the GNU General Public License version 3, as published
46+ * by the Free Software Foundation.
47+ *
48+ * This program is distributed in the hope that it will be useful, but
49+ * WITHOUT ANY WARRANTY; without even the implied warranties of
50+ * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
51+ * PURPOSE. See the GNU General Public License for more details.
52+ *
53+ * You should have received a copy of the GNU General Public License along
54+ * with this program. If not, see <http://www.gnu.org/licenses/>.
55+ *
56+ * Authors:
57+ * Charles Kerr <charles.kerr@canonical.com>
58+ */
59+
60+#ifndef INDICATOR_DATETIME_CLOCK_WATCHER_H
61+#define INDICATOR_DATETIME_CLOCK_WATCHER_H
62+
63+#include <datetime/state.h>
64+#include <datetime/appointment.h>
65+
66+#include <core/signal.h>
67+
68+#include <memory>
69+#include <set>
70+#include <string>
71+
72+namespace unity {
73+namespace indicator {
74+namespace datetime {
75+
76+
77+/**
78+ * \brief Watches the clock and appointments to notify when an
79+ * appointment's time is reached.
80+ */
81+class ClockWatcher
82+{
83+public:
84+ ClockWatcher() =default;
85+ virtual ~ClockWatcher() =default;
86+ virtual core::Signal<const Appointment&>& alarm_reached() = 0;
87+};
88+
89+
90+/**
91+ * \brief A #ClockWatcher implementation
92+ */
93+class ClockWatcherImpl: public ClockWatcher
94+{
95+public:
96+ ClockWatcherImpl(const std::shared_ptr<const State>& state);
97+ ~ClockWatcherImpl() =default;
98+ core::Signal<const Appointment&>& alarm_reached();
99+
100+private:
101+ void pulse();
102+ std::set<std::string> m_triggered;
103+ std::shared_ptr<const State> m_state;
104+ core::Signal<const Appointment&> m_alarm_reached;
105+};
106+
107+
108+} // namespace datetime
109+} // namespace indicator
110+} // namespace unity
111+
112+#endif // INDICATOR_DATETIME_CLOCK_WATCHER_H
113
114=== modified file 'include/datetime/date-time.h'
115--- include/datetime/date-time.h 2014-01-30 19:00:22 +0000
116+++ include/datetime/date-time.h 2014-02-19 15:36:23 +0000
117@@ -41,6 +41,7 @@
118 DateTime& operator=(GDateTime* in);
119 DateTime& operator=(const DateTime& in);
120 DateTime to_timezone(const std::string& zone) const;
121+ DateTime add_full(int years, int months, int days, int hours, int minutes, double seconds) const;
122 void reset(GDateTime* in=nullptr);
123
124 GDateTime* get() const;
125@@ -48,9 +49,11 @@
126
127 std::string format(const std::string& fmt) const;
128 int day_of_month() const;
129+ double seconds() const;
130 int64_t to_unix() const;
131
132 bool operator<(const DateTime& that) const;
133+ bool operator<=(const DateTime& that) const;
134 bool operator!=(const DateTime& that) const;
135 bool operator==(const DateTime& that) const;
136
137
138=== modified file 'include/datetime/planner-eds.h'
139--- include/datetime/planner-eds.h 2014-01-15 05:07:10 +0000
140+++ include/datetime/planner-eds.h 2014-02-19 15:36:23 +0000
141@@ -20,9 +20,10 @@
142 #ifndef INDICATOR_DATETIME_PLANNER_EDS_H
143 #define INDICATOR_DATETIME_PLANNER_EDS_H
144
145+#include <datetime/clock.h>
146 #include <datetime/planner.h>
147
148-#include <memory> // unique_ptr
149+#include <memory> // shared_ptr, unique_ptr
150
151 namespace unity {
152 namespace indicator {
153@@ -34,7 +35,7 @@
154 class PlannerEds: public Planner
155 {
156 public:
157- PlannerEds();
158+ PlannerEds(const std::shared_ptr<Clock>& clock);
159 virtual ~PlannerEds();
160
161 private:
162
163=== added file 'include/datetime/snap.h'
164--- include/datetime/snap.h 1970-01-01 00:00:00 +0000
165+++ include/datetime/snap.h 2014-02-19 15:36:23 +0000
166@@ -0,0 +1,51 @@
167+/*
168+ * Copyright 2014 Canonical Ltd.
169+ *
170+ * This program is free software: you can redistribute it and/or modify it
171+ * under the terms of the GNU General Public License version 3, as published
172+ * by the Free Software Foundation.
173+ *
174+ * This program is distributed in the hope that it will be useful, but
175+ * WITHOUT ANY WARRANTY; without even the implied warranties of
176+ * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
177+ * PURPOSE. See the GNU General Public License for more details.
178+ *
179+ * You should have received a copy of the GNU General Public License along
180+ * with this program. If not, see <http://www.gnu.org/licenses/>.
181+ *
182+ * Authors:
183+ * Charles Kerr <charles.kerr@canonical.com>
184+ */
185+
186+#ifndef INDICATOR_DATETIME_SNAP_H
187+#define INDICATOR_DATETIME_SNAP_H
188+
189+#include <datetime/appointment.h>
190+
191+#include <memory>
192+#include <functional>
193+
194+namespace unity {
195+namespace indicator {
196+namespace datetime {
197+
198+/**
199+ * \brief Pops up Snap Decisions for appointments
200+ */
201+class Snap
202+{
203+public:
204+ Snap();
205+ virtual ~Snap();
206+
207+ typedef std::function<void(const Appointment&)> appointment_func;
208+ void operator()(const Appointment& appointment,
209+ appointment_func show,
210+ appointment_func dismiss);
211+};
212+
213+} // namespace datetime
214+} // namespace indicator
215+} // namespace unity
216+
217+#endif // INDICATOR_DATETIME_SNAP_H
218
219=== modified file 'include/datetime/timezone-file.h'
220--- include/datetime/timezone-file.h 2014-01-30 19:00:22 +0000
221+++ include/datetime/timezone-file.h 2014-02-19 15:36:23 +0000
222@@ -42,8 +42,8 @@
223 ~FileTimezone();
224
225 private:
226- void setFilename(const std::string& filename);
227- static void onFileChanged(gpointer gself);
228+ void set_filename(const std::string& filename);
229+ static void on_file_changed(gpointer gself);
230 void clear();
231 void reload();
232
233
234=== modified file 'po/POTFILES.in'
235--- po/POTFILES.in 2014-02-10 22:59:02 +0000
236+++ po/POTFILES.in 2014-02-19 15:36:23 +0000
237@@ -1,3 +1,5 @@
238 src/formatter.cpp
239 src/formatter-desktop.cpp
240 src/menu.cpp
241+src/snap.cpp
242+src/utils.c
243
244=== modified file 'src/CMakeLists.txt'
245--- src/CMakeLists.txt 2014-02-10 22:59:02 +0000
246+++ src/CMakeLists.txt 2014-02-19 15:36:23 +0000
247@@ -13,6 +13,7 @@
248 appointment.cpp
249 clock.cpp
250 clock-live.cpp
251+ clock-watcher.cpp
252 date-time.cpp
253 exporter.cpp
254 formatter.cpp
255@@ -22,6 +23,7 @@
256 menu.cpp
257 planner-eds.cpp
258 settings-live.cpp
259+ snap.cpp
260 timezone-file.cpp
261 timezone-geoclue.cpp
262 timezones-live.cpp
263
264=== modified file 'src/actions-live.cpp'
265--- src/actions-live.cpp 2014-02-10 22:59:02 +0000
266+++ src/actions-live.cpp 2014-02-19 15:36:23 +0000
267@@ -156,7 +156,7 @@
268
269 GError * err = nullptr;
270 auto proxy = g_dbus_proxy_new_for_bus_finish(res, &err);
271- if (err != NULL)
272+ if (err != nullptr)
273 {
274 if (!g_error_matches(err, G_IO_ERROR, G_IO_ERROR_CANCELLED))
275 g_warning("Could not grab DBus proxy for timedated: %s", err->message);
276
277=== added file 'src/clock-watcher.cpp'
278--- src/clock-watcher.cpp 1970-01-01 00:00:00 +0000
279+++ src/clock-watcher.cpp 2014-02-19 15:36:23 +0000
280@@ -0,0 +1,71 @@
281+/*
282+ * Copyright 2014 Canonical Ltd.
283+ *
284+ * This program is free software: you can redistribute it and/or modify it
285+ * under the terms of the GNU General Public License version 3, as published
286+ * by the Free Software Foundation.
287+ *
288+ * This program is distributed in the hope that it will be useful, but
289+ * WITHOUT ANY WARRANTY; without even the implied warranties of
290+ * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
291+ * PURPOSE. See the GNU General Public License for more details.
292+ *
293+ * You should have received a copy of the GNU General Public License along
294+ * with this program. If not, see <http://www.gnu.org/licenses/>.
295+ *
296+ * Authors:
297+ * Charles Kerr <charles.kerr@canonical.com>
298+ */
299+
300+#include <datetime/clock-watcher.h>
301+
302+namespace unity {
303+namespace indicator {
304+namespace datetime {
305+
306+/***
307+****
308+***/
309+
310+ClockWatcherImpl::ClockWatcherImpl(const std::shared_ptr<const State>& state):
311+ m_state(state)
312+{
313+ m_state->planner->upcoming.changed().connect([this](const std::vector<Appointment>&){
314+ g_debug("ClockWatcher pulse because upcoming appointments changed");
315+ pulse();
316+ });
317+ m_state->clock->minute_changed.connect([this](){
318+ g_debug("ClockWatcher pulse because clock minute_changed");
319+ pulse();
320+ });
321+ pulse();
322+}
323+
324+core::Signal<const Appointment&>& ClockWatcherImpl::alarm_reached()
325+{
326+ return m_alarm_reached;
327+}
328+
329+void ClockWatcherImpl::pulse()
330+{
331+ const auto now = m_state->clock->localtime();
332+
333+ for(const auto& appointment : m_state->planner->upcoming.get())
334+ {
335+ if (m_triggered.count(appointment.uid))
336+ continue;
337+ if (!DateTime::is_same_minute(now, appointment.begin))
338+ continue;
339+
340+ m_triggered.insert(appointment.uid);
341+ m_alarm_reached(appointment);
342+ }
343+}
344+
345+/***
346+****
347+***/
348+
349+} // namespace datetime
350+} // namespace indicator
351+} // namespace unity
352
353=== modified file 'src/date-time.cpp'
354--- src/date-time.cpp 2014-01-30 19:00:22 +0000
355+++ src/date-time.cpp 2014-02-19 15:36:23 +0000
356@@ -69,6 +69,14 @@
357 return dt;
358 }
359
360+DateTime DateTime::add_full(int years, int months, int days, int hours, int minutes, double seconds) const
361+{
362+ auto gdt = g_date_time_add_full(get(), years, months, days, hours, minutes, seconds);
363+ DateTime dt(gdt);
364+ g_date_time_unref(gdt);
365+ return dt;
366+}
367+
368 GDateTime* DateTime::get() const
369 {
370 g_assert(m_dt);
371@@ -88,6 +96,11 @@
372 return g_date_time_get_day_of_month(get());
373 }
374
375+double DateTime::seconds() const
376+{
377+ return g_date_time_get_seconds(get());
378+}
379+
380 int64_t DateTime::to_unix() const
381 {
382 return g_date_time_to_unix(get());
383@@ -112,6 +125,11 @@
384 return g_date_time_compare(get(), that.get()) < 0;
385 }
386
387+bool DateTime::operator<=(const DateTime& that) const
388+{
389+ return g_date_time_compare(get(), that.get()) <= 0;
390+}
391+
392 bool DateTime::operator!=(const DateTime& that) const
393 {
394 // return true if this isn't set, or if it's not equal
395
396=== modified file 'src/main.cpp'
397--- src/main.cpp 2014-01-30 19:06:33 +0000
398+++ src/main.cpp 2014-02-19 15:36:23 +0000
399@@ -17,24 +17,25 @@
400 * with this program. If not, see <http://www.gnu.org/licenses/>.
401 */
402
403-
404-
405 #include <datetime/actions-live.h>
406 #include <datetime/clock.h>
407+#include <datetime/clock-watcher.h>
408 #include <datetime/exporter.h>
409 #include <datetime/locations-settings.h>
410 #include <datetime/menu.h>
411 #include <datetime/planner-eds.h>
412 #include <datetime/settings-live.h>
413+#include <datetime/snap.h>
414 #include <datetime/state.h>
415 #include <datetime/timezones-live.h>
416
417 #include <glib/gi18n.h> // bindtextdomain()
418 #include <gio/gio.h>
419-#include <libnotify/notify.h>
420+
421+#include <url-dispatcher.h>
422
423 #include <locale.h>
424-#include <stdlib.h> // exit()
425+#include <cstdlib> // exit()
426
427 using namespace unity::indicator::datetime;
428
429@@ -50,10 +51,6 @@
430 bindtextdomain(GETTEXT_PACKAGE, GNOMELOCALEDIR);
431 textdomain(GETTEXT_PACKAGE);
432
433- // init libnotify
434- if(!notify_init("indicator-datetime-service"))
435- g_critical("libnotify initialization failed");
436-
437 // build the state, actions, and menufactory
438 std::shared_ptr<State> state(new State);
439 std::shared_ptr<Settings> live_settings(new LiveSettings);
440@@ -62,11 +59,27 @@
441 state->settings = live_settings;
442 state->clock = live_clock;
443 state->locations.reset(new SettingsLocations(live_settings, live_timezones));
444- state->planner.reset(new PlannerEds);
445+ state->planner.reset(new PlannerEds(live_clock));
446 state->planner->time = live_clock->localtime();
447 std::shared_ptr<Actions> actions(new LiveActions(state));
448 MenuFactory factory(actions, state);
449
450+ // snap decisions
451+ ClockWatcherImpl clock_watcher(state);
452+ Snap snap;
453+ clock_watcher.alarm_reached().connect([&snap](const Appointment& appt){
454+ auto snap_show = [](const Appointment& a){
455+ const char* url;
456+ if(!a.url.empty())
457+ url = a.url.c_str();
458+ else // alarm doesn't have a URl associated with it; use a fallback
459+ url = "appid://com.ubuntu.clock/clock/current-user-version";
460+ url_dispatch_send(url, nullptr, nullptr);
461+ };
462+ auto snap_dismiss = [](const Appointment&){};
463+ snap(appt, snap_show, snap_dismiss);
464+ });
465+
466 // create the menus
467 std::vector<std::shared_ptr<Menu>> menus;
468 for(int i=0, n=Menu::NUM_PROFILES; i<n; i++)
469
470=== modified file 'src/menu.cpp'
471--- src/menu.cpp 2014-02-05 18:22:42 +0000
472+++ src/menu.cpp 2014-02-19 15:36:23 +0000
473@@ -22,11 +22,11 @@
474 #include <datetime/formatter.h>
475 #include <datetime/state.h>
476
477-#include <json-glib/json-glib.h>
478-
479 #include <glib/gi18n.h>
480 #include <gio/gio.h>
481
482+#include <vector>
483+
484 namespace unity {
485 namespace indicator {
486 namespace datetime {
487@@ -62,7 +62,7 @@
488 ****/
489
490
491-#define FALLBACK_ALARM_CLOCK_ICON_NAME "clock"
492+#define ALARM_ICON_NAME "alarm-clock"
493 #define CALENDAR_ICON_NAME "calendar"
494
495 class MenuImpl: public Menu
496@@ -105,12 +105,15 @@
497 update_section(Appointments); // showing events got toggled
498 });
499 m_state->planner->upcoming.changed().connect([this](const std::vector<Appointment>&){
500- update_section(Appointments); // "upcoming" is the list of Appointments we show
501+ update_upcoming(); // our m_upcoming is planner->upcoming() filtered by time
502 });
503 m_state->clock->date_changed.connect([this](){
504 update_section(Calendar); // need to update the Date menuitem
505 update_section(Locations); // locations' relative time may have changed
506 });
507+ m_state->clock->minute_changed.connect([this](){
508+ update_upcoming(); // our m_upcoming is planner->upcoming() filtered by time
509+ });
510 m_state->locations->locations.changed().connect([this](const std::vector<Location>&) {
511 update_section(Locations); // "locations" is the list of Locations we show
512 });
513@@ -133,6 +136,24 @@
514 g_action_group_change_action_state(action_group, action_name.c_str(), state);
515 }
516
517+ void update_upcoming()
518+ {
519+ const auto now = m_state->clock->localtime();
520+ const auto next_minute = now.add_full(0,0,0,0,1,-now.seconds());
521+
522+ std::vector<Appointment> upcoming;
523+ for(const auto& a : m_state->planner->upcoming.get())
524+ if (next_minute <= a.begin)
525+ upcoming.push_back(a);
526+
527+ if (m_upcoming != upcoming)
528+ {
529+ m_upcoming.swap(upcoming);
530+ update_header(); // show an 'alarm' icon if there are upcoming alarms
531+ update_section(Appointments); // "upcoming" is the list of Appointments we show
532+ }
533+ }
534+
535 std::shared_ptr<const State> m_state;
536 std::shared_ptr<Actions> m_actions;
537 std::shared_ptr<const Formatter> m_formatter;
538@@ -141,71 +162,19 @@
539 GVariant* get_serialized_alarm_icon()
540 {
541 if (G_UNLIKELY(m_serialized_alarm_icon == nullptr))
542- m_serialized_alarm_icon = create_alarm_icon();
543+ {
544+ auto i = g_themed_icon_new_with_default_fallbacks(ALARM_ICON_NAME);
545+ m_serialized_alarm_icon = g_icon_serialize(i);
546+ g_object_unref(i);
547+ }
548
549 return m_serialized_alarm_icon;
550 }
551
552+ std::vector<Appointment> m_upcoming;
553+
554 private:
555
556- /* try to get the clock app's filename from click. (/$pkgdir/$icon) */
557- static GVariant* create_alarm_icon()
558- {
559- GVariant* serialized = nullptr;
560- gchar* icon_filename = nullptr;
561- gchar* standard_error = nullptr;
562- gchar* pkgdir = nullptr;
563-
564- g_spawn_command_line_sync("click pkgdir com.ubuntu.clock", &pkgdir, &standard_error, nullptr, nullptr);
565- g_clear_pointer(&standard_error, g_free);
566- if (pkgdir != nullptr)
567- {
568- gchar* manifest = nullptr;
569- g_strstrip(pkgdir);
570- g_spawn_command_line_sync("click info com.ubuntu.clock", &manifest, &standard_error, nullptr, nullptr);
571- g_clear_pointer(&standard_error, g_free);
572- if (manifest != nullptr)
573- {
574- JsonParser* parser = json_parser_new();
575- if (json_parser_load_from_data(parser, manifest, -1, nullptr))
576- {
577- JsonNode* root = json_parser_get_root(parser); /* transfer-none */
578- if ((root != nullptr) && (JSON_NODE_TYPE(root) == JSON_NODE_OBJECT))
579- {
580- JsonObject* o = json_node_get_object(root); /* transfer-none */
581- const gchar* icon_name = json_object_get_string_member(o, "icon");
582- if (icon_name != nullptr)
583- icon_filename = g_build_filename(pkgdir, icon_name, nullptr);
584- }
585- }
586- g_object_unref(parser);
587- g_free(manifest);
588- }
589- g_free(pkgdir);
590- }
591-
592- if (icon_filename != nullptr)
593- {
594- GFile* file = g_file_new_for_path(icon_filename);
595- GIcon* icon = g_file_icon_new(file);
596-
597- serialized = g_icon_serialize(icon);
598-
599- g_object_unref(icon);
600- g_object_unref(file);
601- g_free(icon_filename);
602- }
603-
604- if (serialized == nullptr)
605- {
606- auto i = g_themed_icon_new_with_default_fallbacks(FALLBACK_ALARM_CLOCK_ICON_NAME);
607- serialized = g_icon_serialize(i);
608- g_object_unref(i);
609- }
610-
611- return serialized;
612- }
613-
614 GVariant* get_serialized_calendar_icon()
615 {
616 if (G_UNLIKELY(m_serialized_calendar_icon == nullptr))
617@@ -273,7 +242,7 @@
618 // add calendar
619 if (show_calendar)
620 {
621- item = g_menu_item_new ("[calendar]", NULL);
622+ item = g_menu_item_new ("[calendar]", nullptr);
623 v = g_variant_new_int64(0);
624 g_menu_item_set_action_and_target_value (item, "indicator.calendar", v);
625 g_menu_item_set_attribute (item, "x-canonical-type",
626@@ -296,11 +265,13 @@
627 const int MAX_APPTS = 5;
628 std::set<std::string> added;
629
630- for (const auto& appt : m_state->planner->upcoming.get())
631+ for (const auto& appt : m_upcoming)
632 {
633+ // don't show too many
634 if (n++ >= MAX_APPTS)
635 break;
636
637+ // don't show duplicates
638 if (added.count(appt.uid))
639 continue;
640
641@@ -508,7 +479,7 @@
642 {
643 // are there alarms?
644 bool has_alarms = false;
645- for(const auto& appointment : m_state->planner->upcoming.get())
646+ for(const auto& appointment : m_upcoming)
647 if((has_alarms = appointment.has_alarms))
648 break;
649
650
651=== modified file 'src/planner-eds.cpp'
652--- src/planner-eds.cpp 2014-01-31 00:33:14 +0000
653+++ src/planner-eds.cpp 2014-02-19 15:36:23 +0000
654@@ -26,6 +26,9 @@
655 #include <libecal/libecal.h>
656 #include <libedataserver/libedataserver.h>
657
658+#include <map>
659+#include <set>
660+
661 namespace unity {
662 namespace indicator {
663 namespace datetime {
664@@ -34,25 +37,28 @@
665 *****
666 ****/
667
668-G_DEFINE_QUARK("source-client", source_client)
669-
670-
671 class PlannerEds::Impl
672 {
673 public:
674
675- Impl(PlannerEds& owner):
676+ Impl(PlannerEds& owner, const std::shared_ptr<Clock>& clock):
677 m_owner(owner),
678+ m_clock(clock),
679 m_cancellable(g_cancellable_new())
680 {
681 e_source_registry_new(m_cancellable, on_source_registry_ready, this);
682
683+ m_clock->minute_changed.connect([this](){
684+ g_debug("rebuilding upcoming because the clock's minute_changed");
685+ rebuild_soon(UPCOMING);
686+ });
687+
688 m_owner.time.changed().connect([this](const DateTime& dt) {
689- g_debug("planner's datetime property changed to %s; calling rebuildSoon()", dt.format("%F %T").c_str());
690- rebuildSoon();
691+ g_debug("planner's datetime property changed to %s; calling rebuild_soon()", dt.format("%F %T").c_str());
692+ rebuild_soon(MONTH);
693 });
694
695- rebuildSoon();
696+ rebuild_soon(ALL);
697 }
698
699 ~Impl()
700@@ -60,6 +66,9 @@
701 g_cancellable_cancel(m_cancellable);
702 g_clear_object(&m_cancellable);
703
704+ while(!m_sources.empty())
705+ remove_source(*m_sources.begin());
706+
707 if (m_rebuild_tag)
708 g_source_remove(m_rebuild_tag);
709
710@@ -83,26 +92,31 @@
711 }
712 else
713 {
714+ g_signal_connect(r, "source-added", G_CALLBACK(on_source_added), gself);
715+ g_signal_connect(r, "source-removed", G_CALLBACK(on_source_removed), gself);
716+ g_signal_connect(r, "source-changed", G_CALLBACK(on_source_changed), gself);
717+ g_signal_connect(r, "source-disabled", G_CALLBACK(on_source_disabled), gself);
718+ g_signal_connect(r, "source-enabled", G_CALLBACK(on_source_enabled), gself);
719+
720 auto self = static_cast<Impl*>(gself);
721-
722- g_signal_connect(r, "source-added", G_CALLBACK(on_source_added), self);
723- g_signal_connect(r, "source-removed", G_CALLBACK(on_source_removed), self);
724- g_signal_connect(r, "source-changed", G_CALLBACK(on_source_changed), self);
725- g_signal_connect(r, "source-disabled", G_CALLBACK(on_source_disabled), self);
726- g_signal_connect(r, "source-enabled", G_CALLBACK(on_source_enabled), self);
727-
728 self->m_source_registry = r;
729-
730- GList* sources = e_source_registry_list_sources(r, E_SOURCE_EXTENSION_CALENDAR);
731- for (auto l=sources; l!=nullptr; l=l->next)
732- on_source_added(r, E_SOURCE(l->data), gself);
733- g_list_free_full(sources, g_object_unref);
734+ self->add_sources_by_extension(E_SOURCE_EXTENSION_CALENDAR);
735+ self->add_sources_by_extension(E_SOURCE_EXTENSION_TASK_LIST);
736 }
737 }
738
739+ void add_sources_by_extension(const char* extension)
740+ {
741+ auto& r = m_source_registry;
742+ auto sources = e_source_registry_list_sources(r, extension);
743+ for (auto l=sources; l!=nullptr; l=l->next)
744+ on_source_added(r, E_SOURCE(l->data), this);
745+ g_list_free_full(sources, g_object_unref);
746+ }
747+
748 static void on_source_added(ESourceRegistry* registry, ESource* source, gpointer gself)
749 {
750- auto self = static_cast<PlannerEds::Impl*>(gself);
751+ auto self = static_cast<Impl*>(gself);
752
753 self->m_sources.insert(E_SOURCE(g_object_ref(source)));
754
755@@ -112,10 +126,19 @@
756
757 static void on_source_enabled(ESourceRegistry* /*registry*/, ESource* source, gpointer gself)
758 {
759- auto self = static_cast<PlannerEds::Impl*>(gself);
760-
761+ auto self = static_cast<Impl*>(gself);
762+ ECalClientSourceType source_type;
763+
764+ if (e_source_has_extension(source, E_SOURCE_EXTENSION_CALENDAR))
765+ source_type = E_CAL_CLIENT_SOURCE_TYPE_EVENTS;
766+ else if (e_source_has_extension(source, E_SOURCE_EXTENSION_TASK_LIST))
767+ source_type = E_CAL_CLIENT_SOURCE_TYPE_TASKS;
768+ else
769+ g_assert_not_reached();
770+
771+ g_debug("connecting a client to source %s", e_source_get_uid(source));
772 e_cal_client_connect(source,
773- E_CAL_CLIENT_SOURCE_TYPE_EVENTS,
774+ source_type,
775 self->m_cancellable,
776 on_client_connected,
777 gself);
778@@ -134,45 +157,118 @@
779 }
780 else
781 {
782- // we've got a new connected ECalClient, so store it & notify clients
783- g_object_set_qdata_full(G_OBJECT(e_client_get_source(client)),
784- source_client_quark(),
785- client,
786- g_object_unref);
787-
788- g_debug("client connected; calling rebuildSoon()");
789- static_cast<Impl*>(gself)->rebuildSoon();
790- }
791+ // add the client to our collection
792+ auto self = static_cast<Impl*>(gself);
793+ g_debug("got a client for %s", e_cal_client_get_local_attachment_store(E_CAL_CLIENT(client)));
794+ self->m_clients[e_client_get_source(client)] = E_CAL_CLIENT(client);
795+
796+ // now create a view for it so that we can listen for changes
797+ e_cal_client_get_view (E_CAL_CLIENT(client),
798+ "#t", // match all
799+ self->m_cancellable,
800+ on_client_view_ready,
801+ self);
802+
803+ g_debug("client connected; calling rebuild_soon()");
804+ self->rebuild_soon(ALL);
805+ }
806+ }
807+
808+ static void on_client_view_ready (GObject* client, GAsyncResult* res, gpointer gself)
809+ {
810+ GError* error = nullptr;
811+ ECalClientView* view = nullptr;
812+
813+ if (e_cal_client_get_view_finish (E_CAL_CLIENT(client), res, &view, &error))
814+ {
815+ // add the view to our collection
816+ e_cal_client_view_start(view, &error);
817+ g_debug("got a view for %s", e_cal_client_get_local_attachment_store(E_CAL_CLIENT(client)));
818+ auto self = static_cast<Impl*>(gself);
819+ self->m_views[e_client_get_source(E_CLIENT(client))] = view;
820+
821+ g_signal_connect(view, "objects-added", G_CALLBACK(on_view_objects_added), self);
822+ g_signal_connect(view, "objects-modified", G_CALLBACK(on_view_objects_modified), self);
823+ g_signal_connect(view, "objects-removed", G_CALLBACK(on_view_objects_removed), self);
824+ g_debug("view connected; calling rebuild_soon()");
825+ self->rebuild_soon(ALL);
826+ }
827+ else if(error != nullptr)
828+ {
829+ if (!g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
830+ g_warning("indicator-datetime cannot get View to EDS client: %s", error->message);
831+
832+ g_error_free(error);
833+ }
834+ }
835+
836+ static void on_view_objects_added(ECalClientView* /*view*/, gpointer /*objects*/, gpointer gself)
837+ {
838+ g_debug("%s", G_STRFUNC);
839+ static_cast<Impl*>(gself)->rebuild_soon(ALL);
840+ }
841+ static void on_view_objects_modified(ECalClientView* /*view*/, gpointer /*objects*/, gpointer gself)
842+ {
843+ g_debug("%s", G_STRFUNC);
844+ static_cast<Impl*>(gself)->rebuild_soon(ALL);
845+ }
846+ static void on_view_objects_removed(ECalClientView* /*view*/, gpointer /*objects*/, gpointer gself)
847+ {
848+ g_debug("%s", G_STRFUNC);
849+ static_cast<Impl*>(gself)->rebuild_soon(ALL);
850 }
851
852 static void on_source_disabled(ESourceRegistry* /*registry*/, ESource* source, gpointer gself)
853 {
854- gpointer e_cal_client;
855-
856- // if this source has a connected ECalClient, remove it & notify clients
857- if ((e_cal_client = g_object_steal_qdata(G_OBJECT(source), source_client_quark())))
858- {
859- g_object_unref(e_cal_client);
860-
861- g_debug("source disabled; calling rebuildSoon()");
862- static_cast<Impl*>(gself)->rebuildSoon();
863- }
864- }
865-
866- static void on_source_removed(ESourceRegistry* registry, ESource* source, gpointer gself)
867- {
868- auto self = static_cast<PlannerEds::Impl*>(gself);
869-
870- on_source_disabled(registry, source, gself);
871-
872- self->m_sources.erase(source);
873- g_object_unref(source);
874+ static_cast<Impl*>(gself)->disable_source(source);
875+ }
876+ void disable_source(ESource* source)
877+ {
878+ // if an ECalClientView is associated with this source, remove it
879+ auto vit = m_views.find(source);
880+ if (vit != m_views.end())
881+ {
882+ auto& view = vit->second;
883+ e_cal_client_view_stop(view, nullptr);
884+ const auto n_disconnected = g_signal_handlers_disconnect_by_data(view, this);
885+ g_warn_if_fail(n_disconnected == 3);
886+ g_object_unref(view);
887+ m_views.erase(vit);
888+ rebuild_soon(ALL);
889+ }
890+
891+ // if an ECalClient is associated with this source, remove it
892+ auto cit = m_clients.find(source);
893+ if (cit != m_clients.end())
894+ {
895+ auto& client = cit->second;
896+ g_object_unref(client);
897+ m_clients.erase(cit);
898+ rebuild_soon(ALL);
899+ }
900+ }
901+
902+ static void on_source_removed(ESourceRegistry* /*registry*/, ESource* source, gpointer gself)
903+ {
904+ static_cast<Impl*>(gself)->remove_source(source);
905+ }
906+ void remove_source(ESource* source)
907+ {
908+ disable_source(source);
909+
910+ auto sit = m_sources.find(source);
911+ if (sit != m_sources.end())
912+ {
913+ g_object_unref(*sit);
914+ m_sources.erase(sit);
915+ rebuild_soon(ALL);
916+ }
917 }
918
919 static void on_source_changed(ESourceRegistry* /*registry*/, ESource* /*source*/, gpointer gself)
920 {
921- g_debug("source changed; calling rebuildSoon()");
922- static_cast<Impl*>(gself)->rebuildSoon();
923+ g_debug("source changed; calling rebuild_soon()");
924+ static_cast<Impl*>(gself)->rebuild_soon(ALL);
925 }
926
927 private:
928@@ -196,58 +292,72 @@
929 task(task_in), client(client_in), color(color_in) {}
930 };
931
932- void rebuildSoon()
933+ void rebuild_soon(int rebuild_flags)
934 {
935- const static guint ARBITRARY_INTERVAL_SECS = 2;
936+ static const guint ARBITRARY_INTERVAL_SECS = 2;
937+
938+ m_rebuild_flags |= rebuild_flags;
939
940 if (m_rebuild_tag == 0)
941- m_rebuild_tag = g_timeout_add_seconds(ARBITRARY_INTERVAL_SECS, rebuildNowStatic, this);
942+ m_rebuild_tag = g_timeout_add_seconds(ARBITRARY_INTERVAL_SECS, rebuild_now_static, this);
943 }
944
945- static gboolean rebuildNowStatic(gpointer gself)
946+ static gboolean rebuild_now_static(gpointer gself)
947 {
948 auto self = static_cast<Impl*>(gself);
949+ const auto flags = self->m_rebuild_flags;
950 self->m_rebuild_tag = 0;
951- self->rebuildNow();
952+ self->m_rebuild_flags = 0;
953+ self->rebuild_now(flags);
954 return G_SOURCE_REMOVE;
955 }
956
957- void rebuildNow()
958- {
959- const auto calendar_date = m_owner.time.get().get();
960- GDateTime* begin;
961- GDateTime* end;
962- int y, m, d;
963-
964- // get all the appointments in the calendar month
965- g_date_time_get_ymd(calendar_date, &y, &m, &d);
966- begin = g_date_time_new_local(y, m, 1, 0, 0, 0.1);
967- end = g_date_time_new_local(y, m, g_date_get_days_in_month(GDateMonth(m),GDateYear(y)), 23, 59, 59.9);
968- if (begin && end)
969- {
970- getAppointments(begin, end, [this](const std::vector<Appointment>& appointments) {
971- g_debug("got %d appointments in this calendar month", (int)appointments.size());
972- m_owner.this_month.set(appointments);
973- });
974- }
975- g_clear_pointer(&begin, g_date_time_unref);
976- g_clear_pointer(&end, g_date_time_unref);
977-
978- // get the upcoming appointments
979- begin = g_date_time_ref(calendar_date);
980- end = g_date_time_add_months(begin, 1);
981- if (begin && end)
982- {
983- getAppointments(begin, end, [this](const std::vector<Appointment>& appointments) {
984- g_debug("got %d upcoming appointments", (int)appointments.size());
985- m_owner.upcoming.set(appointments);
986- });
987- }
988- g_clear_pointer(&begin, g_date_time_unref);
989- g_clear_pointer(&end, g_date_time_unref);
990- }
991-
992- void getAppointments(GDateTime* begin_dt, GDateTime* end_dt, appointment_func func)
993+ void rebuild_now(int rebuild_flags)
994+ {
995+ if (rebuild_flags & UPCOMING)
996+ rebuild_upcoming();
997+
998+ if (rebuild_flags & MONTH)
999+ rebuild_month();
1000+ }
1001+
1002+ void rebuild_month()
1003+ {
1004+ const auto ref = m_owner.time.get().get();
1005+ auto month_begin = g_date_time_add_full(ref,
1006+ 0, // subtract no years
1007+ 0, // subtract no months
1008+ -(g_date_time_get_day_of_month(ref)-1),
1009+ -g_date_time_get_hour(ref),
1010+ -g_date_time_get_minute(ref),
1011+ -g_date_time_get_seconds(ref));
1012+ auto month_end = g_date_time_add_full(month_begin, 0, 1, 0, 0, 0, -0.1);
1013+
1014+ get_appointments(month_begin, month_end, [this](const std::vector<Appointment>& appointments) {
1015+ g_debug("got %d appointments in this calendar month", (int)appointments.size());
1016+ m_owner.this_month.set(appointments);
1017+ });
1018+
1019+ g_date_time_unref(month_end);
1020+ g_date_time_unref(month_begin);
1021+ }
1022+
1023+ void rebuild_upcoming()
1024+ {
1025+ const auto ref = m_clock->localtime();
1026+ const auto begin = g_date_time_add_minutes(ref.get(),-10);
1027+ const auto end = g_date_time_add_months(begin,1);
1028+
1029+ get_appointments(begin, end, [this](const std::vector<Appointment>& appointments) {
1030+ g_debug("got %d upcoming appointments", (int)appointments.size());
1031+ m_owner.upcoming.set(appointments);
1032+ });
1033+
1034+ g_date_time_unref(end);
1035+ g_date_time_unref(begin);
1036+ }
1037+
1038+ void get_appointments(GDateTime* begin_dt, GDateTime* end_dt, appointment_func func)
1039 {
1040 const auto begin = g_date_time_to_unix(begin_dt);
1041 const auto end = g_date_time_to_unix(end_dt);
1042@@ -286,16 +396,14 @@
1043 delete task;
1044 });
1045
1046- for (auto& source : m_sources)
1047+ for (auto& kv : m_clients)
1048 {
1049- auto client = E_CAL_CLIENT(g_object_get_qdata(G_OBJECT(source), source_client_quark()));
1050- if (client == nullptr)
1051- continue;
1052-
1053+ auto& client = kv.second;
1054 if (default_timezone != nullptr)
1055 e_cal_client_set_default_timezone(client, default_timezone);
1056
1057 // start a new subtask to enumerate all the components in this client.
1058+ auto& source = kv.first;
1059 auto extension = e_source_get_extension(source, E_SOURCE_EXTENSION_CALENDAR);
1060 const auto color = e_source_selectable_get_color(E_SOURCE_SELECTABLE(extension));
1061 g_debug("calling e_cal_client_generate_instances for %p", (void*)client);
1062@@ -407,16 +515,19 @@
1063 delete subtask;
1064 }
1065
1066-private:
1067-
1068 PlannerEds& m_owner;
1069+ std::shared_ptr<Clock> m_clock;
1070 std::set<ESource*> m_sources;
1071- GCancellable * m_cancellable = nullptr;
1072- ESourceRegistry * m_source_registry = nullptr;
1073+ std::map<ESource*,ECalClient*> m_clients;
1074+ std::map<ESource*,ECalClientView*> m_views;
1075+ GCancellable* m_cancellable = nullptr;
1076+ ESourceRegistry* m_source_registry = nullptr;
1077 guint m_rebuild_tag = 0;
1078+ guint m_rebuild_flags = 0;
1079+ enum { UPCOMING=(1<<0), MONTH=(1<<1), ALL=UPCOMING|MONTH };
1080 };
1081
1082-PlannerEds::PlannerEds(): p(new Impl(*this)) {}
1083+PlannerEds::PlannerEds(const std::shared_ptr<Clock>& clock): p(new Impl(*this, clock)) {}
1084
1085 PlannerEds::~PlannerEds() =default;
1086
1087
1088=== added file 'src/snap.cpp'
1089--- src/snap.cpp 1970-01-01 00:00:00 +0000
1090+++ src/snap.cpp 2014-02-19 15:36:23 +0000
1091@@ -0,0 +1,256 @@
1092+/*
1093+ * Copyright 2014 Canonical Ltd.
1094+ *
1095+ * This program is free software: you can redistribute it and/or modify it
1096+ * under the terms of the GNU General Public License version 3, as published
1097+ * by the Free Software Foundation.
1098+ *
1099+ * This program is distributed in the hope that it will be useful, but
1100+ * WITHOUT ANY WARRANTY; without even the implied warranties of
1101+ * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
1102+ * PURPOSE. See the GNU General Public License for more details.
1103+ *
1104+ * You should have received a copy of the GNU General Public License along
1105+ * with this program. If not, see <http://www.gnu.org/licenses/>.
1106+ *
1107+ * Authors:
1108+ * Charles Kerr <charles.kerr@canonical.com>
1109+ */
1110+
1111+#include <datetime/appointment.h>
1112+#include <datetime/formatter.h>
1113+#include <datetime/snap.h>
1114+
1115+#include <canberra.h>
1116+#include <libnotify/notify.h>
1117+
1118+#include <glib/gi18n.h>
1119+#include <glib.h>
1120+
1121+#define ALARM_SOUND_FILENAME "/usr/share/sounds/ubuntu/stereo/phone-incoming-call.ogg"
1122+
1123+namespace unity {
1124+namespace indicator {
1125+namespace datetime {
1126+
1127+/***
1128+****
1129+***/
1130+
1131+namespace
1132+{
1133+
1134+/**
1135+*** libcanberra -- play sounds
1136+**/
1137+
1138+// arbitrary number, but we need a consistent id for play/cancel
1139+const int32_t alarm_ca_id = 1;
1140+
1141+gboolean media_cached = FALSE;
1142+ca_context *c_context = nullptr;
1143+guint timeout_tag = 0;
1144+
1145+ca_context* get_ca_context()
1146+{
1147+ if (G_UNLIKELY(c_context == nullptr))
1148+ {
1149+ int rv;
1150+
1151+ if ((rv = ca_context_create(&c_context)) != CA_SUCCESS)
1152+ {
1153+ g_warning("Failed to create canberra context: %s\n", ca_strerror(rv));
1154+ c_context = nullptr;
1155+ }
1156+ else
1157+ {
1158+ const char* filename = ALARM_SOUND_FILENAME;
1159+ rv = ca_context_cache(c_context,
1160+ CA_PROP_EVENT_ID, "alarm",
1161+ CA_PROP_MEDIA_FILENAME, filename,
1162+ CA_PROP_CANBERRA_CACHE_CONTROL, "permanent",
1163+ NULL);
1164+ media_cached = rv == CA_SUCCESS;
1165+ if (rv != CA_SUCCESS)
1166+ g_warning("Couldn't add '%s' to canberra cache: %s", filename, ca_strerror(rv));
1167+ }
1168+ }
1169+
1170+ return c_context;
1171+}
1172+
1173+void play_alarm_sound();
1174+
1175+gboolean play_alarm_sound_idle (gpointer)
1176+{
1177+ timeout_tag = 0;
1178+ play_alarm_sound();
1179+ return G_SOURCE_REMOVE;
1180+}
1181+
1182+void on_alarm_play_done (ca_context* /*context*/, uint32_t /*id*/, int rv, void* /*user_data*/)
1183+{
1184+ // wait one second, then play it again
1185+ if ((rv == CA_SUCCESS) && (timeout_tag == 0))
1186+ timeout_tag = g_timeout_add_seconds (1, play_alarm_sound_idle, nullptr);
1187+}
1188+
1189+void play_alarm_sound()
1190+{
1191+ const gchar* filename = ALARM_SOUND_FILENAME;
1192+ auto context = get_ca_context();
1193+ g_return_if_fail(context != nullptr);
1194+
1195+ ca_proplist* props = nullptr;
1196+ ca_proplist_create(&props);
1197+ if (media_cached)
1198+ ca_proplist_sets(props, CA_PROP_EVENT_ID, "alarm");
1199+ ca_proplist_sets(props, CA_PROP_MEDIA_FILENAME, filename);
1200+
1201+ const auto rv = ca_context_play_full(context, alarm_ca_id, props, on_alarm_play_done, nullptr);
1202+ if (rv != CA_SUCCESS)
1203+ g_warning("Failed to play file '%s': %s", filename, ca_strerror(rv));
1204+
1205+ g_clear_pointer(&props, ca_proplist_destroy);
1206+}
1207+
1208+void stop_alarm_sound()
1209+{
1210+ auto context = get_ca_context();
1211+ if (context != nullptr)
1212+ {
1213+ const auto rv = ca_context_cancel(context, alarm_ca_id);
1214+ if (rv != CA_SUCCESS)
1215+ g_warning("Failed to cancel alarm sound: %s", ca_strerror(rv));
1216+ }
1217+
1218+ if (timeout_tag != 0)
1219+ {
1220+ g_source_remove(timeout_tag);
1221+ timeout_tag = 0;
1222+ }
1223+}
1224+
1225+/**
1226+*** libnotify -- snap decisions
1227+**/
1228+
1229+void first_time_init()
1230+{
1231+ static bool inited = false;
1232+
1233+ if (G_UNLIKELY(!inited))
1234+ {
1235+ inited = true;
1236+
1237+ if(!notify_init("indicator-datetime-service"))
1238+ g_critical("libnotify initialization failed");
1239+ }
1240+}
1241+
1242+struct SnapData
1243+{
1244+ Snap::appointment_func show;
1245+ Snap::appointment_func dismiss;
1246+ Appointment appointment;
1247+};
1248+
1249+void on_snap_show(NotifyNotification*, gchar* /*action*/, gpointer gdata)
1250+{
1251+ stop_alarm_sound();
1252+ auto data = static_cast<SnapData*>(gdata);
1253+ data->show(data->appointment);
1254+}
1255+
1256+void on_snap_dismiss(NotifyNotification*, gchar* /*action*/, gpointer gdata)
1257+{
1258+ stop_alarm_sound();
1259+ auto data = static_cast<SnapData*>(gdata);
1260+ data->dismiss(data->appointment);
1261+}
1262+
1263+void snap_data_destroy_notify(gpointer gdata)
1264+{
1265+ delete static_cast<SnapData*>(gdata);
1266+}
1267+
1268+void show_snap_decision(SnapData* data)
1269+{
1270+ const Appointment& appointment = data->appointment;
1271+
1272+ const auto timestr = appointment.begin.format("%a, %X");
1273+ auto title = g_strdup_printf(_("Alarm %s"), timestr.c_str());
1274+ const auto body = appointment.summary;
1275+ const gchar* icon_name = "alarm-clock";
1276+
1277+ auto nn = notify_notification_new(title, body.c_str(), icon_name);
1278+ notify_notification_set_hint_string(nn, "x-canonical-snap-decisions", "true");
1279+ notify_notification_set_hint_string(nn, "x-canonical-private-button-tint", "true");
1280+ notify_notification_add_action(nn, "show", _("Show"), on_snap_show, data, nullptr);
1281+ notify_notification_add_action(nn, "dismiss", _("Dismiss"), on_snap_dismiss, data, nullptr);
1282+ g_object_set_data_full(G_OBJECT(nn), "snap-data", data, snap_data_destroy_notify);
1283+
1284+ GError * error = nullptr;
1285+ notify_notification_show(nn, &error);
1286+ if (error != NULL)
1287+ {
1288+ g_warning("Unable to show snap decision for '%s': %s", body.c_str(), error->message);
1289+ g_error_free(error);
1290+ data->show(data->appointment);
1291+ }
1292+
1293+ g_free(title);
1294+}
1295+
1296+/**
1297+***
1298+**/
1299+
1300+void notify(const Appointment& appointment,
1301+ Snap::appointment_func show,
1302+ Snap::appointment_func dismiss)
1303+{
1304+ auto data = new SnapData;
1305+ data->appointment = appointment;
1306+ data->show = show;
1307+ data->dismiss = dismiss;
1308+
1309+ play_alarm_sound();
1310+ show_snap_decision(data);
1311+}
1312+
1313+} // unnamed namespace
1314+
1315+
1316+/***
1317+****
1318+***/
1319+
1320+Snap::Snap()
1321+{
1322+ first_time_init();
1323+}
1324+
1325+Snap::~Snap()
1326+{
1327+ media_cached = false;
1328+ g_clear_pointer(&c_context, ca_context_destroy);
1329+}
1330+
1331+void Snap::operator()(const Appointment& appointment,
1332+ appointment_func show,
1333+ appointment_func dismiss)
1334+{
1335+ if (appointment.has_alarms)
1336+ notify(appointment, show, dismiss);
1337+ else
1338+ dismiss(appointment);
1339+}
1340+
1341+/***
1342+****
1343+***/
1344+
1345+} // namespace datetime
1346+} // namespace indicator
1347+} // namespace unity
1348
1349=== modified file 'src/timezone-file.cpp'
1350--- src/timezone-file.cpp 2014-02-19 15:36:23 +0000
1351+++ src/timezone-file.cpp 2014-02-19 15:36:23 +0000
1352@@ -32,7 +32,7 @@
1353
1354 FileTimezone::FileTimezone(const std::string& filename)
1355 {
1356- setFilename(filename);
1357+ set_filename(filename);
1358 }
1359
1360 FileTimezone::~FileTimezone()
1361@@ -52,7 +52,7 @@
1362 }
1363
1364 void
1365-FileTimezone::setFilename(const std::string& filename)
1366+FileTimezone::set_filename(const std::string& filename)
1367 {
1368 clear();
1369
1370@@ -79,7 +79,7 @@
1371 }
1372 else
1373 {
1374- m_monitor_handler_id = g_signal_connect_swapped(m_monitor, "changed", G_CALLBACK(onFileChanged), this);
1375+ m_monitor_handler_id = g_signal_connect_swapped(m_monitor, "changed", G_CALLBACK(on_file_changed), this);
1376 g_debug("%s Monitoring timezone file '%s'", G_STRLOC, m_filename.c_str());
1377 }
1378
1379@@ -87,7 +87,7 @@
1380 }
1381
1382 void
1383-FileTimezone::onFileChanged(gpointer gself)
1384+FileTimezone::on_file_changed(gpointer gself)
1385 {
1386 static_cast<FileTimezone*>(gself)->reload();
1387 }
1388
1389=== modified file 'tests/CMakeLists.txt'
1390--- tests/CMakeLists.txt 2014-01-29 03:42:05 +0000
1391+++ tests/CMakeLists.txt 2014-02-19 15:36:23 +0000
1392@@ -42,6 +42,7 @@
1393 endfunction()
1394 add_test_by_name(test-actions)
1395 add_test_by_name(test-clock)
1396+add_test_by_name(test-clock-watcher)
1397 add_test_by_name(test-exporter)
1398 add_test_by_name(test-formatter)
1399 add_test_by_name(test-live-actions)
1400@@ -52,6 +53,10 @@
1401 add_test_by_name(test-timezone-file)
1402 add_test_by_name(test-utils)
1403
1404+set (TEST_NAME manual-test-snap)
1405+add_executable (${TEST_NAME} ${TEST_NAME}.cpp)
1406+add_dependencies (${TEST_NAME} libindicatordatetimeservice)
1407+target_link_libraries (${TEST_NAME} indicatordatetimeservice gtest ${SERVICE_DEPS_LIBRARIES} ${GTEST_LIBS})
1408
1409 # disabling the timezone unit tests because they require
1410 # https://code.launchpad.net/~ted/dbus-test-runner/multi-interface-test/+merge/199724
1411
1412=== modified file 'tests/geoclue-fixture.h'
1413--- tests/geoclue-fixture.h 2014-01-17 03:23:57 +0000
1414+++ tests/geoclue-fixture.h 2014-02-19 15:36:23 +0000
1415@@ -95,7 +95,7 @@
1416
1417 // I've looked and can't find where this extra ref is coming from.
1418 // is there an unbalanced ref to the bus in the test harness?!
1419- while (bus != NULL)
1420+ while (bus != nullptr)
1421 {
1422 g_object_unref (bus);
1423 wait_msec (1000);
1424
1425=== added file 'tests/manual-test-snap.cpp'
1426--- tests/manual-test-snap.cpp 1970-01-01 00:00:00 +0000
1427+++ tests/manual-test-snap.cpp 2014-02-19 15:36:23 +0000
1428@@ -0,0 +1,63 @@
1429+
1430+/*
1431+ * Copyright 2013 Canonical Ltd.
1432+ *
1433+ * Authors:
1434+ * Charles Kerr <charles.kerr@canonical.com>
1435+ *
1436+ * This program is free software: you can redistribute it and/or modify it
1437+ * under the terms of the GNU General Public License version 3, as published
1438+ * by the Free Software Foundation.
1439+ *
1440+ * This program is distributed in the hope that it will be useful, but
1441+ * WITHOUT ANY WARRANTY; without even the implied warranties of
1442+ * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
1443+ * PURPOSE. See the GNU General Public License for more details.
1444+ *
1445+ * You should have received a copy of the GNU General Public License along
1446+ * with this program. If not, see <http://www.gnu.org/licenses/>.
1447+ */
1448+
1449+#include <datetime/appointment.h>
1450+#include <datetime/snap.h>
1451+
1452+#include <glib.h>
1453+
1454+using namespace unity::indicator::datetime;
1455+
1456+/***
1457+****
1458+***/
1459+
1460+int main()
1461+{
1462+ Appointment a;
1463+ a.color = "green";
1464+ a.summary = "Alarm";
1465+ a.url = "alarm:///hello-world";
1466+ a.uid = "D4B57D50247291478ED31DED17FF0A9838DED402";
1467+ a.is_event = false;
1468+ a.is_daily = false;
1469+ a.has_alarms = true;
1470+ auto begin = g_date_time_new_local(2014,12,25,0,0,0);
1471+ auto end = g_date_time_add_full(begin,0,0,1,0,0,-1);
1472+ a.begin = begin;
1473+ a.end = end;
1474+ g_date_time_unref(end);
1475+ g_date_time_unref(begin);
1476+
1477+ auto loop = g_main_loop_new(nullptr, false);
1478+ auto show = [loop](const Appointment& appt){
1479+ g_message("You clicked 'show' for appt url '%s'", appt.url.c_str());
1480+ g_main_loop_quit(loop);
1481+ };
1482+ auto dismiss = [loop](const Appointment&){
1483+ g_message("You clicked 'dismiss'");
1484+ g_main_loop_quit(loop);
1485+ };
1486+
1487+ Snap snap;
1488+ snap(a, show, dismiss);
1489+ g_main_loop_run(loop);
1490+ return 0;
1491+}
1492
1493=== added file 'tests/test-clock-watcher.cpp'
1494--- tests/test-clock-watcher.cpp 1970-01-01 00:00:00 +0000
1495+++ tests/test-clock-watcher.cpp 2014-02-19 15:36:23 +0000
1496@@ -0,0 +1,166 @@
1497+/*
1498+ * Copyright 2014 Canonical Ltd.
1499+ *
1500+ * This program is free software: you can redistribute it and/or modify it
1501+ * under the terms of the GNU General Public License version 3, as published
1502+ * by the Free Software Foundation.
1503+ *
1504+ * This program is distributed in the hope that it will be useful, but
1505+ * WITHOUT ANY WARRANTY; without even the implied warranties of
1506+ * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
1507+ * PURPOSE. See the GNU General Public License for more details.
1508+ *
1509+ * You should have received a copy of the GNU General Public License along
1510+ * with this program. If not, see <http://www.gnu.org/licenses/>.
1511+ *
1512+ * Authors:
1513+ * Charles Kerr <charles.kerr@canonical.com>
1514+ */
1515+
1516+#include <datetime/clock-watcher.h>
1517+
1518+#include <gtest/gtest.h>
1519+
1520+#include "state-fixture.h"
1521+
1522+using namespace unity::indicator::datetime;
1523+
1524+class ClockWatcherFixture: public StateFixture
1525+{
1526+private:
1527+
1528+ typedef StateFixture super;
1529+
1530+protected:
1531+
1532+ std::vector<std::string> m_triggered;
1533+ std::unique_ptr<ClockWatcher> m_watcher;
1534+
1535+ void SetUp()
1536+ {
1537+ super::SetUp();
1538+
1539+ m_watcher.reset(new ClockWatcherImpl(m_state));
1540+ m_watcher->alarm_reached().connect([this](const Appointment& appt){
1541+ m_triggered.push_back(appt.uid);
1542+ });
1543+
1544+ EXPECT_TRUE(m_triggered.empty());
1545+ }
1546+
1547+ void TearDown()
1548+ {
1549+ m_triggered.clear();
1550+ m_watcher.reset();
1551+
1552+ super::TearDown();
1553+ }
1554+
1555+ std::vector<Appointment> build_some_appointments()
1556+ {
1557+ const auto now = m_state->clock->localtime();
1558+ auto tomorrow = g_date_time_add_days (now.get(), 1);
1559+ auto tomorrow_begin = g_date_time_add_full (tomorrow, 0, 0, 0,
1560+ -g_date_time_get_hour(tomorrow),
1561+ -g_date_time_get_minute(tomorrow),
1562+ -g_date_time_get_seconds(tomorrow));
1563+ auto tomorrow_end = g_date_time_add_full (tomorrow_begin, 0, 0, 1, 0, 0, -1);
1564+
1565+ Appointment a1; // an alarm clock appointment
1566+ a1.color = "red";
1567+ a1.summary = "Alarm";
1568+ a1.summary = "http://www.example.com/";
1569+ a1.uid = "example";
1570+ a1.has_alarms = true;
1571+ a1.begin = tomorrow_begin;
1572+ a1.end = tomorrow_end;
1573+
1574+ auto ubermorgen_begin = g_date_time_add_days (tomorrow, 1);
1575+ auto ubermorgen_end = g_date_time_add_full (tomorrow_begin, 0, 0, 1, 0, 0, -1);
1576+
1577+ Appointment a2; // a non-alarm appointment
1578+ a2.color = "green";
1579+ a2.summary = "Other Text";
1580+ a2.summary = "http://www.monkey.com/";
1581+ a2.uid = "monkey";
1582+ a2.has_alarms = false;
1583+ a2.begin = ubermorgen_begin;
1584+ a2.end = ubermorgen_end;
1585+
1586+ // cleanup
1587+ g_date_time_unref(ubermorgen_end);
1588+ g_date_time_unref(ubermorgen_begin);
1589+ g_date_time_unref(tomorrow_end);
1590+ g_date_time_unref(tomorrow_begin);
1591+ g_date_time_unref(tomorrow);
1592+
1593+ return std::vector<Appointment>({a1, a2});
1594+ }
1595+};
1596+
1597+/***
1598+****
1599+***/
1600+
1601+TEST_F(ClockWatcherFixture, AppointmentsChanged)
1602+{
1603+ // Add some appointments to the planner.
1604+ // One of these matches our state's localtime, so that should get triggered.
1605+ std::vector<Appointment> a = build_some_appointments();
1606+ a[0].begin = m_state->clock->localtime();
1607+ m_state->planner->upcoming.set(a);
1608+
1609+ // Confirm that it got fired
1610+ EXPECT_EQ(1, m_triggered.size());
1611+ EXPECT_EQ(a[0].uid, m_triggered[0]);
1612+}
1613+
1614+
1615+TEST_F(ClockWatcherFixture, TimeChanged)
1616+{
1617+ // Add some appointments to the planner.
1618+ // Neither of these match the state's localtime, so nothing should be triggered.
1619+ std::vector<Appointment> a = build_some_appointments();
1620+ m_state->planner->upcoming.set(a);
1621+ EXPECT_TRUE(m_triggered.empty());
1622+
1623+ // Set the state's clock to a time that matches one of the appointments.
1624+ // That appointment should get triggered.
1625+ m_mock_state->mock_clock->set_localtime(a[1].begin);
1626+ EXPECT_EQ(1, m_triggered.size());
1627+ EXPECT_EQ(a[1].uid, m_triggered[0]);
1628+}
1629+
1630+
1631+TEST_F(ClockWatcherFixture, MoreThanOne)
1632+{
1633+ const auto now = m_state->clock->localtime();
1634+ std::vector<Appointment> a = build_some_appointments();
1635+ a[0].begin = a[1].begin = now;
1636+ m_state->planner->upcoming.set(a);
1637+
1638+ EXPECT_EQ(2, m_triggered.size());
1639+ EXPECT_EQ(a[0].uid, m_triggered[0]);
1640+ EXPECT_EQ(a[1].uid, m_triggered[1]);
1641+}
1642+
1643+
1644+TEST_F(ClockWatcherFixture, NoDuplicates)
1645+{
1646+ // Setup: add an appointment that gets triggered.
1647+ const auto now = m_state->clock->localtime();
1648+ const std::vector<Appointment> appointments = build_some_appointments();
1649+ std::vector<Appointment> a;
1650+ a.push_back(appointments[0]);
1651+ a[0].begin = now;
1652+ m_state->planner->upcoming.set(a);
1653+ EXPECT_EQ(1, m_triggered.size());
1654+ EXPECT_EQ(a[0].uid, m_triggered[0]);
1655+
1656+ // Now change the appointment vector by adding one to it.
1657+ // Confirm that the ClockWatcher doesn't re-trigger a[0]
1658+ a.push_back(appointments[1]);
1659+ m_state->planner->upcoming.set(a);
1660+ EXPECT_EQ(1, m_triggered.size());
1661+ EXPECT_EQ(a[0].uid, m_triggered[0]);
1662+}
1663
1664=== modified file 'tests/test-clock.cpp'
1665--- tests/test-clock.cpp 2014-01-31 00:33:14 +0000
1666+++ tests/test-clock.cpp 2014-02-19 15:36:23 +0000
1667@@ -37,12 +37,12 @@
1668 void emitPrepareForSleep()
1669 {
1670 g_dbus_connection_emit_signal(g_bus_get_sync(G_BUS_TYPE_SYSTEM, nullptr, nullptr),
1671- NULL,
1672+ nullptr,
1673 "/org/freedesktop/login1", // object path
1674 "org.freedesktop.login1.Manager", // interface
1675 "PrepareForSleep", // signal name
1676 g_variant_new("(b)", FALSE),
1677- NULL);
1678+ nullptr);
1679 }
1680 };
1681
1682
1683=== modified file 'tests/test-planner.cpp'
1684--- tests/test-planner.cpp 2014-01-31 00:33:14 +0000
1685+++ tests/test-planner.cpp 2014-02-19 15:36:23 +0000
1686@@ -28,9 +28,7 @@
1687 #include <langinfo.h>
1688 #include <locale.h>
1689
1690-using unity::indicator::datetime::Appointment;
1691-using unity::indicator::datetime::DateTime;
1692-using unity::indicator::datetime::PlannerEds;
1693+using namespace unity::indicator::datetime;
1694
1695 /***
1696 ****
1697@@ -40,11 +38,15 @@
1698
1699 TEST_F(PlannerFixture, EDS)
1700 {
1701- PlannerEds planner;
1702+ auto tmp = g_date_time_new_now_local();
1703+ const auto now = DateTime(tmp);
1704+ g_date_time_unref(tmp);
1705+
1706+ std::shared_ptr<Clock> clock(new MockClock(now));
1707+ PlannerEds planner(clock);
1708 wait_msec(100);
1709
1710- auto now = g_date_time_new_now_local();
1711- planner.time.set(DateTime(now));
1712+ planner.time.set(now);
1713 wait_msec(2500);
1714
1715 std::vector<Appointment> this_month = planner.this_month.get();
1716
1717=== modified file 'tests/test-settings.cpp'
1718--- tests/test-settings.cpp 2014-01-31 00:40:13 +0000
1719+++ tests/test-settings.cpp 2014-02-19 15:36:23 +0000
1720@@ -167,8 +167,8 @@
1721 {
1722 const auto key = SETTINGS_LOCATIONS_S;
1723
1724- const gchar* astrv[] = {"America/Los_Angeles Oakland", "America/Chicago Oklahoma City", "Europe/London London", NULL};
1725- const gchar* bstrv[] = {"America/Denver", "Europe/London London", "Europe/Berlin Berlin", NULL};
1726+ const gchar* astrv[] = {"America/Los_Angeles Oakland", "America/Chicago Oklahoma City", "Europe/London London", nullptr};
1727+ const gchar* bstrv[] = {"America/Denver", "Europe/London London", "Europe/Berlin Berlin", nullptr};
1728 const std::vector<std::string> av = strv_to_vector(astrv);
1729 const std::vector<std::string> bv = strv_to_vector(bstrv);
1730
1731
1732=== modified file 'tests/test-utils.cpp'
1733--- tests/test-utils.cpp 2014-01-27 07:26:02 +0000
1734+++ tests/test-utils.cpp 2014-02-19 15:36:23 +0000
1735@@ -59,7 +59,7 @@
1736 const char* location;
1737 const char* expected_name;
1738 } beautify_timezone_test_cases[] = {
1739- { "America/Chicago", NULL, "Chicago" },
1740+ { "America/Chicago", nullptr, "Chicago" },
1741 { "America/Chicago", "America/Chicago", "Chicago" },
1742 { "America/Chicago", "America/Chigago Chicago", "Chicago" },
1743 { "America/Chicago", "America/Chicago Oklahoma City", "Oklahoma City" },

Subscribers

People subscribed via source and target branches