aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--include/datetime/snap.h6
-rw-r--r--src/main.cpp2
-rw-r--r--src/snap.cpp467
-rw-r--r--tests/manual-test-snap.cpp25
4 files changed, 314 insertions, 186 deletions
diff --git a/include/datetime/snap.h b/include/datetime/snap.h
index a493772..ffd3aa0 100644
--- a/include/datetime/snap.h
+++ b/include/datetime/snap.h
@@ -21,6 +21,7 @@
#define INDICATOR_DATETIME_SNAP_H
#include <datetime/appointment.h>
+#include <datetime/settings.h>
#include <memory>
#include <functional>
@@ -35,13 +36,16 @@ namespace datetime {
class Snap
{
public:
- Snap();
+ Snap(const std::shared_ptr<const Settings>& settings);
virtual ~Snap();
typedef std::function<void(const Appointment&)> appointment_func;
void operator()(const Appointment& appointment,
appointment_func show,
appointment_func dismiss);
+
+private:
+ const std::shared_ptr<const Settings> m_settings;
};
} // namespace datetime
diff --git a/src/main.cpp b/src/main.cpp
index 079fe35..5c6d41d 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -141,7 +141,7 @@ main(int /*argc*/, char** /*argv*/)
MenuFactory factory(actions, state);
// set up the snap decisions
- Snap snap;
+ Snap snap (state->settings);
auto alarm_queue = create_simple_alarm_queue(state->clock, engine, timezone);
alarm_queue->alarm_reached().connect([&snap](const Appointment& appt){
auto snap_show = [](const Appointment& a){
diff --git a/src/snap.cpp b/src/snap.cpp
index a087a75..8e9dfbe 100644
--- a/src/snap.cpp
+++ b/src/snap.cpp
@@ -21,6 +21,8 @@
#include <datetime/formatter.h>
#include <datetime/snap.h>
+#include <core/signal.h>
+
#include <canberra.h>
#include <libnotify/notify.h>
@@ -30,7 +32,7 @@
#include <set>
#include <string>
-#define ALARM_SOUND_FILENAME "/usr/share/sounds/ubuntu/ringtones/Suru arpeggio.ogg"
+#define FALLBACK_ALARM_SOUND "/usr/share/sounds/ubuntu/ringtones/Suru arpeggio.ogg"
namespace unity {
namespace indicator {
@@ -43,266 +45,371 @@ namespace datetime {
namespace
{
-/**
-*** libcanberra -- play sounds
-**/
-
-// arbitrary number, but we need a consistent id for play/cancel
-const int32_t alarm_ca_id = 1;
+/**
+ * Plays a sound in a loop.
+ */
+class LoopedSound
+{
+ typedef LoopedSound Self;
-ca_context *c_context = nullptr;
-guint timeout_tag = 0;
+public:
-ca_context* get_ca_context()
-{
- if (G_UNLIKELY(c_context == nullptr))
+ LoopedSound(const std::shared_ptr<const Settings>& settings):
+ m_filename(settings->alarm_sound.get()),
+ m_volume(settings->alarm_volume.get()),
+ m_canberra_id(get_next_canberra_id())
{
- int rv;
-
- if ((rv = ca_context_create(&c_context)) != CA_SUCCESS)
+ const auto rv = ca_context_create(&m_context);
+ if (rv == CA_SUCCESS)
{
- g_warning("Failed to create canberra context: %s\n", ca_strerror(rv));
- c_context = nullptr;
+ play();
+ }
+ else
+ {
+ g_warning("Failed to create canberra context: %s", ca_strerror(rv));
+ m_context = nullptr;
}
}
- return c_context;
-}
+ ~LoopedSound()
+ {
+ stop();
-void play_alarm_sound();
+ g_clear_pointer(&m_context, ca_context_destroy);
+ }
-gboolean play_alarm_sound_idle (gpointer)
-{
- timeout_tag = 0;
- play_alarm_sound();
- return G_SOURCE_REMOVE;
-}
+private:
-void on_alarm_play_done (ca_context* /*context*/, uint32_t /*id*/, int rv, void* /*user_data*/)
-{
- // wait one second, then play it again
- if ((rv == CA_SUCCESS) && (timeout_tag == 0))
- timeout_tag = g_timeout_add_seconds (1, play_alarm_sound_idle, nullptr);
-}
+ void stop()
+ {
+ if (m_context != nullptr)
+ {
+ const auto rv = ca_context_cancel(m_context, m_canberra_id);
+ if (rv != CA_SUCCESS)
+ g_warning("Failed to cancel alarm sound: %s", ca_strerror(rv));
+ }
-void play_alarm_sound()
-{
- const gchar* filename = ALARM_SOUND_FILENAME;
- auto context = get_ca_context();
- g_return_if_fail(context != nullptr);
+ if (m_timeout_tag != 0)
+ {
+ g_source_remove(m_timeout_tag);
+ m_timeout_tag = 0;
+ }
+ }
- ca_proplist* props = nullptr;
- ca_proplist_create(&props);
- ca_proplist_sets(props, CA_PROP_MEDIA_FILENAME, filename);
+ static void on_done_playing(ca_context*, uint32_t /*id*/, int rv, void* gself)
+ {
+ auto self = static_cast<Self*>(gself);
- const auto rv = ca_context_play_full(context, alarm_ca_id, props, on_alarm_play_done, nullptr);
- if (rv != CA_SUCCESS)
- g_warning("Failed to play file '%s': %s", filename, ca_strerror(rv));
+ // wait a second, then play it again
+ if ((rv == CA_SUCCESS) && (self->m_timeout_tag == 0))
+ self->m_timeout_tag = g_timeout_add_seconds(1, play_idle, self);
+ }
- g_clear_pointer(&props, ca_proplist_destroy);
-}
+ static gboolean play_idle(gpointer gself)
+ {
+ auto self = static_cast<Self*>(gself);
+ self->m_timeout_tag = 0;
+ self->play();
+ return G_SOURCE_REMOVE;
+ }
-void stop_alarm_sound()
-{
- auto context = get_ca_context();
- if (context != nullptr)
+ void play()
{
- const auto rv = ca_context_cancel(context, alarm_ca_id);
+ auto context = m_context;
+ g_return_if_fail(context != nullptr);
+
+ std::string filename = m_filename;
+ if (!g_file_test(filename.c_str(), G_FILE_TEST_EXISTS))
+ {
+ g_warning("Unable to find '%s' -- using fallback '%s' instead",
+ filename.c_str(), FALLBACK_ALARM_SOUND);
+ filename = FALLBACK_ALARM_SOUND;
+ }
+
+ ca_proplist* props = nullptr;
+ ca_proplist_create(&props);
+ ca_proplist_sets(props, CA_PROP_MEDIA_FILENAME, filename.c_str());
+ ca_proplist_setf(props, CA_PROP_CANBERRA_VOLUME, "%f", get_decibel_multiplier(m_volume));
+ const auto rv = ca_context_play_full(context, m_canberra_id, props, on_done_playing, this);
if (rv != CA_SUCCESS)
- g_warning("Failed to cancel alarm sound: %s", ca_strerror(rv));
+ g_warning("Failed to play file '%s': %s", filename.c_str(), ca_strerror(rv));
+
+ g_clear_pointer(&props, ca_proplist_destroy);
}
- if (timeout_tag != 0)
+ static float get_decibel_multiplier(const AlarmVolume volume)
{
- g_source_remove(timeout_tag);
- timeout_tag = 0;
+ /* These values aren't set in stone --
+ arrived at from from manual tests on Nexus 4 */
+ switch (volume)
+ {
+ case ALARM_VOLUME_VERY_QUIET: return -8;
+ case ALARM_VOLUME_QUIET: return -4;
+ case ALARM_VOLUME_VERY_LOUD: return 8;
+ case ALARM_VOLUME_LOUD: return 4;
+ default: return 0;
+ }
}
-}
-/**
-*** libnotify -- snap decisions
-**/
-
-void first_time_init()
-{
- static bool inited = false;
+ /***
+ ****
+ ***/
- if (G_UNLIKELY(!inited))
+ static int32_t get_next_canberra_id()
{
- inited = true;
-
- if(!notify_init("indicator-datetime-service"))
- g_critical("libnotify initialization failed");
+ static int32_t next_canberra_id = 1;
+ return next_canberra_id++;
}
-}
-struct SnapData
-{
- Snap::appointment_func show;
- Snap::appointment_func dismiss;
- Appointment appointment;
+ ca_context* m_context = nullptr;
+ guint m_timeout_tag = 0;
+ const std::string m_filename;
+ const AlarmVolume m_volume;
+ const int32_t m_canberra_id;
};
-void on_snap_show(NotifyNotification*, gchar* /*action*/, gpointer gdata)
+/**
+ * A popup notification (with optional sound)
+ * that emits a Response signal when done.
+ */
+class Popup
{
- stop_alarm_sound();
- auto data = static_cast<SnapData*>(gdata);
- data->show(data->appointment);
-}
+public:
-void on_snap_dismiss(NotifyNotification*, gchar* /*action*/, gpointer gdata)
-{
- stop_alarm_sound();
- auto data = static_cast<SnapData*>(gdata);
- data->dismiss(data->appointment);
-}
+ Popup(const Appointment& appointment,
+ const std::shared_ptr<const Settings>& settings):
+ m_appointment(appointment),
+ m_settings(settings),
+ m_mode(get_mode())
+ {
+ show();
+ }
-void on_snap_closed(NotifyNotification*, gpointer)
-{
- stop_alarm_sound();
-}
+ ~Popup()
+ {
+ if (m_nn != nullptr)
+ {
+ notify_notification_clear_actions(m_nn);
+ g_signal_handlers_disconnect_by_data(m_nn, this);
+ g_clear_object(&m_nn);
+ }
+ }
-void snap_data_destroy_notify(gpointer gdata)
-{
- delete static_cast<SnapData*>(gdata);
-}
+ typedef enum
+ {
+ RESPONSE_SHOW,
+ RESPONSE_DISMISS,
+ RESPONSE_CLOSE
+ }
+ Response;
-std::set<std::string> get_server_caps()
-{
- std::set<std::string> caps_set;
- auto caps_gl = notify_get_server_caps();
- std::string caps_str;
- for(auto l=caps_gl; l!=nullptr; l=l->next)
+ core::Signal<Response>& response() { return m_response; }
+
+private:
+
+ void show()
{
- caps_set.insert((const char*)l->data);
+ const Appointment& appointment = m_appointment;
- caps_str += (const char*) l->data;;
- if (l->next != nullptr)
- caps_str += ", ";
- }
- g_debug ("%s notify_get_server() returned [%s]", G_STRFUNC, caps_str.c_str());
- g_list_free_full(caps_gl, g_free);
- return caps_set;
-}
+ const auto timestr = appointment.begin.format("%a, %X");
+ auto title = g_strdup_printf(_("Alarm %s"), timestr.c_str());
+ const auto body = appointment.summary;
+ const gchar* icon_name = "alarm-clock";
-typedef enum
-{
- // just a bubble... no actions, no audio
- NOTIFY_MODE_BUBBLE,
+ m_nn = notify_notification_new(title, body.c_str(), icon_name);
+ if (m_mode == MODE_SNAP)
+ {
+ notify_notification_set_hint_string(m_nn, "x-canonical-snap-decisions", "true");
+ notify_notification_set_hint_string(m_nn, "x-canonical-private-button-tint", "true");
+ // text for the alarm popup dialog's button to show the active alarm
+ notify_notification_add_action(m_nn, "show", _("Show"), on_snap_show, this, nullptr);
+ // text for the alarm popup dialog's button to shut up the alarm
+ notify_notification_add_action(m_nn, "dismiss", _("Dismiss"), on_snap_dismiss, this, nullptr);
+ g_signal_connect(m_nn, "closed", G_CALLBACK(on_snap_closed), this);
+ }
- // a snap decision popup dialog + audio
- NOTIFY_MODE_SNAP
-}
-NotifyMode;
+ bool shown = true;
+ GError* error = nullptr;
+ notify_notification_show(m_nn, &error);
+ if (error != NULL)
+ {
+ g_critical("Unable to show snap decision for '%s': %s", body.c_str(), error->message);
+ g_error_free(error);
+ shown = false;
+ }
-NotifyMode get_notify_mode()
-{
- static NotifyMode mode;
- static bool mode_inited = false;
+ // if we were able to show a popup that requires
+ // user response, play the sound in a loop
+ if (shown && (m_mode == MODE_SNAP))
+ m_sound.reset(new LoopedSound(m_settings));
+
+ // if showing the notification didn't work,
+ // treat it as if the user clicked the 'show' button
+ if (!shown)
+ {
+ on_snap_show(nullptr, nullptr, this);
+ on_snap_dismiss(nullptr, nullptr, this);
+ }
- if (G_UNLIKELY(!mode_inited))
+ g_free(title);
+ }
+
+ // user clicked 'show'
+ static void on_snap_show(NotifyNotification*, gchar* /*action*/, gpointer gself)
{
- const auto caps = get_server_caps();
+ auto self = static_cast<Self*>(gself);
+ self->m_response_value = RESPONSE_SHOW;
+ self->m_sound.reset();
+ }
- if (caps.count("actions"))
- mode = NOTIFY_MODE_SNAP;
- else
- mode = NOTIFY_MODE_BUBBLE;
+ // user clicked 'dismiss'
+ static void on_snap_dismiss(NotifyNotification*, gchar* /*action*/, gpointer gself)
+ {
+ auto self = static_cast<Self*>(gself);
+ self->m_response_value = RESPONSE_DISMISS;
+ self->m_sound.reset();
+ }
- mode_inited = true;
+ // the popup was closed
+ static void on_snap_closed(NotifyNotification*, gpointer gself)
+ {
+ auto self = static_cast<Self*>(gself);
+ self->m_sound.reset();
+ self->m_response(self->m_response_value);
}
- return mode;
-}
+ /***
+ ****
+ ***/
-bool show_notification (SnapData* data, NotifyMode mode)
-{
- const Appointment& appointment = data->appointment;
+ static std::set<std::string> get_server_caps()
+ {
+ std::set<std::string> caps_set;
+ auto caps_gl = notify_get_server_caps();
+ std::string caps_str;
+ for(auto l=caps_gl; l!=nullptr; l=l->next)
+ {
+ caps_set.insert((const char*)l->data);
- const auto timestr = appointment.begin.format("%a, %X");
- auto title = g_strdup_printf(_("Alarm %s"), timestr.c_str());
- const auto body = appointment.summary;
- const gchar* icon_name = "alarm-clock";
+ caps_str += (const char*) l->data;;
+ if (l->next != nullptr)
+ caps_str += ", ";
+ }
+ g_debug("%s notify_get_server() returned [%s]", G_STRFUNC, caps_str.c_str());
+ g_list_free_full(caps_gl, g_free);
+ return caps_set;
+ }
- auto nn = notify_notification_new(title, body.c_str(), icon_name);
- if (mode == NOTIFY_MODE_SNAP)
+ typedef enum
{
- notify_notification_set_hint_string(nn, "x-canonical-snap-decisions", "true");
- notify_notification_set_hint_string(nn, "x-canonical-private-button-tint", "true");
- /* text for the alarm popup dialog's button to show the active alarm */
- notify_notification_add_action(nn, "show", _("Show"), on_snap_show, data, nullptr);
- /* text for the alarm popup dialog's button to shut up the alarm */
- notify_notification_add_action(nn, "dismiss", _("Dismiss"), on_snap_dismiss, data, nullptr);
- g_signal_connect(G_OBJECT(nn), "closed", G_CALLBACK(on_snap_closed), data);
+ // just a bubble... no actions, no audio
+ MODE_BUBBLE,
+
+ // a snap decision popup dialog + audio
+ MODE_SNAP
}
- g_object_set_data_full(G_OBJECT(nn), "snap-data", data, snap_data_destroy_notify);
+ Mode;
- bool shown = true;
- GError * error = nullptr;
- notify_notification_show(nn, &error);
- if (error != NULL)
+ static Mode get_mode()
{
- g_critical("Unable to show snap decision for '%s': %s", body.c_str(), error->message);
- g_error_free(error);
- data->show(data->appointment);
- shown = false;
+ static Mode mode;
+ static bool mode_inited = false;
+
+ if (G_UNLIKELY(!mode_inited))
+ {
+ const auto caps = get_server_caps();
+
+ if (caps.count("actions"))
+ mode = MODE_SNAP;
+ else
+ mode = MODE_BUBBLE;
+
+ mode_inited = true;
+ }
+
+ return mode;
}
- g_free(title);
- return shown;
-}
+ /***
+ ****
+ ***/
+
+ typedef Popup Self;
+
+ const Appointment m_appointment;
+ const std::shared_ptr<const Settings> m_settings;
+ const Mode m_mode;
+ std::unique_ptr<LoopedSound> m_sound;
+ core::Signal<Response> m_response;
+ Response m_response_value = RESPONSE_CLOSE;
+ NotifyNotification* m_nn = nullptr;
+};
/**
-***
+*** libnotify -- snap decisions
**/
-void notify(const Appointment& appointment,
- Snap::appointment_func show,
- Snap::appointment_func dismiss)
+void first_time_init()
{
- auto data = new SnapData;
- data->appointment = appointment;
- data->show = show;
- data->dismiss = dismiss;
+ static bool inited = false;
- switch (get_notify_mode())
+ if (G_UNLIKELY(!inited))
{
- case NOTIFY_MODE_BUBBLE:
- show_notification(data, NOTIFY_MODE_BUBBLE);
- break;
-
- default:
- if (show_notification(data, NOTIFY_MODE_SNAP))
- play_alarm_sound();
- break;
- }
+ inited = true;
+
+ if(!notify_init("indicator-datetime-service"))
+ g_critical("libnotify initialization failed");
+ }
}
} // unnamed namespace
-
/***
****
***/
-Snap::Snap()
+Snap::Snap(const std::shared_ptr<const Settings>& settings):
+ m_settings(settings)
{
first_time_init();
}
Snap::~Snap()
{
- g_clear_pointer(&c_context, ca_context_destroy);
}
void Snap::operator()(const Appointment& appointment,
appointment_func show,
appointment_func dismiss)
{
- if (appointment.has_alarms)
- notify(appointment, show, dismiss);
- else
+ if (!appointment.has_alarms)
+ {
dismiss(appointment);
+ return;
+ }
+
+ // create a popup...
+ auto popup = new Popup(appointment, m_settings);
+
+ // listen for it to finish...
+ popup->response().connect([appointment,
+ show,
+ dismiss,
+ popup](Popup::Response response){
+
+ // we can't delete the Popup inside its response() signal handler,
+ // so push that to an idle func
+ g_idle_add([](gpointer gdata){
+ delete static_cast<Popup*>(gdata);
+ return G_SOURCE_REMOVE;
+ }, popup);
+
+ // maybe notify the client code that the popup's done
+ if (response == Popup::RESPONSE_SHOW)
+ show(appointment);
+ else if (response == Popup::RESPONSE_DISMISS)
+ dismiss(appointment);
+ });
}
/***
diff --git a/tests/manual-test-snap.cpp b/tests/manual-test-snap.cpp
index 16e606a..a78fb9a 100644
--- a/tests/manual-test-snap.cpp
+++ b/tests/manual-test-snap.cpp
@@ -19,6 +19,7 @@
*/
#include <datetime/appointment.h>
+#include <datetime/settings-live.h>
#include <datetime/snap.h>
#include <glib.h>
@@ -29,6 +30,15 @@ using namespace unity::indicator::datetime;
****
***/
+namespace
+{
+ gboolean quit_idle (gpointer gloop)
+ {
+ g_main_loop_quit(static_cast<GMainLoop*>(gloop));
+ return G_SOURCE_REMOVE;
+ };
+}
+
int main()
{
Appointment a;
@@ -47,15 +57,22 @@ int main()
auto loop = g_main_loop_new(nullptr, false);
auto show = [loop](const Appointment& appt){
g_message("You clicked 'show' for appt url '%s'", appt.url.c_str());
- g_main_loop_quit(loop);
+ g_idle_add(quit_idle, loop);
};
auto dismiss = [loop](const Appointment&){
g_message("You clicked 'dismiss'");
- g_main_loop_quit(loop);
+ g_idle_add(quit_idle, loop);
};
-
- Snap snap;
+
+ // only use local, temporary settings
+ g_assert(g_setenv("GSETTINGS_SCHEMA_DIR", SCHEMA_DIR, true));
+ g_assert(g_setenv("GSETTINGS_BACKEND", "memory", true));
+ g_debug("SCHEMA_DIR is %s", SCHEMA_DIR);
+
+ auto settings = std::make_shared<LiveSettings>();
+ Snap snap (settings);
snap(a, show, dismiss);
g_main_loop_run(loop);
+ g_main_loop_unref(loop);
return 0;
}