aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/CMakeLists.txt31
-rw-r--r--src/engine-eds.cpp24
-rw-r--r--src/exporter.cpp249
-rw-r--r--src/main.cpp6
-rw-r--r--src/settings-live.cpp38
-rw-r--r--src/snap.cpp536
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);
+ });
}
/***