diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/CMakeLists.txt | 31 | ||||
-rw-r--r-- | src/engine-eds.cpp | 24 | ||||
-rw-r--r-- | src/exporter.cpp | 249 | ||||
-rw-r--r-- | src/main.cpp | 6 | ||||
-rw-r--r-- | src/settings-live.cpp | 38 | ||||
-rw-r--r-- | src/snap.cpp | 536 |
6 files changed, 617 insertions, 267 deletions
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index ffa1523..af09c71 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,13 +1,13 @@ set (SERVICE_LIB "indicatordatetimeservice") set (SERVICE_EXEC "indicator-datetime-service") -SET (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -std=c99 -g ${CXX_WARNING_ARGS} ${GCOV_FLAGS}") -SET (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11 -g ${CXX_WARNING_ARGS} ${GCOV_FLAGS}") - add_definitions (-DTIMEZONE_FILE="/etc/timezone" -DG_LOG_DOMAIN="Indicator-Datetime") -set (SERVICE_SOURCES +# handwritten sources +set (SERVICE_C_SOURCES + utils.c) +set (SERVICE_CXX_SOURCES actions.cpp actions-live.cpp alarm-queue-simple.cpp @@ -32,16 +32,33 @@ set (SERVICE_SOURCES timezones-live.cpp utils.c wakeup-timer-mainloop.cpp) - if (HAVE_UBUNTU_HW_ALARM_H) - set (SERVICE_SOURCES ${SERVICE_SOURCES} wakeup-timer-uha.cpp) + set (SERVICE_CXX_SOURCES ${SERVICE_CXX_SOURCES} wakeup-timer-uha.cpp) endif () -add_library (${SERVICE_LIB} STATIC ${SERVICE_SOURCES}) +# generated sources +include (GdbusCodegen) +set(SERVICE_GENERATED_SOURCES) +add_gdbus_codegen(SERVICE_GENERATED_SOURCES dbus-alarm-properties + com.canonical.indicator + ${CMAKE_SOURCE_DIR}/data/com.canonical.indicator.datetime.AlarmProperties.xml) + +# add warnings/coverage info on handwritten files +# but not the autogenerated ones... +set_source_files_properties(${SERVICE_CXX_SOURCES} + PROPERTIES COMPILE_FLAGS "${CXX_WARNING_ARGS} ${GCOV_FLAGS} -g -std=c++11") +set_source_files_properties(${SERVICE_C_SOURCES} + PROPERTIES COMPILE_FLAGS "${CXX_WARNING_ARGS} ${GCOV_FLAGS} -g -std=c99") + +# add the bin dir to our include path so our code can find the generated header files +include_directories (${CMAKE_CURRENT_BINARY_DIR}) + +add_library (${SERVICE_LIB} STATIC ${SERVICE_C_SOURCES} ${SERVICE_CXX_SOURCES} ${SERVICE_GENERATED_SOURCES}) include_directories (${CMAKE_SOURCE_DIR}) link_directories (${SERVICE_DEPS_LIBRARY_DIRS}) add_executable (${SERVICE_EXEC} main.cpp) +set_source_files_properties(${SERVICE_SOURCES} main.cpp PROPERTIES COMPILE_FLAGS "${CXX_WARNING_ARGS} -g -std=c++11") target_link_libraries (${SERVICE_EXEC} ${SERVICE_LIB} ${SERVICE_DEPS_LIBRARIES} ${GCOV_LIBS}) install (TARGETS ${SERVICE_EXEC} RUNTIME DESTINATION ${CMAKE_INSTALL_FULL_PKGLIBEXECDIR}) diff --git a/src/engine-eds.cpp b/src/engine-eds.cpp index 1949193..58be0c4 100644 --- a/src/engine-eds.cpp +++ b/src/engine-eds.cpp @@ -41,8 +41,7 @@ class EdsEngine::Impl { public: - Impl(EdsEngine& owner): - m_owner(owner), + Impl(): m_cancellable(g_cancellable_new()) { e_source_registry_new(m_cancellable, on_source_registry_ready, this); @@ -443,8 +442,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 +453,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 +463,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); @@ -482,7 +491,6 @@ private: return G_SOURCE_CONTINUE; } - EdsEngine& m_owner; core::Signal<> m_changed; std::set<ESource*> m_sources; std::map<ESource*,ECalClient*> m_clients; @@ -498,7 +506,7 @@ private: ***/ EdsEngine::EdsEngine(): - p(new Impl(*this)) + p(new Impl()) { } diff --git a/src/exporter.cpp b/src/exporter.cpp index ccd6e5c..e2b60f2 100644 --- a/src/exporter.cpp +++ b/src/exporter.cpp @@ -20,6 +20,8 @@ #include <datetime/dbus-shared.h> #include <datetime/exporter.h> +#include "dbus-alarm-properties.h" + #include <glib/gi18n.h> #include <gio/gio.h> @@ -31,108 +33,223 @@ namespace datetime { **** ***/ -Exporter::~Exporter() +class Exporter::Impl { - if (m_dbus_connection != nullptr) +public: + + Impl(const std::shared_ptr<Settings>& settings): + m_settings(settings), + m_alarm_props(datetime_alarm_properties_skeleton_new()) + { + alarm_properties_init(); + } + + ~Impl() { - for(auto& id : m_exported_menu_ids) - g_dbus_connection_unexport_menu_model(m_dbus_connection, id); + if (m_bus != nullptr) + { + for(auto& id : m_exported_menu_ids) + g_dbus_connection_unexport_menu_model(m_bus, id); - if (m_exported_actions_id) - g_dbus_connection_unexport_action_group(m_dbus_connection, m_exported_actions_id); + if (m_exported_actions_id) + g_dbus_connection_unexport_action_group(m_bus, m_exported_actions_id); + } + + g_dbus_interface_skeleton_unexport(G_DBUS_INTERFACE_SKELETON(m_alarm_props)); + g_clear_object(&m_alarm_props); + + if (m_own_id) + g_bus_unown_name(m_own_id); + + g_clear_object(&m_bus); } - if (m_own_id) - g_bus_unown_name(m_own_id); + core::Signal<> name_lost; - g_clear_object(&m_dbus_connection); -} + void publish(const std::shared_ptr<Actions>& actions, + const std::vector<std::shared_ptr<Menu>>& menus) + { + m_actions = actions; + m_menus = menus; + m_own_id = g_bus_own_name(G_BUS_TYPE_SESSION, + BUS_NAME, + G_BUS_NAME_OWNER_FLAGS_ALLOW_REPLACEMENT, + on_bus_acquired, + nullptr, + on_name_lost, + this, + nullptr); + } -/*** -**** -***/ +private: -void -Exporter::on_bus_acquired(GDBusConnection* connection, const gchar* name, gpointer gthis) -{ - g_debug("bus acquired: %s", name); - static_cast<Exporter*>(gthis)->on_bus_acquired(connection, name); -} + /*** + **** + ***/ -void -Exporter::on_bus_acquired(GDBusConnection* connection, const gchar* /*name*/) -{ - m_dbus_connection = static_cast<GDBusConnection*>(g_object_ref(G_OBJECT(connection))); - - // export the actions - GError * error = nullptr; - const auto id = g_dbus_connection_export_action_group(m_dbus_connection, - BUS_PATH, - m_actions->action_group(), - &error); - if (id) + static void + on_gobject_notify_string(GObject* o, GParamSpec* pspec, gpointer p) + { + gchar* val = nullptr; + g_object_get (o, pspec->name, &val, nullptr); + static_cast<core::Property<std::string>*>(p)->set(val); + g_free(val); + } + void bind_string_property(gpointer o, const char* propname, + core::Property<std::string>& p) + { + // initialize the GObject property from the Settings + g_object_set(o, propname, p.get().c_str(), nullptr); + + // when the GObject changes, update the Settings + const std::string notify_propname = std::string("notify::") + propname; + g_signal_connect(o, notify_propname.c_str(), + G_CALLBACK(on_gobject_notify_string), &p); + + // when the Settings changes, update the GObject + p.changed().connect([o, propname](const std::string& val){ + g_object_set(o, propname, val.c_str(), nullptr); + }); + } + + + static void + on_gobject_notify_uint(GObject* o, GParamSpec* pspec, gpointer p) + { + uint val = 0; + g_object_get (o, pspec->name, &val, nullptr); + static_cast<core::Property<unsigned int>*>(p)->set(val); + } + void bind_uint_property(gpointer o, + const char* propname, + core::Property<unsigned int>& p) { - m_exported_actions_id = id; + // initialize the GObject property from the Settings + g_object_set(o, propname, p.get(), nullptr); + + // when the GObject changes, update the Settings + const std::string notify_propname = std::string("notify::") + propname; + g_signal_connect(o, notify_propname.c_str(), + G_CALLBACK(on_gobject_notify_uint), &p); + + // when the Settings changes, update the GObject + p.changed().connect([o, propname](unsigned int val){ + g_object_set(o, propname, val, nullptr); + }); } - else + + + void alarm_properties_init() { - g_warning("cannot export action group: %s", error->message); - g_clear_error(&error); + bind_uint_property(m_alarm_props, "duration", m_settings->alarm_duration); + bind_uint_property(m_alarm_props, "default-volume", m_settings->alarm_volume); + bind_string_property(m_alarm_props, "default-sound", m_settings->alarm_sound); } - // export the menus - for(auto& menu : m_menus) + /*** + **** + ***/ + + static void on_bus_acquired(GDBusConnection* connection, + const gchar* name, + gpointer gthis) { - const auto path = std::string(BUS_PATH) + "/" + menu->name(); - const auto id = g_dbus_connection_export_menu_model(m_dbus_connection, path.c_str(), menu->menu_model(), &error); + g_debug("bus acquired: %s", name); + static_cast<Impl*>(gthis)->on_bus_acquired(connection, name); + } + + void on_bus_acquired(GDBusConnection* bus, const gchar* /*name*/) + { + m_bus = static_cast<GDBusConnection*>(g_object_ref(G_OBJECT(bus))); + + // export the alarm properties + GError * error = nullptr; + g_dbus_interface_skeleton_export(G_DBUS_INTERFACE_SKELETON(m_alarm_props), + m_bus, + BUS_PATH"/AlarmProperties", + &error); + + // export the actions + const auto id = g_dbus_connection_export_action_group(m_bus, + BUS_PATH, + m_actions->action_group(), + &error); if (id) { - m_exported_menu_ids.insert(id); + m_exported_actions_id = id; } else { - if (error != nullptr) - g_warning("cannot export %s menu: %s", menu->name().c_str(), error->message); + g_warning("cannot export action group: %s", error->message); g_clear_error(&error); } + + // export the menus + for(auto& menu : m_menus) + { + const auto path = std::string(BUS_PATH) + "/" + menu->name(); + const auto id = g_dbus_connection_export_menu_model(m_bus, path.c_str(), menu->menu_model(), &error); + if (id) + { + m_exported_menu_ids.insert(id); + } + else + { + if (error != nullptr) + g_warning("cannot export %s menu: %s", menu->name().c_str(), error->message); + g_clear_error(&error); + } + } } -} + + /*** + **** + ***/ + + static void on_name_lost(GDBusConnection*, const gchar* name, gpointer gthis) + { + g_debug("name lost: %s", name); + static_cast<Impl*>(gthis)->name_lost(); + } + + /*** + **** + ***/ + + std::shared_ptr<Settings> m_settings; + std::set<guint> m_exported_menu_ids; + guint m_own_id = 0; + guint m_exported_actions_id = 0; + GDBusConnection* m_bus = nullptr; + std::shared_ptr<Actions> m_actions; + std::vector<std::shared_ptr<Menu>> m_menus; + DatetimeAlarmProperties* m_alarm_props = nullptr; +}; + /*** **** ***/ -void -Exporter::on_name_lost(GDBusConnection* connection, const gchar* name, gpointer gthis) +Exporter::Exporter(const std::shared_ptr<Settings>& settings): + p(new Impl(settings)) { - g_debug("name lost: %s", name); - static_cast<Exporter*>(gthis)->on_name_lost(connection, name); } -void -Exporter::on_name_lost(GDBusConnection* /*connection*/, const gchar* /*name*/) + +Exporter::~Exporter() { - name_lost(); } -/*** -**** -***/ +core::Signal<>& Exporter::name_lost() +{ + return p->name_lost; +} -void -Exporter::publish(const std::shared_ptr<Actions>& actions, - const std::vector<std::shared_ptr<Menu>>& menus) +void Exporter::publish(const std::shared_ptr<Actions>& actions, + const std::vector<std::shared_ptr<Menu>>& menus) { - m_actions = actions; - m_menus = menus; - m_own_id = g_bus_own_name(G_BUS_TYPE_SESSION, - BUS_NAME, - G_BUS_NAME_OWNER_FLAGS_ALLOW_REPLACEMENT, - on_bus_acquired, - nullptr, - on_name_lost, - this, - nullptr); + p->publish(actions, menus); } /*** diff --git a/src/main.cpp b/src/main.cpp index 079fe35..cc81cd7 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->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){ @@ -163,8 +163,8 @@ main(int /*argc*/, char** /*argv*/) // export them & run until we lose the busname auto loop = g_main_loop_new(nullptr, false); - Exporter exporter; - exporter.name_lost.connect([loop](){ + Exporter exporter(state->settings); + exporter.name_lost().connect([loop](){ g_message("%s exiting; failed/lost bus ownership", GETTEXT_PACKAGE); g_main_loop_quit(loop); }); diff --git a/src/settings-live.cpp b/src/settings-live.cpp index 2305c93..71bbd96 100644 --- a/src/settings-live.cpp +++ b/src/settings-live.cpp @@ -52,6 +52,9 @@ LiveSettings::LiveSettings(): update_show_year(); update_time_format_mode(); 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 @@ -115,6 +118,18 @@ 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](unsigned int value){ + g_settings_set_uint(m_settings, SETTINGS_ALARM_VOLUME_S, value); + }); + + alarm_duration.changed().connect([this](unsigned int value){ + g_settings_set_uint(m_settings, SETTINGS_ALARM_DURATION_S, value); + }); } /*** @@ -205,6 +220,23 @@ 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(g_settings_get_uint(m_settings, SETTINGS_ALARM_VOLUME_S)); +} + +void LiveSettings::update_alarm_duration() +{ + alarm_duration.set(g_settings_get_uint(m_settings, SETTINGS_ALARM_DURATION_S)); +} + /*** **** ***/ @@ -246,6 +278,12 @@ 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(); + else if (key == SETTINGS_ALARM_DURATION_S) + update_alarm_duration(); } /*** 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); + }); } /*** |