diff options
-rw-r--r-- | include/datetime/snap.h | 6 | ||||
-rw-r--r-- | src/main.cpp | 2 | ||||
-rw-r--r-- | src/snap.cpp | 467 | ||||
-rw-r--r-- | tests/manual-test-snap.cpp | 25 |
4 files changed, 314 insertions, 186 deletions
diff --git a/include/datetime/snap.h b/include/datetime/snap.h index a493772..ffd3aa0 100644 --- a/include/datetime/snap.h +++ b/include/datetime/snap.h @@ -21,6 +21,7 @@ #define INDICATOR_DATETIME_SNAP_H #include <datetime/appointment.h> +#include <datetime/settings.h> #include <memory> #include <functional> @@ -35,13 +36,16 @@ namespace datetime { class Snap { public: - Snap(); + Snap(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<const Settings> m_settings; }; } // namespace datetime diff --git a/src/main.cpp b/src/main.cpp index 079fe35..5c6d41d 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->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/snap.cpp b/src/snap.cpp index a087a75..8e9dfbe 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,7 +32,7 @@ #include <set> #include <string> -#define ALARM_SOUND_FILENAME "/usr/share/sounds/ubuntu/ringtones/Suru arpeggio.ogg" +#define FALLBACK_ALARM_SOUND "/usr/share/sounds/ubuntu/ringtones/Suru arpeggio.ogg" namespace unity { namespace indicator { @@ -43,266 +45,371 @@ namespace datetime { namespace { -/** -*** libcanberra -- play sounds -**/ - -// arbitrary number, but we need a consistent id for play/cancel -const int32_t alarm_ca_id = 1; +/** + * Plays a sound in a loop. + */ +class LoopedSound +{ + typedef LoopedSound Self; -ca_context *c_context = nullptr; -guint timeout_tag = 0; +public: -ca_context* get_ca_context() -{ - if (G_UNLIKELY(c_context == nullptr)) + LoopedSound(const std::shared_ptr<const Settings>& settings): + m_filename(settings->alarm_sound.get()), + m_volume(settings->alarm_volume.get()), + m_canberra_id(get_next_canberra_id()) { - int rv; - - 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; -} + ~LoopedSound() + { + stop(); -void play_alarm_sound(); + g_clear_pointer(&m_context, ca_context_destroy); + } -gboolean play_alarm_sound_idle (gpointer) -{ - timeout_tag = 0; - play_alarm_sound(); - return G_SOURCE_REMOVE; -} +private: -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 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)); + } -void play_alarm_sound() -{ - const gchar* filename = ALARM_SOUND_FILENAME; - auto context = get_ca_context(); - g_return_if_fail(context != nullptr); + if (m_timeout_tag != 0) + { + g_source_remove(m_timeout_tag); + m_timeout_tag = 0; + } + } - ca_proplist* props = nullptr; - ca_proplist_create(&props); - ca_proplist_sets(props, CA_PROP_MEDIA_FILENAME, filename); + static void on_done_playing(ca_context*, uint32_t /*id*/, int rv, void* gself) + { + auto self = static_cast<Self*>(gself); - 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)); + // wait a second, then play it again + if ((rv == CA_SUCCESS) && (self->m_timeout_tag == 0)) + self->m_timeout_tag = g_timeout_add_seconds(1, play_idle, self); + } - g_clear_pointer(&props, ca_proplist_destroy); -} + static gboolean play_idle(gpointer gself) + { + auto self = static_cast<Self*>(gself); + self->m_timeout_tag = 0; + self->play(); + return G_SOURCE_REMOVE; + } -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); + + std::string filename = m_filename; + if (!g_file_test(filename.c_str(), G_FILE_TEST_EXISTS)) + { + g_warning("Unable to find '%s' -- using fallback '%s' instead", + filename.c_str(), FALLBACK_ALARM_SOUND); + filename = FALLBACK_ALARM_SOUND; + } + + ca_proplist* props = nullptr; + ca_proplist_create(&props); + ca_proplist_sets(props, CA_PROP_MEDIA_FILENAME, filename.c_str()); + ca_proplist_setf(props, CA_PROP_CANBERRA_VOLUME, "%f", get_decibel_multiplier(m_volume)); + 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("Failed to play file '%s': %s", filename.c_str(), ca_strerror(rv)); + + g_clear_pointer(&props, ca_proplist_destroy); } - if (timeout_tag != 0) + static float get_decibel_multiplier(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_VERY_LOUD: return 8; + case ALARM_VOLUME_LOUD: return 4; + default: return 0; + } } -} -/** -*** libnotify -- snap decisions -**/ - -void first_time_init() -{ - static bool inited = false; + /*** + **** + ***/ - if (G_UNLIKELY(!inited)) + static int32_t get_next_canberra_id() { - inited = true; - - if(!notify_init("indicator-datetime-service")) - g_critical("libnotify initialization failed"); + static int32_t next_canberra_id = 1; + return next_canberra_id++; } -} -struct SnapData -{ - Snap::appointment_func show; - Snap::appointment_func dismiss; - Appointment appointment; + ca_context* m_context = nullptr; + guint m_timeout_tag = 0; + const std::string m_filename; + const AlarmVolume m_volume; + const int32_t m_canberra_id; }; -void on_snap_show(NotifyNotification*, gchar* /*action*/, gpointer gdata) +/** + * A popup notification (with optional sound) + * that emits a Response signal when done. + */ +class Popup { - stop_alarm_sound(); - auto data = static_cast<SnapData*>(gdata); - data->show(data->appointment); -} +public: -void on_snap_dismiss(NotifyNotification*, gchar* /*action*/, gpointer gdata) -{ - stop_alarm_sound(); - auto data = static_cast<SnapData*>(gdata); - data->dismiss(data->appointment); -} + Popup(const Appointment& appointment, + const std::shared_ptr<const Settings>& settings): + m_appointment(appointment), + m_settings(settings), + m_mode(get_mode()) + { + show(); + } -void on_snap_closed(NotifyNotification*, gpointer) -{ - stop_alarm_sound(); -} + ~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); + } + } -void snap_data_destroy_notify(gpointer gdata) -{ - delete static_cast<SnapData*>(gdata); -} + typedef enum + { + RESPONSE_SHOW, + RESPONSE_DISMISS, + RESPONSE_CLOSE + } + Response; -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) + core::Signal<Response>& response() { return m_response; } + +private: + + void show() { - caps_set.insert((const char*)l->data); + const Appointment& appointment = m_appointment; - 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; -} + 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"; -typedef enum -{ - // just a bubble... no actions, no audio - NOTIFY_MODE_BUBBLE, + m_nn = notify_notification_new(title, body.c_str(), icon_name); + if (m_mode == MODE_SNAP) + { + 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"); + // text for the alarm popup dialog's button to show the active alarm + notify_notification_add_action(m_nn, "show", _("Show"), on_snap_show, this, nullptr); + // text for the 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); + } - // a snap decision popup dialog + audio - NOTIFY_MODE_SNAP -} -NotifyMode; + 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; + } -NotifyMode get_notify_mode() -{ - static NotifyMode mode; - static bool mode_inited = false; + // if we were able to show a popup that requires + // user response, play the sound in a loop + if (shown && (m_mode == MODE_SNAP)) + m_sound.reset(new LoopedSound(m_settings)); + + // 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); + } - if (G_UNLIKELY(!mode_inited)) + g_free(title); + } + + // user clicked 'show' + static void on_snap_show(NotifyNotification*, gchar* /*action*/, gpointer gself) { - const auto caps = get_server_caps(); + auto self = static_cast<Self*>(gself); + self->m_response_value = RESPONSE_SHOW; + self->m_sound.reset(); + } - if (caps.count("actions")) - mode = NOTIFY_MODE_SNAP; - else - mode = NOTIFY_MODE_BUBBLE; + // user clicked 'dismiss' + static void on_snap_dismiss(NotifyNotification*, gchar* /*action*/, gpointer gself) + { + auto self = static_cast<Self*>(gself); + self->m_response_value = RESPONSE_DISMISS; + self->m_sound.reset(); + } - mode_inited = true; + // 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); } - return mode; -} + /*** + **** + ***/ -bool show_notification (SnapData* data, NotifyMode mode) -{ - const Appointment& appointment = data->appointment; + static 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); - 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"; + 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; + } - auto nn = notify_notification_new(title, body.c_str(), icon_name); - if (mode == NOTIFY_MODE_SNAP) + typedef enum { - 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); + // just a bubble... no actions, no audio + MODE_BUBBLE, + + // a snap decision popup dialog + audio + MODE_SNAP } - g_object_set_data_full(G_OBJECT(nn), "snap-data", data, snap_data_destroy_notify); + Mode; - bool shown = true; - GError * error = nullptr; - notify_notification_show(nn, &error); - if (error != NULL) + static Mode get_mode() { - 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 Mode mode; + static bool mode_inited = false; + + if (G_UNLIKELY(!mode_inited)) + { + const auto caps = get_server_caps(); + + if (caps.count("actions")) + mode = MODE_SNAP; + else + mode = MODE_BUBBLE; + + mode_inited = true; + } + + return mode; } - g_free(title); - return shown; -} + /*** + **** + ***/ + + typedef Popup Self; + + const Appointment m_appointment; + const std::shared_ptr<const Settings> m_settings; + const Mode m_mode; + std::unique_ptr<LoopedSound> 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) +void first_time_init() { - auto data = new SnapData; - data->appointment = appointment; - data->show = show; - data->dismiss = dismiss; + static bool inited = false; - switch (get_notify_mode()) + if (G_UNLIKELY(!inited)) { - case NOTIFY_MODE_BUBBLE: - show_notification(data, NOTIFY_MODE_BUBBLE); - break; - - default: - if (show_notification(data, NOTIFY_MODE_SNAP)) - play_alarm_sound(); - break; - } + inited = true; + + if(!notify_init("indicator-datetime-service")) + g_critical("libnotify initialization failed"); + } } } // unnamed namespace - /*** **** ***/ -Snap::Snap() +Snap::Snap(const std::shared_ptr<const Settings>& settings): + 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... + auto popup = new Popup(appointment, m_settings); + + // 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..a78fb9a 100644 --- a/tests/manual-test-snap.cpp +++ b/tests/manual-test-snap.cpp @@ -19,6 +19,7 @@ */ #include <datetime/appointment.h> +#include <datetime/settings-live.h> #include <datetime/snap.h> #include <glib.h> @@ -29,6 +30,15 @@ using namespace unity::indicator::datetime; **** ***/ +namespace +{ + gboolean quit_idle (gpointer gloop) + { + g_main_loop_quit(static_cast<GMainLoop*>(gloop)); + return G_SOURCE_REMOVE; + }; +} + int main() { Appointment a; @@ -47,15 +57,22 @@ 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>(); + Snap snap (settings); snap(a, show, dismiss); g_main_loop_run(loop); + g_main_loop_unref(loop); return 0; } |