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
=== modified file 'data/com.canonical.indicator.datetime.gschema.xml'
--- data/com.canonical.indicator.datetime.gschema.xml 2013-10-30 22:29:43 +0000
+++ data/com.canonical.indicator.datetime.gschema.xml 2014-06-27 14:06:11 +0000
@@ -5,6 +5,13 @@
5 <value nick="24-hour" value="2" />5 <value nick="24-hour" value="2" />
6 <value nick="custom" value="3" />6 <value nick="custom" value="3" />
7 </enum>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>
8 <schema id="com.canonical.indicator.datetime" path="/com/canonical/indicator/datetime/" gettext-domain="indicator-datetime">15 <schema id="com.canonical.indicator.datetime" path="/com/canonical/indicator/datetime/" gettext-domain="indicator-datetime">
9 <key name="show-clock" type="b">16 <key name="show-clock" type="b">
10 <default>true</default>17 <default>true</default>
@@ -123,5 +130,27 @@
123 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).130 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).
124 </description>131 </description>
125 </key>132 </key>
133 <key name="alarm-default-sound" type="s">
134 <default>'/usr/share/sounds/ubuntu/ringtones/Suru arpeggio.ogg'</default>
135 <summary>The alarm's default sound file.</summary>
136 <description>
137 If an alarm doesn't specify its own sound file, this file will be used as the fallback sound.
138 </description>
139 </key>
140 <key name="alarm-default-volume" enum="alarm-volume-enum">
141 <default>'normal'</default>
142 <summary>The alarm's default volume level.</summary>
143 <description>
144 The volume at which alarms will be played.
145 </description>
146 </key>
147 <key name="alarm-duration-minutes" type="i">
148 <range min="1" max="60"/>
149 <default>30</default>
150 <summary>The alarm's duration.</summary>
151 <description>
152 How long the alarm's sound will be looped if its snap decision is not dismissed by the user.
153 </description>
154 </key>
126 </schema>155 </schema>
127</schemalist>156</schemalist>
128157
=== modified file 'include/datetime/appointment.h'
--- include/datetime/appointment.h 2014-04-15 15:40:31 +0000
+++ include/datetime/appointment.h 2014-06-27 14:06:11 +0000
@@ -39,6 +39,7 @@
39 std::string summary;39 std::string summary;
40 std::string url;40 std::string url;
41 std::string uid;41 std::string uid;
42 std::string audio_url;
42 bool has_alarms = false;43 bool has_alarms = false;
43 DateTime begin;44 DateTime begin;
44 DateTime end;45 DateTime end;
4546
=== modified file 'include/datetime/settings-live.h'
--- include/datetime/settings-live.h 2014-01-16 21:10:39 +0000
+++ include/datetime/settings-live.h 2014-06-27 14:06:11 +0000
@@ -55,6 +55,9 @@
55 void update_show_year();55 void update_show_year();
56 void update_time_format_mode();56 void update_time_format_mode();
57 void update_timezone_name();57 void update_timezone_name();
58 void update_alarm_sound();
59 void update_alarm_volume();
60 void update_alarm_duration();
5861
59 GSettings* m_settings;62 GSettings* m_settings;
6063
6164
=== modified file 'include/datetime/settings-shared.h'
--- include/datetime/settings-shared.h 2014-01-15 05:07:10 +0000
+++ include/datetime/settings-shared.h 2014-06-27 14:06:11 +0000
@@ -30,6 +30,16 @@
30}30}
31TimeFormatMode;31TimeFormatMode;
3232
33typedef enum
34{
35 ALARM_VOLUME_VERY_QUIET,
36 ALARM_VOLUME_QUIET,
37 ALARM_VOLUME_NORMAL,
38 ALARM_VOLUME_LOUD,
39 ALARM_VOLUME_VERY_LOUD
40}
41AlarmVolume;
42
33#define SETTINGS_INTERFACE "com.canonical.indicator.datetime"43#define SETTINGS_INTERFACE "com.canonical.indicator.datetime"
34#define SETTINGS_SHOW_CLOCK_S "show-clock"44#define SETTINGS_SHOW_CLOCK_S "show-clock"
35#define SETTINGS_TIME_FORMAT_S "time-format"45#define SETTINGS_TIME_FORMAT_S "time-format"
@@ -45,5 +55,8 @@
45#define SETTINGS_SHOW_DETECTED_S "show-auto-detected-location"55#define SETTINGS_SHOW_DETECTED_S "show-auto-detected-location"
46#define SETTINGS_LOCATIONS_S "locations"56#define SETTINGS_LOCATIONS_S "locations"
47#define SETTINGS_TIMEZONE_NAME_S "timezone-name"57#define SETTINGS_TIMEZONE_NAME_S "timezone-name"
58#define SETTINGS_ALARM_SOUND_S "alarm-default-sound"
59#define SETTINGS_ALARM_VOLUME_S "alarm-default-volume"
60#define SETTINGS_ALARM_DURATION_S "alarm-duration-minutes"
4861
49#endif // INDICATOR_DATETIME_SETTINGS_SHARED62#endif // INDICATOR_DATETIME_SETTINGS_SHARED
5063
=== modified file 'include/datetime/settings.h'
--- include/datetime/settings.h 2014-01-22 16:03:57 +0000
+++ include/datetime/settings.h 2014-06-27 14:06:11 +0000
@@ -56,6 +56,9 @@
56 core::Property<bool> show_year;56 core::Property<bool> show_year;
57 core::Property<TimeFormatMode> time_format_mode;57 core::Property<TimeFormatMode> time_format_mode;
58 core::Property<std::string> timezone_name;58 core::Property<std::string> timezone_name;
59 core::Property<std::string> alarm_sound;
60 core::Property<AlarmVolume> alarm_volume;
61 core::Property<int> alarm_duration;
59};62};
6063
61} // namespace datetime64} // namespace datetime
6265
=== modified file 'include/datetime/snap.h'
--- include/datetime/snap.h 2014-02-04 19:00:22 +0000
+++ include/datetime/snap.h 2014-06-27 14:06:11 +0000
@@ -21,6 +21,8 @@
21#define INDICATOR_DATETIME_SNAP_H21#define INDICATOR_DATETIME_SNAP_H
2222
23#include <datetime/appointment.h>23#include <datetime/appointment.h>
24#include <datetime/clock.h>
25#include <datetime/settings.h>
2426
25#include <memory>27#include <memory>
26#include <functional>28#include <functional>
@@ -35,13 +37,18 @@
35class Snap37class Snap
36{38{
37public:39public:
38 Snap();40 Snap(const std::shared_ptr<Clock>& clock,
41 const std::shared_ptr<const Settings>& settings);
39 virtual ~Snap();42 virtual ~Snap();
4043
41 typedef std::function<void(const Appointment&)> appointment_func;44 typedef std::function<void(const Appointment&)> appointment_func;
42 void operator()(const Appointment& appointment,45 void operator()(const Appointment& appointment,
43 appointment_func show,46 appointment_func show,
44 appointment_func dismiss);47 appointment_func dismiss);
48
49private:
50 const std::shared_ptr<Clock> m_clock;
51 const std::shared_ptr<const Settings> m_settings;
45};52};
4653
47} // namespace datetime54} // namespace datetime
4855
=== modified file 'src/engine-eds.cpp'
--- src/engine-eds.cpp 2014-06-10 14:02:17 +0000
+++ src/engine-eds.cpp 2014-06-27 14:06:11 +0000
@@ -443,8 +443,9 @@
443 appointment.color = subtask->color;443 appointment.color = subtask->color;
444 appointment.uid = uid;444 appointment.uid = uid;
445445
446 // if the component has display alarms that have a url,446 // Look through all of this component's alarms
447 // use the first one as our Appointment.url447 // for DISPLAY or AUDIO url attachments.
448 // If we find any, use them for appointment.url and audio_sound
448 auto alarm_uids = e_cal_component_get_alarm_uids(component);449 auto alarm_uids = e_cal_component_get_alarm_uids(component);
449 appointment.has_alarms = alarm_uids != nullptr;450 appointment.has_alarms = alarm_uids != nullptr;
450 for(auto walk=alarm_uids; appointment.url.empty() && walk!=nullptr; walk=walk->next)451 for(auto walk=alarm_uids; appointment.url.empty() && walk!=nullptr; walk=walk->next)
@@ -453,7 +454,7 @@
453454
454 ECalComponentAlarmAction action;455 ECalComponentAlarmAction action;
455 e_cal_component_alarm_get_action(alarm, &action);456 e_cal_component_alarm_get_action(alarm, &action);
456 if (action == E_CAL_COMPONENT_ALARM_DISPLAY)457 if ((action == E_CAL_COMPONENT_ALARM_DISPLAY) || (action == E_CAL_COMPONENT_ALARM_AUDIO))
457 {458 {
458 icalattach* attach = nullptr;459 icalattach* attach = nullptr;
459 e_cal_component_alarm_get_attach(alarm, &attach);460 e_cal_component_alarm_get_attach(alarm, &attach);
@@ -463,7 +464,16 @@
463 {464 {
464 const char* url = icalattach_get_url(attach);465 const char* url = icalattach_get_url(attach);
465 if (url != nullptr)466 if (url != nullptr)
466 appointment.url = url;467 {
468 if ((action == E_CAL_COMPONENT_ALARM_DISPLAY) && appointment.url.empty())
469 {
470 appointment.url = url;
471 }
472 else if ((action == E_CAL_COMPONENT_ALARM_AUDIO) && appointment.audio_url.empty())
473 {
474 appointment.audio_url = url;
475 }
476 }
467 }477 }
468478
469 icalattach_unref(attach);479 icalattach_unref(attach);
470480
=== modified file 'src/main.cpp'
--- src/main.cpp 2014-06-11 04:06:46 +0000
+++ src/main.cpp 2014-06-27 14:06:11 +0000
@@ -141,7 +141,7 @@
141 MenuFactory factory(actions, state);141 MenuFactory factory(actions, state);
142142
143 // set up the snap decisions143 // set up the snap decisions
144 Snap snap;144 Snap snap (state->clock, state->settings);
145 auto alarm_queue = create_simple_alarm_queue(state->clock, engine, timezone);145 auto alarm_queue = create_simple_alarm_queue(state->clock, engine, timezone);
146 alarm_queue->alarm_reached().connect([&snap](const Appointment& appt){146 alarm_queue->alarm_reached().connect([&snap](const Appointment& appt){
147 auto snap_show = [](const Appointment& a){147 auto snap_show = [](const Appointment& a){
148148
=== modified file 'src/settings-live.cpp'
--- src/settings-live.cpp 2014-01-16 21:10:39 +0000
+++ src/settings-live.cpp 2014-06-27 14:06:11 +0000
@@ -52,6 +52,9 @@
52 update_show_year();52 update_show_year();
53 update_time_format_mode();53 update_time_format_mode();
54 update_timezone_name();54 update_timezone_name();
55 update_alarm_sound();
56 update_alarm_volume();
57 update_alarm_duration();
5558
56 // now listen for clients to change the properties s.t. we can sync update GSettings59 // now listen for clients to change the properties s.t. we can sync update GSettings
5760
@@ -115,6 +118,18 @@
115 timezone_name.changed().connect([this](const std::string& value){118 timezone_name.changed().connect([this](const std::string& value){
116 g_settings_set_string(m_settings, SETTINGS_TIMEZONE_NAME_S, value.c_str());119 g_settings_set_string(m_settings, SETTINGS_TIMEZONE_NAME_S, value.c_str());
117 });120 });
121
122 alarm_sound.changed().connect([this](const std::string& value){
123 g_settings_set_string(m_settings, SETTINGS_ALARM_SOUND_S, value.c_str());
124 });
125
126 alarm_volume.changed().connect([this](AlarmVolume value){
127 g_settings_set_enum(m_settings, SETTINGS_ALARM_VOLUME_S, gint(value));
128 });
129
130 alarm_duration.changed().connect([this](int value){
131 g_settings_set_int(m_settings, SETTINGS_ALARM_DURATION_S, value);
132 });
118}133}
119134
120/***135/***
@@ -205,6 +220,23 @@
205 g_free(val);220 g_free(val);
206}221}
207222
223void LiveSettings::update_alarm_sound()
224{
225 auto val = g_settings_get_string(m_settings, SETTINGS_ALARM_SOUND_S);
226 alarm_sound.set(val);
227 g_free(val);
228}
229
230void LiveSettings::update_alarm_volume()
231{
232 alarm_volume.set((AlarmVolume)g_settings_get_enum(m_settings, SETTINGS_ALARM_VOLUME_S));
233}
234
235void LiveSettings::update_alarm_duration()
236{
237 alarm_duration.set(g_settings_get_int(m_settings, SETTINGS_ALARM_DURATION_S));
238}
239
208/***240/***
209****241****
210***/242***/
@@ -246,6 +278,12 @@
246 update_show_detected_locations();278 update_show_detected_locations();
247 else if (key == SETTINGS_TIMEZONE_NAME_S)279 else if (key == SETTINGS_TIMEZONE_NAME_S)
248 update_timezone_name();280 update_timezone_name();
281 else if (key == SETTINGS_ALARM_SOUND_S)
282 update_alarm_sound();
283 else if (key == SETTINGS_ALARM_VOLUME_S)
284 update_alarm_volume();
285 else if (key == SETTINGS_ALARM_DURATION_S)
286 update_alarm_duration();
249}287}
250288
251/***289/***
252290
=== modified file 'src/snap.cpp'
--- src/snap.cpp 2014-05-28 07:35:09 +0000
+++ src/snap.cpp 2014-06-27 14:06:11 +0000
@@ -21,6 +21,8 @@
21#include <datetime/formatter.h>21#include <datetime/formatter.h>
22#include <datetime/snap.h>22#include <datetime/snap.h>
2323
24#include <core/signal.h>
25
24#include <canberra.h>26#include <canberra.h>
25#include <libnotify/notify.h>27#include <libnotify/notify.h>
2628
@@ -30,8 +32,6 @@
30#include <set>32#include <set>
31#include <string>33#include <string>
3234
33#define ALARM_SOUND_FILENAME "/usr/share/sounds/ubuntu/ringtones/Suru arpeggio.ogg"
34
35namespace unity {35namespace unity {
36namespace indicator {36namespace indicator {
37namespace datetime {37namespace datetime {
@@ -43,266 +43,472 @@
43namespace43namespace
44{44{
4545
46/** 46/**
47*** libcanberra -- play sounds47 * Plays a sound, possibly looping.
48**/48 */
4949class Sound
50// arbitrary number, but we need a consistent id for play/cancel50{
51const int32_t alarm_ca_id = 1;51 typedef Sound Self;
5252
53ca_context *c_context = nullptr;53public:
54guint timeout_tag = 0;54
5555 Sound(const std::shared_ptr<Clock>& clock,
56ca_context* get_ca_context()56 const std::string& filename,
57{57 AlarmVolume volume,
58 if (G_UNLIKELY(c_context == nullptr))58 int duration_minutes,
59 {59 bool loop):
60 int rv;60 m_clock(clock),
6161 m_filename(filename),
62 if ((rv = ca_context_create(&c_context)) != CA_SUCCESS)62 m_volume(volume),
63 {63 m_duration_minutes(duration_minutes),
64 g_warning("Failed to create canberra context: %s\n", ca_strerror(rv));64 m_loop(loop),
65 c_context = nullptr;65 m_canberra_id(get_next_canberra_id()),
66 }66 m_loop_end_time(clock->localtime().add_full(0, 0, 0, 0, duration_minutes, 0.0))
67 }67 {
6868 if (m_loop)
69 return c_context;69 {
70}70 g_debug("Looping '%s' until cutoff time %s",
7171 m_filename.c_str(),
72void play_alarm_sound();72 m_loop_end_time.format("%F %T").c_str());
7373 }
74gboolean play_alarm_sound_idle (gpointer)74 else
75{75 {
76 timeout_tag = 0;76 g_debug("Playing '%s' once", m_filename.c_str());
77 play_alarm_sound();77 }
78 return G_SOURCE_REMOVE;78
79}79 const auto rv = ca_context_create(&m_context);
8080 if (rv == CA_SUCCESS)
81void on_alarm_play_done (ca_context* /*context*/, uint32_t /*id*/, int rv, void* /*user_data*/)81 {
82{82 play();
83 // wait one second, then play it again83 }
84 if ((rv == CA_SUCCESS) && (timeout_tag == 0))84 else
85 timeout_tag = g_timeout_add_seconds (1, play_alarm_sound_idle, nullptr);85 {
86}86 g_warning("Failed to create canberra context: %s", ca_strerror(rv));
8787 m_context = nullptr;
88void play_alarm_sound()88 }
89{89 }
90 const gchar* filename = ALARM_SOUND_FILENAME;90
91 auto context = get_ca_context();91 ~Sound()
92 g_return_if_fail(context != nullptr);92 {
9393 stop();
94 ca_proplist* props = nullptr;94
95 ca_proplist_create(&props);95 g_clear_pointer(&m_context, ca_context_destroy);
96 ca_proplist_sets(props, CA_PROP_MEDIA_FILENAME, filename);96 }
9797
98 const auto rv = ca_context_play_full(context, alarm_ca_id, props, on_alarm_play_done, nullptr);98private:
99 if (rv != CA_SUCCESS)99
100 g_warning("Failed to play file '%s': %s", filename, ca_strerror(rv));100 void stop()
101101 {
102 g_clear_pointer(&props, ca_proplist_destroy);102 if (m_context != nullptr)
103}103 {
104104 const auto rv = ca_context_cancel(m_context, m_canberra_id);
105void stop_alarm_sound()105 if (rv != CA_SUCCESS)
106{106 g_warning("Failed to cancel alarm sound: %s", ca_strerror(rv));
107 auto context = get_ca_context();107 }
108 if (context != nullptr)108
109 {109 if (m_loop_tag != 0)
110 const auto rv = ca_context_cancel(context, alarm_ca_id);110 {
111 g_source_remove(m_loop_tag);
112 m_loop_tag = 0;
113 }
114 }
115
116 void play()
117 {
118 auto context = m_context;
119 g_return_if_fail(context != nullptr);
120
121 const auto filename = m_filename.c_str();
122 const float gain = get_gain_level(m_volume);
123
124 ca_proplist* props = nullptr;
125 ca_proplist_create(&props);
126 ca_proplist_sets(props, CA_PROP_MEDIA_FILENAME, filename);
127 ca_proplist_setf(props, CA_PROP_CANBERRA_VOLUME, "%f", gain);
128 const auto rv = ca_context_play_full(context, m_canberra_id, props,
129 on_done_playing, this);
111 if (rv != CA_SUCCESS)130 if (rv != CA_SUCCESS)
112 g_warning("Failed to cancel alarm sound: %s", ca_strerror(rv));131 g_warning("Unable to play '%s': %s", filename, ca_strerror(rv));
113 }132
114133 g_clear_pointer(&props, ca_proplist_destroy);
115 if (timeout_tag != 0)134 }
116 {135
117 g_source_remove(timeout_tag);136 static float get_gain_level(const AlarmVolume volume)
118 timeout_tag = 0;137 {
119 }138 /* These values aren't set in stone --
120}139 arrived at from from manual tests on Nexus 4 */
140 switch (volume)
141 {
142 case ALARM_VOLUME_VERY_QUIET: return -8;
143 case ALARM_VOLUME_QUIET: return -4;
144 case ALARM_VOLUME_LOUD: return 4;
145 case ALARM_VOLUME_VERY_LOUD: return 8;
146 default: return 0;
147 }
148 }
149
150 static void on_done_playing(ca_context*, uint32_t, int rv, void* gself)
151 {
152 // if we still need to loop, wait a second, then play it again
153
154 if (rv == CA_SUCCESS)
155 {
156 auto self = static_cast<Self*>(gself);
157 if ((self->m_loop_tag == 0) &&
158 (self->m_loop) &&
159 (self->m_clock->localtime() < self->m_loop_end_time))
160 {
161 self->m_loop_tag = g_timeout_add_seconds(1, play_idle, self);
162 }
163 }
164 }
165
166 static gboolean play_idle(gpointer gself)
167 {
168 auto self = static_cast<Self*>(gself);
169 self->m_loop_tag = 0;
170 self->play();
171 return G_SOURCE_REMOVE;
172 }
173
174 /***
175 ****
176 ***/
177
178 static int32_t get_next_canberra_id()
179 {
180 static int32_t next_canberra_id = 1;
181 return next_canberra_id++;
182 }
183
184 const std::shared_ptr<Clock> m_clock;
185 const std::string m_filename;
186 const AlarmVolume m_volume;
187 const int m_duration_minutes;
188 const bool m_loop;
189 const int32_t m_canberra_id;
190 const DateTime m_loop_end_time;
191 ca_context* m_context = nullptr;
192 guint m_loop_tag = 0;
193};
194
195class SoundBuilder
196{
197public:
198 void set_clock(const std::shared_ptr<Clock>& c) {m_clock = c;}
199 void set_filename(const std::string& s) {m_filename = s;}
200 void set_volume(const AlarmVolume v) {m_volume = v;}
201 void set_duration_minutes(int i) {m_duration_minutes=i;}
202 void set_looping(bool b) {m_looping=b;}
203
204 Sound* operator()() {
205 return new Sound (m_clock,
206 m_filename,
207 m_volume,
208 m_duration_minutes,
209 m_looping);
210 }
211
212private:
213 std::shared_ptr<Clock> m_clock;
214 std::string m_filename;
215 AlarmVolume m_volume = ALARM_VOLUME_NORMAL;
216 int m_duration_minutes = 30;
217 bool m_looping = true;
218};
219
220/**
221 * A popup notification (with optional sound)
222 * that emits a Response signal when done.
223 */
224class Popup
225{
226public:
227
228 Popup(const Appointment& appointment, const SoundBuilder& sound_builder):
229 m_appointment(appointment),
230 m_interactive(get_interactive()),
231 m_sound_builder(sound_builder)
232 {
233 // ensure notify_init() is called once
234 // before we start popping up dialogs
235 static bool m_nn_inited = false;
236 if (G_UNLIKELY(!m_nn_inited))
237 {
238 if(!notify_init("indicator-datetime-service"))
239 g_critical("libnotify initialization failed");
240
241 m_nn_inited = true;
242 }
243
244 show();
245 }
246
247 ~Popup()
248 {
249 if (m_nn != nullptr)
250 {
251 notify_notification_clear_actions(m_nn);
252 g_signal_handlers_disconnect_by_data(m_nn, this);
253 g_clear_object(&m_nn);
254 }
255 }
256
257 typedef enum
258 {
259 RESPONSE_SHOW,
260 RESPONSE_DISMISS,
261 RESPONSE_CLOSE
262 }
263 Response;
264
265 core::Signal<Response>& response() { return m_response; }
266
267private:
268
269 void show()
270 {
271 const Appointment& appointment = m_appointment;
272
273 /// strftime(3) format string for an alarm's snap decision
274 const auto timestr = appointment.begin.format(_("%a, %X"));
275 auto title = g_strdup_printf(_("Alarm %s"), timestr.c_str());
276 const auto body = appointment.summary;
277 const gchar* icon_name = "alarm-clock";
278
279 m_nn = notify_notification_new(title, body.c_str(), icon_name);
280 if (m_interactive)
281 {
282 notify_notification_set_hint_string(m_nn,
283 "x-canonical-snap-decisions",
284 "true");
285 notify_notification_set_hint_string(m_nn,
286 "x-canonical-private-button-tint",
287 "true");
288 /// alarm popup dialog's button to show the active alarm
289 notify_notification_add_action(m_nn, "show", _("Show"),
290 on_snap_show, this, nullptr);
291 /// alarm popup dialog's button to shut up the alarm
292 notify_notification_add_action(m_nn, "dismiss", _("Dismiss"),
293 on_snap_dismiss, this, nullptr);
294 g_signal_connect(m_nn, "closed", G_CALLBACK(on_snap_closed), this);
295 }
296
297 bool shown = true;
298 GError* error = nullptr;
299 notify_notification_show(m_nn, &error);
300 if (error != NULL)
301 {
302 g_critical("Unable to show snap decision for '%s': %s",
303 body.c_str(), error->message);
304 g_error_free(error);
305 shown = false;
306 }
307
308 // Loop the sound *only* if we're prompting the user for a response.
309 // Otherwise, just play the sound once.
310 m_sound_builder.set_looping (shown && m_interactive);
311 m_sound.reset (m_sound_builder());
312
313 // if showing the notification didn't work,
314 // treat it as if the user clicked the 'show' button
315 if (!shown)
316 {
317 on_snap_show(nullptr, nullptr, this);
318 on_snap_dismiss(nullptr, nullptr, this);
319 }
320
321 g_free(title);
322 }
323
324 // user clicked 'show'
325 static void on_snap_show(NotifyNotification*, gchar*, gpointer gself)
326 {
327 auto self = static_cast<Self*>(gself);
328 self->m_response_value = RESPONSE_SHOW;
329 self->m_sound.reset();
330 }
331
332 // user clicked 'dismiss'
333 static void on_snap_dismiss(NotifyNotification*, gchar*, gpointer gself)
334 {
335 auto self = static_cast<Self*>(gself);
336 self->m_response_value = RESPONSE_DISMISS;
337 self->m_sound.reset();
338 }
339
340 // the popup was closed
341 static void on_snap_closed(NotifyNotification*, gpointer gself)
342 {
343 auto self = static_cast<Self*>(gself);
344 self->m_sound.reset();
345 self->m_response(self->m_response_value);
346 }
347
348 /***
349 **** Interactive
350 ***/
351
352 static std::set<std::string> get_server_caps()
353 {
354 std::set<std::string> caps_set;
355 auto caps_gl = notify_get_server_caps();
356 std::string caps_str;
357 for(auto l=caps_gl; l!=nullptr; l=l->next)
358 {
359 caps_set.insert((const char*)l->data);
360
361 caps_str += (const char*) l->data;;
362 if (l->next != nullptr)
363 caps_str += ", ";
364 }
365 g_debug("%s notify_get_server() returned [%s]", G_STRFUNC, caps_str.c_str());
366 g_list_free_full(caps_gl, g_free);
367 return caps_set;
368 }
369
370 static bool get_interactive()
371 {
372 static bool interactive;
373 static bool inited = false;
374
375 if (G_UNLIKELY(!inited))
376 {
377 interactive = get_server_caps().count("actions") != 0;
378 inited = true;
379 }
380
381 return interactive;
382 }
383
384 /***
385 ****
386 ***/
387
388 typedef Popup Self;
389
390 const Appointment m_appointment;
391 const bool m_interactive;
392 SoundBuilder m_sound_builder;
393 std::unique_ptr<Sound> m_sound;
394 core::Signal<Response> m_response;
395 Response m_response_value = RESPONSE_CLOSE;
396 NotifyNotification* m_nn = nullptr;
397};
121398
122/** 399/**
123*** libnotify -- snap decisions400*** libnotify -- snap decisions
124**/401**/
125402
126void first_time_init()403std::string get_local_filename (const std::string& str)
127{404{
128 static bool inited = false;405 std::string ret;
129406
130 if (G_UNLIKELY(!inited))407 if (!str.empty())
131 {408 {
132 inited = true;409 GFile* files[] = { g_file_new_for_path(str.c_str()),
133410 g_file_new_for_uri(str.c_str()) };
134 if(!notify_init("indicator-datetime-service"))411
135 g_critical("libnotify initialization failed");412 for(auto& file : files)
136 }413 {
137}414 if (g_file_is_native(file) && g_file_query_exists(file, nullptr))
138415 {
139struct SnapData416 char* tmp = g_file_get_path(file);
140{417 if (tmp != nullptr)
141 Snap::appointment_func show;418 {
142 Snap::appointment_func dismiss;419 ret = tmp;
143 Appointment appointment;420 g_free(tmp);
144};421 break;
145422 }
146void on_snap_show(NotifyNotification*, gchar* /*action*/, gpointer gdata)423 }
147{424 }
148 stop_alarm_sound();425
149 auto data = static_cast<SnapData*>(gdata);426 for(auto& file : files)
150 data->show(data->appointment);427 g_object_unref(file);
151}428 }
152429
153void on_snap_dismiss(NotifyNotification*, gchar* /*action*/, gpointer gdata)430 return ret;
154{431}
155 stop_alarm_sound();432
156 auto data = static_cast<SnapData*>(gdata);433std::string get_alarm_sound(const Appointment& appointment,
157 data->dismiss(data->appointment);434 const std::shared_ptr<const Settings>& settings)
158}435{
159436 const char* FALLBACK {"/usr/share/sounds/ubuntu/ringtones/Suru arpeggio.ogg"};
160void on_snap_closed(NotifyNotification*, gpointer)437
161{438 const std::string candidates[] = { appointment.audio_url,
162 stop_alarm_sound();439 settings->alarm_sound.get(),
163}440 FALLBACK };
164441
165void snap_data_destroy_notify(gpointer gdata)442 std::string alarm_sound;
166{443
167 delete static_cast<SnapData*>(gdata);444 for(const auto& candidate : candidates)
168}445 {
169446 alarm_sound = get_local_filename(candidate);
170std::set<std::string> get_server_caps()447
171{448 if (!alarm_sound.empty())
172 std::set<std::string> caps_set;449 break;
173 auto caps_gl = notify_get_server_caps();450 }
174 std::string caps_str;451
175 for(auto l=caps_gl; l!=nullptr; l=l->next)452 g_debug("%s: Appointment \"%s\" using alarm sound \"%s\"",
176 {453 G_STRFUNC, appointment.summary.c_str(), alarm_sound.c_str());
177 caps_set.insert((const char*)l->data);454
178455 return alarm_sound;
179 caps_str += (const char*) l->data;;
180 if (l->next != nullptr)
181 caps_str += ", ";
182 }
183 g_debug ("%s notify_get_server() returned [%s]", G_STRFUNC, caps_str.c_str());
184 g_list_free_full(caps_gl, g_free);
185 return caps_set;
186}
187
188typedef enum
189{
190 // just a bubble... no actions, no audio
191 NOTIFY_MODE_BUBBLE,
192
193 // a snap decision popup dialog + audio
194 NOTIFY_MODE_SNAP
195}
196NotifyMode;
197
198NotifyMode get_notify_mode()
199{
200 static NotifyMode mode;
201 static bool mode_inited = false;
202
203 if (G_UNLIKELY(!mode_inited))
204 {
205 const auto caps = get_server_caps();
206
207 if (caps.count("actions"))
208 mode = NOTIFY_MODE_SNAP;
209 else
210 mode = NOTIFY_MODE_BUBBLE;
211
212 mode_inited = true;
213 }
214
215 return mode;
216}
217
218bool show_notification (SnapData* data, NotifyMode mode)
219{
220 const Appointment& appointment = data->appointment;
221
222 const auto timestr = appointment.begin.format("%a, %X");
223 auto title = g_strdup_printf(_("Alarm %s"), timestr.c_str());
224 const auto body = appointment.summary;
225 const gchar* icon_name = "alarm-clock";
226
227 auto nn = notify_notification_new(title, body.c_str(), icon_name);
228 if (mode == NOTIFY_MODE_SNAP)
229 {
230 notify_notification_set_hint_string(nn, "x-canonical-snap-decisions", "true");
231 notify_notification_set_hint_string(nn, "x-canonical-private-button-tint", "true");
232 /* text for the alarm popup dialog's button to show the active alarm */
233 notify_notification_add_action(nn, "show", _("Show"), on_snap_show, data, nullptr);
234 /* text for the alarm popup dialog's button to shut up the alarm */
235 notify_notification_add_action(nn, "dismiss", _("Dismiss"), on_snap_dismiss, data, nullptr);
236 g_signal_connect(G_OBJECT(nn), "closed", G_CALLBACK(on_snap_closed), data);
237 }
238 g_object_set_data_full(G_OBJECT(nn), "snap-data", data, snap_data_destroy_notify);
239
240 bool shown = true;
241 GError * error = nullptr;
242 notify_notification_show(nn, &error);
243 if (error != NULL)
244 {
245 g_critical("Unable to show snap decision for '%s': %s", body.c_str(), error->message);
246 g_error_free(error);
247 data->show(data->appointment);
248 shown = false;
249 }
250
251 g_free(title);
252 return shown;
253}
254
255/**
256***
257**/
258
259void notify(const Appointment& appointment,
260 Snap::appointment_func show,
261 Snap::appointment_func dismiss)
262{
263 auto data = new SnapData;
264 data->appointment = appointment;
265 data->show = show;
266 data->dismiss = dismiss;
267
268 switch (get_notify_mode())
269 {
270 case NOTIFY_MODE_BUBBLE:
271 show_notification(data, NOTIFY_MODE_BUBBLE);
272 break;
273
274 default:
275 if (show_notification(data, NOTIFY_MODE_SNAP))
276 play_alarm_sound();
277 break;
278 }
279}456}
280457
281} // unnamed namespace458} // unnamed namespace
282459
283
284/***460/***
285****461****
286***/462***/
287463
288Snap::Snap()464Snap::Snap(const std::shared_ptr<Clock>& clock,
465 const std::shared_ptr<const Settings>& settings):
466 m_clock(clock),
467 m_settings(settings)
289{468{
290 first_time_init();
291}469}
292470
293Snap::~Snap()471Snap::~Snap()
294{472{
295 g_clear_pointer(&c_context, ca_context_destroy);
296}473}
297474
298void Snap::operator()(const Appointment& appointment,475void Snap::operator()(const Appointment& appointment,
299 appointment_func show,476 appointment_func show,
300 appointment_func dismiss)477 appointment_func dismiss)
301{478{
302 if (appointment.has_alarms)479 if (!appointment.has_alarms)
303 notify(appointment, show, dismiss);480 {
304 else
305 dismiss(appointment);481 dismiss(appointment);
482 return;
483 }
484
485 // create a popup...
486 SoundBuilder sound_builder;
487 sound_builder.set_filename(get_alarm_sound(appointment, m_settings));
488 sound_builder.set_volume(m_settings->alarm_volume.get());
489 sound_builder.set_clock(m_clock);
490 sound_builder.set_duration_minutes(m_settings->alarm_duration.get());
491 auto popup = new Popup(appointment, sound_builder);
492
493 // listen for it to finish...
494 popup->response().connect([appointment,
495 show,
496 dismiss,
497 popup](Popup::Response response){
498
499 // we can't delete the Popup inside its response() signal handler,
500 // so push that to an idle func
501 g_idle_add([](gpointer gdata){
502 delete static_cast<Popup*>(gdata);
503 return G_SOURCE_REMOVE;
504 }, popup);
505
506 // maybe notify the client code that the popup's done
507 if (response == Popup::RESPONSE_SHOW)
508 show(appointment);
509 else if (response == Popup::RESPONSE_DISMISS)
510 dismiss(appointment);
511 });
306}512}
307513
308/***514/***
309515
=== modified file 'tests/manual-test-snap.cpp'
--- tests/manual-test-snap.cpp 2014-04-15 15:40:31 +0000
+++ tests/manual-test-snap.cpp 2014-06-27 14:06:11 +0000
@@ -19,16 +19,30 @@
19 */19 */
2020
21#include <datetime/appointment.h>21#include <datetime/appointment.h>
22#include <datetime/settings-live.h>
22#include <datetime/snap.h>23#include <datetime/snap.h>
24#include <datetime/timezones-live.h>
2325
24#include <glib.h>26#include <glib.h>
2527
26using namespace unity::indicator::datetime;28using namespace unity::indicator::datetime;
2729
30#define TIMEZONE_FILE ("/etc/timezone")
31
32
28/***33/***
29****34****
30***/35***/
3136
37namespace
38{
39 gboolean quit_idle (gpointer gloop)
40 {
41 g_main_loop_quit(static_cast<GMainLoop*>(gloop));
42 return G_SOURCE_REMOVE;
43 };
44}
45
32int main()46int main()
33{47{
34 Appointment a;48 Appointment a;
@@ -47,15 +61,24 @@
47 auto loop = g_main_loop_new(nullptr, false);61 auto loop = g_main_loop_new(nullptr, false);
48 auto show = [loop](const Appointment& appt){62 auto show = [loop](const Appointment& appt){
49 g_message("You clicked 'show' for appt url '%s'", appt.url.c_str());63 g_message("You clicked 'show' for appt url '%s'", appt.url.c_str());
50 g_main_loop_quit(loop);64 g_idle_add(quit_idle, loop);
51 };65 };
52 auto dismiss = [loop](const Appointment&){66 auto dismiss = [loop](const Appointment&){
53 g_message("You clicked 'dismiss'");67 g_message("You clicked 'dismiss'");
54 g_main_loop_quit(loop);68 g_idle_add(quit_idle, loop);
55 };69 };
56 70
57 Snap snap;71 // only use local, temporary settings
72 g_assert(g_setenv("GSETTINGS_SCHEMA_DIR", SCHEMA_DIR, true));
73 g_assert(g_setenv("GSETTINGS_BACKEND", "memory", true));
74 g_debug("SCHEMA_DIR is %s", SCHEMA_DIR);
75
76 auto settings = std::make_shared<LiveSettings>();
77 auto timezones = std::make_shared<LiveTimezones>(settings, TIMEZONE_FILE);
78 auto clock = std::make_shared<LiveClock>(timezones);
79 Snap snap (clock, settings);
58 snap(a, show, dismiss);80 snap(a, show, dismiss);
59 g_main_loop_run(loop);81 g_main_loop_run(loop);
82 g_main_loop_unref(loop);
60 return 0;83 return 0;
61}84}
6285
=== modified file 'tests/test-settings.cpp'
--- tests/test-settings.cpp 2014-02-02 21:29:29 +0000
+++ tests/test-settings.cpp 2014-06-27 14:06:11 +0000
@@ -100,6 +100,29 @@
100 EXPECT_EQ(str, tmp);100 EXPECT_EQ(str, tmp);
101 g_clear_pointer(&tmp, g_free);101 g_clear_pointer(&tmp, g_free);
102 }102 }
103
104 void TestIntProperty(core::Property<int>& property, const gchar* key)
105 {
106 EXPECT_EQ(g_settings_get_int(m_gsettings, key), property.get());
107
108 int expected_values[] = { 1, 2, 3 };
109
110 // modify GSettings and confirm that the new value is propagated
111 for(const int& expected_value : expected_values)
112 {
113 g_settings_set_int(m_gsettings, key, expected_value);
114 EXPECT_EQ(expected_value, property.get());
115 EXPECT_EQ(expected_value, g_settings_get_int(m_gsettings, key));
116 }
117
118 // modify the property and confirm that the new value is propagated
119 for(const int& expected_value : expected_values)
120 {
121 property.set(expected_value);
122 EXPECT_EQ(expected_value, property.get());
123 EXPECT_EQ(expected_value, g_settings_get_int(m_gsettings, key));
124 }
125 }
103};126};
104127
105/***128/***
@@ -125,10 +148,16 @@
125 TestBoolProperty(m_settings->show_year, SETTINGS_SHOW_YEAR_S);148 TestBoolProperty(m_settings->show_year, SETTINGS_SHOW_YEAR_S);
126}149}
127150
151TEST_F(SettingsFixture, IntProperties)
152{
153 TestIntProperty(m_settings->alarm_duration, SETTINGS_ALARM_DURATION_S);
154}
155
128TEST_F(SettingsFixture, StringProperties)156TEST_F(SettingsFixture, StringProperties)
129{157{
130 TestStringProperty(m_settings->custom_time_format, SETTINGS_CUSTOM_TIME_FORMAT_S);158 TestStringProperty(m_settings->custom_time_format, SETTINGS_CUSTOM_TIME_FORMAT_S);
131 TestStringProperty(m_settings->timezone_name, SETTINGS_TIMEZONE_NAME_S);159 TestStringProperty(m_settings->timezone_name, SETTINGS_TIMEZONE_NAME_S);
160 TestStringProperty(m_settings->alarm_sound, SETTINGS_ALARM_SOUND_S);
132}161}
133162
134TEST_F(SettingsFixture, TimeFormatMode)163TEST_F(SettingsFixture, TimeFormatMode)
@@ -152,6 +181,31 @@
152 }181 }
153}182}
154183
184TEST_F(SettingsFixture, AlarmVolume)
185{
186 const auto key = SETTINGS_ALARM_VOLUME_S;
187 const AlarmVolume volumes[] = { ALARM_VOLUME_VERY_QUIET,
188 ALARM_VOLUME_QUIET,
189 ALARM_VOLUME_NORMAL,
190 ALARM_VOLUME_LOUD,
191 ALARM_VOLUME_VERY_LOUD };
192
193 for(const auto& val : volumes)
194 {
195 g_settings_set_enum(m_gsettings, key, val);
196 EXPECT_EQ(val, m_settings->alarm_volume.get());
197 EXPECT_EQ(val, g_settings_get_enum(m_gsettings, key));
198 }
199
200 for(const auto& val : volumes)
201 {
202 m_settings->alarm_volume.set(val);
203 EXPECT_EQ(val, m_settings->alarm_volume.get());
204 EXPECT_EQ(val, g_settings_get_enum(m_gsettings, key));
205 }
206}
207
208
155namespace209namespace
156{210{
157 std::vector<std::string> strv_to_vector(const gchar** strv)211 std::vector<std::string> strv_to_vector(const gchar** strv)

Subscribers

People subscribed via source and target branches