Merge lp:~charlesk/indicator-datetime/lp-1318997-customizable-alarm-sounds into lp:indicator-datetime/13.10

Proposed by Charles Kerr
Status: Merged
Approved by: Ted Gould
Approved revision: 357
Merged at revision: 350
Proposed branch: lp:~charlesk/indicator-datetime/lp-1318997-customizable-alarm-sounds
Merge into: lp:indicator-datetime/13.10
Diff against target: 1150 lines (+633/-246)
12 files modified
data/com.canonical.indicator.datetime.gschema.xml (+29/-0)
include/datetime/appointment.h (+1/-0)
include/datetime/settings-live.h (+3/-0)
include/datetime/settings-shared.h (+13/-0)
include/datetime/settings.h (+3/-0)
include/datetime/snap.h (+8/-1)
src/engine-eds.cpp (+14/-4)
src/main.cpp (+1/-1)
src/settings-live.cpp (+38/-0)
src/snap.cpp (+442/-236)
tests/manual-test-snap.cpp (+27/-4)
tests/test-settings.cpp (+54/-0)
To merge this branch: bzr merge lp:~charlesk/indicator-datetime/lp-1318997-customizable-alarm-sounds
Reviewer Review Type Date Requested Status
Ted Gould (community) Approve
PS Jenkins bot (community) continuous-integration Approve
Review via email: mp+224231@code.launchpad.net

Commit message

Add the ability to have per-alarm custom sounds.

Description of the change

Add the ability to have customizable alarm sounds, durations, and volume levels.

(1) Add support in the GSettings schema for setting alarm volume, duration, and default sound (to be used if an alarm doesn't have a sound set, or if the one set isn't usable).

(2) Refactor snap.cpp to allow for per-alarm resources -- since these weren't configurable before, the same resources were hardcoded and reused across all alarms. I've moved this logic into two new private snap.cpp classes, Sound (to manage an alarm's canberra resources and handle looping & duration) and Popup (to manage an alarm's NotifyNotification resources). These do what they say on the tin, so even though the file's diff is largish the ideas are straightforward.

(3) If a per-alarm sound is set in the EDS event, use it instead of the default sound.

(4) Sync the unit tests to match these changes.

To post a comment you must log in.
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)
355. By Charles Kerr

copyedit the new snap decision: fix linewraps, give some variables/methods clearer names, better grouping of related methods, etc.

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

in snap.cpp, replace Sound::Properties with a SoundBuilder class to make the pattern use better.

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Approve (continuous-integration)
357. By Charles Kerr

make get_gain_level() a little easier to read.

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

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'data/com.canonical.indicator.datetime.gschema.xml'
2--- data/com.canonical.indicator.datetime.gschema.xml 2013-10-30 22:29:43 +0000
3+++ data/com.canonical.indicator.datetime.gschema.xml 2014-06-27 14:06:11 +0000
4@@ -5,6 +5,13 @@
5 <value nick="24-hour" value="2" />
6 <value nick="custom" value="3" />
7 </enum>
8+ <enum id="alarm-volume-enum">
9+ <value nick="very-quiet" value="0" />
10+ <value nick="quiet" value="1" />
11+ <value nick="normal" value="2" />
12+ <value nick="loud" value="3" />
13+ <value nick="very-loud" value="4" />
14+ </enum>
15 <schema id="com.canonical.indicator.datetime" path="/com/canonical/indicator/datetime/" gettext-domain="indicator-datetime">
16 <key name="show-clock" type="b">
17 <default>true</default>
18@@ -123,5 +130,27 @@
19 Some timezones can be known by many different cities or names. This setting describes how the current zone prefers to be named. Format is "TIMEZONE NAME" (e.g. "America/New_York Boston" to name the New_York zone Boston).
20 </description>
21 </key>
22+ <key name="alarm-default-sound" type="s">
23+ <default>'/usr/share/sounds/ubuntu/ringtones/Suru arpeggio.ogg'</default>
24+ <summary>The alarm's default sound file.</summary>
25+ <description>
26+ If an alarm doesn't specify its own sound file, this file will be used as the fallback sound.
27+ </description>
28+ </key>
29+ <key name="alarm-default-volume" enum="alarm-volume-enum">
30+ <default>'normal'</default>
31+ <summary>The alarm's default volume level.</summary>
32+ <description>
33+ The volume at which alarms will be played.
34+ </description>
35+ </key>
36+ <key name="alarm-duration-minutes" type="i">
37+ <range min="1" max="60"/>
38+ <default>30</default>
39+ <summary>The alarm's duration.</summary>
40+ <description>
41+ How long the alarm's sound will be looped if its snap decision is not dismissed by the user.
42+ </description>
43+ </key>
44 </schema>
45 </schemalist>
46
47=== modified file 'include/datetime/appointment.h'
48--- include/datetime/appointment.h 2014-04-15 15:40:31 +0000
49+++ include/datetime/appointment.h 2014-06-27 14:06:11 +0000
50@@ -39,6 +39,7 @@
51 std::string summary;
52 std::string url;
53 std::string uid;
54+ std::string audio_url;
55 bool has_alarms = false;
56 DateTime begin;
57 DateTime end;
58
59=== modified file 'include/datetime/settings-live.h'
60--- include/datetime/settings-live.h 2014-01-16 21:10:39 +0000
61+++ include/datetime/settings-live.h 2014-06-27 14:06:11 +0000
62@@ -55,6 +55,9 @@
63 void update_show_year();
64 void update_time_format_mode();
65 void update_timezone_name();
66+ void update_alarm_sound();
67+ void update_alarm_volume();
68+ void update_alarm_duration();
69
70 GSettings* m_settings;
71
72
73=== modified file 'include/datetime/settings-shared.h'
74--- include/datetime/settings-shared.h 2014-01-15 05:07:10 +0000
75+++ include/datetime/settings-shared.h 2014-06-27 14:06:11 +0000
76@@ -30,6 +30,16 @@
77 }
78 TimeFormatMode;
79
80+typedef enum
81+{
82+ ALARM_VOLUME_VERY_QUIET,
83+ ALARM_VOLUME_QUIET,
84+ ALARM_VOLUME_NORMAL,
85+ ALARM_VOLUME_LOUD,
86+ ALARM_VOLUME_VERY_LOUD
87+}
88+AlarmVolume;
89+
90 #define SETTINGS_INTERFACE "com.canonical.indicator.datetime"
91 #define SETTINGS_SHOW_CLOCK_S "show-clock"
92 #define SETTINGS_TIME_FORMAT_S "time-format"
93@@ -45,5 +55,8 @@
94 #define SETTINGS_SHOW_DETECTED_S "show-auto-detected-location"
95 #define SETTINGS_LOCATIONS_S "locations"
96 #define SETTINGS_TIMEZONE_NAME_S "timezone-name"
97+#define SETTINGS_ALARM_SOUND_S "alarm-default-sound"
98+#define SETTINGS_ALARM_VOLUME_S "alarm-default-volume"
99+#define SETTINGS_ALARM_DURATION_S "alarm-duration-minutes"
100
101 #endif // INDICATOR_DATETIME_SETTINGS_SHARED
102
103=== modified file 'include/datetime/settings.h'
104--- include/datetime/settings.h 2014-01-22 16:03:57 +0000
105+++ include/datetime/settings.h 2014-06-27 14:06:11 +0000
106@@ -56,6 +56,9 @@
107 core::Property<bool> show_year;
108 core::Property<TimeFormatMode> time_format_mode;
109 core::Property<std::string> timezone_name;
110+ core::Property<std::string> alarm_sound;
111+ core::Property<AlarmVolume> alarm_volume;
112+ core::Property<int> alarm_duration;
113 };
114
115 } // namespace datetime
116
117=== modified file 'include/datetime/snap.h'
118--- include/datetime/snap.h 2014-02-04 19:00:22 +0000
119+++ include/datetime/snap.h 2014-06-27 14:06:11 +0000
120@@ -21,6 +21,8 @@
121 #define INDICATOR_DATETIME_SNAP_H
122
123 #include <datetime/appointment.h>
124+#include <datetime/clock.h>
125+#include <datetime/settings.h>
126
127 #include <memory>
128 #include <functional>
129@@ -35,13 +37,18 @@
130 class Snap
131 {
132 public:
133- Snap();
134+ Snap(const std::shared_ptr<Clock>& clock,
135+ const std::shared_ptr<const Settings>& settings);
136 virtual ~Snap();
137
138 typedef std::function<void(const Appointment&)> appointment_func;
139 void operator()(const Appointment& appointment,
140 appointment_func show,
141 appointment_func dismiss);
142+
143+private:
144+ const std::shared_ptr<Clock> m_clock;
145+ const std::shared_ptr<const Settings> m_settings;
146 };
147
148 } // namespace datetime
149
150=== modified file 'src/engine-eds.cpp'
151--- src/engine-eds.cpp 2014-06-10 14:02:17 +0000
152+++ src/engine-eds.cpp 2014-06-27 14:06:11 +0000
153@@ -443,8 +443,9 @@
154 appointment.color = subtask->color;
155 appointment.uid = uid;
156
157- // if the component has display alarms that have a url,
158- // use the first one as our Appointment.url
159+ // Look through all of this component's alarms
160+ // for DISPLAY or AUDIO url attachments.
161+ // If we find any, use them for appointment.url and audio_sound
162 auto alarm_uids = e_cal_component_get_alarm_uids(component);
163 appointment.has_alarms = alarm_uids != nullptr;
164 for(auto walk=alarm_uids; appointment.url.empty() && walk!=nullptr; walk=walk->next)
165@@ -453,7 +454,7 @@
166
167 ECalComponentAlarmAction action;
168 e_cal_component_alarm_get_action(alarm, &action);
169- if (action == E_CAL_COMPONENT_ALARM_DISPLAY)
170+ if ((action == E_CAL_COMPONENT_ALARM_DISPLAY) || (action == E_CAL_COMPONENT_ALARM_AUDIO))
171 {
172 icalattach* attach = nullptr;
173 e_cal_component_alarm_get_attach(alarm, &attach);
174@@ -463,7 +464,16 @@
175 {
176 const char* url = icalattach_get_url(attach);
177 if (url != nullptr)
178- appointment.url = url;
179+ {
180+ if ((action == E_CAL_COMPONENT_ALARM_DISPLAY) && appointment.url.empty())
181+ {
182+ appointment.url = url;
183+ }
184+ else if ((action == E_CAL_COMPONENT_ALARM_AUDIO) && appointment.audio_url.empty())
185+ {
186+ appointment.audio_url = url;
187+ }
188+ }
189 }
190
191 icalattach_unref(attach);
192
193=== modified file 'src/main.cpp'
194--- src/main.cpp 2014-06-11 04:06:46 +0000
195+++ src/main.cpp 2014-06-27 14:06:11 +0000
196@@ -141,7 +141,7 @@
197 MenuFactory factory(actions, state);
198
199 // set up the snap decisions
200- Snap snap;
201+ Snap snap (state->clock, state->settings);
202 auto alarm_queue = create_simple_alarm_queue(state->clock, engine, timezone);
203 alarm_queue->alarm_reached().connect([&snap](const Appointment& appt){
204 auto snap_show = [](const Appointment& a){
205
206=== modified file 'src/settings-live.cpp'
207--- src/settings-live.cpp 2014-01-16 21:10:39 +0000
208+++ src/settings-live.cpp 2014-06-27 14:06:11 +0000
209@@ -52,6 +52,9 @@
210 update_show_year();
211 update_time_format_mode();
212 update_timezone_name();
213+ update_alarm_sound();
214+ update_alarm_volume();
215+ update_alarm_duration();
216
217 // now listen for clients to change the properties s.t. we can sync update GSettings
218
219@@ -115,6 +118,18 @@
220 timezone_name.changed().connect([this](const std::string& value){
221 g_settings_set_string(m_settings, SETTINGS_TIMEZONE_NAME_S, value.c_str());
222 });
223+
224+ alarm_sound.changed().connect([this](const std::string& value){
225+ g_settings_set_string(m_settings, SETTINGS_ALARM_SOUND_S, value.c_str());
226+ });
227+
228+ alarm_volume.changed().connect([this](AlarmVolume value){
229+ g_settings_set_enum(m_settings, SETTINGS_ALARM_VOLUME_S, gint(value));
230+ });
231+
232+ alarm_duration.changed().connect([this](int value){
233+ g_settings_set_int(m_settings, SETTINGS_ALARM_DURATION_S, value);
234+ });
235 }
236
237 /***
238@@ -205,6 +220,23 @@
239 g_free(val);
240 }
241
242+void LiveSettings::update_alarm_sound()
243+{
244+ auto val = g_settings_get_string(m_settings, SETTINGS_ALARM_SOUND_S);
245+ alarm_sound.set(val);
246+ g_free(val);
247+}
248+
249+void LiveSettings::update_alarm_volume()
250+{
251+ alarm_volume.set((AlarmVolume)g_settings_get_enum(m_settings, SETTINGS_ALARM_VOLUME_S));
252+}
253+
254+void LiveSettings::update_alarm_duration()
255+{
256+ alarm_duration.set(g_settings_get_int(m_settings, SETTINGS_ALARM_DURATION_S));
257+}
258+
259 /***
260 ****
261 ***/
262@@ -246,6 +278,12 @@
263 update_show_detected_locations();
264 else if (key == SETTINGS_TIMEZONE_NAME_S)
265 update_timezone_name();
266+ else if (key == SETTINGS_ALARM_SOUND_S)
267+ update_alarm_sound();
268+ else if (key == SETTINGS_ALARM_VOLUME_S)
269+ update_alarm_volume();
270+ else if (key == SETTINGS_ALARM_DURATION_S)
271+ update_alarm_duration();
272 }
273
274 /***
275
276=== modified file 'src/snap.cpp'
277--- src/snap.cpp 2014-05-28 07:35:09 +0000
278+++ src/snap.cpp 2014-06-27 14:06:11 +0000
279@@ -21,6 +21,8 @@
280 #include <datetime/formatter.h>
281 #include <datetime/snap.h>
282
283+#include <core/signal.h>
284+
285 #include <canberra.h>
286 #include <libnotify/notify.h>
287
288@@ -30,8 +32,6 @@
289 #include <set>
290 #include <string>
291
292-#define ALARM_SOUND_FILENAME "/usr/share/sounds/ubuntu/ringtones/Suru arpeggio.ogg"
293-
294 namespace unity {
295 namespace indicator {
296 namespace datetime {
297@@ -43,266 +43,472 @@
298 namespace
299 {
300
301-/**
302-*** libcanberra -- play sounds
303-**/
304-
305-// arbitrary number, but we need a consistent id for play/cancel
306-const int32_t alarm_ca_id = 1;
307-
308-ca_context *c_context = nullptr;
309-guint timeout_tag = 0;
310-
311-ca_context* get_ca_context()
312-{
313- if (G_UNLIKELY(c_context == nullptr))
314- {
315- int rv;
316-
317- if ((rv = ca_context_create(&c_context)) != CA_SUCCESS)
318- {
319- g_warning("Failed to create canberra context: %s\n", ca_strerror(rv));
320- c_context = nullptr;
321- }
322- }
323-
324- return c_context;
325-}
326-
327-void play_alarm_sound();
328-
329-gboolean play_alarm_sound_idle (gpointer)
330-{
331- timeout_tag = 0;
332- play_alarm_sound();
333- return G_SOURCE_REMOVE;
334-}
335-
336-void on_alarm_play_done (ca_context* /*context*/, uint32_t /*id*/, int rv, void* /*user_data*/)
337-{
338- // wait one second, then play it again
339- if ((rv == CA_SUCCESS) && (timeout_tag == 0))
340- timeout_tag = g_timeout_add_seconds (1, play_alarm_sound_idle, nullptr);
341-}
342-
343-void play_alarm_sound()
344-{
345- const gchar* filename = ALARM_SOUND_FILENAME;
346- auto context = get_ca_context();
347- g_return_if_fail(context != nullptr);
348-
349- ca_proplist* props = nullptr;
350- ca_proplist_create(&props);
351- ca_proplist_sets(props, CA_PROP_MEDIA_FILENAME, filename);
352-
353- const auto rv = ca_context_play_full(context, alarm_ca_id, props, on_alarm_play_done, nullptr);
354- if (rv != CA_SUCCESS)
355- g_warning("Failed to play file '%s': %s", filename, ca_strerror(rv));
356-
357- g_clear_pointer(&props, ca_proplist_destroy);
358-}
359-
360-void stop_alarm_sound()
361-{
362- auto context = get_ca_context();
363- if (context != nullptr)
364- {
365- const auto rv = ca_context_cancel(context, alarm_ca_id);
366+/**
367+ * Plays a sound, possibly looping.
368+ */
369+class Sound
370+{
371+ typedef Sound Self;
372+
373+public:
374+
375+ Sound(const std::shared_ptr<Clock>& clock,
376+ const std::string& filename,
377+ AlarmVolume volume,
378+ int duration_minutes,
379+ bool loop):
380+ m_clock(clock),
381+ m_filename(filename),
382+ m_volume(volume),
383+ m_duration_minutes(duration_minutes),
384+ m_loop(loop),
385+ m_canberra_id(get_next_canberra_id()),
386+ m_loop_end_time(clock->localtime().add_full(0, 0, 0, 0, duration_minutes, 0.0))
387+ {
388+ if (m_loop)
389+ {
390+ g_debug("Looping '%s' until cutoff time %s",
391+ m_filename.c_str(),
392+ m_loop_end_time.format("%F %T").c_str());
393+ }
394+ else
395+ {
396+ g_debug("Playing '%s' once", m_filename.c_str());
397+ }
398+
399+ const auto rv = ca_context_create(&m_context);
400+ if (rv == CA_SUCCESS)
401+ {
402+ play();
403+ }
404+ else
405+ {
406+ g_warning("Failed to create canberra context: %s", ca_strerror(rv));
407+ m_context = nullptr;
408+ }
409+ }
410+
411+ ~Sound()
412+ {
413+ stop();
414+
415+ g_clear_pointer(&m_context, ca_context_destroy);
416+ }
417+
418+private:
419+
420+ void stop()
421+ {
422+ if (m_context != nullptr)
423+ {
424+ const auto rv = ca_context_cancel(m_context, m_canberra_id);
425+ if (rv != CA_SUCCESS)
426+ g_warning("Failed to cancel alarm sound: %s", ca_strerror(rv));
427+ }
428+
429+ if (m_loop_tag != 0)
430+ {
431+ g_source_remove(m_loop_tag);
432+ m_loop_tag = 0;
433+ }
434+ }
435+
436+ void play()
437+ {
438+ auto context = m_context;
439+ g_return_if_fail(context != nullptr);
440+
441+ const auto filename = m_filename.c_str();
442+ const float gain = get_gain_level(m_volume);
443+
444+ ca_proplist* props = nullptr;
445+ ca_proplist_create(&props);
446+ ca_proplist_sets(props, CA_PROP_MEDIA_FILENAME, filename);
447+ ca_proplist_setf(props, CA_PROP_CANBERRA_VOLUME, "%f", gain);
448+ const auto rv = ca_context_play_full(context, m_canberra_id, props,
449+ on_done_playing, this);
450 if (rv != CA_SUCCESS)
451- g_warning("Failed to cancel alarm sound: %s", ca_strerror(rv));
452- }
453-
454- if (timeout_tag != 0)
455- {
456- g_source_remove(timeout_tag);
457- timeout_tag = 0;
458- }
459-}
460+ g_warning("Unable to play '%s': %s", filename, ca_strerror(rv));
461+
462+ g_clear_pointer(&props, ca_proplist_destroy);
463+ }
464+
465+ static float get_gain_level(const AlarmVolume volume)
466+ {
467+ /* These values aren't set in stone --
468+ arrived at from from manual tests on Nexus 4 */
469+ switch (volume)
470+ {
471+ case ALARM_VOLUME_VERY_QUIET: return -8;
472+ case ALARM_VOLUME_QUIET: return -4;
473+ case ALARM_VOLUME_LOUD: return 4;
474+ case ALARM_VOLUME_VERY_LOUD: return 8;
475+ default: return 0;
476+ }
477+ }
478+
479+ static void on_done_playing(ca_context*, uint32_t, int rv, void* gself)
480+ {
481+ // if we still need to loop, wait a second, then play it again
482+
483+ if (rv == CA_SUCCESS)
484+ {
485+ auto self = static_cast<Self*>(gself);
486+ if ((self->m_loop_tag == 0) &&
487+ (self->m_loop) &&
488+ (self->m_clock->localtime() < self->m_loop_end_time))
489+ {
490+ self->m_loop_tag = g_timeout_add_seconds(1, play_idle, self);
491+ }
492+ }
493+ }
494+
495+ static gboolean play_idle(gpointer gself)
496+ {
497+ auto self = static_cast<Self*>(gself);
498+ self->m_loop_tag = 0;
499+ self->play();
500+ return G_SOURCE_REMOVE;
501+ }
502+
503+ /***
504+ ****
505+ ***/
506+
507+ static int32_t get_next_canberra_id()
508+ {
509+ static int32_t next_canberra_id = 1;
510+ return next_canberra_id++;
511+ }
512+
513+ const std::shared_ptr<Clock> m_clock;
514+ const std::string m_filename;
515+ const AlarmVolume m_volume;
516+ const int m_duration_minutes;
517+ const bool m_loop;
518+ const int32_t m_canberra_id;
519+ const DateTime m_loop_end_time;
520+ ca_context* m_context = nullptr;
521+ guint m_loop_tag = 0;
522+};
523+
524+class SoundBuilder
525+{
526+public:
527+ void set_clock(const std::shared_ptr<Clock>& c) {m_clock = c;}
528+ void set_filename(const std::string& s) {m_filename = s;}
529+ void set_volume(const AlarmVolume v) {m_volume = v;}
530+ void set_duration_minutes(int i) {m_duration_minutes=i;}
531+ void set_looping(bool b) {m_looping=b;}
532+
533+ Sound* operator()() {
534+ return new Sound (m_clock,
535+ m_filename,
536+ m_volume,
537+ m_duration_minutes,
538+ m_looping);
539+ }
540+
541+private:
542+ std::shared_ptr<Clock> m_clock;
543+ std::string m_filename;
544+ AlarmVolume m_volume = ALARM_VOLUME_NORMAL;
545+ int m_duration_minutes = 30;
546+ bool m_looping = true;
547+};
548+
549+/**
550+ * A popup notification (with optional sound)
551+ * that emits a Response signal when done.
552+ */
553+class Popup
554+{
555+public:
556+
557+ Popup(const Appointment& appointment, const SoundBuilder& sound_builder):
558+ m_appointment(appointment),
559+ m_interactive(get_interactive()),
560+ m_sound_builder(sound_builder)
561+ {
562+ // ensure notify_init() is called once
563+ // before we start popping up dialogs
564+ static bool m_nn_inited = false;
565+ if (G_UNLIKELY(!m_nn_inited))
566+ {
567+ if(!notify_init("indicator-datetime-service"))
568+ g_critical("libnotify initialization failed");
569+
570+ m_nn_inited = true;
571+ }
572+
573+ show();
574+ }
575+
576+ ~Popup()
577+ {
578+ if (m_nn != nullptr)
579+ {
580+ notify_notification_clear_actions(m_nn);
581+ g_signal_handlers_disconnect_by_data(m_nn, this);
582+ g_clear_object(&m_nn);
583+ }
584+ }
585+
586+ typedef enum
587+ {
588+ RESPONSE_SHOW,
589+ RESPONSE_DISMISS,
590+ RESPONSE_CLOSE
591+ }
592+ Response;
593+
594+ core::Signal<Response>& response() { return m_response; }
595+
596+private:
597+
598+ void show()
599+ {
600+ const Appointment& appointment = m_appointment;
601+
602+ /// strftime(3) format string for an alarm's snap decision
603+ const auto timestr = appointment.begin.format(_("%a, %X"));
604+ auto title = g_strdup_printf(_("Alarm %s"), timestr.c_str());
605+ const auto body = appointment.summary;
606+ const gchar* icon_name = "alarm-clock";
607+
608+ m_nn = notify_notification_new(title, body.c_str(), icon_name);
609+ if (m_interactive)
610+ {
611+ notify_notification_set_hint_string(m_nn,
612+ "x-canonical-snap-decisions",
613+ "true");
614+ notify_notification_set_hint_string(m_nn,
615+ "x-canonical-private-button-tint",
616+ "true");
617+ /// alarm popup dialog's button to show the active alarm
618+ notify_notification_add_action(m_nn, "show", _("Show"),
619+ on_snap_show, this, nullptr);
620+ /// alarm popup dialog's button to shut up the alarm
621+ notify_notification_add_action(m_nn, "dismiss", _("Dismiss"),
622+ on_snap_dismiss, this, nullptr);
623+ g_signal_connect(m_nn, "closed", G_CALLBACK(on_snap_closed), this);
624+ }
625+
626+ bool shown = true;
627+ GError* error = nullptr;
628+ notify_notification_show(m_nn, &error);
629+ if (error != NULL)
630+ {
631+ g_critical("Unable to show snap decision for '%s': %s",
632+ body.c_str(), error->message);
633+ g_error_free(error);
634+ shown = false;
635+ }
636+
637+ // Loop the sound *only* if we're prompting the user for a response.
638+ // Otherwise, just play the sound once.
639+ m_sound_builder.set_looping (shown && m_interactive);
640+ m_sound.reset (m_sound_builder());
641+
642+ // if showing the notification didn't work,
643+ // treat it as if the user clicked the 'show' button
644+ if (!shown)
645+ {
646+ on_snap_show(nullptr, nullptr, this);
647+ on_snap_dismiss(nullptr, nullptr, this);
648+ }
649+
650+ g_free(title);
651+ }
652+
653+ // user clicked 'show'
654+ static void on_snap_show(NotifyNotification*, gchar*, gpointer gself)
655+ {
656+ auto self = static_cast<Self*>(gself);
657+ self->m_response_value = RESPONSE_SHOW;
658+ self->m_sound.reset();
659+ }
660+
661+ // user clicked 'dismiss'
662+ static void on_snap_dismiss(NotifyNotification*, gchar*, gpointer gself)
663+ {
664+ auto self = static_cast<Self*>(gself);
665+ self->m_response_value = RESPONSE_DISMISS;
666+ self->m_sound.reset();
667+ }
668+
669+ // the popup was closed
670+ static void on_snap_closed(NotifyNotification*, gpointer gself)
671+ {
672+ auto self = static_cast<Self*>(gself);
673+ self->m_sound.reset();
674+ self->m_response(self->m_response_value);
675+ }
676+
677+ /***
678+ **** Interactive
679+ ***/
680+
681+ static std::set<std::string> get_server_caps()
682+ {
683+ std::set<std::string> caps_set;
684+ auto caps_gl = notify_get_server_caps();
685+ std::string caps_str;
686+ for(auto l=caps_gl; l!=nullptr; l=l->next)
687+ {
688+ caps_set.insert((const char*)l->data);
689+
690+ caps_str += (const char*) l->data;;
691+ if (l->next != nullptr)
692+ caps_str += ", ";
693+ }
694+ g_debug("%s notify_get_server() returned [%s]", G_STRFUNC, caps_str.c_str());
695+ g_list_free_full(caps_gl, g_free);
696+ return caps_set;
697+ }
698+
699+ static bool get_interactive()
700+ {
701+ static bool interactive;
702+ static bool inited = false;
703+
704+ if (G_UNLIKELY(!inited))
705+ {
706+ interactive = get_server_caps().count("actions") != 0;
707+ inited = true;
708+ }
709+
710+ return interactive;
711+ }
712+
713+ /***
714+ ****
715+ ***/
716+
717+ typedef Popup Self;
718+
719+ const Appointment m_appointment;
720+ const bool m_interactive;
721+ SoundBuilder m_sound_builder;
722+ std::unique_ptr<Sound> m_sound;
723+ core::Signal<Response> m_response;
724+ Response m_response_value = RESPONSE_CLOSE;
725+ NotifyNotification* m_nn = nullptr;
726+};
727
728 /**
729 *** libnotify -- snap decisions
730 **/
731
732-void first_time_init()
733-{
734- static bool inited = false;
735-
736- if (G_UNLIKELY(!inited))
737- {
738- inited = true;
739-
740- if(!notify_init("indicator-datetime-service"))
741- g_critical("libnotify initialization failed");
742- }
743-}
744-
745-struct SnapData
746-{
747- Snap::appointment_func show;
748- Snap::appointment_func dismiss;
749- Appointment appointment;
750-};
751-
752-void on_snap_show(NotifyNotification*, gchar* /*action*/, gpointer gdata)
753-{
754- stop_alarm_sound();
755- auto data = static_cast<SnapData*>(gdata);
756- data->show(data->appointment);
757-}
758-
759-void on_snap_dismiss(NotifyNotification*, gchar* /*action*/, gpointer gdata)
760-{
761- stop_alarm_sound();
762- auto data = static_cast<SnapData*>(gdata);
763- data->dismiss(data->appointment);
764-}
765-
766-void on_snap_closed(NotifyNotification*, gpointer)
767-{
768- stop_alarm_sound();
769-}
770-
771-void snap_data_destroy_notify(gpointer gdata)
772-{
773- delete static_cast<SnapData*>(gdata);
774-}
775-
776-std::set<std::string> get_server_caps()
777-{
778- std::set<std::string> caps_set;
779- auto caps_gl = notify_get_server_caps();
780- std::string caps_str;
781- for(auto l=caps_gl; l!=nullptr; l=l->next)
782- {
783- caps_set.insert((const char*)l->data);
784-
785- caps_str += (const char*) l->data;;
786- if (l->next != nullptr)
787- caps_str += ", ";
788- }
789- g_debug ("%s notify_get_server() returned [%s]", G_STRFUNC, caps_str.c_str());
790- g_list_free_full(caps_gl, g_free);
791- return caps_set;
792-}
793-
794-typedef enum
795-{
796- // just a bubble... no actions, no audio
797- NOTIFY_MODE_BUBBLE,
798-
799- // a snap decision popup dialog + audio
800- NOTIFY_MODE_SNAP
801-}
802-NotifyMode;
803-
804-NotifyMode get_notify_mode()
805-{
806- static NotifyMode mode;
807- static bool mode_inited = false;
808-
809- if (G_UNLIKELY(!mode_inited))
810- {
811- const auto caps = get_server_caps();
812-
813- if (caps.count("actions"))
814- mode = NOTIFY_MODE_SNAP;
815- else
816- mode = NOTIFY_MODE_BUBBLE;
817-
818- mode_inited = true;
819- }
820-
821- return mode;
822-}
823-
824-bool show_notification (SnapData* data, NotifyMode mode)
825-{
826- const Appointment& appointment = data->appointment;
827-
828- const auto timestr = appointment.begin.format("%a, %X");
829- auto title = g_strdup_printf(_("Alarm %s"), timestr.c_str());
830- const auto body = appointment.summary;
831- const gchar* icon_name = "alarm-clock";
832-
833- auto nn = notify_notification_new(title, body.c_str(), icon_name);
834- if (mode == NOTIFY_MODE_SNAP)
835- {
836- notify_notification_set_hint_string(nn, "x-canonical-snap-decisions", "true");
837- notify_notification_set_hint_string(nn, "x-canonical-private-button-tint", "true");
838- /* text for the alarm popup dialog's button to show the active alarm */
839- notify_notification_add_action(nn, "show", _("Show"), on_snap_show, data, nullptr);
840- /* text for the alarm popup dialog's button to shut up the alarm */
841- notify_notification_add_action(nn, "dismiss", _("Dismiss"), on_snap_dismiss, data, nullptr);
842- g_signal_connect(G_OBJECT(nn), "closed", G_CALLBACK(on_snap_closed), data);
843- }
844- g_object_set_data_full(G_OBJECT(nn), "snap-data", data, snap_data_destroy_notify);
845-
846- bool shown = true;
847- GError * error = nullptr;
848- notify_notification_show(nn, &error);
849- if (error != NULL)
850- {
851- g_critical("Unable to show snap decision for '%s': %s", body.c_str(), error->message);
852- g_error_free(error);
853- data->show(data->appointment);
854- shown = false;
855- }
856-
857- g_free(title);
858- return shown;
859-}
860-
861-/**
862-***
863-**/
864-
865-void notify(const Appointment& appointment,
866- Snap::appointment_func show,
867- Snap::appointment_func dismiss)
868-{
869- auto data = new SnapData;
870- data->appointment = appointment;
871- data->show = show;
872- data->dismiss = dismiss;
873-
874- switch (get_notify_mode())
875- {
876- case NOTIFY_MODE_BUBBLE:
877- show_notification(data, NOTIFY_MODE_BUBBLE);
878- break;
879-
880- default:
881- if (show_notification(data, NOTIFY_MODE_SNAP))
882- play_alarm_sound();
883- break;
884- }
885+std::string get_local_filename (const std::string& str)
886+{
887+ std::string ret;
888+
889+ if (!str.empty())
890+ {
891+ GFile* files[] = { g_file_new_for_path(str.c_str()),
892+ g_file_new_for_uri(str.c_str()) };
893+
894+ for(auto& file : files)
895+ {
896+ if (g_file_is_native(file) && g_file_query_exists(file, nullptr))
897+ {
898+ char* tmp = g_file_get_path(file);
899+ if (tmp != nullptr)
900+ {
901+ ret = tmp;
902+ g_free(tmp);
903+ break;
904+ }
905+ }
906+ }
907+
908+ for(auto& file : files)
909+ g_object_unref(file);
910+ }
911+
912+ return ret;
913+}
914+
915+std::string get_alarm_sound(const Appointment& appointment,
916+ const std::shared_ptr<const Settings>& settings)
917+{
918+ const char* FALLBACK {"/usr/share/sounds/ubuntu/ringtones/Suru arpeggio.ogg"};
919+
920+ const std::string candidates[] = { appointment.audio_url,
921+ settings->alarm_sound.get(),
922+ FALLBACK };
923+
924+ std::string alarm_sound;
925+
926+ for(const auto& candidate : candidates)
927+ {
928+ alarm_sound = get_local_filename(candidate);
929+
930+ if (!alarm_sound.empty())
931+ break;
932+ }
933+
934+ g_debug("%s: Appointment \"%s\" using alarm sound \"%s\"",
935+ G_STRFUNC, appointment.summary.c_str(), alarm_sound.c_str());
936+
937+ return alarm_sound;
938 }
939
940 } // unnamed namespace
941
942-
943 /***
944 ****
945 ***/
946
947-Snap::Snap()
948+Snap::Snap(const std::shared_ptr<Clock>& clock,
949+ const std::shared_ptr<const Settings>& settings):
950+ m_clock(clock),
951+ m_settings(settings)
952 {
953- first_time_init();
954 }
955
956 Snap::~Snap()
957 {
958- g_clear_pointer(&c_context, ca_context_destroy);
959 }
960
961 void Snap::operator()(const Appointment& appointment,
962 appointment_func show,
963 appointment_func dismiss)
964 {
965- if (appointment.has_alarms)
966- notify(appointment, show, dismiss);
967- else
968+ if (!appointment.has_alarms)
969+ {
970 dismiss(appointment);
971+ return;
972+ }
973+
974+ // create a popup...
975+ SoundBuilder sound_builder;
976+ sound_builder.set_filename(get_alarm_sound(appointment, m_settings));
977+ sound_builder.set_volume(m_settings->alarm_volume.get());
978+ sound_builder.set_clock(m_clock);
979+ sound_builder.set_duration_minutes(m_settings->alarm_duration.get());
980+ auto popup = new Popup(appointment, sound_builder);
981+
982+ // listen for it to finish...
983+ popup->response().connect([appointment,
984+ show,
985+ dismiss,
986+ popup](Popup::Response response){
987+
988+ // we can't delete the Popup inside its response() signal handler,
989+ // so push that to an idle func
990+ g_idle_add([](gpointer gdata){
991+ delete static_cast<Popup*>(gdata);
992+ return G_SOURCE_REMOVE;
993+ }, popup);
994+
995+ // maybe notify the client code that the popup's done
996+ if (response == Popup::RESPONSE_SHOW)
997+ show(appointment);
998+ else if (response == Popup::RESPONSE_DISMISS)
999+ dismiss(appointment);
1000+ });
1001 }
1002
1003 /***
1004
1005=== modified file 'tests/manual-test-snap.cpp'
1006--- tests/manual-test-snap.cpp 2014-04-15 15:40:31 +0000
1007+++ tests/manual-test-snap.cpp 2014-06-27 14:06:11 +0000
1008@@ -19,16 +19,30 @@
1009 */
1010
1011 #include <datetime/appointment.h>
1012+#include <datetime/settings-live.h>
1013 #include <datetime/snap.h>
1014+#include <datetime/timezones-live.h>
1015
1016 #include <glib.h>
1017
1018 using namespace unity::indicator::datetime;
1019
1020+#define TIMEZONE_FILE ("/etc/timezone")
1021+
1022+
1023 /***
1024 ****
1025 ***/
1026
1027+namespace
1028+{
1029+ gboolean quit_idle (gpointer gloop)
1030+ {
1031+ g_main_loop_quit(static_cast<GMainLoop*>(gloop));
1032+ return G_SOURCE_REMOVE;
1033+ };
1034+}
1035+
1036 int main()
1037 {
1038 Appointment a;
1039@@ -47,15 +61,24 @@
1040 auto loop = g_main_loop_new(nullptr, false);
1041 auto show = [loop](const Appointment& appt){
1042 g_message("You clicked 'show' for appt url '%s'", appt.url.c_str());
1043- g_main_loop_quit(loop);
1044+ g_idle_add(quit_idle, loop);
1045 };
1046 auto dismiss = [loop](const Appointment&){
1047 g_message("You clicked 'dismiss'");
1048- g_main_loop_quit(loop);
1049+ g_idle_add(quit_idle, loop);
1050 };
1051-
1052- Snap snap;
1053+
1054+ // only use local, temporary settings
1055+ g_assert(g_setenv("GSETTINGS_SCHEMA_DIR", SCHEMA_DIR, true));
1056+ g_assert(g_setenv("GSETTINGS_BACKEND", "memory", true));
1057+ g_debug("SCHEMA_DIR is %s", SCHEMA_DIR);
1058+
1059+ auto settings = std::make_shared<LiveSettings>();
1060+ auto timezones = std::make_shared<LiveTimezones>(settings, TIMEZONE_FILE);
1061+ auto clock = std::make_shared<LiveClock>(timezones);
1062+ Snap snap (clock, settings);
1063 snap(a, show, dismiss);
1064 g_main_loop_run(loop);
1065+ g_main_loop_unref(loop);
1066 return 0;
1067 }
1068
1069=== modified file 'tests/test-settings.cpp'
1070--- tests/test-settings.cpp 2014-02-02 21:29:29 +0000
1071+++ tests/test-settings.cpp 2014-06-27 14:06:11 +0000
1072@@ -100,6 +100,29 @@
1073 EXPECT_EQ(str, tmp);
1074 g_clear_pointer(&tmp, g_free);
1075 }
1076+
1077+ void TestIntProperty(core::Property<int>& property, const gchar* key)
1078+ {
1079+ EXPECT_EQ(g_settings_get_int(m_gsettings, key), property.get());
1080+
1081+ int expected_values[] = { 1, 2, 3 };
1082+
1083+ // modify GSettings and confirm that the new value is propagated
1084+ for(const int& expected_value : expected_values)
1085+ {
1086+ g_settings_set_int(m_gsettings, key, expected_value);
1087+ EXPECT_EQ(expected_value, property.get());
1088+ EXPECT_EQ(expected_value, g_settings_get_int(m_gsettings, key));
1089+ }
1090+
1091+ // modify the property and confirm that the new value is propagated
1092+ for(const int& expected_value : expected_values)
1093+ {
1094+ property.set(expected_value);
1095+ EXPECT_EQ(expected_value, property.get());
1096+ EXPECT_EQ(expected_value, g_settings_get_int(m_gsettings, key));
1097+ }
1098+ }
1099 };
1100
1101 /***
1102@@ -125,10 +148,16 @@
1103 TestBoolProperty(m_settings->show_year, SETTINGS_SHOW_YEAR_S);
1104 }
1105
1106+TEST_F(SettingsFixture, IntProperties)
1107+{
1108+ TestIntProperty(m_settings->alarm_duration, SETTINGS_ALARM_DURATION_S);
1109+}
1110+
1111 TEST_F(SettingsFixture, StringProperties)
1112 {
1113 TestStringProperty(m_settings->custom_time_format, SETTINGS_CUSTOM_TIME_FORMAT_S);
1114 TestStringProperty(m_settings->timezone_name, SETTINGS_TIMEZONE_NAME_S);
1115+ TestStringProperty(m_settings->alarm_sound, SETTINGS_ALARM_SOUND_S);
1116 }
1117
1118 TEST_F(SettingsFixture, TimeFormatMode)
1119@@ -152,6 +181,31 @@
1120 }
1121 }
1122
1123+TEST_F(SettingsFixture, AlarmVolume)
1124+{
1125+ const auto key = SETTINGS_ALARM_VOLUME_S;
1126+ const AlarmVolume volumes[] = { ALARM_VOLUME_VERY_QUIET,
1127+ ALARM_VOLUME_QUIET,
1128+ ALARM_VOLUME_NORMAL,
1129+ ALARM_VOLUME_LOUD,
1130+ ALARM_VOLUME_VERY_LOUD };
1131+
1132+ for(const auto& val : volumes)
1133+ {
1134+ g_settings_set_enum(m_gsettings, key, val);
1135+ EXPECT_EQ(val, m_settings->alarm_volume.get());
1136+ EXPECT_EQ(val, g_settings_get_enum(m_gsettings, key));
1137+ }
1138+
1139+ for(const auto& val : volumes)
1140+ {
1141+ m_settings->alarm_volume.set(val);
1142+ EXPECT_EQ(val, m_settings->alarm_volume.get());
1143+ EXPECT_EQ(val, g_settings_get_enum(m_gsettings, key));
1144+ }
1145+}
1146+
1147+
1148 namespace
1149 {
1150 std::vector<std::string> strv_to_vector(const gchar** strv)

Subscribers

People subscribed via source and target branches