aboutsummaryrefslogtreecommitdiff
path: root/src/snap.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/snap.cpp')
-rw-r--r--src/snap.cpp536
1 files changed, 353 insertions, 183 deletions
diff --git a/src/snap.cpp b/src/snap.cpp
index a087a75..45eb14e 100644
--- a/src/snap.cpp
+++ b/src/snap.cpp
@@ -21,17 +21,19 @@
#include <datetime/formatter.h>
#include <datetime/snap.h>
-#include <canberra.h>
+#include <core/signal.h>
+
+#include <gst/gst.h>
#include <libnotify/notify.h>
#include <glib/gi18n.h>
#include <glib.h>
+#include <chrono>
+#include <mutex> // std::call_once()
#include <set>
#include <string>
-#define ALARM_SOUND_FILENAME "/usr/share/sounds/ubuntu/ringtones/Suru arpeggio.ogg"
-
namespace unity {
namespace indicator {
namespace datetime {
@@ -43,266 +45,434 @@ 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& uri,
+ unsigned int volume,
+ unsigned int duration_minutes,
+ bool loop):
+ m_clock(clock),
+ m_uri(uri),
+ m_volume(volume),
+ m_loop(loop),
+ m_loop_end_time(clock->localtime().add_full(0, 0, 0, 0, (int)duration_minutes, 0.0))
{
- int rv;
-
- if ((rv = ca_context_create(&c_context)) != CA_SUCCESS)
+ // init GST once
+ static std::once_flag once;
+ std::call_once(once, [](){
+ GError* error = nullptr;
+ gst_init_check (nullptr, nullptr, &error);
+ if (error)
+ {
+ g_critical("Unable to play alarm sound: %s", error->message);
+ g_error_free(error);
+ }
+ });
+
+ if (m_loop)
{
- g_warning("Failed to create canberra context: %s\n", ca_strerror(rv));
- c_context = nullptr;
+ g_debug("Looping '%s' until cutoff time %s",
+ m_uri.c_str(),
+ m_loop_end_time.format("%F %T").c_str());
+ }
+ else
+ {
+ g_debug("Playing '%s' once", m_uri.c_str());
}
- }
-
- return c_context;
-}
-void play_alarm_sound();
+ m_play = gst_element_factory_make("playbin", "play");
-gboolean play_alarm_sound_idle (gpointer)
-{
- timeout_tag = 0;
- play_alarm_sound();
- return G_SOURCE_REMOVE;
-}
+ auto bus = gst_pipeline_get_bus(GST_PIPELINE(m_play));
+ m_watch_source = gst_bus_add_watch(bus, bus_callback, this);
+ gst_object_unref(bus);
-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);
-}
+ play();
+ }
-void play_alarm_sound()
-{
- const gchar* filename = ALARM_SOUND_FILENAME;
- auto context = get_ca_context();
- g_return_if_fail(context != nullptr);
+ ~Sound()
+ {
+ stop();
- ca_proplist* props = nullptr;
- ca_proplist_create(&props);
- ca_proplist_sets(props, CA_PROP_MEDIA_FILENAME, filename);
+ g_source_remove(m_watch_source);
- 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));
+ if (m_play != nullptr)
+ {
+ gst_element_set_state (m_play, GST_STATE_NULL);
+ g_clear_pointer (&m_play, gst_object_unref);
+ }
+ }
- g_clear_pointer(&props, ca_proplist_destroy);
-}
+private:
-void stop_alarm_sound()
-{
- auto context = get_ca_context();
- if (context != nullptr)
+ void stop()
{
- const auto rv = ca_context_cancel(context, alarm_ca_id);
- if (rv != CA_SUCCESS)
- g_warning("Failed to cancel alarm sound: %s", ca_strerror(rv));
+ if (m_play != nullptr)
+ {
+ gst_element_set_state (m_play, GST_STATE_PAUSED);
+ }
}
- if (timeout_tag != 0)
+ void play()
{
- g_source_remove(timeout_tag);
- timeout_tag = 0;
- }
-}
+ g_return_if_fail(m_play != nullptr);
-/**
-*** libnotify -- snap decisions
-**/
+ g_object_set(G_OBJECT (m_play), "uri", m_uri.c_str(),
+ "volume", get_volume(),
+ nullptr);
+ gst_element_set_state (m_play, GST_STATE_PLAYING);
+ }
-void first_time_init()
-{
- static bool inited = false;
+ // convert settings range [1..100] to gst playbin's range is [0...1.0]
+ gdouble get_volume() const
+ {
+ constexpr int in_range_lo = 1;
+ constexpr int in_range_hi = 100;
+ const double in = CLAMP(m_volume, in_range_lo, in_range_hi);
+ const double pct = (in - in_range_lo) / (in_range_hi - in_range_lo);
+
+ constexpr double out_range_lo = 0.0;
+ constexpr double out_range_hi = 1.0;
+ return out_range_lo + (pct * (out_range_hi - out_range_lo));
+ }
- if (G_UNLIKELY(!inited))
+ static gboolean bus_callback(GstBus*, GstMessage* msg, gpointer gself)
{
- inited = true;
+ auto self = static_cast<Sound*>(gself);
+
+ if ((GST_MESSAGE_TYPE(msg) == GST_MESSAGE_EOS) &&
+ (self->m_loop) &&
+ (self->m_clock->localtime() < self->m_loop_end_time))
+ {
+ gst_element_seek(self->m_play,
+ 1.0,
+ GST_FORMAT_TIME,
+ GST_SEEK_FLAG_FLUSH,
+ GST_SEEK_TYPE_SET,
+ 0,
+ GST_SEEK_TYPE_NONE,
+ (gint64)GST_CLOCK_TIME_NONE);
+ }
- if(!notify_init("indicator-datetime-service"))
- g_critical("libnotify initialization failed");
+ return G_SOURCE_CONTINUE; // keep listening
}
-}
-struct SnapData
-{
- Snap::appointment_func show;
- Snap::appointment_func dismiss;
- Appointment appointment;
+ /***
+ ****
+ ***/
+
+ const std::shared_ptr<Clock> m_clock;
+ const std::string m_uri;
+ const unsigned int m_volume;
+ const bool m_loop;
+ const DateTime m_loop_end_time;
+ guint m_watch_source = 0;
+ GstElement* m_play = nullptr;
};
-void on_snap_show(NotifyNotification*, gchar* /*action*/, gpointer gdata)
+class SoundBuilder
{
- stop_alarm_sound();
- auto data = static_cast<SnapData*>(gdata);
- data->show(data->appointment);
-}
-
-void on_snap_dismiss(NotifyNotification*, gchar* /*action*/, gpointer gdata)
-{
- 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_uri(const std::string& uri) {m_uri = uri;}
+ void set_volume(const unsigned int v) {m_volume = v;}
+ void set_duration_minutes(unsigned int i) {m_duration_minutes=i;}
+ unsigned int duration_minutes() const {return m_duration_minutes;}
+ void set_looping(bool b) {m_looping=b;}
+
+ Sound* operator()() {
+ return new Sound (m_clock,
+ m_uri,
+ 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_uri;
+ unsigned int m_volume = 50;
+ unsigned 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);
+ show();
+ }
- caps_str += (const char*) l->data;;
- if (l->next != nullptr)
- caps_str += ", ";
+ ~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);
+ }
}
- 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,
+ typedef enum
+ {
+ RESPONSE_SHOW,
+ RESPONSE_DISMISS,
+ RESPONSE_CLOSE
+ }
+ Response;
- // a snap decision popup dialog + audio
- NOTIFY_MODE_SNAP
-}
-NotifyMode;
+ core::Signal<Response>& response() { return m_response; }
-NotifyMode get_notify_mode()
-{
- static NotifyMode mode;
- static bool mode_inited = false;
+private:
- if (G_UNLIKELY(!mode_inited))
+ 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";
+
+ m_nn = notify_notification_new(title, body.c_str(), icon_name);
+ if (m_interactive)
+ {
+ const auto duration = std::chrono::minutes(m_sound_builder.duration_minutes());
+
+ notify_notification_set_hint(m_nn, HINT_SNAP,
+ g_variant_new_boolean(true));
+ notify_notification_set_hint(m_nn, HINT_TINT,
+ g_variant_new_boolean(true));
+ notify_notification_set_hint(m_nn, HINT_TIMEOUT,
+ g_variant_new_int32(std::chrono::duration_cast<std::chrono::milliseconds>(duration).count()));
+
+ /// 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);
+ }
- mode_inited = true;
+ 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 std::once_flag once;
+ std::call_once(once, [](){
+ interactive = get_server_caps().count("actions") != 0;
+ });
+
+ 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;
+
+ static constexpr char const * HINT_SNAP {"x-canonical-snap-decisions"};
+ static constexpr char const * HINT_TINT {"x-canonical-private-button-tint"};
+ static constexpr char const * HINT_TIMEOUT {"x-canonical-snap-decisions-timeout"};
+};
/**
-***
+*** libnotify -- snap decisions
**/
-void notify(const Appointment& appointment,
- Snap::appointment_func show,
- Snap::appointment_func dismiss)
+std::string get_alarm_uri(const Appointment& appointment,
+ const std::shared_ptr<const Settings>& settings)
{
- auto data = new SnapData;
- data->appointment = appointment;
- data->show = show;
- data->dismiss = dismiss;
+ const char* FALLBACK {"/usr/share/sounds/ubuntu/ringtones/Suru arpeggio.ogg"};
- switch (get_notify_mode())
+ const std::string candidates[] = { appointment.audio_url,
+ settings->alarm_sound.get(),
+ FALLBACK };
+
+ std::string uri;
+
+ for(const auto& candidate : candidates)
{
- case NOTIFY_MODE_BUBBLE:
- show_notification(data, NOTIFY_MODE_BUBBLE);
+ if (gst_uri_is_valid (candidate.c_str()))
+ {
+ uri = candidate;
break;
+ }
+ else if (g_file_test(candidate.c_str(), G_FILE_TEST_EXISTS))
+ {
+ gchar* tmp = gst_filename_to_uri(candidate.c_str(), nullptr);
+ if (tmp != nullptr)
+ {
+ uri = tmp;
+ g_free (tmp);
+ break;
+ }
+ }
+ }
- default:
- if (show_notification(data, NOTIFY_MODE_SNAP))
- play_alarm_sound();
- break;
- }
+ return uri;
}
-} // unnamed namespace
+int32_t n_existing_snaps = 0;
+} // 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();
+ if (!n_existing_snaps++ && !notify_init("indicator-datetime-service"))
+ g_critical("libnotify initialization failed");
}
Snap::~Snap()
{
- g_clear_pointer(&c_context, ca_context_destroy);
+ if (!--n_existing_snaps)
+ notify_uninit();
}
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_uri(get_alarm_uri(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);
+ });
}
/***