Merge lp:~charlesk/indicator-datetime/merge-vivid-fixes-and-tests-into-wily into lp:indicator-datetime/15.10

Proposed by Charles Kerr
Status: Merged
Approved by: Ted Gould
Approved revision: 413
Merged at revision: 412
Proposed branch: lp:~charlesk/indicator-datetime/merge-vivid-fixes-and-tests-into-wily
Merge into: lp:indicator-datetime/15.10
Diff against target: 1611 lines (+1133/-268)
16 files modified
src/engine-eds.cpp (+486/-186)
tests/CMakeLists.txt (+16/-13)
tests/print-to.h (+34/-1)
tests/run-eds-ics-test.sh (+10/-0)
tests/test-eds-ics-all-day-events.cpp (+91/-0)
tests/test-eds-ics-all-day-events.ics (+19/-0)
tests/test-eds-ics-config-files/.config/evolution/sources/system-proxy.source (+21/-0)
tests/test-eds-ics-missing-trigger.cpp (+116/-0)
tests/test-eds-ics-missing-trigger.ics (+45/-0)
tests/test-eds-ics-nonrepeating-events.cpp (+93/-0)
tests/test-eds-ics-nonrepeating-events.ics (+27/-0)
tests/test-eds-ics-repeating-events.cpp (+100/-0)
tests/test-eds-ics-repeating-events.ics (+28/-0)
tests/test-eds-ics-repeating-valarms.ics (+47/-0)
tests/test-eds-valarms-config-files/.config/evolution/sources/system-proxy.source (+0/-21)
tests/test-eds-valarms-config-files/.local/share/evolution/calendar/system/calendar.ics (+0/-47)
To merge this branch: bzr merge lp:~charlesk/indicator-datetime/merge-vivid-fixes-and-tests-into-wily
Reviewer Review Type Date Requested Status
PS Jenkins bot (community) continuous-integration Approve
Ted Gould (community) Approve
Review via email: mp+262585@code.launchpad.net

Commit message

Fix bugs relating to timezones and triggers from clock-app alarms.

Description of the change

As the branch name suggests, this brings the recent 15.04 bugfixes and test improvements to the 15.10 branch.

* Fixes https://launchpad.net/bugs/1456281

* Fixes https://launchpad.net/bugs/1465806

* Adds improved EDS testing

To post a comment you must log in.
413. By Charles Kerr

don't copy debian/changelog from vivid

Revision history for this message
Ted Gould (ted) :
review: Approve
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/engine-eds.cpp'
2--- src/engine-eds.cpp 2015-04-06 19:58:34 +0000
3+++ src/engine-eds.cpp 2015-06-22 14:18:10 +0000
4@@ -34,9 +34,11 @@
5 namespace indicator {
6 namespace datetime {
7
8-static constexpr char const * TAG_ALARM {"x-canonical-alarm"};
9+static constexpr char const * TAG_ALARM {"x-canonical-alarm"};
10 static constexpr char const * TAG_DISABLED {"x-canonical-disabled"};
11
12+static constexpr char const * X_PROP_ACTIVATION_URL {"X-CANONICAL-ACTIVATION-URL"};
13+
14 /****
15 *****
16 ****/
17@@ -77,9 +79,6 @@
18 const Timezone& timezone,
19 std::function<void(const std::vector<Appointment>&)> func)
20 {
21- const auto begin_timet = begin.to_unix();
22- const auto end_timet = end.to_unix();
23-
24 const auto b_str = begin.format("%F %T");
25 const auto e_str = end.format("%F %T");
26 g_debug("getting all appointments from [%s ... %s]", b_str.c_str(), e_str.c_str());
27@@ -104,42 +103,51 @@
28 *** walk through the sources to build the appointment list
29 **/
30
31- auto task_deleter = [](Task* task){
32- // give the caller the (sorted) finished product
33- auto& a = task->appointments;
34- std::sort(a.begin(), a.end(), [](const Appointment& a, const Appointment& b){return a.begin < b.begin;});
35- task->func(a);
36- // we're done; delete the task
37- g_debug("time to delete task %p", (void*)task);
38- delete task;
39- };
40-
41- std::shared_ptr<Task> main_task(new Task(this, func), task_deleter);
42+ auto gtz = default_timezone != nullptr
43+ ? g_time_zone_new(icaltimezone_get_location(default_timezone))
44+ : g_time_zone_new_local();
45+ auto main_task = std::make_shared<Task>(this, func, default_timezone, gtz, begin, end);
46
47 for (auto& kv : m_clients)
48 {
49 auto& client = kv.second;
50 if (default_timezone != nullptr)
51 e_cal_client_set_default_timezone(client, default_timezone);
52+ g_debug("calling e_cal_client_generate_instances for %p", (void*)client);
53
54- // start a new subtask to enumerate all the components in this client.
55 auto& source = kv.first;
56 auto extension = e_source_get_extension(source, E_SOURCE_EXTENSION_CALENDAR);
57 const auto color = e_source_selectable_get_color(E_SOURCE_SELECTABLE(extension));
58- g_debug("calling e_cal_client_generate_instances for %p", (void*)client);
59- auto subtask = new AppointmentSubtask(main_task,
60- client,
61- color,
62- default_timezone,
63- begin_timet,
64- end_timet);
65- e_cal_client_generate_instances(client,
66- begin_timet,
67- end_timet,
68- m_cancellable,
69- my_get_appointments_foreach,
70- subtask,
71- [](gpointer g){delete static_cast<AppointmentSubtask*>(g);});
72+
73+ auto begin_str = isodate_from_time_t(begin.to_unix());
74+ auto end_str = isodate_from_time_t(end.to_unix());
75+ auto sexp_fmt = g_strdup_printf("(%%s? (make-time \"%s\") (make-time \"%s\"))", begin_str, end_str);
76+ g_clear_pointer(&begin_str, g_free);
77+ g_clear_pointer(&end_str, g_free);
78+
79+ // ask EDS about alarms that occur in this window...
80+ auto sexp = g_strdup_printf(sexp_fmt, "has-alarms-in-range");
81+ g_debug("%s alarm sexp is %s", G_STRLOC, sexp);
82+ e_cal_client_get_object_list_as_comps(
83+ client,
84+ sexp,
85+ m_cancellable,
86+ on_alarm_component_list_ready,
87+ new ClientSubtask(main_task, client, color));
88+ g_clear_pointer(&sexp, g_free);
89+
90+ // ask EDS about events that occur in this window...
91+ sexp = g_strdup_printf(sexp_fmt, "occur-in-time-range");
92+ g_debug("%s event sexp is %s", G_STRLOC, sexp);
93+ e_cal_client_get_object_list_as_comps(
94+ client,
95+ sexp,
96+ m_cancellable,
97+ on_event_component_list_ready,
98+ new ClientSubtask(main_task, client, color));
99+ g_clear_pointer(&sexp, g_free);
100+
101+ g_clear_pointer(&sexp_fmt, g_free);
102 }
103 }
104
105@@ -289,10 +297,14 @@
106 // add the client to our collection
107 auto self = static_cast<Impl*>(gself);
108 g_debug("got a client for %s", e_cal_client_get_local_attachment_store(E_CAL_CLIENT(client)));
109- self->m_clients[e_client_get_source(client)] = E_CAL_CLIENT(client);
110+ auto source = e_client_get_source(client);
111+ auto ecc = E_CAL_CLIENT(client);
112+ self->m_clients[source] = ecc;
113+
114+ self->ensure_client_alarms_have_triggers(ecc);
115
116 // now create a view for it so that we can listen for changes
117- e_cal_client_get_view (E_CAL_CLIENT(client),
118+ e_cal_client_get_view (ecc,
119 "#t", // match all
120 self->m_cancellable,
121 on_client_view_ready,
122@@ -401,36 +413,180 @@
123 static_cast<Impl*>(gself)->set_dirty_soon();
124 }
125
126+ /***
127+ ****
128+ ***/
129+
130+ // old ubuntu-clock-app alarms created VTODO VALARMS without the
131+ // required 'TRIGGER' property... http://pad.lv/1465806
132+
133+ void ensure_client_alarms_have_triggers(ECalClient* client)
134+ {
135+ // ask the EDS server for all the ubuntu-clock-app alarms...
136+
137+ auto sexp = g_strdup_printf("has-categories? '%s'", TAG_ALARM);
138+
139+ e_cal_client_get_object_list_as_comps(
140+ client,
141+ sexp,
142+ m_cancellable,
143+ ensure_client_alarms_have_triggers_async_cb,
144+ this);
145+
146+ g_clear_pointer(&sexp, g_free);
147+ }
148+
149+ static void ensure_client_alarms_have_triggers_async_cb(
150+ GObject * oclient,
151+ GAsyncResult * res,
152+ gpointer gself)
153+ {
154+ ECalClient * client = E_CAL_CLIENT(oclient);
155+ GError * error = nullptr;
156+ GSList * components = nullptr;
157+
158+ if (e_cal_client_get_object_list_as_comps_finish(client,
159+ res,
160+ &components,
161+ &error))
162+ {
163+ auto self = static_cast<Impl*>(gself);
164+ self->ensure_canonical_alarms_have_triggers(client, components);
165+ e_cal_client_free_ecalcomp_slist(components);
166+ }
167+ else if (error != nullptr)
168+ {
169+ if (!g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
170+ g_warning("can't get clock-app alarm list: %s", error->message);
171+
172+ g_error_free(error);
173+ }
174+ }
175+
176+ void ensure_canonical_alarms_have_triggers(ECalClient * client,
177+ GSList * components)
178+ {
179+ GSList * modify_slist = nullptr;
180+
181+ // for each component..
182+ for (auto l=components; l!=nullptr; l=l->next)
183+ {
184+ bool changed = false;
185+
186+ // for each alarm...
187+ auto component = E_CAL_COMPONENT(l->data);
188+ auto auids = e_cal_component_get_alarm_uids(component);
189+ for(auto l=auids; l!=nullptr; l=l->next)
190+ {
191+ auto auid = static_cast<const char*>(l->data);
192+ auto alarm = e_cal_component_get_alarm(component, auid);
193+ if (alarm == nullptr)
194+ continue;
195+
196+ // if the alarm has no trigger, add one.
197+ ECalComponentAlarmTrigger trigger;
198+ e_cal_component_alarm_get_trigger(alarm, &trigger);
199+ if (trigger.type == E_CAL_COMPONENT_ALARM_TRIGGER_NONE)
200+ {
201+ trigger.type = E_CAL_COMPONENT_ALARM_TRIGGER_RELATIVE_START;
202+ trigger.u.rel_duration = icaldurationtype_from_int(0);
203+ e_cal_component_alarm_set_trigger (alarm, trigger);
204+ changed = true;
205+ }
206+
207+ g_clear_pointer(&alarm, e_cal_component_alarm_free);
208+ }
209+ g_clear_pointer(&auids, cal_obj_uid_list_free);
210+
211+ if (changed)
212+ {
213+ auto icc = e_cal_component_get_icalcomponent(component); // icc owned by ecc
214+ modify_slist = g_slist_prepend(modify_slist, icc);
215+ }
216+ }
217+
218+ if (modify_slist != nullptr)
219+ {
220+ e_cal_client_modify_objects(client,
221+ modify_slist,
222+ E_CAL_OBJ_MOD_ALL,
223+ m_cancellable,
224+ ensure_canonical_alarms_have_triggers_async_cb,
225+ this);
226+
227+ g_clear_pointer(&modify_slist, g_slist_free);
228+ }
229+ }
230+
231+ // log a warning if e_cal_client_modify_objects() failed
232+ static void ensure_canonical_alarms_have_triggers_async_cb(
233+ GObject * oclient,
234+ GAsyncResult * res,
235+ gpointer /*gself*/)
236+ {
237+ GError * error = nullptr;
238+
239+ e_cal_client_modify_objects_finish (E_CAL_CLIENT(oclient), res, &error);
240+
241+ if (error != nullptr)
242+ {
243+ if (!g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
244+ g_warning("couldn't add alarm triggers: %s", error->message);
245+
246+ g_error_free(error);
247+ }
248+ }
249+
250+ /***
251+ ****
252+ ***/
253+
254+
255 typedef std::function<void(const std::vector<Appointment>&)> appointment_func;
256
257 struct Task
258 {
259 Impl* p;
260 appointment_func func;
261+ icaltimezone* default_timezone; // pointer owned by libical
262+ GTimeZone* gtz;
263 std::vector<Appointment> appointments;
264- Task(Impl* p_in, const appointment_func& func_in): p(p_in), func(func_in) {}
265+ const DateTime begin;
266+ const DateTime end;
267+
268+ Task(Impl* p_in,
269+ appointment_func func_in,
270+ icaltimezone* tz_in,
271+ GTimeZone* gtz_in,
272+ const DateTime& begin_in,
273+ const DateTime& end_in):
274+ p{p_in},
275+ func{func_in},
276+ default_timezone{tz_in},
277+ gtz{gtz_in},
278+ begin{begin_in},
279+ end{end_in} {}
280+
281+ ~Task() {
282+ g_clear_pointer(&gtz, g_time_zone_unref);
283+ // give the caller the sorted finished product
284+ auto& a = appointments;
285+ std::sort(a.begin(), a.end(), [](const Appointment& a, const Appointment& b){return a.begin < b.begin;});
286+ func(a);
287+ };
288 };
289
290- struct AppointmentSubtask
291+ struct ClientSubtask
292 {
293 std::shared_ptr<Task> task;
294 ECalClient* client;
295 std::string color;
296- icaltimezone* default_timezone;
297- time_t begin;
298- time_t end;
299
300- AppointmentSubtask(const std::shared_ptr<Task>& task_in,
301- ECalClient* client_in,
302- const char* color_in,
303- icaltimezone* default_tz,
304- time_t begin_,
305- time_t end_):
306+ ClientSubtask(const std::shared_ptr<Task>& task_in,
307+ ECalClient* client_in,
308+ const char* color_in):
309 task(task_in),
310- client(client_in),
311- default_timezone(default_tz),
312- begin(begin_),
313- end(end_)
314+ client(client_in)
315 {
316 if (color_in)
317 color = color_in;
318@@ -480,146 +636,290 @@
319 return ret;
320 }
321
322- static gboolean
323- my_get_appointments_foreach(ECalComponent* component,
324- time_t begin,
325- time_t end,
326- gpointer gsubtask)
327- {
328+ static void
329+ on_alarm_component_list_ready(GObject * oclient,
330+ GAsyncResult * res,
331+ gpointer gsubtask)
332+ {
333+ GError * error = NULL;
334+ GSList * comps_slist = NULL;
335+ auto subtask = static_cast<ClientSubtask*>(gsubtask);
336+
337+ if (e_cal_client_get_object_list_as_comps_finish(E_CAL_CLIENT(oclient),
338+ res,
339+ &comps_slist,
340+ &error))
341+ {
342+ // _generate_alarms takes a GList, so make a shallow one
343+ GList * comps_list = nullptr;
344+ for (auto l=comps_slist; l!=nullptr; l=l->next)
345+ comps_list = g_list_prepend(comps_list, l->data);
346+
347+ constexpr std::array<ECalComponentAlarmAction,1> omit = {
348+ (ECalComponentAlarmAction)-1
349+ }; // list of action types to omit, terminated with -1
350+ GSList * comp_alarms = nullptr;
351+ e_cal_util_generate_alarms_for_list(
352+ comps_list,
353+ subtask->task->begin.to_unix(),
354+ subtask->task->end.to_unix(),
355+ const_cast<ECalComponentAlarmAction*>(omit.data()),
356+ &comp_alarms,
357+ e_cal_client_resolve_tzid_cb,
358+ oclient,
359+ subtask->task->default_timezone);
360+
361+ // walk the alarms & add them
362+ for (auto l=comp_alarms; l!=nullptr; l=l->next)
363+ add_alarms_to_subtask(static_cast<ECalComponentAlarms*>(l->data), subtask, subtask->task->gtz);
364+
365+ // cleanup
366+ e_cal_free_alarms(comp_alarms);
367+ g_list_free(comps_list);
368+ e_cal_client_free_ecalcomp_slist(comps_slist);
369+ }
370+ else if (error != nullptr)
371+ {
372+ if (!g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
373+ g_warning("can't get ecalcomponent list: %s", error->message);
374+
375+ g_error_free(error);
376+ }
377+
378+ delete subtask;
379+ }
380+
381+ static void
382+ on_event_component_list_ready(GObject * oclient,
383+ GAsyncResult * res,
384+ gpointer gsubtask)
385+ {
386+ GError * error = NULL;
387+ GSList * comps_slist = NULL;
388+ auto subtask = static_cast<ClientSubtask*>(gsubtask);
389+
390+ if (e_cal_client_get_object_list_as_comps_finish(E_CAL_CLIENT(oclient),
391+ res,
392+ &comps_slist,
393+ &error))
394+ {
395+ for (auto l=comps_slist; l!=nullptr; l=l->next)
396+ add_event_to_subtask(static_cast<ECalComponent*>(l->data), subtask, subtask->task->gtz);
397+
398+ e_cal_client_free_ecalcomp_slist(comps_slist);
399+ }
400+ else if (error != nullptr)
401+ {
402+ if (!g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
403+ g_warning("can't get ecalcomponent list: %s", error->message);
404+
405+ g_error_free(error);
406+ }
407+
408+ delete subtask;
409+ }
410+
411+ static DateTime
412+ datetime_from_component_date_time(const ECalComponentDateTime & in,
413+ GTimeZone * default_timezone)
414+ {
415+ DateTime out;
416+
417+ g_return_val_if_fail(in.value != nullptr, out);
418+
419+ auto gtz = in.tzid == nullptr ? g_time_zone_ref(default_timezone)
420+ : g_time_zone_new(in.tzid);
421+ out = DateTime(gtz,
422+ in.value->year,
423+ in.value->month,
424+ in.value->day,
425+ in.value->hour,
426+ in.value->minute,
427+ in.value->second);
428+ g_time_zone_unref(gtz);
429+ return out;
430+ }
431+
432+ static bool
433+ is_component_interesting(ECalComponent * component)
434+ {
435+ // we only want calendar events and vtodos
436 const auto vtype = e_cal_component_get_vtype(component);
437- auto subtask = static_cast<AppointmentSubtask*>(gsubtask);
438-
439- if ((vtype == E_CAL_COMPONENT_EVENT) || (vtype == E_CAL_COMPONENT_TODO))
440- {
441- const gchar* uid = nullptr;
442- e_cal_component_get_uid(component, &uid);
443-
444- auto status = ICAL_STATUS_NONE;
445- e_cal_component_get_status(component, &status);
446-
447- // get the timezone we want to use for generated Appointments/Alarms
448- const char * location = icaltimezone_get_location(subtask->default_timezone);
449- auto gtz = g_time_zone_new(location);
450- g_debug("timezone abbreviation is %s", g_time_zone_get_abbreviation (gtz, 0));
451-
452- const DateTime begin_dt { gtz, begin };
453- const DateTime end_dt { gtz, end };
454- g_debug ("got appointment from %s to %s, uid %s status %d",
455- begin_dt.format("%F %T %z").c_str(),
456- end_dt.format("%F %T %z").c_str(),
457- uid,
458- (int)status);
459-
460- // look for the in-house tags
461- bool disabled = false;
462- Appointment::Type type = Appointment::EVENT;
463- GSList * categ_list = nullptr;
464- e_cal_component_get_categories_list (component, &categ_list);
465- for (GSList * l=categ_list; l!=nullptr; l=l->next) {
466- auto tag = static_cast<const char*>(l->data);
467- if (!g_strcmp0(tag, TAG_ALARM))
468- type = Appointment::UBUNTU_ALARM;
469- if (!g_strcmp0(tag, TAG_DISABLED))
470- disabled = true;
471- }
472- e_cal_component_free_categories_list(categ_list);
473-
474- if ((uid != nullptr) &&
475- (!disabled) &&
476- (status != ICAL_STATUS_COMPLETED) &&
477- (status != ICAL_STATUS_CANCELLED))
478- {
479- constexpr std::array<ECalComponentAlarmAction,1> omit = { (ECalComponentAlarmAction)-1 }; // list of action types to omit, terminated with -1
480- Appointment appointment;
481-
482- ECalComponentText text {};
483- e_cal_component_get_summary(component, &text);
484- if (text.value)
485- appointment.summary = text.value;
486-
487- appointment.begin = begin_dt;
488- appointment.end = end_dt;
489- appointment.color = subtask->color;
490- appointment.uid = uid;
491- appointment.type = type;
492-
493- icalcomponent * icc = e_cal_component_get_icalcomponent(component);
494- g_debug("%s", icalcomponent_as_ical_string(icc)); // libical owns this string; no leak
495-
496- auto e_alarms = e_cal_util_generate_alarms_for_comp(component,
497- subtask->begin,
498- subtask->end,
499- const_cast<ECalComponentAlarmAction*>(omit.data()),
500- e_cal_client_resolve_tzid_cb,
501- subtask->client,
502- subtask->default_timezone);
503-
504- std::map<DateTime,Alarm> alarms;
505-
506- if (e_alarms != nullptr)
507- {
508- for (auto l=e_alarms->alarms; l!=nullptr; l=l->next)
509- {
510- auto ai = static_cast<ECalComponentAlarmInstance*>(l->data);
511- auto a = e_cal_component_get_alarm(component, ai->auid);
512-
513- if (a != nullptr)
514- {
515- const DateTime alarm_begin{gtz, ai->trigger};
516- auto& alarm = alarms[alarm_begin];
517-
518- if (alarm.text.empty())
519- alarm.text = get_alarm_text(a);
520- if (alarm.audio_url.empty())
521- alarm.audio_url = get_alarm_sound_url(a);
522- if (!alarm.time.is_set())
523- alarm.time = alarm_begin;
524-
525- e_cal_component_alarm_free(a);
526- }
527- }
528-
529- e_cal_component_alarms_free(e_alarms);
530- }
531- // Hm, no alarm triggers?
532- // That's a bug in alarms created by some versions of ubuntu-ui-toolkit.
533- // If that's what's happening here, let's handle those alarms anyway
534- // by effectively injecting a TRIGGER;VALUE=DURATION;RELATED=START:PT0S
535- else if (appointment.is_ubuntu_alarm())
536- {
537- Alarm tmp;
538- tmp.time = appointment.begin;
539-
540- auto auids = e_cal_component_get_alarm_uids(component);
541- for(auto l=auids; l!=nullptr; l=l->next)
542- {
543- const auto auid = static_cast<const char*>(l->data);
544- auto a = e_cal_component_get_alarm(component, auid);
545- if (a != nullptr)
546- {
547- if (tmp.text.empty())
548- tmp.text = get_alarm_text(a);
549- if (tmp.audio_url.empty())
550- tmp.audio_url = get_alarm_sound_url(a);
551- e_cal_component_alarm_free(a);
552- }
553- }
554- cal_obj_uid_list_free(auids);
555-
556- alarms[tmp.time] = tmp;
557- }
558-
559- appointment.alarms.reserve(alarms.size());
560- for (const auto& it : alarms)
561- appointment.alarms.push_back(it.second);
562-
563- subtask->task->appointments.push_back(appointment);
564- }
565-
566- g_time_zone_unref(gtz);
567- }
568-
569- return G_SOURCE_CONTINUE;
570+ if ((vtype != E_CAL_COMPONENT_EVENT) &&
571+ (vtype != E_CAL_COMPONENT_TODO))
572+ return false;
573+
574+ // we're not interested in completed or cancelled components
575+ auto status = ICAL_STATUS_NONE;
576+ e_cal_component_get_status(component, &status);
577+ if ((status == ICAL_STATUS_COMPLETED) ||
578+ (status == ICAL_STATUS_CANCELLED))
579+ return false;
580+
581+ // we don't want disabled alarms
582+ bool disabled = false;
583+ GSList * categ_list = nullptr;
584+ e_cal_component_get_categories_list (component, &categ_list);
585+ for (GSList * l=categ_list; l!=nullptr; l=l->next) {
586+ auto tag = static_cast<const char*>(l->data);
587+ if (!g_strcmp0(tag, TAG_DISABLED))
588+ disabled = true;
589+ }
590+ e_cal_component_free_categories_list(categ_list);
591+ if (disabled)
592+ return false;
593+
594+ return true;
595+ }
596+
597+ static Appointment
598+ get_appointment(ECalComponent * component, GTimeZone * gtz)
599+ {
600+ Appointment baseline;
601+
602+ // get appointment.uid
603+ const gchar* uid = nullptr;
604+ e_cal_component_get_uid(component, &uid);
605+ if (uid != nullptr)
606+ baseline.uid = uid;
607+
608+ // get appointment.summary
609+ ECalComponentText text {};
610+ e_cal_component_get_summary(component, &text);
611+ if (text.value)
612+ baseline.summary = text.value;
613+
614+ // get appointment.begin
615+ ECalComponentDateTime eccdt_tmp {};
616+ e_cal_component_get_dtstart(component, &eccdt_tmp);
617+ baseline.begin = datetime_from_component_date_time(eccdt_tmp, gtz);
618+ e_cal_component_free_datetime(&eccdt_tmp);
619+
620+ // get appointment.end
621+ e_cal_component_get_dtend(component, &eccdt_tmp);
622+ baseline.end = eccdt_tmp.value != nullptr
623+ ? datetime_from_component_date_time(eccdt_tmp, gtz)
624+ : baseline.begin;
625+ e_cal_component_free_datetime(&eccdt_tmp);
626+
627+ // get appointment.activation_url from x-props
628+ auto icc = e_cal_component_get_icalcomponent(component); // icc owned by component
629+ auto icalprop = icalcomponent_get_first_property(icc, ICAL_X_PROPERTY);
630+ while (icalprop != nullptr) {
631+ const char * x_name = icalproperty_get_x_name(icalprop);
632+ if ((x_name != nullptr) && !g_ascii_strcasecmp(x_name, X_PROP_ACTIVATION_URL)) {
633+ const char * url = icalproperty_get_value_as_string(icalprop);
634+ if ((url != nullptr) && baseline.activation_url.empty())
635+ baseline.activation_url = url;
636+ }
637+ icalprop = icalcomponent_get_next_property(icc, ICAL_X_PROPERTY);
638+ }
639+
640+ // get appointment.type
641+ baseline.type = Appointment::EVENT;
642+ GSList * categ_list = nullptr;
643+ e_cal_component_get_categories_list (component, &categ_list);
644+ for (GSList * l=categ_list; l!=nullptr; l=l->next) {
645+ auto tag = static_cast<const char*>(l->data);
646+ if (!g_strcmp0(tag, TAG_ALARM))
647+ baseline.type = Appointment::UBUNTU_ALARM;
648+ }
649+ e_cal_component_free_categories_list(categ_list);
650+
651+ g_debug("%s got appointment from %s to %s: %s", G_STRLOC,
652+ baseline.begin.format("%F %T %z").c_str(),
653+ baseline.end.format("%F %T %z").c_str(),
654+ icalcomponent_as_ical_string(icc) /* string owned by ical */);
655+
656+ return baseline;
657+ }
658+
659+ static void
660+ add_event_to_subtask(ECalComponent * component,
661+ ClientSubtask * subtask,
662+ GTimeZone * gtz)
663+ {
664+ // events with alarms are covered by add_alarm_to_subtask(),
665+ // so skip them here
666+ auto auids = e_cal_component_get_alarm_uids(component);
667+ const bool has_alarms = auids != nullptr;
668+ cal_obj_uid_list_free(auids);
669+ if (has_alarms)
670+ return;
671+
672+ // add it. simple, eh?
673+ if (is_component_interesting(component))
674+ {
675+ Appointment appointment = get_appointment(component, gtz);
676+ appointment.color = subtask->color;
677+ subtask->task->appointments.push_back(appointment);
678+ }
679+ }
680+
681+ static void
682+ add_alarms_to_subtask(ECalComponentAlarms * comp_alarms,
683+ ClientSubtask * subtask,
684+ GTimeZone * gtz)
685+ {
686+ auto& component = comp_alarms->comp;
687+
688+ if (!is_component_interesting(component))
689+ return;
690+
691+ Appointment baseline = get_appointment(component, gtz);
692+ baseline.color = subtask->color;
693+
694+ /**
695+ *** Now loop through comp_alarms to get information that we need
696+ *** to build the instance appointments and their alarms.
697+ ***
698+ *** Outer map key is the instance component's start + end time.
699+ *** We build Appointment.begin and .end from that.
700+ ***
701+ *** inner map key is the alarm trigger, we build Alarm.time from that.
702+ ***
703+ *** inner map value is the Alarm.
704+ ***
705+ *** We map the alarms based on their trigger time so that we
706+ *** can fold together multiple valarms that trigger for the
707+ *** same componeng at the same time. This is commonplace;
708+ *** e.g. one valarm will have a display action and another
709+ *** will specify a sound to be played.
710+ */
711+ std::map<std::pair<DateTime,DateTime>,std::map<DateTime,Alarm>> alarms;
712+ for (auto l=comp_alarms->alarms; l!=nullptr; l=l->next)
713+ {
714+ auto ai = static_cast<ECalComponentAlarmInstance*>(l->data);
715+ auto a = e_cal_component_get_alarm(component, ai->auid);
716+ if (a == nullptr)
717+ continue;
718+
719+ auto instance_time = std::make_pair(DateTime{gtz, ai->occur_start},
720+ DateTime{gtz, ai->occur_end});
721+ auto trigger_time = DateTime{gtz, ai->trigger};
722+
723+ auto& alarm = alarms[instance_time][trigger_time];
724+
725+ if (alarm.text.empty())
726+ alarm.text = get_alarm_text(a);
727+ if (alarm.audio_url.empty())
728+ alarm.audio_url = get_alarm_sound_url(a);
729+ if (!alarm.time.is_set())
730+ alarm.time = trigger_time;
731+
732+ e_cal_component_alarm_free(a);
733+ }
734+
735+ for (auto& i : alarms)
736+ {
737+ Appointment appointment = baseline;
738+ appointment.begin = i.first.first;
739+ appointment.end = i.first.second;
740+ appointment.alarms.reserve(i.second.size());
741+ for (auto& j : i.second)
742+ appointment.alarms.push_back(j.second);
743+ subtask->task->appointments.push_back(appointment);
744+ }
745 }
746
747 /***
748
749=== modified file 'tests/CMakeLists.txt'
750--- tests/CMakeLists.txt 2015-04-06 00:19:01 +0000
751+++ tests/CMakeLists.txt 2015-06-22 14:18:10 +0000
752@@ -78,24 +78,27 @@
753 set(EVOLUTION_SOURCE_SERVICE_NAME "org.gnome.evolution.dataserver.Sources3")
754 endif()
755
756-function(add_eds_test_by_name name)
757+function(add_eds_ics_test_by_name name)
758 set (TEST_NAME ${name})
759 add_executable(${TEST_NAME} ${TEST_NAME}.cpp gschemas.compiled)
760 target_link_libraries (${TEST_NAME} indicatordatetimeservice gtest ${DBUSTEST_LIBRARIES} ${SERVICE_DEPS_LIBRARIES} ${GTEST_LIBS})
761 add_test (${TEST_NAME}
762- ${CMAKE_CURRENT_SOURCE_DIR}/run-eds-test.sh
763- ${DBUS_RUNNER} # arg1: dbus-test-runner exec
764- ${CMAKE_CURRENT_BINARY_DIR}/${TEST_NAME} # arg2: test executable path
765- ${TEST_NAME} # arg3: test name
766- ${EVOLUTION_CALENDAR_FACTORY} # arg4: evolution-calendar-factory exec
767- ${EVOLUTION_SOURCE_SERVICE_NAME} # arg5: dbus name for source registry
768- ${EVOLUTION_SOURCE_REGISTRY} # arg6: evolution-source-registry exec
769- ${GVFSD} # arg7: gvfsd exec
770- ${CMAKE_CURRENT_SOURCE_DIR}/${TEST_NAME}-config-files) # arg8: canned config files
771+ ${CMAKE_CURRENT_SOURCE_DIR}/run-eds-ics-test.sh
772+ ${DBUS_RUNNER} # arg1: dbus-test-runner exec
773+ ${CMAKE_CURRENT_BINARY_DIR}/${TEST_NAME} # arg2: test executable path
774+ ${TEST_NAME} # arg3: test name
775+ ${EVOLUTION_CALENDAR_FACTORY} # arg4: evolution-calendar-factory exec
776+ ${EVOLUTION_SOURCE_SERVICE_NAME} # arg5: dbus name for source registry
777+ ${EVOLUTION_SOURCE_REGISTRY} # arg6: evolution-source-registry exec
778+ ${GVFSD} # arg7: gvfsd exec
779+ ${CMAKE_CURRENT_SOURCE_DIR}/test-eds-ics-config-files # arg8: base directory for config file template
780+ ${CMAKE_CURRENT_SOURCE_DIR}/${TEST_NAME}.ics) # arg9: the ical file for this test
781 endfunction()
782-add_eds_test_by_name(test-eds-valarms)
783-
784-
785+add_eds_ics_test_by_name(test-eds-ics-all-day-events)
786+add_eds_ics_test_by_name(test-eds-ics-repeating-events)
787+add_eds_ics_test_by_name(test-eds-ics-nonrepeating-events)
788+add_eds_ics_test_by_name(test-eds-ics-repeating-valarms)
789+add_eds_ics_test_by_name(test-eds-ics-missing-trigger)
790
791
792 # disabling the timezone unit tests because they require
793
794=== modified file 'tests/print-to.h'
795--- tests/print-to.h 2015-04-06 15:20:49 +0000
796+++ tests/print-to.h 2015-06-22 14:18:10 +0000
797@@ -33,9 +33,42 @@
798 ***/
799
800 void
801+PrintTo(const DateTime& datetime, std::ostream* os)
802+{
803+ *os << "{time:'" << datetime.format("%F %T %z") << '}';
804+}
805+
806+void
807 PrintTo(const Alarm& alarm, std::ostream* os)
808 {
809- *os << "{text:'" << alarm.text << "', audio_url:'" << alarm.audio_url << "', time:'"<<alarm.time.format("%F %T")<<"'}";
810+ *os << '{';
811+ *os << "{text:" << alarm.text << '}';
812+ PrintTo(alarm.time, os);
813+ *os << '}';
814+}
815+
816+void
817+PrintTo(const Appointment& appointment, std::ostream* os)
818+{
819+ *os << '{';
820+
821+ *os << "{uid:'" << appointment.uid << "'}"
822+ << "{color:'" << appointment.color << "'}"
823+ << "{summary:'" << appointment.summary << "'}"
824+ << "{activation_url:'" << appointment.activation_url << "'}";
825+
826+ *os << "{begin:";
827+ PrintTo(appointment.begin, os);
828+ *os << '}';
829+
830+ *os << "{end:";
831+ PrintTo(appointment.end, os);
832+ *os << '}';
833+
834+ for(const auto& alarm : appointment.alarms)
835+ PrintTo(alarm, os);
836+
837+ *os << '}';
838 }
839
840 } // namespace datetime
841
842=== renamed file 'tests/run-eds-test.sh' => 'tests/run-eds-ics-test.sh'
843--- tests/run-eds-test.sh 2015-04-06 20:09:16 +0000
844+++ tests/run-eds-ics-test.sh 2015-06-22 14:18:10 +0000
845@@ -9,6 +9,7 @@
846 echo ARG6=$6 # full exectuable path of evolution-source-registry
847 echo ARG7=$7 # full executable path of gvfs
848 echo ARG8=$8 # config files
849+echo ARG8=$9 # ics file
850
851 # set up the tmpdir and tell the shell to purge it when we exit
852 export TEST_TMP_DIR=$(mktemp -p "${TMPDIR:-/tmp}" -d $3-XXXXXXXXXX) || exit 1
853@@ -32,6 +33,9 @@
854 export QORGANIZER_EDS_DEBUG=On
855 export GIO_USE_VFS=local # needed to ensure GVFS shuts down cleanly after the test is over
856
857+export G_MESSAGES_DEBUG=all
858+export G_DBUS_DEBUG=messages
859+
860 echo HOMEDIR=${HOME}
861 rm -rf ${XDG_DATA_HOME}
862
863@@ -41,6 +45,12 @@
864 cp --verbose --archive $8/. $HOME
865 fi
866
867+# if there's a specific ics file to test, copy it on top of the canned confilg files
868+if [ -e $9 ]; then
869+ echo "copying $9 into $HOME"
870+ cp --verbose --archive $9 ${XDG_DATA_HOME}/evolution/tasks/system/tasks.ics
871+fi
872+
873 # run dbus-test-runner
874 $1 --keep-env --max-wait=90 \
875 --task $2 --task-name $3 --wait-until-complete --wait-for=org.gnome.evolution.dataserver.Calendar4 \
876
877=== added file 'tests/test-eds-ics-all-day-events.cpp'
878--- tests/test-eds-ics-all-day-events.cpp 1970-01-01 00:00:00 +0000
879+++ tests/test-eds-ics-all-day-events.cpp 2015-06-22 14:18:10 +0000
880@@ -0,0 +1,91 @@
881+/*
882+ * Copyright 2015 Canonical Ltd.
883+ *
884+ * This program is free software: you can redistribute it and/or modify it
885+ * under the terms of the GNU General Public License version 3, as published
886+ * by the Free Software Foundation.
887+ *
888+ * This program is distributed in the hope that it will be useful, but
889+ * WITHOUT ANY WARRANTY; without even the implied warranties of
890+ * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
891+ * PURPOSE. See the GNU General Public License for more details.
892+ *
893+ * You should have received a copy of the GNU General Public License along
894+ * with this program. If not, see <http://www.gnu.org/licenses/>.
895+ *
896+ * Authors:
897+ * Charles Kerr <charles.kerr@canonical.com>
898+ */
899+
900+#include <algorithm>
901+
902+#include <datetime/alarm-queue-simple.h>
903+#include <datetime/clock-mock.h>
904+#include <datetime/engine-eds.h>
905+#include <datetime/planner-range.h>
906+
907+#include <gtest/gtest.h>
908+
909+#include "glib-fixture.h"
910+#include "print-to.h"
911+#include "timezone-mock.h"
912+#include "wakeup-timer-mock.h"
913+
914+using namespace unity::indicator::datetime;
915+using VAlarmFixture = GlibFixture;
916+
917+/***
918+****
919+***/
920+
921+TEST_F(VAlarmFixture, MultipleAppointments)
922+{
923+ // start the EDS engine
924+ auto engine = std::make_shared<EdsEngine>();
925+
926+ // we need a consistent timezone for the planner and our local DateTimes
927+ constexpr char const * zone_str {"America/Chicago"};
928+ auto tz = std::make_shared<MockTimezone>(zone_str);
929+ auto gtz = g_time_zone_new(zone_str);
930+
931+ // make a planner that looks at the first half of 2015 in EDS
932+ auto planner = std::make_shared<SimpleRangePlanner>(engine, tz);
933+ const DateTime range_begin {gtz, 2015,1, 1, 0, 0, 0.0};
934+ const DateTime range_end {gtz, 2015,6,31,23,59,59.5};
935+ planner->range().set(std::make_pair(range_begin, range_end));
936+
937+ // give EDS a moment to load
938+ if (planner->appointments().get().empty()) {
939+ g_message("waiting a moment for EDS to load...");
940+ auto on_appointments_changed = [this](const std::vector<Appointment>& appointments){
941+ g_message("ah, they loaded");
942+ if (!appointments.empty())
943+ g_main_loop_quit(loop);
944+ };
945+ core::ScopedConnection conn(planner->appointments().changed().connect(on_appointments_changed));
946+ constexpr int max_wait_sec = 10;
947+ wait_msec(max_wait_sec * G_TIME_SPAN_MILLISECOND);
948+ }
949+
950+ // what we expect to get...
951+ Appointment expected_appt;
952+ expected_appt.uid = "20150521T111538Z-7449-1000-3572-0@ghidorah";
953+ expected_appt.color = "#becedd";
954+ expected_appt.summary = "Memorial Day";
955+ expected_appt.begin = DateTime{gtz,2015,5,25,0,0,0};
956+ expected_appt.end = DateTime{gtz,2015,5,26,0,0,0};
957+
958+ // compare it to what we actually loaded...
959+ const auto appts = planner->appointments().get();
960+ ASSERT_EQ(1, appts.size());
961+ const auto& appt = appts[0];
962+ EXPECT_EQ(expected_appt.begin, appt.begin);
963+ EXPECT_EQ(expected_appt.end, appt.end);
964+ EXPECT_EQ(expected_appt.uid, appt.uid);
965+ EXPECT_EQ(expected_appt.color, appt.color);
966+ EXPECT_EQ(expected_appt.summary, appt.summary);
967+ EXPECT_EQ(0, appt.alarms.size());
968+
969+ // cleanup
970+ g_time_zone_unref(gtz);
971+}
972
973=== added file 'tests/test-eds-ics-all-day-events.ics'
974--- tests/test-eds-ics-all-day-events.ics 1970-01-01 00:00:00 +0000
975+++ tests/test-eds-ics-all-day-events.ics 2015-06-22 14:18:10 +0000
976@@ -0,0 +1,19 @@
977+BEGIN:VCALENDAR
978+CALSCALE:GREGORIAN
979+PRODID:-//Ximian//NONSGML Evolution Calendar//EN
980+VERSION:2.0
981+X-EVOLUTION-DATA-REVISION:2015-05-07T21:14:49.315443Z(0)
982+BEGIN:VEVENT
983+UID:20150521T111538Z-7449-1000-3572-0@ghidorah
984+DTSTAMP:20150521T001128Z
985+DTSTART;VALUE=DATE:20150525
986+DTEND;VALUE=DATE:20150526
987+TRANSP:TRANSPARENT
988+SEQUENCE:2
989+SUMMARY:Memorial Day
990+DESCRIPTION:Today is Memorial Day
991+CLASS:PUBLIC
992+CREATED:20150521T111638Z
993+LAST-MODIFIED:20150521T111638Z
994+END:VEVENT
995+END:VCALENDAR
996
997=== added directory 'tests/test-eds-ics-config-files'
998=== added directory 'tests/test-eds-ics-config-files/.config'
999=== added directory 'tests/test-eds-ics-config-files/.config/evolution'
1000=== added directory 'tests/test-eds-ics-config-files/.config/evolution/sources'
1001=== added file 'tests/test-eds-ics-config-files/.config/evolution/sources/system-proxy.source'
1002--- tests/test-eds-ics-config-files/.config/evolution/sources/system-proxy.source 1970-01-01 00:00:00 +0000
1003+++ tests/test-eds-ics-config-files/.config/evolution/sources/system-proxy.source 2015-06-22 14:18:10 +0000
1004@@ -0,0 +1,21 @@
1005+
1006+[Data Source]
1007+DisplayName=Default Proxy Settings
1008+Enabled=true
1009+Parent=
1010+
1011+[Proxy]
1012+Method=default
1013+IgnoreHosts=localhost;127.0.0.0/8;::1;
1014+AutoconfigUrl=
1015+FtpHost=
1016+FtpPort=0
1017+HttpAuthPassword=
1018+HttpAuthUser=
1019+HttpHost=
1020+HttpPort=8080
1021+HttpUseAuth=false
1022+HttpsHost=
1023+HttpsPort=0
1024+SocksHost=
1025+SocksPort=0
1026
1027=== added directory 'tests/test-eds-ics-config-files/.local'
1028=== added directory 'tests/test-eds-ics-config-files/.local/share'
1029=== added directory 'tests/test-eds-ics-config-files/.local/share/evolution'
1030=== added directory 'tests/test-eds-ics-config-files/.local/share/evolution/calendar'
1031=== added directory 'tests/test-eds-ics-config-files/.local/share/evolution/calendar/system'
1032=== added directory 'tests/test-eds-ics-config-files/.local/share/evolution/tasks'
1033=== added directory 'tests/test-eds-ics-config-files/.local/share/evolution/tasks/system'
1034=== added file 'tests/test-eds-ics-missing-trigger.cpp'
1035--- tests/test-eds-ics-missing-trigger.cpp 1970-01-01 00:00:00 +0000
1036+++ tests/test-eds-ics-missing-trigger.cpp 2015-06-22 14:18:10 +0000
1037@@ -0,0 +1,116 @@
1038+/*
1039+ * Copyright 2015 Canonical Ltd.
1040+ *
1041+ * This program is free software: you can redistribute it and/or modify it
1042+ * under the terms of the GNU General Public License version 3, as published
1043+ * by the Free Software Foundation.
1044+ *
1045+ * This program is distributed in the hope that it will be useful, but
1046+ * WITHOUT ANY WARRANTY; without even the implied warranties of
1047+ * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
1048+ * PURPOSE. See the GNU General Public License for more details.
1049+ *
1050+ * You should have received a copy of the GNU General Public License along
1051+ * with this program. If not, see <http://www.gnu.org/licenses/>.
1052+ *
1053+ * Authors:
1054+ * Charles Kerr <charles.kerr@canonical.com>
1055+ */
1056+
1057+#include <algorithm>
1058+
1059+#include <datetime/alarm-queue-simple.h>
1060+#include <datetime/clock-mock.h>
1061+#include <datetime/engine-eds.h>
1062+#include <datetime/planner-range.h>
1063+
1064+#include <gtest/gtest.h>
1065+
1066+#include "glib-fixture.h"
1067+#include "print-to.h"
1068+#include "timezone-mock.h"
1069+#include "wakeup-timer-mock.h"
1070+
1071+using namespace unity::indicator::datetime;
1072+using VAlarmFixture = GlibFixture;
1073+
1074+/***
1075+****
1076+***/
1077+
1078+TEST_F(VAlarmFixture, MissingTriggers)
1079+{
1080+ // start the EDS engine
1081+ auto engine = std::make_shared<EdsEngine>();
1082+
1083+ // we need a consistent timezone for the planner and our local DateTimes
1084+ constexpr char const * zone_str {"America/Chicago"};
1085+ auto tz = std::make_shared<MockTimezone>(zone_str);
1086+ auto gtz = g_time_zone_new(zone_str);
1087+
1088+ // make a planner that looks at the first half of 2015 in EDS
1089+ auto planner = std::make_shared<SimpleRangePlanner>(engine, tz);
1090+ const DateTime range_begin {gtz, 2015,1, 1, 0, 0, 0.0};
1091+ const DateTime range_end {gtz, 2015,6,31,23,59,59.5};
1092+ planner->range().set(std::make_pair(range_begin, range_end));
1093+
1094+ // give EDS a moment to load
1095+ if (planner->appointments().get().empty()) {
1096+ g_message("waiting a moment for EDS to load...");
1097+ auto on_appointments_changed = [this](const std::vector<Appointment>& appointments){
1098+ g_message("ah, they loaded");
1099+ if (!appointments.empty())
1100+ g_main_loop_quit(loop);
1101+ };
1102+ core::ScopedConnection conn(planner->appointments().changed().connect(on_appointments_changed));
1103+ constexpr int max_wait_sec = 10;
1104+ wait_msec(max_wait_sec * G_TIME_SPAN_MILLISECOND);
1105+ }
1106+
1107+ // build expected: one-time alarm
1108+ std::vector<Appointment> expected;
1109+ Appointment a;
1110+ a.type = Appointment::UBUNTU_ALARM;
1111+ a.uid = "20150617T211838Z-6217-32011-2036-1@ubuntu-phablet";
1112+ a.color = "#becedd";
1113+ a.summary = "One Time Alarm";
1114+ a.begin = DateTime { gtz, 2015, 6, 18, 10, 0, 0};
1115+ a.end = a.begin;
1116+ a.alarms.resize(1);
1117+ a.alarms[0].audio_url = "file:///usr/share/sounds/ubuntu/ringtones/Suru arpeggio.ogg";
1118+ a.alarms[0].time = a.begin;
1119+ a.alarms[0].text = a.summary;
1120+ expected.push_back(a);
1121+
1122+ // build expected: recurring alarm
1123+ a.uid = "20150617T211913Z-6217-32011-2036-5@ubuntu-phablet";
1124+ a.summary = "Recurring Alarm";
1125+ a.alarms[0].text = a.summary;
1126+ std::array<DateTime,14> recurrences {
1127+ DateTime{ gtz, 2015, 6, 18, 10, 1, 0 },
1128+ DateTime{ gtz, 2015, 6, 19, 10, 1, 0 },
1129+ DateTime{ gtz, 2015, 6, 20, 10, 1, 0 },
1130+ DateTime{ gtz, 2015, 6, 21, 10, 1, 0 },
1131+ DateTime{ gtz, 2015, 6, 22, 10, 1, 0 },
1132+ DateTime{ gtz, 2015, 6, 23, 10, 1, 0 },
1133+ DateTime{ gtz, 2015, 6, 24, 10, 1, 0 },
1134+ DateTime{ gtz, 2015, 6, 25, 10, 1, 0 },
1135+ DateTime{ gtz, 2015, 6, 26, 10, 1, 0 },
1136+ DateTime{ gtz, 2015, 6, 27, 10, 1, 0 },
1137+ DateTime{ gtz, 2015, 6, 28, 10, 1, 0 },
1138+ DateTime{ gtz, 2015, 6, 29, 10, 1, 0 },
1139+ DateTime{ gtz, 2015, 6, 30, 10, 1, 0 },
1140+ DateTime{ gtz, 2015, 7, 1, 10, 1, 0 }
1141+ };
1142+ for (const auto& time : recurrences) {
1143+ a.begin = a.end = a.alarms[0].time = time;
1144+ expected.push_back(a);
1145+ }
1146+
1147+ // the planner should match what we've got in the calendar.ics file
1148+ const auto appts = planner->appointments().get();
1149+ EXPECT_EQ(expected, appts);
1150+
1151+ // cleanup
1152+ g_time_zone_unref(gtz);
1153+}
1154
1155=== added file 'tests/test-eds-ics-missing-trigger.ics'
1156--- tests/test-eds-ics-missing-trigger.ics 1970-01-01 00:00:00 +0000
1157+++ tests/test-eds-ics-missing-trigger.ics 2015-06-22 14:18:10 +0000
1158@@ -0,0 +1,45 @@
1159+BEGIN:VCALENDAR
1160+CALSCALE:GREGORIAN
1161+PRODID:-//Ximian//NONSGML Evolution Calendar//EN
1162+VERSION:2.0
1163+X-EVOLUTION-DATA-REVISION:2015-06-17T21:19:13.980613Z(3)
1164+BEGIN:VTODO
1165+UID:20150617T211838Z-6217-32011-2036-1@ubuntu-phablet
1166+DTSTAMP:20150617T211838Z
1167+DTSTART:20150618T100000
1168+SUMMARY:One Time Alarm
1169+CATEGORIES:x-canonical-alarm
1170+CREATED:20150617T211838Z
1171+LAST-MODIFIED:20150617T211838Z
1172+BEGIN:VALARM
1173+X-EVOLUTION-ALARM-UID:20150617T211838Z-6217-32011-2036-2@ubuntu-phablet
1174+ACTION:AUDIO
1175+ATTACH:file:///usr/share/sounds/ubuntu/ringtones/Suru arpeggio.ogg
1176+END:VALARM
1177+BEGIN:VALARM
1178+X-EVOLUTION-ALARM-UID:20150617T211838Z-6217-32011-2036-3@ubuntu-phablet
1179+ACTION:DISPLAY
1180+DESCRIPTION:One Time Alarm
1181+END:VALARM
1182+END:VTODO
1183+BEGIN:VTODO
1184+UID:20150617T211913Z-6217-32011-2036-5@ubuntu-phablet
1185+DTSTAMP:20150617T211913Z
1186+DTSTART:20150618T100100
1187+RRULE:FREQ=DAILY
1188+SUMMARY:Recurring Alarm
1189+CATEGORIES:x-canonical-alarm
1190+CREATED:20150617T211913Z
1191+LAST-MODIFIED:20150617T211913Z
1192+BEGIN:VALARM
1193+X-EVOLUTION-ALARM-UID:20150617T211913Z-6217-32011-2036-6@ubuntu-phablet
1194+ACTION:AUDIO
1195+ATTACH:file:///usr/share/sounds/ubuntu/ringtones/Suru arpeggio.ogg
1196+END:VALARM
1197+BEGIN:VALARM
1198+X-EVOLUTION-ALARM-UID:20150617T211913Z-6217-32011-2036-7@ubuntu-phablet
1199+ACTION:DISPLAY
1200+DESCRIPTION:Recurring Alarm
1201+END:VALARM
1202+END:VTODO
1203+END:VCALENDAR
1204
1205=== added file 'tests/test-eds-ics-nonrepeating-events.cpp'
1206--- tests/test-eds-ics-nonrepeating-events.cpp 1970-01-01 00:00:00 +0000
1207+++ tests/test-eds-ics-nonrepeating-events.cpp 2015-06-22 14:18:10 +0000
1208@@ -0,0 +1,93 @@
1209+/*
1210+ * Copyright 2015 Canonical Ltd.
1211+ *
1212+ * This program is free software: you can redistribute it and/or modify it
1213+ * under the terms of the GNU General Public License version 3, as published
1214+ * by the Free Software Foundation.
1215+ *
1216+ * This program is distributed in the hope that it will be useful, but
1217+ * WITHOUT ANY WARRANTY; without even the implied warranties of
1218+ * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
1219+ * PURPOSE. See the GNU General Public License for more details.
1220+ *
1221+ * You should have received a copy of the GNU General Public License along
1222+ * with this program. If not, see <http://www.gnu.org/licenses/>.
1223+ *
1224+ * Authors:
1225+ * Charles Kerr <charles.kerr@canonical.com>
1226+ */
1227+
1228+#include <algorithm>
1229+
1230+#include <datetime/alarm-queue-simple.h>
1231+#include <datetime/clock-mock.h>
1232+#include <datetime/engine-eds.h>
1233+#include <datetime/planner-range.h>
1234+
1235+#include <gtest/gtest.h>
1236+
1237+#include "glib-fixture.h"
1238+#include "print-to.h"
1239+#include "timezone-mock.h"
1240+#include "wakeup-timer-mock.h"
1241+
1242+using namespace unity::indicator::datetime;
1243+using VAlarmFixture = GlibFixture;
1244+
1245+/***
1246+****
1247+***/
1248+
1249+TEST_F(VAlarmFixture, MultipleAppointments)
1250+{
1251+ // start the EDS engine
1252+ auto engine = std::make_shared<EdsEngine>();
1253+
1254+ // we need a consistent timezone for the planner and our local DateTimes
1255+ constexpr char const * zone_str {"America/Chicago"};
1256+ auto tz = std::make_shared<MockTimezone>(zone_str);
1257+ auto gtz = g_time_zone_new(zone_str);
1258+
1259+ // make a planner that looks at the first half of 2015 in EDS
1260+ auto planner = std::make_shared<SimpleRangePlanner>(engine, tz);
1261+ const DateTime range_begin {gtz, 2015,1, 1, 0, 0, 0.0};
1262+ const DateTime range_end {gtz, 2015,6,31,23,59,59.5};
1263+ planner->range().set(std::make_pair(range_begin, range_end));
1264+
1265+ // give EDS a moment to load
1266+ if (planner->appointments().get().empty()) {
1267+ g_message("waiting a moment for EDS to load...");
1268+ auto on_appointments_changed = [this](const std::vector<Appointment>& appointments){
1269+ g_message("ah, they loaded");
1270+ if (!appointments.empty())
1271+ g_main_loop_quit(loop);
1272+ };
1273+ core::ScopedConnection conn(planner->appointments().changed().connect(on_appointments_changed));
1274+ constexpr int max_wait_sec = 10;
1275+ wait_msec(max_wait_sec * G_TIME_SPAN_MILLISECOND);
1276+ }
1277+
1278+ // what we expect to get...
1279+ Appointment expected_appt;
1280+ expected_appt.uid = "20150520T000726Z-3878-32011-1770-81@ubuntu-phablet";
1281+ expected_appt.color = "#becedd";
1282+ expected_appt.summary = "Alarm";
1283+ std::array<Alarm,1> expected_alarms = {
1284+ Alarm({"Alarm", "file:///usr/share/sounds/ubuntu/ringtones/Suru arpeggio.ogg", DateTime(gtz,2015,5,20,20,00,0)})
1285+ };
1286+
1287+ // compare it to what we actually loaded...
1288+ const auto appts = planner->appointments().get();
1289+ EXPECT_EQ(expected_alarms.size(), appts.size());
1290+ for (size_t i=0, n=expected_alarms.size(); i<n; i++) {
1291+ const auto& appt = appts[i];
1292+ EXPECT_EQ(expected_appt.uid, appt.uid);
1293+ EXPECT_EQ(expected_appt.color, appt.color);
1294+ EXPECT_EQ(expected_appt.summary, appt.summary);
1295+ EXPECT_EQ(1, appt.alarms.size());
1296+ EXPECT_EQ(expected_alarms[i], appt.alarms[0]);
1297+ }
1298+
1299+ // cleanup
1300+ g_time_zone_unref(gtz);
1301+}
1302
1303=== added file 'tests/test-eds-ics-nonrepeating-events.ics'
1304--- tests/test-eds-ics-nonrepeating-events.ics 1970-01-01 00:00:00 +0000
1305+++ tests/test-eds-ics-nonrepeating-events.ics 2015-06-22 14:18:10 +0000
1306@@ -0,0 +1,27 @@
1307+BEGIN:VCALENDAR
1308+CALSCALE:GREGORIAN
1309+PRODID:-//Ximian//NONSGML Evolution Calendar//EN
1310+VERSION:2.0
1311+X-EVOLUTION-DATA-REVISION:2015-05-20T22:39:32.685099Z(1)
1312+BEGIN:VTODO
1313+UID:20150520T000726Z-3878-32011-1770-81@ubuntu-phablet
1314+DTSTAMP:20150520T223932Z
1315+DTSTART:20150520T200000
1316+SUMMARY:Alarm
1317+CATEGORIES:x-canonical-alarm
1318+SEQUENCE:1
1319+LAST-MODIFIED:20150520T223932Z
1320+BEGIN:VALARM
1321+X-EVOLUTION-ALARM-UID:20150520T223932Z-22506-32011-1771-2@ubuntu-phablet
1322+ACTION:AUDIO
1323+ATTACH:file:///usr/share/sounds/ubuntu/ringtones/Suru arpeggio.ogg
1324+TRIGGER;VALUE=DURATION;RELATED=START:PT0S
1325+END:VALARM
1326+BEGIN:VALARM
1327+X-EVOLUTION-ALARM-UID:20150520T223932Z-22506-32011-1771-3@ubuntu-phablet
1328+ACTION:DISPLAY
1329+DESCRIPTION:Alarm
1330+TRIGGER;VALUE=DURATION;RELATED=START:PT0S
1331+END:VALARM
1332+END:VTODO
1333+END:VCALENDAR
1334
1335=== added file 'tests/test-eds-ics-repeating-events.cpp'
1336--- tests/test-eds-ics-repeating-events.cpp 1970-01-01 00:00:00 +0000
1337+++ tests/test-eds-ics-repeating-events.cpp 2015-06-22 14:18:10 +0000
1338@@ -0,0 +1,100 @@
1339+/*
1340+ * Copyright 2015 Canonical Ltd.
1341+ *
1342+ * This program is free software: you can redistribute it and/or modify it
1343+ * under the terms of the GNU General Public License version 3, as published
1344+ * by the Free Software Foundation.
1345+ *
1346+ * This program is distributed in the hope that it will be useful, but
1347+ * WITHOUT ANY WARRANTY; without even the implied warranties of
1348+ * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
1349+ * PURPOSE. See the GNU General Public License for more details.
1350+ *
1351+ * You should have received a copy of the GNU General Public License along
1352+ * with this program. If not, see <http://www.gnu.org/licenses/>.
1353+ *
1354+ * Authors:
1355+ * Charles Kerr <charles.kerr@canonical.com>
1356+ */
1357+
1358+#include <algorithm>
1359+
1360+#include <datetime/alarm-queue-simple.h>
1361+#include <datetime/clock-mock.h>
1362+#include <datetime/engine-eds.h>
1363+#include <datetime/planner-range.h>
1364+
1365+#include <gtest/gtest.h>
1366+
1367+#include "glib-fixture.h"
1368+#include "print-to.h"
1369+#include "timezone-mock.h"
1370+#include "wakeup-timer-mock.h"
1371+
1372+using namespace unity::indicator::datetime;
1373+using VAlarmFixture = GlibFixture;
1374+
1375+/***
1376+****
1377+***/
1378+
1379+TEST_F(VAlarmFixture, MultipleAppointments)
1380+{
1381+ // start the EDS engine
1382+ auto engine = std::make_shared<EdsEngine>();
1383+
1384+ // we need a consistent timezone for the planner and our local DateTimes
1385+ constexpr char const * zone_str {"America/Chicago"};
1386+ auto tz = std::make_shared<MockTimezone>(zone_str);
1387+ auto gtz = g_time_zone_new(zone_str);
1388+
1389+ // make a planner that looks at the first half of 2015 in EDS
1390+ auto planner = std::make_shared<SimpleRangePlanner>(engine, tz);
1391+ const DateTime range_begin {gtz, 2015,1, 1, 0, 0, 0.0};
1392+ const DateTime range_end {gtz, 2015,6,31,23,59,59.5};
1393+ planner->range().set(std::make_pair(range_begin, range_end));
1394+
1395+ // give EDS a moment to load
1396+ if (planner->appointments().get().empty()) {
1397+ g_message("waiting a moment for EDS to load...");
1398+ auto on_appointments_changed = [this](const std::vector<Appointment>& appointments){
1399+ g_message("ah, they loaded");
1400+ if (!appointments.empty())
1401+ g_main_loop_quit(loop);
1402+ };
1403+ core::ScopedConnection conn(planner->appointments().changed().connect(on_appointments_changed));
1404+ constexpr int max_wait_sec = 10;
1405+ wait_msec(max_wait_sec * G_TIME_SPAN_MILLISECOND);
1406+ }
1407+
1408+ // what we expect to get...
1409+ Appointment expected_appt;
1410+ expected_appt.uid = "20150507T211449Z-4262-32011-1418-1@ubuntu-phablet";
1411+ expected_appt.color = "#becedd";
1412+ expected_appt.summary = "Alarm";
1413+ std::array<Alarm,8> expected_alarms = {
1414+ Alarm({"Alarm", "file:///usr/share/sounds/ubuntu/ringtones/Suru arpeggio.ogg", DateTime(gtz,2015,5, 8,16,40,0)}),
1415+ Alarm({"Alarm", "file:///usr/share/sounds/ubuntu/ringtones/Suru arpeggio.ogg", DateTime(gtz,2015,5,15,16,40,0)}),
1416+ Alarm({"Alarm", "file:///usr/share/sounds/ubuntu/ringtones/Suru arpeggio.ogg", DateTime(gtz,2015,5,22,16,40,0)}),
1417+ Alarm({"Alarm", "file:///usr/share/sounds/ubuntu/ringtones/Suru arpeggio.ogg", DateTime(gtz,2015,5,29,16,40,0)}),
1418+ Alarm({"Alarm", "file:///usr/share/sounds/ubuntu/ringtones/Suru arpeggio.ogg", DateTime(gtz,2015,6, 5,16,40,0)}),
1419+ Alarm({"Alarm", "file:///usr/share/sounds/ubuntu/ringtones/Suru arpeggio.ogg", DateTime(gtz,2015,6,12,16,40,0)}),
1420+ Alarm({"Alarm", "file:///usr/share/sounds/ubuntu/ringtones/Suru arpeggio.ogg", DateTime(gtz,2015,6,19,16,40,0)}),
1421+ Alarm({"Alarm", "file:///usr/share/sounds/ubuntu/ringtones/Suru arpeggio.ogg", DateTime(gtz,2015,6,26,16,40,0)})
1422+ };
1423+
1424+ // compare it to what we actually loaded...
1425+ const auto appts = planner->appointments().get();
1426+ EXPECT_EQ(expected_alarms.size(), appts.size());
1427+ for (size_t i=0, n=expected_alarms.size(); i<n; i++) {
1428+ const auto& appt = appts[i];
1429+ EXPECT_EQ(expected_appt.uid, appt.uid);
1430+ EXPECT_EQ(expected_appt.color, appt.color);
1431+ EXPECT_EQ(expected_appt.summary, appt.summary);
1432+ EXPECT_EQ(1, appt.alarms.size());
1433+ EXPECT_EQ(expected_alarms[i], appt.alarms[0]);
1434+ }
1435+
1436+ // cleanup
1437+ g_time_zone_unref(gtz);
1438+}
1439
1440=== added file 'tests/test-eds-ics-repeating-events.ics'
1441--- tests/test-eds-ics-repeating-events.ics 1970-01-01 00:00:00 +0000
1442+++ tests/test-eds-ics-repeating-events.ics 2015-06-22 14:18:10 +0000
1443@@ -0,0 +1,28 @@
1444+BEGIN:VCALENDAR
1445+CALSCALE:GREGORIAN
1446+PRODID:-//Ximian//NONSGML Evolution Calendar//EN
1447+VERSION:2.0
1448+X-EVOLUTION-DATA-REVISION:2015-05-07T21:14:49.315443Z(0)
1449+BEGIN:VTODO
1450+UID:20150507T211449Z-4262-32011-1418-1@ubuntu-phablet
1451+DTSTAMP:20150508T211449Z
1452+DTSTART:20150508T164000
1453+RRULE:FREQ=WEEKLY;BYDAY=FR
1454+SUMMARY:Alarm
1455+CATEGORIES:x-canonical-alarm
1456+CREATED:20150507T211449Z
1457+LAST-MODIFIED:20150507T211449Z
1458+BEGIN:VALARM
1459+X-EVOLUTION-ALARM-UID:20150507T211449Z-4262-32011-1418-2@ubuntu-phablet
1460+ACTION:AUDIO
1461+ATTACH:file:///usr/share/sounds/ubuntu/ringtones/Suru arpeggio.ogg
1462+TRIGGER;VALUE=DURATION;RELATED=START:PT0S
1463+END:VALARM
1464+BEGIN:VALARM
1465+X-EVOLUTION-ALARM-UID:20150507T211449Z-4262-32011-1418-3@ubuntu-phablet
1466+ACTION:DISPLAY
1467+DESCRIPTION:Alarm
1468+TRIGGER;VALUE=DURATION;RELATED=START:PT0S
1469+END:VALARM
1470+END:VTODO
1471+END:VCALENDAR
1472
1473=== renamed file 'tests/test-eds-valarms.cpp' => 'tests/test-eds-ics-repeating-valarms.cpp'
1474=== added file 'tests/test-eds-ics-repeating-valarms.ics'
1475--- tests/test-eds-ics-repeating-valarms.ics 1970-01-01 00:00:00 +0000
1476+++ tests/test-eds-ics-repeating-valarms.ics 2015-06-22 14:18:10 +0000
1477@@ -0,0 +1,47 @@
1478+BEGIN:VCALENDAR
1479+CALSCALE:GREGORIAN
1480+PRODID:-//Ximian//NONSGML Evolution Calendar//EN
1481+VERSION:2.0
1482+X-EVOLUTION-DATA-REVISION:2015-04-05T21:32:47.354433Z(2)
1483+BEGIN:VEVENT
1484+UID:20150405T213247Z-4371-32011-1698-1@ubuntu-phablet
1485+DTSTAMP:20150405T213247Z
1486+DTSTART:20150424T183500Z
1487+DTEND:20150424T193554Z
1488+X-LIC-ERROR;X-LIC-ERRORTYPE=VALUE-PARSE-ERROR:Can't parse as RECUR value
1489+ in RRULE property. Removing entire property: ERROR: No Value
1490+SUMMARY:London Sprint Flight
1491+CREATED:20150405T213247Z
1492+LAST-MODIFIED:20150405T213247Z
1493+BEGIN:VALARM
1494+X-EVOLUTION-ALARM-UID:20150405T213247Z-4371-32011-1698-2@ubuntu-phablet
1495+ACTION:AUDIO
1496+TRIGGER;VALUE=DURATION;RELATED=START:-P1D
1497+REPEAT:3
1498+DURATION:PT2M
1499+END:VALARM
1500+BEGIN:VALARM
1501+X-EVOLUTION-ALARM-UID:20150405T213247Z-4371-32011-1698-3@ubuntu-phablet
1502+ACTION:DISPLAY
1503+DESCRIPTION:Time to pack!
1504+TRIGGER;VALUE=DURATION;RELATED=START:-P1D
1505+REPEAT:3
1506+DURATION:PT2M
1507+END:VALARM
1508+BEGIN:VALARM
1509+X-EVOLUTION-ALARM-UID:20150405T213247Z-4371-32011-1698-5@ubuntu-phablet
1510+ACTION:AUDIO
1511+TRIGGER;VALUE=DURATION;RELATED=START:-PT3H
1512+REPEAT:3
1513+DURATION:PT2M
1514+END:VALARM
1515+BEGIN:VALARM
1516+X-EVOLUTION-ALARM-UID:20150405T213247Z-4371-32011-1698-6@ubuntu-phablet
1517+ACTION:DISPLAY
1518+DESCRIPTION:Go to the airport!
1519+TRIGGER;VALUE=DURATION;RELATED=START:-PT3H
1520+REPEAT:3
1521+DURATION:PT2M
1522+END:VALARM
1523+END:VEVENT
1524+END:VCALENDAR
1525
1526=== removed directory 'tests/test-eds-valarms-config-files'
1527=== removed directory 'tests/test-eds-valarms-config-files/.config'
1528=== removed directory 'tests/test-eds-valarms-config-files/.config/evolution'
1529=== removed directory 'tests/test-eds-valarms-config-files/.config/evolution/sources'
1530=== removed file 'tests/test-eds-valarms-config-files/.config/evolution/sources/system-proxy.source'
1531--- tests/test-eds-valarms-config-files/.config/evolution/sources/system-proxy.source 2015-04-06 00:19:01 +0000
1532+++ tests/test-eds-valarms-config-files/.config/evolution/sources/system-proxy.source 1970-01-01 00:00:00 +0000
1533@@ -1,21 +0,0 @@
1534-
1535-[Data Source]
1536-DisplayName=Default Proxy Settings
1537-Enabled=true
1538-Parent=
1539-
1540-[Proxy]
1541-Method=default
1542-IgnoreHosts=localhost;127.0.0.0/8;::1;
1543-AutoconfigUrl=
1544-FtpHost=
1545-FtpPort=0
1546-HttpAuthPassword=
1547-HttpAuthUser=
1548-HttpHost=
1549-HttpPort=8080
1550-HttpUseAuth=false
1551-HttpsHost=
1552-HttpsPort=0
1553-SocksHost=
1554-SocksPort=0
1555
1556=== removed directory 'tests/test-eds-valarms-config-files/.local'
1557=== removed directory 'tests/test-eds-valarms-config-files/.local/share'
1558=== removed directory 'tests/test-eds-valarms-config-files/.local/share/evolution'
1559=== removed directory 'tests/test-eds-valarms-config-files/.local/share/evolution/calendar'
1560=== removed directory 'tests/test-eds-valarms-config-files/.local/share/evolution/calendar/system'
1561=== removed file 'tests/test-eds-valarms-config-files/.local/share/evolution/calendar/system/calendar.ics'
1562--- tests/test-eds-valarms-config-files/.local/share/evolution/calendar/system/calendar.ics 2015-04-06 00:19:01 +0000
1563+++ tests/test-eds-valarms-config-files/.local/share/evolution/calendar/system/calendar.ics 1970-01-01 00:00:00 +0000
1564@@ -1,47 +0,0 @@
1565-BEGIN:VCALENDAR
1566-CALSCALE:GREGORIAN
1567-PRODID:-//Ximian//NONSGML Evolution Calendar//EN
1568-VERSION:2.0
1569-X-EVOLUTION-DATA-REVISION:2015-04-05T21:32:47.354433Z(2)
1570-BEGIN:VEVENT
1571-UID:20150405T213247Z-4371-32011-1698-1@ubuntu-phablet
1572-DTSTAMP:20150405T213247Z
1573-DTSTART:20150424T183500Z
1574-DTEND:20150424T193554Z
1575-X-LIC-ERROR;X-LIC-ERRORTYPE=VALUE-PARSE-ERROR:Can't parse as RECUR value
1576- in RRULE property. Removing entire property: ERROR: No Value
1577-SUMMARY:London Sprint Flight
1578-CREATED:20150405T213247Z
1579-LAST-MODIFIED:20150405T213247Z
1580-BEGIN:VALARM
1581-X-EVOLUTION-ALARM-UID:20150405T213247Z-4371-32011-1698-2@ubuntu-phablet
1582-ACTION:AUDIO
1583-TRIGGER;VALUE=DURATION;RELATED=START:-P1D
1584-REPEAT:3
1585-DURATION:PT2M
1586-END:VALARM
1587-BEGIN:VALARM
1588-X-EVOLUTION-ALARM-UID:20150405T213247Z-4371-32011-1698-3@ubuntu-phablet
1589-ACTION:DISPLAY
1590-DESCRIPTION:Time to pack!
1591-TRIGGER;VALUE=DURATION;RELATED=START:-P1D
1592-REPEAT:3
1593-DURATION:PT2M
1594-END:VALARM
1595-BEGIN:VALARM
1596-X-EVOLUTION-ALARM-UID:20150405T213247Z-4371-32011-1698-5@ubuntu-phablet
1597-ACTION:AUDIO
1598-TRIGGER;VALUE=DURATION;RELATED=START:-PT3H
1599-REPEAT:3
1600-DURATION:PT2M
1601-END:VALARM
1602-BEGIN:VALARM
1603-X-EVOLUTION-ALARM-UID:20150405T213247Z-4371-32011-1698-6@ubuntu-phablet
1604-ACTION:DISPLAY
1605-DESCRIPTION:Go to the airport!
1606-TRIGGER;VALUE=DURATION;RELATED=START:-PT3H
1607-REPEAT:3
1608-DURATION:PT2M
1609-END:VALARM
1610-END:VEVENT
1611-END:VCALENDAR

Subscribers

People subscribed via source and target branches