aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCharles Kerr <charles.kerr@canonical.com>2014-07-01 09:36:47 +0000
committerCI bot <ps-jenkins@lists.canonical.com>2014-07-01 09:36:47 +0000
commit6880613bbc6535242e95e7f5c65d9a9140eaa28b (patch)
treea5302d6eaa5005ec44b6498566f00e4237e697d8
parentf6778e1cc3e3881225e967c94de4685c732755db (diff)
parentea8bedf5ec63ca42de776de9f4c21343a8163578 (diff)
downloadayatana-indicator-datetime-6880613bbc6535242e95e7f5c65d9a9140eaa28b.tar.gz
ayatana-indicator-datetime-6880613bbc6535242e95e7f5c65d9a9140eaa28b.tar.bz2
ayatana-indicator-datetime-6880613bbc6535242e95e7f5c65d9a9140eaa28b.zip
Add the ability to have per-alarm custom sounds. Fixes: 1318997
-rw-r--r--data/com.canonical.indicator.datetime.gschema.xml29
-rw-r--r--include/datetime/appointment.h1
-rw-r--r--include/datetime/settings-live.h3
-rw-r--r--include/datetime/settings-shared.h13
-rw-r--r--include/datetime/settings.h3
-rw-r--r--include/datetime/snap.h9
-rw-r--r--src/engine-eds.cpp18
-rw-r--r--src/main.cpp2
-rw-r--r--src/settings-live.cpp38
-rw-r--r--src/snap.cpp566
-rw-r--r--tests/manual-test-snap.cpp31
-rw-r--r--tests/test-settings.cpp54
12 files changed, 577 insertions, 190 deletions
diff --git a/data/com.canonical.indicator.datetime.gschema.xml b/data/com.canonical.indicator.datetime.gschema.xml
index 1a5922c..3e0082d 100644
--- a/data/com.canonical.indicator.datetime.gschema.xml
+++ b/data/com.canonical.indicator.datetime.gschema.xml
@@ -5,6 +5,13 @@
<value nick="24-hour" value="2" />
<value nick="custom" value="3" />
</enum>
+ <enum id="alarm-volume-enum">
+ <value nick="very-quiet" value="0" />
+ <value nick="quiet" value="1" />
+ <value nick="normal" value="2" />
+ <value nick="loud" value="3" />
+ <value nick="very-loud" value="4" />
+ </enum>
<schema id="com.canonical.indicator.datetime" path="/com/canonical/indicator/datetime/" gettext-domain="indicator-datetime">
<key name="show-clock" type="b">
<default>true</default>
@@ -123,5 +130,27 @@
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).
</description>
</key>
+ <key name="alarm-default-sound" type="s">
+ <default>'/usr/share/sounds/ubuntu/ringtones/Suru arpeggio.ogg'</default>
+ <summary>The alarm's default sound file.</summary>
+ <description>
+ If an alarm doesn't specify its own sound file, this file will be used as the fallback sound.
+ </description>
+ </key>
+ <key name="alarm-default-volume" enum="alarm-volume-enum">
+ <default>'normal'</default>
+ <summary>The alarm's default volume level.</summary>
+ <description>
+ The volume at which alarms will be played.
+ </description>
+ </key>
+ <key name="alarm-duration-minutes" type="i">
+ <range min="1" max="60"/>
+ <default>30</default>
+ <summary>The alarm's duration.</summary>
+ <description>
+ How long the alarm's sound will be looped if its snap decision is not dismissed by the user.
+ </description>
+ </key>
</schema>
</schemalist>
diff --git a/include/datetime/appointment.h b/include/datetime/appointment.h
index 4778293..2e406a2 100644
--- a/include/datetime/appointment.h
+++ b/include/datetime/appointment.h
@@ -39,6 +39,7 @@ public:
std::string summary;
std::string url;
std::string uid;
+ std::string audio_url;
bool has_alarms = false;
DateTime begin;
DateTime end;
diff --git a/include/datetime/settings-live.h b/include/datetime/settings-live.h
index 202c998..4db2d40 100644
--- a/include/datetime/settings-live.h
+++ b/include/datetime/settings-live.h
@@ -55,6 +55,9 @@ private:
void update_show_year();
void update_time_format_mode();
void update_timezone_name();
+ void update_alarm_sound();
+ void update_alarm_volume();
+ void update_alarm_duration();
GSettings* m_settings;
diff --git a/include/datetime/settings-shared.h b/include/datetime/settings-shared.h
index 17a8ef0..bfddd88 100644
--- a/include/datetime/settings-shared.h
+++ b/include/datetime/settings-shared.h
@@ -30,6 +30,16 @@ typedef enum
}
TimeFormatMode;
+typedef enum
+{
+ ALARM_VOLUME_VERY_QUIET,
+ ALARM_VOLUME_QUIET,
+ ALARM_VOLUME_NORMAL,
+ ALARM_VOLUME_LOUD,
+ ALARM_VOLUME_VERY_LOUD
+}
+AlarmVolume;
+
#define SETTINGS_INTERFACE "com.canonical.indicator.datetime"
#define SETTINGS_SHOW_CLOCK_S "show-clock"
#define SETTINGS_TIME_FORMAT_S "time-format"
@@ -45,5 +55,8 @@ TimeFormatMode;
#define SETTINGS_SHOW_DETECTED_S "show-auto-detected-location"
#define SETTINGS_LOCATIONS_S "locations"
#define SETTINGS_TIMEZONE_NAME_S "timezone-name"
+#define SETTINGS_ALARM_SOUND_S "alarm-default-sound"
+#define SETTINGS_ALARM_VOLUME_S "alarm-default-volume"
+#define SETTINGS_ALARM_DURATION_S "alarm-duration-minutes"
#endif // INDICATOR_DATETIME_SETTINGS_SHARED
diff --git a/include/datetime/settings.h b/include/datetime/settings.h
index ce234d9..a941f05 100644
--- a/include/datetime/settings.h
+++ b/include/datetime/settings.h
@@ -56,6 +56,9 @@ public:
core::Property<bool> show_year;
core::Property<TimeFormatMode> time_format_mode;
core::Property<std::string> timezone_name;
+ core::Property<std::string> alarm_sound;
+ core::Property<AlarmVolume> alarm_volume;
+ core::Property<int> alarm_duration;
};
} // namespace datetime
diff --git a/include/datetime/snap.h b/include/datetime/snap.h
index a493772..9b45b3f 100644
--- a/include/datetime/snap.h
+++ b/include/datetime/snap.h
@@ -21,6 +21,8 @@
#define INDICATOR_DATETIME_SNAP_H
#include <datetime/appointment.h>
+#include <datetime/clock.h>
+#include <datetime/settings.h>
#include <memory>
#include <functional>
@@ -35,13 +37,18 @@ namespace datetime {
class Snap
{
public:
- Snap();
+ Snap(const std::shared_ptr<Clock>& clock,
+ const std::shared_ptr<const Settings>& settings);
virtual ~Snap();
typedef std::function<void(const Appointment&)> appointment_func;
void operator()(const Appointment& appointment,
appointment_func show,
appointment_func dismiss);
+
+private:
+ const std::shared_ptr<Clock> m_clock;
+ const std::shared_ptr<const Settings> m_settings;
};
} // namespace datetime
diff --git a/src/engine-eds.cpp b/src/engine-eds.cpp
index 1949193..80a47da 100644
--- a/src/engine-eds.cpp
+++ b/src/engine-eds.cpp
@@ -443,8 +443,9 @@ private:
appointment.color = subtask->color;
appointment.uid = uid;
- // if the component has display alarms that have a url,
- // use the first one as our Appointment.url
+ // Look through all of this component's alarms
+ // for DISPLAY or AUDIO url attachments.
+ // If we find any, use them for appointment.url and audio_sound
auto alarm_uids = e_cal_component_get_alarm_uids(component);
appointment.has_alarms = alarm_uids != nullptr;
for(auto walk=alarm_uids; appointment.url.empty() && walk!=nullptr; walk=walk->next)
@@ -453,7 +454,7 @@ private:
ECalComponentAlarmAction action;
e_cal_component_alarm_get_action(alarm, &action);
- if (action == E_CAL_COMPONENT_ALARM_DISPLAY)
+ if ((action == E_CAL_COMPONENT_ALARM_DISPLAY) || (action == E_CAL_COMPONENT_ALARM_AUDIO))
{
icalattach* attach = nullptr;
e_cal_component_alarm_get_attach(alarm, &attach);
@@ -463,7 +464,16 @@ private:
{
const char* url = icalattach_get_url(attach);
if (url != nullptr)
- appointment.url = url;
+ {
+ if ((action == E_CAL_COMPONENT_ALARM_DISPLAY) && appointment.url.empty())
+ {
+ appointment.url = url;
+ }
+ else if ((action == E_CAL_COMPONENT_ALARM_AUDIO) && appointment.audio_url.empty())
+ {
+ appointment.audio_url = url;
+ }
+ }
}
icalattach_unref(attach);
diff --git a/src/main.cpp b/src/main.cpp
index 079fe35..1940eb6 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -141,7 +141,7 @@ main(int /*argc*/, char** /*argv*/)
MenuFactory factory(actions, state);
// set up the snap decisions
- Snap snap;
+ Snap snap (state->clock, state->settings);
auto alarm_queue = create_simple_alarm_queue(state->clock, engine, timezone);
alarm_queue->alarm_reached().connect([&snap](const Appointment& appt){
auto snap_show = [](const Appointment& a){
diff --git a/src/settings-live.cpp b/src/settings-live.cpp
index 2305c93..e34ace1 100644
--- a/src/settings-live.cpp
+++ b/src/settings-live.cpp
@@ -52,6 +52,9 @@ LiveSettings::LiveSettings():
update_show_year();
update_time_format_mode();
update_timezone_name();
+ update_alarm_sound();
+ update_alarm_volume();
+ update_alarm_duration();
// now listen for clients to change the properties s.t. we can sync update GSettings
@@ -115,6 +118,18 @@ LiveSettings::LiveSettings():
timezone_name.changed().connect([this](const std::string& value){
g_settings_set_string(m_settings, SETTINGS_TIMEZONE_NAME_S, value.c_str());
});
+
+ alarm_sound.changed().connect([this](const std::string& value){
+ g_settings_set_string(m_settings, SETTINGS_ALARM_SOUND_S, value.c_str());
+ });
+
+ alarm_volume.changed().connect([this](AlarmVolume value){
+ g_settings_set_enum(m_settings, SETTINGS_ALARM_VOLUME_S, gint(value));
+ });
+
+ alarm_duration.changed().connect([this](int value){
+ g_settings_set_int(m_settings, SETTINGS_ALARM_DURATION_S, value);
+ });
}
/***
@@ -205,6 +220,23 @@ void LiveSettings::update_timezone_name()
g_free(val);
}
+void LiveSettings::update_alarm_sound()
+{
+ auto val = g_settings_get_string(m_settings, SETTINGS_ALARM_SOUND_S);
+ alarm_sound.set(val);
+ g_free(val);
+}
+
+void LiveSettings::update_alarm_volume()
+{
+ alarm_volume.set((AlarmVolume)g_settings_get_enum(m_settings, SETTINGS_ALARM_VOLUME_S));
+}
+
+void LiveSettings::update_alarm_duration()
+{
+ alarm_duration.set(g_settings_get_int(m_settings, SETTINGS_ALARM_DURATION_S));
+}
+
/***
****
***/
@@ -246,6 +278,12 @@ void LiveSettings::update_key(const std::string& key)
update_show_detected_locations();
else if (key == SETTINGS_TIMEZONE_NAME_S)
update_timezone_name();
+ else if (key == SETTINGS_ALARM_SOUND_S)
+ update_alarm_sound();
+ else if (key == SETTINGS_ALARM_VOLUME_S)
+ update_alarm_volume();
+ else if (key == SETTINGS_ALARM_DURATION_S)
+ update_alarm_duration();
}
/***
diff --git a/src/snap.cpp b/src/snap.cpp
index a087a75..25632e9 100644
--- a/src/snap.cpp
+++ b/src/snap.cpp
@@ -21,6 +21,8 @@
#include <datetime/formatter.h>
#include <datetime/snap.h>
+#include <core/signal.h>
+
#include <canberra.h>
#include <libnotify/notify.h>
@@ -30,8 +32,6 @@
#include <set>
#include <string>
-#define ALARM_SOUND_FILENAME "/usr/share/sounds/ubuntu/ringtones/Suru arpeggio.ogg"
-
namespace unity {
namespace indicator {
namespace datetime {
@@ -43,266 +43,472 @@ namespace datetime {
namespace
{
-/**
-*** libcanberra -- play sounds
-**/
-
-// arbitrary number, but we need a consistent id for play/cancel
-const int32_t alarm_ca_id = 1;
-
-ca_context *c_context = nullptr;
-guint timeout_tag = 0;
-
-ca_context* get_ca_context()
+/**
+ * Plays a sound, possibly looping.
+ */
+class Sound
{
- if (G_UNLIKELY(c_context == nullptr))
+ typedef Sound Self;
+
+public:
+
+ Sound(const std::shared_ptr<Clock>& clock,
+ const std::string& filename,
+ AlarmVolume volume,
+ int duration_minutes,
+ bool loop):
+ m_clock(clock),
+ m_filename(filename),
+ m_volume(volume),
+ m_duration_minutes(duration_minutes),
+ m_loop(loop),
+ m_canberra_id(get_next_canberra_id()),
+ m_loop_end_time(clock->localtime().add_full(0, 0, 0, 0, duration_minutes, 0.0))
{
- int rv;
+ if (m_loop)
+ {
+ g_debug("Looping '%s' until cutoff time %s",
+ m_filename.c_str(),
+ m_loop_end_time.format("%F %T").c_str());
+ }
+ else
+ {
+ g_debug("Playing '%s' once", m_filename.c_str());
+ }
- if ((rv = ca_context_create(&c_context)) != CA_SUCCESS)
+ const auto rv = ca_context_create(&m_context);
+ if (rv == CA_SUCCESS)
{
- g_warning("Failed to create canberra context: %s\n", ca_strerror(rv));
- c_context = nullptr;
+ play();
+ }
+ else
+ {
+ g_warning("Failed to create canberra context: %s", ca_strerror(rv));
+ m_context = nullptr;
}
}
- return c_context;
-}
-
-void play_alarm_sound();
-
-gboolean play_alarm_sound_idle (gpointer)
-{
- timeout_tag = 0;
- play_alarm_sound();
- return G_SOURCE_REMOVE;
-}
-
-void on_alarm_play_done (ca_context* /*context*/, uint32_t /*id*/, int rv, void* /*user_data*/)
-{
- // wait one second, then play it again
- if ((rv == CA_SUCCESS) && (timeout_tag == 0))
- timeout_tag = g_timeout_add_seconds (1, play_alarm_sound_idle, nullptr);
-}
+ ~Sound()
+ {
+ stop();
-void play_alarm_sound()
-{
- const gchar* filename = ALARM_SOUND_FILENAME;
- auto context = get_ca_context();
- g_return_if_fail(context != nullptr);
+ g_clear_pointer(&m_context, ca_context_destroy);
+ }
- ca_proplist* props = nullptr;
- ca_proplist_create(&props);
- ca_proplist_sets(props, CA_PROP_MEDIA_FILENAME, filename);
+private:
- const auto rv = ca_context_play_full(context, alarm_ca_id, props, on_alarm_play_done, nullptr);
- if (rv != CA_SUCCESS)
- g_warning("Failed to play file '%s': %s", filename, ca_strerror(rv));
+ void stop()
+ {
+ if (m_context != nullptr)
+ {
+ const auto rv = ca_context_cancel(m_context, m_canberra_id);
+ if (rv != CA_SUCCESS)
+ g_warning("Failed to cancel alarm sound: %s", ca_strerror(rv));
+ }
- g_clear_pointer(&props, ca_proplist_destroy);
-}
+ if (m_loop_tag != 0)
+ {
+ g_source_remove(m_loop_tag);
+ m_loop_tag = 0;
+ }
+ }
-void stop_alarm_sound()
-{
- auto context = get_ca_context();
- if (context != nullptr)
+ void play()
{
- const auto rv = ca_context_cancel(context, alarm_ca_id);
+ auto context = m_context;
+ g_return_if_fail(context != nullptr);
+
+ const auto filename = m_filename.c_str();
+ const float gain = get_gain_level(m_volume);
+
+ ca_proplist* props = nullptr;
+ ca_proplist_create(&props);
+ ca_proplist_sets(props, CA_PROP_MEDIA_FILENAME, filename);
+ ca_proplist_setf(props, CA_PROP_CANBERRA_VOLUME, "%f", gain);
+ const auto rv = ca_context_play_full(context, m_canberra_id, props,
+ on_done_playing, this);
if (rv != CA_SUCCESS)
- g_warning("Failed to cancel alarm sound: %s", ca_strerror(rv));
+ g_warning("Unable to play '%s': %s", filename, ca_strerror(rv));
+
+ g_clear_pointer(&props, ca_proplist_destroy);
}
- if (timeout_tag != 0)
+ static float get_gain_level(const AlarmVolume volume)
{
- g_source_remove(timeout_tag);
- timeout_tag = 0;
+ /* These values aren't set in stone --
+ arrived at from from manual tests on Nexus 4 */
+ switch (volume)
+ {
+ case ALARM_VOLUME_VERY_QUIET: return -8;
+ case ALARM_VOLUME_QUIET: return -4;
+ case ALARM_VOLUME_LOUD: return 4;
+ case ALARM_VOLUME_VERY_LOUD: return 8;
+ default: return 0;
+ }
}
-}
-/**
-*** libnotify -- snap decisions
-**/
+ static void on_done_playing(ca_context*, uint32_t, int rv, void* gself)
+ {
+ // if we still need to loop, wait a second, then play it again
-void first_time_init()
-{
- static bool inited = false;
+ if (rv == CA_SUCCESS)
+ {
+ auto self = static_cast<Self*>(gself);
+ if ((self->m_loop_tag == 0) &&
+ (self->m_loop) &&
+ (self->m_clock->localtime() < self->m_loop_end_time))
+ {
+ self->m_loop_tag = g_timeout_add_seconds(1, play_idle, self);
+ }
+ }
+ }
- if (G_UNLIKELY(!inited))
+ static gboolean play_idle(gpointer gself)
{
- inited = true;
+ auto self = static_cast<Self*>(gself);
+ self->m_loop_tag = 0;
+ self->play();
+ return G_SOURCE_REMOVE;
+ }
+
+ /***
+ ****
+ ***/
- if(!notify_init("indicator-datetime-service"))
- g_critical("libnotify initialization failed");
+ static int32_t get_next_canberra_id()
+ {
+ static int32_t next_canberra_id = 1;
+ return next_canberra_id++;
}
-}
-struct SnapData
-{
- Snap::appointment_func show;
- Snap::appointment_func dismiss;
- Appointment appointment;
+ const std::shared_ptr<Clock> m_clock;
+ const std::string m_filename;
+ const AlarmVolume m_volume;
+ const int m_duration_minutes;
+ const bool m_loop;
+ const int32_t m_canberra_id;
+ const DateTime m_loop_end_time;
+ ca_context* m_context = nullptr;
+ guint m_loop_tag = 0;
};
-void on_snap_show(NotifyNotification*, gchar* /*action*/, gpointer gdata)
-{
- stop_alarm_sound();
- auto data = static_cast<SnapData*>(gdata);
- data->show(data->appointment);
-}
-
-void on_snap_dismiss(NotifyNotification*, gchar* /*action*/, gpointer gdata)
+class SoundBuilder
{
- stop_alarm_sound();
- auto data = static_cast<SnapData*>(gdata);
- data->dismiss(data->appointment);
-}
+public:
+ void set_clock(const std::shared_ptr<Clock>& c) {m_clock = c;}
+ void set_filename(const std::string& s) {m_filename = s;}
+ void set_volume(const AlarmVolume v) {m_volume = v;}
+ void set_duration_minutes(int i) {m_duration_minutes=i;}
+ void set_looping(bool b) {m_looping=b;}
+
+ Sound* operator()() {
+ return new Sound (m_clock,
+ m_filename,
+ m_volume,
+ m_duration_minutes,
+ m_looping);
+ }
-void on_snap_closed(NotifyNotification*, gpointer)
-{
- stop_alarm_sound();
-}
+private:
+ std::shared_ptr<Clock> m_clock;
+ std::string m_filename;
+ AlarmVolume m_volume = ALARM_VOLUME_NORMAL;
+ int m_duration_minutes = 30;
+ bool m_looping = true;
+};
-void snap_data_destroy_notify(gpointer gdata)
+/**
+ * A popup notification (with optional sound)
+ * that emits a Response signal when done.
+ */
+class Popup
{
- delete static_cast<SnapData*>(gdata);
-}
+public:
-std::set<std::string> get_server_caps()
-{
- std::set<std::string> caps_set;
- auto caps_gl = notify_get_server_caps();
- std::string caps_str;
- for(auto l=caps_gl; l!=nullptr; l=l->next)
+ Popup(const Appointment& appointment, const SoundBuilder& sound_builder):
+ m_appointment(appointment),
+ m_interactive(get_interactive()),
+ m_sound_builder(sound_builder)
{
- caps_set.insert((const char*)l->data);
+ // ensure notify_init() is called once
+ // before we start popping up dialogs
+ static bool m_nn_inited = false;
+ if (G_UNLIKELY(!m_nn_inited))
+ {
+ if(!notify_init("indicator-datetime-service"))
+ g_critical("libnotify initialization failed");
+
+ m_nn_inited = true;
+ }
- caps_str += (const char*) l->data;;
- if (l->next != nullptr)
- caps_str += ", ";
+ show();
}
- g_debug ("%s notify_get_server() returned [%s]", G_STRFUNC, caps_str.c_str());
- g_list_free_full(caps_gl, g_free);
- return caps_set;
-}
-typedef enum
-{
- // just a bubble... no actions, no audio
- NOTIFY_MODE_BUBBLE,
+ ~Popup()
+ {
+ if (m_nn != nullptr)
+ {
+ notify_notification_clear_actions(m_nn);
+ g_signal_handlers_disconnect_by_data(m_nn, this);
+ g_clear_object(&m_nn);
+ }
+ }
- // a snap decision popup dialog + audio
- NOTIFY_MODE_SNAP
-}
-NotifyMode;
+ typedef enum
+ {
+ RESPONSE_SHOW,
+ RESPONSE_DISMISS,
+ RESPONSE_CLOSE
+ }
+ Response;
-NotifyMode get_notify_mode()
-{
- static NotifyMode mode;
- static bool mode_inited = false;
+ core::Signal<Response>& response() { return m_response; }
- if (G_UNLIKELY(!mode_inited))
+private:
+
+ void show()
{
- const auto caps = get_server_caps();
+ const Appointment& appointment = m_appointment;
- if (caps.count("actions"))
- mode = NOTIFY_MODE_SNAP;
- else
- mode = NOTIFY_MODE_BUBBLE;
+ /// strftime(3) format string for an alarm's snap decision
+ const auto timestr = appointment.begin.format(_("%a, %X"));
+ auto title = g_strdup_printf(_("Alarm %s"), timestr.c_str());
+ const auto body = appointment.summary;
+ const gchar* icon_name = "alarm-clock";
- mode_inited = true;
+ m_nn = notify_notification_new(title, body.c_str(), icon_name);
+ if (m_interactive)
+ {
+ notify_notification_set_hint_string(m_nn,
+ "x-canonical-snap-decisions",
+ "true");
+ notify_notification_set_hint_string(m_nn,
+ "x-canonical-private-button-tint",
+ "true");
+ /// alarm popup dialog's button to show the active alarm
+ notify_notification_add_action(m_nn, "show", _("Show"),
+ on_snap_show, this, nullptr);
+ /// alarm popup dialog's button to shut up the alarm
+ notify_notification_add_action(m_nn, "dismiss", _("Dismiss"),
+ on_snap_dismiss, this, nullptr);
+ g_signal_connect(m_nn, "closed", G_CALLBACK(on_snap_closed), this);
+ }
+
+ bool shown = true;
+ GError* error = nullptr;
+ notify_notification_show(m_nn, &error);
+ if (error != NULL)
+ {
+ g_critical("Unable to show snap decision for '%s': %s",
+ body.c_str(), error->message);
+ g_error_free(error);
+ shown = false;
+ }
+
+ // Loop the sound *only* if we're prompting the user for a response.
+ // Otherwise, just play the sound once.
+ m_sound_builder.set_looping (shown && m_interactive);
+ m_sound.reset (m_sound_builder());
+
+ // if showing the notification didn't work,
+ // treat it as if the user clicked the 'show' button
+ if (!shown)
+ {
+ on_snap_show(nullptr, nullptr, this);
+ on_snap_dismiss(nullptr, nullptr, this);
+ }
+
+ g_free(title);
}
- return mode;
-}
+ // user clicked 'show'
+ static void on_snap_show(NotifyNotification*, gchar*, gpointer gself)
+ {
+ auto self = static_cast<Self*>(gself);
+ self->m_response_value = RESPONSE_SHOW;
+ self->m_sound.reset();
+ }
-bool show_notification (SnapData* data, NotifyMode mode)
-{
- const Appointment& appointment = data->appointment;
+ // user clicked 'dismiss'
+ static void on_snap_dismiss(NotifyNotification*, gchar*, gpointer gself)
+ {
+ auto self = static_cast<Self*>(gself);
+ self->m_response_value = RESPONSE_DISMISS;
+ self->m_sound.reset();
+ }
- const auto timestr = appointment.begin.format("%a, %X");
- auto title = g_strdup_printf(_("Alarm %s"), timestr.c_str());
- const auto body = appointment.summary;
- const gchar* icon_name = "alarm-clock";
+ // the popup was closed
+ static void on_snap_closed(NotifyNotification*, gpointer gself)
+ {
+ auto self = static_cast<Self*>(gself);
+ self->m_sound.reset();
+ self->m_response(self->m_response_value);
+ }
- auto nn = notify_notification_new(title, body.c_str(), icon_name);
- if (mode == NOTIFY_MODE_SNAP)
+ /***
+ **** Interactive
+ ***/
+
+ static std::set<std::string> get_server_caps()
{
- notify_notification_set_hint_string(nn, "x-canonical-snap-decisions", "true");
- notify_notification_set_hint_string(nn, "x-canonical-private-button-tint", "true");
- /* text for the alarm popup dialog's button to show the active alarm */
- notify_notification_add_action(nn, "show", _("Show"), on_snap_show, data, nullptr);
- /* text for the alarm popup dialog's button to shut up the alarm */
- notify_notification_add_action(nn, "dismiss", _("Dismiss"), on_snap_dismiss, data, nullptr);
- g_signal_connect(G_OBJECT(nn), "closed", G_CALLBACK(on_snap_closed), data);
+ std::set<std::string> caps_set;
+ auto caps_gl = notify_get_server_caps();
+ std::string caps_str;
+ for(auto l=caps_gl; l!=nullptr; l=l->next)
+ {
+ caps_set.insert((const char*)l->data);
+
+ caps_str += (const char*) l->data;;
+ if (l->next != nullptr)
+ caps_str += ", ";
+ }
+ g_debug("%s notify_get_server() returned [%s]", G_STRFUNC, caps_str.c_str());
+ g_list_free_full(caps_gl, g_free);
+ return caps_set;
}
- g_object_set_data_full(G_OBJECT(nn), "snap-data", data, snap_data_destroy_notify);
- bool shown = true;
- GError * error = nullptr;
- notify_notification_show(nn, &error);
- if (error != NULL)
+ static bool get_interactive()
{
- g_critical("Unable to show snap decision for '%s': %s", body.c_str(), error->message);
- g_error_free(error);
- data->show(data->appointment);
- shown = false;
+ static bool interactive;
+ static bool inited = false;
+
+ if (G_UNLIKELY(!inited))
+ {
+ interactive = get_server_caps().count("actions") != 0;
+ inited = true;
+ }
+
+ return interactive;
}
- g_free(title);
- return shown;
-}
+ /***
+ ****
+ ***/
+
+ typedef Popup Self;
+
+ const Appointment m_appointment;
+ const bool m_interactive;
+ SoundBuilder m_sound_builder;
+ std::unique_ptr<Sound> m_sound;
+ core::Signal<Response> m_response;
+ Response m_response_value = RESPONSE_CLOSE;
+ NotifyNotification* m_nn = nullptr;
+};
/**
-***
+*** libnotify -- snap decisions
**/
-void notify(const Appointment& appointment,
- Snap::appointment_func show,
- Snap::appointment_func dismiss)
+std::string get_local_filename (const std::string& str)
{
- auto data = new SnapData;
- data->appointment = appointment;
- data->show = show;
- data->dismiss = dismiss;
+ std::string ret;
- switch (get_notify_mode())
+ if (!str.empty())
{
- case NOTIFY_MODE_BUBBLE:
- show_notification(data, NOTIFY_MODE_BUBBLE);
- break;
+ GFile* files[] = { g_file_new_for_path(str.c_str()),
+ g_file_new_for_uri(str.c_str()) };
+
+ for(auto& file : files)
+ {
+ if (g_file_is_native(file) && g_file_query_exists(file, nullptr))
+ {
+ char* tmp = g_file_get_path(file);
+ if (tmp != nullptr)
+ {
+ ret = tmp;
+ g_free(tmp);
+ break;
+ }
+ }
+ }
+
+ for(auto& file : files)
+ g_object_unref(file);
+ }
+
+ return ret;
+}
+
+std::string get_alarm_sound(const Appointment& appointment,
+ const std::shared_ptr<const Settings>& settings)
+{
+ const char* FALLBACK {"/usr/share/sounds/ubuntu/ringtones/Suru arpeggio.ogg"};
+
+ const std::string candidates[] = { appointment.audio_url,
+ settings->alarm_sound.get(),
+ FALLBACK };
+
+ std::string alarm_sound;
+
+ for(const auto& candidate : candidates)
+ {
+ alarm_sound = get_local_filename(candidate);
- default:
- if (show_notification(data, NOTIFY_MODE_SNAP))
- play_alarm_sound();
+ if (!alarm_sound.empty())
break;
- }
+ }
+
+ g_debug("%s: Appointment \"%s\" using alarm sound \"%s\"",
+ G_STRFUNC, appointment.summary.c_str(), alarm_sound.c_str());
+
+ return alarm_sound;
}
} // unnamed namespace
-
/***
****
***/
-Snap::Snap()
+Snap::Snap(const std::shared_ptr<Clock>& clock,
+ const std::shared_ptr<const Settings>& settings):
+ m_clock(clock),
+ m_settings(settings)
{
- first_time_init();
}
Snap::~Snap()
{
- g_clear_pointer(&c_context, ca_context_destroy);
}
void Snap::operator()(const Appointment& appointment,
appointment_func show,
appointment_func dismiss)
{
- if (appointment.has_alarms)
- notify(appointment, show, dismiss);
- else
+ if (!appointment.has_alarms)
+ {
dismiss(appointment);
+ return;
+ }
+
+ // create a popup...
+ SoundBuilder sound_builder;
+ sound_builder.set_filename(get_alarm_sound(appointment, m_settings));
+ sound_builder.set_volume(m_settings->alarm_volume.get());
+ sound_builder.set_clock(m_clock);
+ sound_builder.set_duration_minutes(m_settings->alarm_duration.get());
+ auto popup = new Popup(appointment, sound_builder);
+
+ // listen for it to finish...
+ popup->response().connect([appointment,
+ show,
+ dismiss,
+ popup](Popup::Response response){
+
+ // we can't delete the Popup inside its response() signal handler,
+ // so push that to an idle func
+ g_idle_add([](gpointer gdata){
+ delete static_cast<Popup*>(gdata);
+ return G_SOURCE_REMOVE;
+ }, popup);
+
+ // maybe notify the client code that the popup's done
+ if (response == Popup::RESPONSE_SHOW)
+ show(appointment);
+ else if (response == Popup::RESPONSE_DISMISS)
+ dismiss(appointment);
+ });
}
/***
diff --git a/tests/manual-test-snap.cpp b/tests/manual-test-snap.cpp
index 16e606a..90dbe08 100644
--- a/tests/manual-test-snap.cpp
+++ b/tests/manual-test-snap.cpp
@@ -19,16 +19,30 @@
*/
#include <datetime/appointment.h>
+#include <datetime/settings-live.h>
#include <datetime/snap.h>
+#include <datetime/timezones-live.h>
#include <glib.h>
using namespace unity::indicator::datetime;
+#define TIMEZONE_FILE ("/etc/timezone")
+
+
/***
****
***/
+namespace
+{
+ gboolean quit_idle (gpointer gloop)
+ {
+ g_main_loop_quit(static_cast<GMainLoop*>(gloop));
+ return G_SOURCE_REMOVE;
+ };
+}
+
int main()
{
Appointment a;
@@ -47,15 +61,24 @@ int main()
auto loop = g_main_loop_new(nullptr, false);
auto show = [loop](const Appointment& appt){
g_message("You clicked 'show' for appt url '%s'", appt.url.c_str());
- g_main_loop_quit(loop);
+ g_idle_add(quit_idle, loop);
};
auto dismiss = [loop](const Appointment&){
g_message("You clicked 'dismiss'");
- g_main_loop_quit(loop);
+ g_idle_add(quit_idle, loop);
};
-
- Snap snap;
+
+ // only use local, temporary settings
+ g_assert(g_setenv("GSETTINGS_SCHEMA_DIR", SCHEMA_DIR, true));
+ g_assert(g_setenv("GSETTINGS_BACKEND", "memory", true));
+ g_debug("SCHEMA_DIR is %s", SCHEMA_DIR);
+
+ auto settings = std::make_shared<LiveSettings>();
+ auto timezones = std::make_shared<LiveTimezones>(settings, TIMEZONE_FILE);
+ auto clock = std::make_shared<LiveClock>(timezones);
+ Snap snap (clock, settings);
snap(a, show, dismiss);
g_main_loop_run(loop);
+ g_main_loop_unref(loop);
return 0;
}
diff --git a/tests/test-settings.cpp b/tests/test-settings.cpp
index 707247d..2b500b2 100644
--- a/tests/test-settings.cpp
+++ b/tests/test-settings.cpp
@@ -100,6 +100,29 @@ protected:
EXPECT_EQ(str, tmp);
g_clear_pointer(&tmp, g_free);
}
+
+ void TestIntProperty(core::Property<int>& property, const gchar* key)
+ {
+ EXPECT_EQ(g_settings_get_int(m_gsettings, key), property.get());
+
+ int expected_values[] = { 1, 2, 3 };
+
+ // modify GSettings and confirm that the new value is propagated
+ for(const int& expected_value : expected_values)
+ {
+ g_settings_set_int(m_gsettings, key, expected_value);
+ EXPECT_EQ(expected_value, property.get());
+ EXPECT_EQ(expected_value, g_settings_get_int(m_gsettings, key));
+ }
+
+ // modify the property and confirm that the new value is propagated
+ for(const int& expected_value : expected_values)
+ {
+ property.set(expected_value);
+ EXPECT_EQ(expected_value, property.get());
+ EXPECT_EQ(expected_value, g_settings_get_int(m_gsettings, key));
+ }
+ }
};
/***
@@ -125,10 +148,16 @@ TEST_F(SettingsFixture, BoolProperties)
TestBoolProperty(m_settings->show_year, SETTINGS_SHOW_YEAR_S);
}
+TEST_F(SettingsFixture, IntProperties)
+{
+ TestIntProperty(m_settings->alarm_duration, SETTINGS_ALARM_DURATION_S);
+}
+
TEST_F(SettingsFixture, StringProperties)
{
TestStringProperty(m_settings->custom_time_format, SETTINGS_CUSTOM_TIME_FORMAT_S);
TestStringProperty(m_settings->timezone_name, SETTINGS_TIMEZONE_NAME_S);
+ TestStringProperty(m_settings->alarm_sound, SETTINGS_ALARM_SOUND_S);
}
TEST_F(SettingsFixture, TimeFormatMode)
@@ -152,6 +181,31 @@ TEST_F(SettingsFixture, TimeFormatMode)
}
}
+TEST_F(SettingsFixture, AlarmVolume)
+{
+ const auto key = SETTINGS_ALARM_VOLUME_S;
+ const AlarmVolume volumes[] = { ALARM_VOLUME_VERY_QUIET,
+ ALARM_VOLUME_QUIET,
+ ALARM_VOLUME_NORMAL,
+ ALARM_VOLUME_LOUD,
+ ALARM_VOLUME_VERY_LOUD };
+
+ for(const auto& val : volumes)
+ {
+ g_settings_set_enum(m_gsettings, key, val);
+ EXPECT_EQ(val, m_settings->alarm_volume.get());
+ EXPECT_EQ(val, g_settings_get_enum(m_gsettings, key));
+ }
+
+ for(const auto& val : volumes)
+ {
+ m_settings->alarm_volume.set(val);
+ EXPECT_EQ(val, m_settings->alarm_volume.get());
+ EXPECT_EQ(val, g_settings_get_enum(m_gsettings, key));
+ }
+}
+
+
namespace
{
std::vector<std::string> strv_to_vector(const gchar** strv)