aboutsummaryrefslogtreecommitdiff
path: root/src/snap.cpp
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 /src/snap.cpp
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
Diffstat (limited to 'src/snap.cpp')
-rw-r--r--src/snap.cpp566
1 files changed, 386 insertions, 180 deletions
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);
+ });
}
/***