From a85fd44e7b2dc67b3e0712e174e88d0eb6c467e7 Mon Sep 17 00:00:00 2001 From: Charles Kerr Date: Mon, 23 Jun 2014 18:52:52 -0500 Subject: add GSettings support for specifying a default alarm sound and default alarm volume. --- data/com.canonical.indicator.datetime.gschema.xml | 21 ++++++++++ include/datetime/settings-live.h | 2 + include/datetime/settings-shared.h | 12 ++++++ include/datetime/settings.h | 2 + src/settings-live.cpp | 26 ++++++++++++ tests/test-settings.cpp | 51 +++++++++++++++++++++++ 6 files changed, 114 insertions(+) diff --git a/data/com.canonical.indicator.datetime.gschema.xml b/data/com.canonical.indicator.datetime.gschema.xml index 1a5922c..7e74a63 100644 --- a/data/com.canonical.indicator.datetime.gschema.xml +++ b/data/com.canonical.indicator.datetime.gschema.xml @@ -5,6 +5,13 @@ + + + + + + + true @@ -123,5 +130,19 @@ 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). + + '/usr/share/sounds/ubuntu/ringtones/Suru arpeggio.ogg' + The alarm's default sound file. + + If an alarm doesn't specify its own sound file, this file will be used as the fallback sound. + + + + 'normal' + The alarm's default volume level. + + The volume at which alarms will be played. + + diff --git a/include/datetime/settings-live.h b/include/datetime/settings-live.h index 202c998..a075a85 100644 --- a/include/datetime/settings-live.h +++ b/include/datetime/settings-live.h @@ -55,6 +55,8 @@ private: void update_show_year(); void update_time_format_mode(); void update_timezone_name(); + void update_alarm_sound(); + void update_alarm_volume(); GSettings* m_settings; diff --git a/include/datetime/settings-shared.h b/include/datetime/settings-shared.h index 17a8ef0..b8d7737 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,7 @@ 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" #endif // INDICATOR_DATETIME_SETTINGS_SHARED diff --git a/include/datetime/settings.h b/include/datetime/settings.h index ce234d9..5a0f1eb 100644 --- a/include/datetime/settings.h +++ b/include/datetime/settings.h @@ -56,6 +56,8 @@ public: core::Property show_year; core::Property time_format_mode; core::Property timezone_name; + core::Property alarm_sound; + core::Property alarm_volume; }; } // namespace datetime diff --git a/src/settings-live.cpp b/src/settings-live.cpp index 2305c93..ec78feb 100644 --- a/src/settings-live.cpp +++ b/src/settings-live.cpp @@ -52,6 +52,8 @@ LiveSettings::LiveSettings(): update_show_year(); update_time_format_mode(); update_timezone_name(); + update_alarm_sound(); + update_alarm_volume(); // now listen for clients to change the properties s.t. we can sync update GSettings @@ -115,6 +117,14 @@ 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)); + }); } /*** @@ -205,6 +215,18 @@ 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)); +} + /*** **** ***/ @@ -246,6 +268,10 @@ 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(); } /*** diff --git a/tests/test-settings.cpp b/tests/test-settings.cpp index 707247d..9af803f 100644 --- a/tests/test-settings.cpp +++ b/tests/test-settings.cpp @@ -100,6 +100,31 @@ protected: EXPECT_EQ(str, tmp); g_clear_pointer(&tmp, g_free); } + +#if 0 + void TestIntProperty(core::Property& 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)); + } + } +#endif }; /*** @@ -129,6 +154,7 @@ 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 +178,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 strv_to_vector(const gchar** strv) -- cgit v1.2.3 From 9c34627337380a3125ffc7173606d72a66feec2e Mon Sep 17 00:00:00 2001 From: Charles Kerr Date: Mon, 23 Jun 2014 19:03:47 -0500 Subject: in the snap decision code, handle custom sounds and volume levels. --- include/datetime/snap.h | 6 +- src/main.cpp | 2 +- src/snap.cpp | 467 ++++++++++++++++++++++++++++----------------- 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 +#include #include #include @@ -35,13 +36,16 @@ namespace datetime { class Snap { public: - Snap(); + Snap(const std::shared_ptr& settings); virtual ~Snap(); typedef std::function appointment_func; void operator()(const Appointment& appointment, appointment_func show, appointment_func dismiss); + +private: + const std::shared_ptr 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 #include +#include + #include #include @@ -30,7 +32,7 @@ #include #include -#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& 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(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(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(gdata); - data->show(data->appointment); -} +public: -void on_snap_dismiss(NotifyNotification*, gchar* /*action*/, gpointer gdata) -{ - stop_alarm_sound(); - auto data = static_cast(gdata); - data->dismiss(data->appointment); -} + Popup(const Appointment& appointment, + const std::shared_ptr& 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(gdata); -} + typedef enum + { + RESPONSE_SHOW, + RESPONSE_DISMISS, + RESPONSE_CLOSE + } + Response; -std::set get_server_caps() -{ - std::set 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() { 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(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(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(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 get_server_caps() + { + std::set 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 m_settings; + const Mode m_mode; + std::unique_ptr m_sound; + core::Signal 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& 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(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 +#include #include #include @@ -29,6 +30,15 @@ using namespace unity::indicator::datetime; **** ***/ +namespace +{ + gboolean quit_idle (gpointer gloop) + { + g_main_loop_quit(static_cast(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(); + Snap snap (settings); snap(a, show, dismiss); g_main_loop_run(loop); + g_main_loop_unref(loop); return 0; } -- cgit v1.2.3 From 0463fa7657b1aab4a3f5c6e7909ba1513c047968 Mon Sep 17 00:00:00 2001 From: Charles Kerr Date: Mon, 23 Jun 2014 20:26:09 -0500 Subject: add support for per-alarm ringtone sounds set in EDS via E_CAL_COMPONENT_ALARM_AUDIO --- include/datetime/appointment.h | 1 + src/engine-eds.cpp | 18 +++++++-- src/snap.cpp | 89 +++++++++++++++++++++++++++++++----------- 3 files changed, 82 insertions(+), 26 deletions(-) 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/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/snap.cpp b/src/snap.cpp index 8e9dfbe..78602d3 100644 --- a/src/snap.cpp +++ b/src/snap.cpp @@ -32,8 +32,6 @@ #include #include -#define FALLBACK_ALARM_SOUND "/usr/share/sounds/ubuntu/ringtones/Suru arpeggio.ogg" - namespace unity { namespace indicator { namespace datetime { @@ -54,9 +52,9 @@ class LoopedSound public: - LoopedSound(const std::shared_ptr& settings): - m_filename(settings->alarm_sound.get()), - m_volume(settings->alarm_volume.get()), + LoopedSound(const std::string& filename, const AlarmVolume volume): + m_filename(filename), + m_volume(volume), m_canberra_id(get_next_canberra_id()) { const auto rv = ca_context_create(&m_context); @@ -118,21 +116,13 @@ private: 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_sets(props, CA_PROP_MEDIA_FILENAME, m_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 play file '%s': %s", filename.c_str(), ca_strerror(rv)); + g_warning("Failed to play file '%s': %s", m_filename.c_str(), ca_strerror(rv)); g_clear_pointer(&props, ca_proplist_destroy); } @@ -177,9 +167,11 @@ class Popup public: Popup(const Appointment& appointment, - const std::shared_ptr& settings): + const std::string& sound_filename, + const AlarmVolume sound_volume): m_appointment(appointment), - m_settings(settings), + m_sound_filename(sound_filename), + m_sound_volume(sound_volume), m_mode(get_mode()) { show(); @@ -238,10 +230,10 @@ private: shown = false; } - // if we were able to show a popup that requires - // user response, play the sound in a loop + // 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)); + m_sound.reset(new LoopedSound(m_sound_filename, m_sound_volume)); // if showing the notification didn't work, // treat it as if the user clicked the 'show' button @@ -337,7 +329,8 @@ private: typedef Popup Self; const Appointment m_appointment; - const std::shared_ptr m_settings; + const std::string m_sound_filename; + const AlarmVolume m_sound_volume; const Mode m_mode; std::unique_ptr m_sound; core::Signal m_response; @@ -362,6 +355,56 @@ void first_time_init() } } +std::string get_local_filename (const std::string& str) +{ + std::string ret; + + // maybe try it as a file path + if (ret.empty() && !str.empty()) + { + auto file = g_file_new_for_path (str.c_str()); + if (g_file_is_native(file) && g_file_query_exists(file,nullptr)) + ret = g_file_get_path(file); + g_clear_object(&file); + } + + // maybe try it as a uri + if (ret.empty() && !str.empty()) + { + auto file = g_file_new_for_uri (str.c_str()); + if (g_file_is_native(file) && g_file_query_exists(file,nullptr)) + ret = g_file_get_path(file); + g_clear_object(&file); + } + + return ret; +} + +std::string get_alarm_sound(const Appointment& appointment, + const std::shared_ptr& settings) +{ + static const constexpr char* const FALLBACK_AUDIO_FILENAME {"/usr/share/sounds/ubuntu/ringtones/Suru arpeggio.ogg"}; + + const std::string candidates[] = { appointment.audio_url, + settings->alarm_sound.get(), + FALLBACK_AUDIO_FILENAME }; + + std::string alarm_sound; + + for (const auto& candidate : candidates) + { + alarm_sound = get_local_filename (candidate); + + 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 /*** @@ -389,7 +432,9 @@ void Snap::operator()(const Appointment& appointment, } // create a popup... - auto popup = new Popup(appointment, m_settings); + auto popup = new Popup(appointment, + get_alarm_sound(appointment, m_settings), + m_settings->alarm_volume.get()); // listen for it to finish... popup->response().connect([appointment, -- cgit v1.2.3 From 373832769c30a950629a4ca9474bd7f5bf03a6b4 Mon Sep 17 00:00:00 2001 From: Charles Kerr Date: Tue, 24 Jun 2014 00:06:04 -0500 Subject: add GSettings support for specifying an alarm loop duration --- data/com.canonical.indicator.datetime.gschema.xml | 8 ++++++++ include/datetime/settings-live.h | 1 + include/datetime/settings-shared.h | 1 + include/datetime/settings.h | 1 + src/settings-live.cpp | 12 ++++++++++++ tests/test-settings.cpp | 7 +++++-- 6 files changed, 28 insertions(+), 2 deletions(-) diff --git a/data/com.canonical.indicator.datetime.gschema.xml b/data/com.canonical.indicator.datetime.gschema.xml index 7e74a63..3e0082d 100644 --- a/data/com.canonical.indicator.datetime.gschema.xml +++ b/data/com.canonical.indicator.datetime.gschema.xml @@ -144,5 +144,13 @@ The volume at which alarms will be played. + + + 30 + The alarm's duration. + + How long the alarm's sound will be looped if its snap decision is not dismissed by the user. + + diff --git a/include/datetime/settings-live.h b/include/datetime/settings-live.h index a075a85..4db2d40 100644 --- a/include/datetime/settings-live.h +++ b/include/datetime/settings-live.h @@ -57,6 +57,7 @@ private: 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 b8d7737..bfddd88 100644 --- a/include/datetime/settings-shared.h +++ b/include/datetime/settings-shared.h @@ -57,5 +57,6 @@ AlarmVolume; #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 5a0f1eb..a941f05 100644 --- a/include/datetime/settings.h +++ b/include/datetime/settings.h @@ -58,6 +58,7 @@ public: core::Property timezone_name; core::Property alarm_sound; core::Property alarm_volume; + core::Property alarm_duration; }; } // namespace datetime diff --git a/src/settings-live.cpp b/src/settings-live.cpp index ec78feb..e34ace1 100644 --- a/src/settings-live.cpp +++ b/src/settings-live.cpp @@ -54,6 +54,7 @@ LiveSettings::LiveSettings(): 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 @@ -125,6 +126,10 @@ LiveSettings::LiveSettings(): 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); + }); } /*** @@ -227,6 +232,11 @@ 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)); +} + /*** **** ***/ @@ -272,6 +282,8 @@ void LiveSettings::update_key(const std::string& key) 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/tests/test-settings.cpp b/tests/test-settings.cpp index 9af803f..2b500b2 100644 --- a/tests/test-settings.cpp +++ b/tests/test-settings.cpp @@ -101,7 +101,6 @@ protected: g_clear_pointer(&tmp, g_free); } -#if 0 void TestIntProperty(core::Property& property, const gchar* key) { EXPECT_EQ(g_settings_get_int(m_gsettings, key), property.get()); @@ -124,7 +123,6 @@ protected: EXPECT_EQ(expected_value, g_settings_get_int(m_gsettings, key)); } } -#endif }; /*** @@ -150,6 +148,11 @@ 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); -- cgit v1.2.3 From dd54649aa56790ec43fc4272bc41e6916b4ebb31 Mon Sep 17 00:00:00 2001 From: Charles Kerr Date: Tue, 24 Jun 2014 00:06:55 -0500 Subject: in the snap decision code, handle sound custom loop durations --- include/datetime/snap.h | 5 +- src/main.cpp | 2 +- src/snap.cpp | 120 ++++++++++++++++++++++++++++----------------- tests/manual-test-snap.cpp | 8 ++- 4 files changed, 88 insertions(+), 47 deletions(-) diff --git a/include/datetime/snap.h b/include/datetime/snap.h index ffd3aa0..9b45b3f 100644 --- a/include/datetime/snap.h +++ b/include/datetime/snap.h @@ -21,6 +21,7 @@ #define INDICATOR_DATETIME_SNAP_H #include +#include #include #include @@ -36,7 +37,8 @@ namespace datetime { class Snap { public: - Snap(const std::shared_ptr& settings); + Snap(const std::shared_ptr& clock, + const std::shared_ptr& settings); virtual ~Snap(); typedef std::function appointment_func; @@ -45,6 +47,7 @@ public: appointment_func dismiss); private: + const std::shared_ptr m_clock; const std::shared_ptr m_settings; }; diff --git a/src/main.cpp b/src/main.cpp index 5c6d41d..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 (state->settings); + 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/snap.cpp b/src/snap.cpp index 78602d3..f9ed9cc 100644 --- a/src/snap.cpp +++ b/src/snap.cpp @@ -44,19 +44,39 @@ namespace { /** - * Plays a sound in a loop. + * Plays a sound, possibly looping. */ -class LoopedSound +class Sound { - typedef LoopedSound Self; + typedef Sound Self; public: - LoopedSound(const std::string& filename, const AlarmVolume volume): - m_filename(filename), - m_volume(volume), - m_canberra_id(get_next_canberra_id()) + struct Properties { + std::shared_ptr clock; + std::string filename; + AlarmVolume volume; + int duration_minutes; + bool loop; + }; + + Sound(const Properties& properties): + m_properties(properties), + m_canberra_id(get_next_canberra_id()), + m_cutoff(properties.clock->localtime().add_full(0, 0, 0, 0, properties.duration_minutes, 0.0)) + { + if (m_properties.loop) + { + g_debug ("Looping '%s' until cutoff time %s", + m_properties.filename.c_str(), + m_cutoff.format("%F %T").c_str()); + } + else + { + g_debug ("Playing '%s' once", m_properties.filename.c_str()); + } + const auto rv = ca_context_create(&m_context); if (rv == CA_SUCCESS) { @@ -69,7 +89,7 @@ public: } } - ~LoopedSound() + ~Sound() { stop(); @@ -98,9 +118,14 @@ private: { auto self = static_cast(gself); - // wait a second, then play it again - if ((rv == CA_SUCCESS) && (self->m_timeout_tag == 0)) + // if we still need to loop, wait a second, then play it again + if ((self->m_properties.loop) && + (rv == CA_SUCCESS) && + (self->m_timeout_tag == 0) && + (self->m_properties.clock->localtime() < self->m_cutoff)) + { self->m_timeout_tag = g_timeout_add_seconds(1, play_idle, self); + } } static gboolean play_idle(gpointer gself) @@ -118,11 +143,11 @@ private: ca_proplist* props = nullptr; ca_proplist_create(&props); - ca_proplist_sets(props, CA_PROP_MEDIA_FILENAME, m_filename.c_str()); - ca_proplist_setf(props, CA_PROP_CANBERRA_VOLUME, "%f", get_decibel_multiplier(m_volume)); + ca_proplist_sets(props, CA_PROP_MEDIA_FILENAME, m_properties.filename.c_str()); + ca_proplist_setf(props, CA_PROP_CANBERRA_VOLUME, "%f", get_decibel_multiplier(m_properties.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 play file '%s': %s", m_filename.c_str(), ca_strerror(rv)); + g_warning("Failed to play file '%s': %s", m_properties.filename.c_str(), ca_strerror(rv)); g_clear_pointer(&props, ca_proplist_destroy); } @@ -153,9 +178,9 @@ private: ca_context* m_context = nullptr; guint m_timeout_tag = 0; - const std::string m_filename; - const AlarmVolume m_volume; + const Properties m_properties; const int32_t m_canberra_id; + const DateTime m_cutoff; }; /** @@ -167,11 +192,9 @@ class Popup public: Popup(const Appointment& appointment, - const std::string& sound_filename, - const AlarmVolume sound_volume): + const Sound::Properties& sound_properties): m_appointment(appointment), - m_sound_filename(sound_filename), - m_sound_volume(sound_volume), + m_sound_properties(sound_properties), m_mode(get_mode()) { show(); @@ -230,10 +253,11 @@ private: shown = 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_sound_filename, m_sound_volume)); + // Loop the sound *only* if we're prompting the user for a response. + // Otherwise, just play the sound once. + Sound::Properties tmp = m_sound_properties; + tmp.loop = shown && (m_mode == MODE_SNAP); + m_sound.reset(new Sound(tmp)); // if showing the notification didn't work, // treat it as if the user clicked the 'show' button @@ -329,10 +353,9 @@ private: typedef Popup Self; const Appointment m_appointment; - const std::string m_sound_filename; - const AlarmVolume m_sound_volume; + const Sound::Properties m_sound_properties; const Mode m_mode; - std::unique_ptr m_sound; + std::unique_ptr m_sound; core::Signal m_response; Response m_response_value = RESPONSE_CLOSE; NotifyNotification* m_nn = nullptr; @@ -359,22 +382,26 @@ std::string get_local_filename (const std::string& str) { std::string ret; - // maybe try it as a file path - if (ret.empty() && !str.empty()) + if (!str.empty()) { - auto file = g_file_new_for_path (str.c_str()); - if (g_file_is_native(file) && g_file_query_exists(file,nullptr)) - ret = g_file_get_path(file); - g_clear_object(&file); - } + GFile* files[] = { g_file_new_for_path(str.c_str()), + g_file_new_for_uri(str.c_str()) }; - // maybe try it as a uri - if (ret.empty() && !str.empty()) - { - auto file = g_file_new_for_uri (str.c_str()); - if (g_file_is_native(file) && g_file_query_exists(file,nullptr)) - ret = g_file_get_path(file); - g_clear_object(&file); + for(auto& file : files) + { + if (g_file_is_native(file) && g_file_query_exists(file,nullptr)) + { + char * tmp = g_file_get_path(file); + ret = tmp; + g_free(tmp); + } + + if (!ret.empty()) + break; + } + + for(auto& file : files) + g_object_unref(file); } return ret; @@ -411,7 +438,9 @@ std::string get_alarm_sound(const Appointment& appointment, **** ***/ -Snap::Snap(const std::shared_ptr& settings): +Snap::Snap(const std::shared_ptr& clock, + const std::shared_ptr& settings): + m_clock(clock), m_settings(settings) { first_time_init(); @@ -431,10 +460,13 @@ void Snap::operator()(const Appointment& appointment, return; } + const Sound::Properties sound_properties { m_clock, + get_alarm_sound(appointment, m_settings), + m_settings->alarm_volume.get(), + m_settings->alarm_duration.get() }; + // create a popup... - auto popup = new Popup(appointment, - get_alarm_sound(appointment, m_settings), - m_settings->alarm_volume.get()); + auto popup = new Popup(appointment, sound_properties); // listen for it to finish... popup->response().connect([appointment, diff --git a/tests/manual-test-snap.cpp b/tests/manual-test-snap.cpp index a78fb9a..90dbe08 100644 --- a/tests/manual-test-snap.cpp +++ b/tests/manual-test-snap.cpp @@ -21,11 +21,15 @@ #include #include #include +#include #include using namespace unity::indicator::datetime; +#define TIMEZONE_FILE ("/etc/timezone") + + /*** **** ***/ @@ -70,7 +74,9 @@ int main() g_debug("SCHEMA_DIR is %s", SCHEMA_DIR); auto settings = std::make_shared(); - Snap snap (settings); + auto timezones = std::make_shared(settings, TIMEZONE_FILE); + auto clock = std::make_shared(timezones); + Snap snap (clock, settings); snap(a, show, dismiss); g_main_loop_run(loop); g_main_loop_unref(loop); -- cgit v1.2.3 From 5f59fce41173767dc45cfb77b606b65718631091 Mon Sep 17 00:00:00 2001 From: Charles Kerr Date: Tue, 24 Jun 2014 09:39:14 -0500 Subject: copyedit the new snap decision: fix linewraps, give some variables/methods clearer names, better grouping of related methods, etc. --- src/snap.cpp | 210 +++++++++++++++++++++++++++++------------------------------ 1 file changed, 103 insertions(+), 107 deletions(-) diff --git a/src/snap.cpp b/src/snap.cpp index f9ed9cc..0480032 100644 --- a/src/snap.cpp +++ b/src/snap.cpp @@ -64,17 +64,18 @@ public: Sound(const Properties& properties): m_properties(properties), m_canberra_id(get_next_canberra_id()), - m_cutoff(properties.clock->localtime().add_full(0, 0, 0, 0, properties.duration_minutes, 0.0)) + m_loop_end_time(properties.clock->localtime().add_full(0, 0, 0, 0, + properties.duration_minutes, 0.0)) { if (m_properties.loop) { - g_debug ("Looping '%s' until cutoff time %s", - m_properties.filename.c_str(), - m_cutoff.format("%F %T").c_str()); + g_debug("Looping '%s' until cutoff time %s", + m_properties.filename.c_str(), + m_loop_end_time.format("%F %T").c_str()); } else { - g_debug ("Playing '%s' once", m_properties.filename.c_str()); + g_debug("Playing '%s' once", m_properties.filename.c_str()); } const auto rv = ca_context_create(&m_context); @@ -107,52 +108,34 @@ private: g_warning("Failed to cancel alarm sound: %s", ca_strerror(rv)); } - if (m_timeout_tag != 0) + if (m_loop_tag != 0) { - g_source_remove(m_timeout_tag); - m_timeout_tag = 0; + g_source_remove(m_loop_tag); + m_loop_tag = 0; } } - static void on_done_playing(ca_context*, uint32_t /*id*/, int rv, void* gself) - { - auto self = static_cast(gself); - - // if we still need to loop, wait a second, then play it again - if ((self->m_properties.loop) && - (rv == CA_SUCCESS) && - (self->m_timeout_tag == 0) && - (self->m_properties.clock->localtime() < self->m_cutoff)) - { - self->m_timeout_tag = g_timeout_add_seconds(1, play_idle, self); - } - } - - static gboolean play_idle(gpointer gself) - { - auto self = static_cast(gself); - self->m_timeout_tag = 0; - self->play(); - return G_SOURCE_REMOVE; - } - void play() { auto context = m_context; g_return_if_fail(context != nullptr); + const auto filename = m_properties.filename.c_str(); + const float gain = get_gain_level(m_properties.volume); + ca_proplist* props = nullptr; ca_proplist_create(&props); - ca_proplist_sets(props, CA_PROP_MEDIA_FILENAME, m_properties.filename.c_str()); - ca_proplist_setf(props, CA_PROP_CANBERRA_VOLUME, "%f", get_decibel_multiplier(m_properties.volume)); - const auto rv = ca_context_play_full(context, m_canberra_id, props, on_done_playing, this); + 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 play file '%s': %s", m_properties.filename.c_str(), ca_strerror(rv)); + g_warning("Unable to play '%s': %s", filename, ca_strerror(rv)); g_clear_pointer(&props, ca_proplist_destroy); } - static float get_decibel_multiplier(const AlarmVolume volume) + static float get_gain_level(const AlarmVolume volume) { /* These values aren't set in stone -- arrived at from from manual tests on Nexus 4 */ @@ -166,6 +149,30 @@ private: } } + 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 + + if (rv == CA_SUCCESS) + { + auto self = static_cast(gself); + if ((self->m_loop_tag == 0) && + (self->m_properties.loop) && + (self->m_properties.clock->localtime() < self->m_loop_end_time)) + { + self->m_loop_tag = g_timeout_add_seconds(1, play_idle, self); + } + } + } + + static gboolean play_idle(gpointer gself) + { + auto self = static_cast(gself); + self->m_loop_tag = 0; + self->play(); + return G_SOURCE_REMOVE; + } + /*** **** ***/ @@ -176,11 +183,11 @@ private: return next_canberra_id++; } - ca_context* m_context = nullptr; - guint m_timeout_tag = 0; const Properties m_properties; const int32_t m_canberra_id; - const DateTime m_cutoff; + const DateTime m_loop_end_time; + ca_context* m_context = nullptr; + guint m_loop_tag = 0; }; /** @@ -195,8 +202,19 @@ public: const Sound::Properties& sound_properties): m_appointment(appointment), m_sound_properties(sound_properties), - m_mode(get_mode()) + m_interactive(get_interactive()) { + // 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; + } + show(); } @@ -226,20 +244,27 @@ private: { const Appointment& appointment = m_appointment; - const auto timestr = appointment.begin.format("%a, %X"); + /// 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_mode == MODE_SNAP) + 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"); - // 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); + 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); } @@ -248,7 +273,8 @@ private: 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_critical("Unable to show snap decision for '%s': %s", + body.c_str(), error->message); g_error_free(error); shown = false; } @@ -256,7 +282,7 @@ private: // Loop the sound *only* if we're prompting the user for a response. // Otherwise, just play the sound once. Sound::Properties tmp = m_sound_properties; - tmp.loop = shown && (m_mode == MODE_SNAP); + tmp.loop = shown && m_interactive; m_sound.reset(new Sound(tmp)); // if showing the notification didn't work, @@ -271,7 +297,7 @@ private: } // user clicked 'show' - static void on_snap_show(NotifyNotification*, gchar* /*action*/, gpointer gself) + static void on_snap_show(NotifyNotification*, gchar*, gpointer gself) { auto self = static_cast(gself); self->m_response_value = RESPONSE_SHOW; @@ -279,7 +305,7 @@ private: } // user clicked 'dismiss' - static void on_snap_dismiss(NotifyNotification*, gchar* /*action*/, gpointer gself) + static void on_snap_dismiss(NotifyNotification*, gchar*, gpointer gself) { auto self = static_cast(gself); self->m_response_value = RESPONSE_DISMISS; @@ -295,7 +321,7 @@ private: } /*** - **** + **** Interactive ***/ static std::set get_server_caps() @@ -316,34 +342,18 @@ private: return caps_set; } - typedef enum - { - // just a bubble... no actions, no audio - MODE_BUBBLE, - - // a snap decision popup dialog + audio - MODE_SNAP - } - Mode; - - static Mode get_mode() + static bool get_interactive() { - static Mode mode; - static bool mode_inited = false; + static bool interactive; + static bool inited = false; - if (G_UNLIKELY(!mode_inited)) + if (G_UNLIKELY(!inited)) { - const auto caps = get_server_caps(); - - if (caps.count("actions")) - mode = MODE_SNAP; - else - mode = MODE_BUBBLE; - - mode_inited = true; + interactive = get_server_caps().count("actions") != 0; + inited = true; } - return mode; + return interactive; } /*** @@ -354,7 +364,7 @@ private: const Appointment m_appointment; const Sound::Properties m_sound_properties; - const Mode m_mode; + const bool m_interactive; std::unique_ptr m_sound; core::Signal m_response; Response m_response_value = RESPONSE_CLOSE; @@ -365,19 +375,6 @@ private: *** 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"); - } -} - std::string get_local_filename (const std::string& str) { std::string ret; @@ -389,15 +386,16 @@ std::string get_local_filename (const std::string& str) for(auto& file : files) { - if (g_file_is_native(file) && g_file_query_exists(file,nullptr)) + if (g_file_is_native(file) && g_file_query_exists(file, nullptr)) { - char * tmp = g_file_get_path(file); - ret = tmp; - g_free(tmp); + char* tmp = g_file_get_path(file); + if (tmp != nullptr) + { + ret = tmp; + g_free(tmp); + break; + } } - - if (!ret.empty()) - break; } for(auto& file : files) @@ -410,17 +408,17 @@ std::string get_local_filename (const std::string& str) std::string get_alarm_sound(const Appointment& appointment, const std::shared_ptr& settings) { - static const constexpr char* const FALLBACK_AUDIO_FILENAME {"/usr/share/sounds/ubuntu/ringtones/Suru arpeggio.ogg"}; + const char* FALLBACK {"/usr/share/sounds/ubuntu/ringtones/Suru arpeggio.ogg"}; const std::string candidates[] = { appointment.audio_url, settings->alarm_sound.get(), - FALLBACK_AUDIO_FILENAME }; + FALLBACK }; std::string alarm_sound; - for (const auto& candidate : candidates) + for(const auto& candidate : candidates) { - alarm_sound = get_local_filename (candidate); + alarm_sound = get_local_filename(candidate); if (!alarm_sound.empty()) break; @@ -443,7 +441,6 @@ Snap::Snap(const std::shared_ptr& clock, m_clock(clock), m_settings(settings) { - first_time_init(); } Snap::~Snap() @@ -460,13 +457,12 @@ void Snap::operator()(const Appointment& appointment, return; } - const Sound::Properties sound_properties { m_clock, - get_alarm_sound(appointment, m_settings), - m_settings->alarm_volume.get(), - m_settings->alarm_duration.get() }; - // create a popup... - auto popup = new Popup(appointment, sound_properties); + const Sound::Properties sp {m_clock, + get_alarm_sound(appointment, m_settings), + m_settings->alarm_volume.get(), + m_settings->alarm_duration.get()}; + auto popup = new Popup(appointment, sp); // listen for it to finish... popup->response().connect([appointment, -- cgit v1.2.3 From f630aa9b515a181e0647f0005a01ada32f813199 Mon Sep 17 00:00:00 2001 From: Charles Kerr Date: Wed, 25 Jun 2014 13:36:26 -0500 Subject: in snap.cpp, replace Sound::Properties with a SoundBuilder class to make the pattern use better. --- src/snap.cpp | 94 ++++++++++++++++++++++++++++++++++++++---------------------- 1 file changed, 60 insertions(+), 34 deletions(-) diff --git a/src/snap.cpp b/src/snap.cpp index 0480032..cc6a36d 100644 --- a/src/snap.cpp +++ b/src/snap.cpp @@ -52,30 +52,28 @@ class Sound public: - struct Properties - { - std::shared_ptr clock; - std::string filename; - AlarmVolume volume; - int duration_minutes; - bool loop; - }; - - Sound(const Properties& properties): - m_properties(properties), + Sound(const std::shared_ptr& 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(properties.clock->localtime().add_full(0, 0, 0, 0, - properties.duration_minutes, 0.0)) + m_loop_end_time(clock->localtime().add_full(0, 0, 0, 0, duration_minutes, 0.0)) { - if (m_properties.loop) + if (m_loop) { g_debug("Looping '%s' until cutoff time %s", - m_properties.filename.c_str(), + m_filename.c_str(), m_loop_end_time.format("%F %T").c_str()); } else { - g_debug("Playing '%s' once", m_properties.filename.c_str()); + g_debug("Playing '%s' once", m_filename.c_str()); } const auto rv = ca_context_create(&m_context); @@ -120,8 +118,8 @@ private: auto context = m_context; g_return_if_fail(context != nullptr); - const auto filename = m_properties.filename.c_str(); - const float gain = get_gain_level(m_properties.volume); + const auto filename = m_filename.c_str(); + const float gain = get_gain_level(m_volume); ca_proplist* props = nullptr; ca_proplist_create(&props); @@ -157,8 +155,8 @@ private: { auto self = static_cast(gself); if ((self->m_loop_tag == 0) && - (self->m_properties.loop) && - (self->m_properties.clock->localtime() < self->m_loop_end_time)) + (self->m_loop) && + (self->m_clock->localtime() < self->m_loop_end_time)) { self->m_loop_tag = g_timeout_add_seconds(1, play_idle, self); } @@ -183,13 +181,42 @@ private: return next_canberra_id++; } - const Properties m_properties; + const std::shared_ptr 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; }; +class SoundBuilder +{ +public: + void set_clock(const std::shared_ptr& 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); + } + +private: + std::shared_ptr m_clock; + std::string m_filename; + AlarmVolume m_volume = ALARM_VOLUME_NORMAL; + int m_duration_minutes = 30; + bool m_looping = true; +}; + /** * A popup notification (with optional sound) * that emits a Response signal when done. @@ -198,11 +225,10 @@ class Popup { public: - Popup(const Appointment& appointment, - const Sound::Properties& sound_properties): + Popup(const Appointment& appointment, const SoundBuilder& sound_builder): m_appointment(appointment), - m_sound_properties(sound_properties), - m_interactive(get_interactive()) + m_interactive(get_interactive()), + m_sound_builder(sound_builder) { // ensure notify_init() is called once // before we start popping up dialogs @@ -281,9 +307,8 @@ private: // Loop the sound *only* if we're prompting the user for a response. // Otherwise, just play the sound once. - Sound::Properties tmp = m_sound_properties; - tmp.loop = shown && m_interactive; - m_sound.reset(new Sound(tmp)); + 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 @@ -363,8 +388,8 @@ private: typedef Popup Self; const Appointment m_appointment; - const Sound::Properties m_sound_properties; const bool m_interactive; + SoundBuilder m_sound_builder; std::unique_ptr m_sound; core::Signal m_response; Response m_response_value = RESPONSE_CLOSE; @@ -458,11 +483,12 @@ void Snap::operator()(const Appointment& appointment, } // create a popup... - const Sound::Properties sp {m_clock, - get_alarm_sound(appointment, m_settings), - m_settings->alarm_volume.get(), - m_settings->alarm_duration.get()}; - auto popup = new Popup(appointment, sp); + 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, -- cgit v1.2.3 From ea8bedf5ec63ca42de776de9f4c21343a8163578 Mon Sep 17 00:00:00 2001 From: Charles Kerr Date: Wed, 25 Jun 2014 14:27:53 -0500 Subject: make get_gain_level() a little easier to read. --- src/snap.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/snap.cpp b/src/snap.cpp index cc6a36d..25632e9 100644 --- a/src/snap.cpp +++ b/src/snap.cpp @@ -141,8 +141,8 @@ private: { 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; + case ALARM_VOLUME_VERY_LOUD: return 8; default: return 0; } } -- cgit v1.2.3