aboutsummaryrefslogtreecommitdiff
path: root/src/snap.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/snap.cpp')
-rw-r--r--src/snap.cpp328
1 files changed, 328 insertions, 0 deletions
diff --git a/src/snap.cpp b/src/snap.cpp
new file mode 100644
index 0000000..3ab2941
--- /dev/null
+++ b/src/snap.cpp
@@ -0,0 +1,328 @@
+/*
+ * Copyright 2014 Canonical Ltd.
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 3, as published
+ * by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranties of
+ * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
+ * PURPOSE. See the GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors:
+ * Charles Kerr <charles.kerr@canonical.com>
+ */
+
+#include <datetime/appointment.h>
+#include <datetime/formatter.h>
+#include <datetime/snap.h>
+
+#include <canberra.h>
+#include <libnotify/notify.h>
+
+#include <glib/gi18n.h>
+#include <glib.h>
+
+#include <set>
+#include <string>
+
+#define ALARM_SOUND_FILENAME "/usr/share/sounds/ubuntu/stereo/phone-incoming-call.ogg"
+
+namespace unity {
+namespace indicator {
+namespace datetime {
+
+/***
+****
+***/
+
+namespace
+{
+
+/**
+*** libcanberra -- play sounds
+**/
+
+// arbitrary number, but we need a consistent id for play/cancel
+const int32_t alarm_ca_id = 1;
+
+gboolean media_cached = FALSE;
+ca_context *c_context = nullptr;
+guint timeout_tag = 0;
+
+ca_context* get_ca_context()
+{
+ if (G_UNLIKELY(c_context == nullptr))
+ {
+ int rv;
+
+ if ((rv = ca_context_create(&c_context)) != CA_SUCCESS)
+ {
+ g_warning("Failed to create canberra context: %s\n", ca_strerror(rv));
+ c_context = nullptr;
+ }
+ else
+ {
+ const char* filename = ALARM_SOUND_FILENAME;
+ rv = ca_context_cache(c_context,
+ CA_PROP_EVENT_ID, "alarm",
+ CA_PROP_MEDIA_FILENAME, filename,
+ CA_PROP_CANBERRA_CACHE_CONTROL, "permanent",
+ NULL);
+ media_cached = rv == CA_SUCCESS;
+ if (rv != CA_SUCCESS)
+ g_warning("Couldn't add '%s' to canberra cache: %s", filename, ca_strerror(rv));
+ }
+ }
+
+ 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);
+}
+
+void play_alarm_sound()
+{
+ const gchar* filename = ALARM_SOUND_FILENAME;
+ auto context = get_ca_context();
+ g_return_if_fail(context != nullptr);
+
+ ca_proplist* props = nullptr;
+ ca_proplist_create(&props);
+ if (media_cached)
+ ca_proplist_sets(props, CA_PROP_EVENT_ID, "alarm");
+ ca_proplist_sets(props, CA_PROP_MEDIA_FILENAME, filename);
+
+ 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));
+
+ g_clear_pointer(&props, ca_proplist_destroy);
+}
+
+void stop_alarm_sound()
+{
+ auto context = get_ca_context();
+ if (context != nullptr)
+ {
+ 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 (timeout_tag != 0)
+ {
+ g_source_remove(timeout_tag);
+ timeout_tag = 0;
+ }
+}
+
+/**
+*** libnotify -- snap decisions
+**/
+
+void first_time_init()
+{
+ static bool inited = false;
+
+ if (G_UNLIKELY(!inited))
+ {
+ inited = true;
+
+ if(!notify_init("indicator-datetime-service"))
+ g_critical("libnotify initialization failed");
+ }
+}
+
+struct SnapData
+{
+ Snap::appointment_func show;
+ Snap::appointment_func dismiss;
+ Appointment appointment;
+};
+
+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)
+{
+ stop_alarm_sound();
+ auto data = static_cast<SnapData*>(gdata);
+ data->dismiss(data->appointment);
+}
+
+void on_snap_closed(NotifyNotification*, gpointer)
+{
+ stop_alarm_sound();
+}
+
+void snap_data_destroy_notify(gpointer gdata)
+{
+ delete static_cast<SnapData*>(gdata);
+}
+
+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)
+ {
+ 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;
+}
+
+typedef enum
+{
+ // just a bubble... no actions, no audio
+ NOTIFY_MODE_BUBBLE,
+
+ // a snap decision popup dialog + audio
+ NOTIFY_MODE_SNAP
+}
+NotifyMode;
+
+NotifyMode get_notify_mode()
+{
+ static NotifyMode mode;
+ static bool mode_inited = false;
+
+ if (G_UNLIKELY(!mode_inited))
+ {
+ const auto caps = get_server_caps();
+
+ if (caps.count("actions"))
+ mode = NOTIFY_MODE_SNAP;
+ else
+ mode = NOTIFY_MODE_BUBBLE;
+
+ mode_inited = true;
+ }
+
+ return mode;
+}
+
+bool show_notification (SnapData* data, NotifyMode mode)
+{
+ const Appointment& appointment = data->appointment;
+
+ 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";
+
+ auto nn = notify_notification_new(title, body.c_str(), icon_name);
+ if (mode == NOTIFY_MODE_SNAP)
+ {
+ notify_notification_set_hint_string(nn, "x-canonical-snap-decisions", "true");
+ notify_notification_set_hint_string(nn, "x-canonical-private-button-tint", "true");
+ notify_notification_add_action(nn, "show", _("Show"), on_snap_show, data, nullptr);
+ 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);
+ }
+ 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)
+ {
+ 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;
+ }
+
+ g_free(title);
+ return shown;
+}
+
+/**
+***
+**/
+
+void notify(const Appointment& appointment,
+ Snap::appointment_func show,
+ Snap::appointment_func dismiss)
+{
+ auto data = new SnapData;
+ data->appointment = appointment;
+ data->show = show;
+ data->dismiss = dismiss;
+
+ switch (get_notify_mode())
+ {
+ case NOTIFY_MODE_BUBBLE:
+ show_notification(data, NOTIFY_MODE_BUBBLE);
+ break;
+
+ default:
+ if (show_notification(data, NOTIFY_MODE_SNAP))
+ play_alarm_sound();
+ break;
+ }
+}
+
+} // unnamed namespace
+
+
+/***
+****
+***/
+
+Snap::Snap()
+{
+ first_time_init();
+}
+
+Snap::~Snap()
+{
+ media_cached = false;
+ 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
+ dismiss(appointment);
+}
+
+/***
+****
+***/
+
+} // namespace datetime
+} // namespace indicator
+} // namespace unity