From fcd77b806a8826d5f694f78c63943d0f768ef6ec Mon Sep 17 00:00:00 2001 From: Charles Kerr Date: Sat, 26 Jul 2014 23:35:38 -0500 Subject: refactor the Notifications / sound / awake code --- include/CMakeLists.txt | 1 + include/datetime/dbus-shared.h | 14 +- include/datetime/snap.h | 12 +- include/notifications/CMakeLists.txt | 2 + include/notifications/awake.h | 55 +++ include/notifications/dbus-shared.h | 32 ++ include/notifications/notifications.h | 116 +++++++ include/notifications/sound.h | 60 ++++ src/CMakeLists.txt | 3 + src/awake.cpp | 250 ++++++++++++++ src/main.cpp | 9 +- src/notifications.cpp | 367 ++++++++++++++++++++ src/snap.cpp | 627 +++------------------------------- src/sound.cpp | 161 +++++++++ tests/manual-test-snap.cpp | 11 +- tests/test-snap.cpp | 74 ++-- 16 files changed, 1168 insertions(+), 626 deletions(-) create mode 100644 include/notifications/CMakeLists.txt create mode 100644 include/notifications/awake.h create mode 100644 include/notifications/dbus-shared.h create mode 100644 include/notifications/notifications.h create mode 100644 include/notifications/sound.h create mode 100644 src/awake.cpp create mode 100644 src/notifications.cpp create mode 100644 src/sound.cpp diff --git a/include/CMakeLists.txt b/include/CMakeLists.txt index 486e9c7..15a7c33 100644 --- a/include/CMakeLists.txt +++ b/include/CMakeLists.txt @@ -1 +1,2 @@ add_subdirectory(datetime) +add_subdirectory(notifications) diff --git a/include/datetime/dbus-shared.h b/include/datetime/dbus-shared.h index 4b71ce5..db10c1d 100644 --- a/include/datetime/dbus-shared.h +++ b/include/datetime/dbus-shared.h @@ -18,18 +18,10 @@ * Charles Kerr */ -#ifndef _DBUS_SHARED_H_ -#define _DBUS_SHARED_H_ +#ifndef _INDICATOR_DATETIME_DBUS_SHARED_H_ +#define _INDICATOR_DATETIME_DBUS_SHARED_H_ #define BUS_DATETIME_NAME "com.canonical.indicator.datetime" #define BUS_DATETIME_PATH "/com/canonical/indicator/datetime" -#define BUS_SCREEN_NAME "com.canonical.Unity.Screen" -#define BUS_SCREEN_PATH "/com/canonical/Unity/Screen" -#define BUS_SCREEN_INTERFACE "com.canonical.Unity.Screen" - -#define BUS_POWERD_NAME "com.canonical.powerd" -#define BUS_POWERD_PATH "/com/canonical/powerd" -#define BUS_POWERD_INTERFACE "com.canonical.powerd" - -#endif /* _DBUS_SHARED_H_ */ +#endif /* _INDICATOR_DATETIME_DBUS_SHARED_H_ */ diff --git a/include/datetime/snap.h b/include/datetime/snap.h index 1c90496..78d9f65 100644 --- a/include/datetime/snap.h +++ b/include/datetime/snap.h @@ -21,9 +21,10 @@ #define INDICATOR_DATETIME_SNAP_H #include -#include #include +#include + #include #include #include @@ -38,7 +39,7 @@ namespace datetime { class Snap { public: - Snap(const std::shared_ptr& clock, + Snap(const std::shared_ptr& engine, const std::shared_ptr& settings); virtual ~Snap(); @@ -48,12 +49,9 @@ public: appointment_func dismiss); private: - const std::shared_ptr m_clock; + const std::shared_ptr m_engine; const std::shared_ptr m_settings; - - class Popup; - friend class Popup; - std::set m_pending; + std::set m_notifications; }; } // namespace datetime diff --git a/include/notifications/CMakeLists.txt b/include/notifications/CMakeLists.txt new file mode 100644 index 0000000..139597f --- /dev/null +++ b/include/notifications/CMakeLists.txt @@ -0,0 +1,2 @@ + + diff --git a/include/notifications/awake.h b/include/notifications/awake.h new file mode 100644 index 0000000..fd812c1 --- /dev/null +++ b/include/notifications/awake.h @@ -0,0 +1,55 @@ +/* + * Copyright 2014 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranties of + * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + * + * Authors: + * Charles Kerr + */ + +#ifndef UNITY_INDICATOR_NOTIFICATIONS_AWAKE_H +#define UNITY_INDICATOR_NOTIFICATIONS_AWAKE_H + +#include + +namespace unity { +namespace indicator { +namespace notifications { + +/*** +**** +***/ + +/** + * A class that forces the screen display on and inhibits sleep + */ +class Awake +{ +public: + Awake(const std::string& app_name); + ~Awake(); + +private: + class Impl; + std::unique_ptr impl; +}; + +/*** +**** +***/ + +} // namespace notifications +} // namespace indicator +} // namespace unity + +#endif // UNITY_INDICATOR_NOTIFICATIONS_AWAKE_H diff --git a/include/notifications/dbus-shared.h b/include/notifications/dbus-shared.h new file mode 100644 index 0000000..7738cb7 --- /dev/null +++ b/include/notifications/dbus-shared.h @@ -0,0 +1,32 @@ +/* + * Copyright 2013 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranties of + * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + * + * Authors: + * Ted Gould + * Charles Kerr + */ + +#ifndef UNITY_INDICATOR_NOTIFICATIONS_DBUS_SHARED_H +#define UNITY_INDICATOR_NOTIFICATIONS_DBUS_SHARED_H + +#define BUS_SCREEN_NAME "com.canonical.Unity.Screen" +#define BUS_SCREEN_PATH "/com/canonical/Unity/Screen" +#define BUS_SCREEN_INTERFACE "com.canonical.Unity.Screen" + +#define BUS_POWERD_NAME "com.canonical.powerd" +#define BUS_POWERD_PATH "/com/canonical/powerd" +#define BUS_POWERD_INTERFACE "com.canonical.powerd" + +#endif /* INDICATOR_NOTIFICATIONS_DBUS_SHARED_H */ diff --git a/include/notifications/notifications.h b/include/notifications/notifications.h new file mode 100644 index 0000000..b4c88b4 --- /dev/null +++ b/include/notifications/notifications.h @@ -0,0 +1,116 @@ +/* + * Copyright 2014 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranties of + * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + * + * Authors: + * Charles Kerr + */ + +#ifndef UNITY_INDICATOR_NOTIFICATIONS_NOTIFICATIONS_H +#define UNITY_INDICATOR_NOTIFICATIONS_NOTIFICATIONS_H + +#include +#include +#include +#include + +namespace unity { +namespace indicator { +namespace notifications { + +class Engine; + +/** + * Helper class for showing notifications. + * + * Populate the builder, with the relevant properites, + * then pass it to Engine::show(). + * + * @see Engine::show(Builder) + */ +class Builder +{ +public: + Builder(); + ~Builder(); + + void set_title (const std::string& title); + void set_body (const std::string& body); + void set_icon_name (const std::string& icon_name); + + /* Set an interval, after which the notification will automatically + be closed. If not set, the notification server's default timeout + is used. */ + void set_timeout (const std::chrono::seconds& duration); + + /* Add a notification hint. + These keys may be dependent on the notification server. */ + void add_hint (const std::string& name); + static constexpr char const * HINT_SNAP {"x-canonical-snap-decisions"}; + static constexpr char const * HINT_TINT {"x-canonical-private-button-tint"}; + + /* Add an action button. + This may fail if the Engine doesn't support actions. + @see Engine::supports_actions() */ + void add_action (const std::string& action, const std::string& label); + + /** Sets the closed callback. This will be called exactly once. */ + void set_closed_callback (std::function); + +private: + friend class Engine; + class Impl; + std::unique_ptr impl; +}; + +/** + * Manages Notifications and the connection to the notification server. + * + * When this class is destroyed, any remaining notifications it created + * will be closed and their closed() callbacks will be invoked. + */ +class Engine +{ +public: + Engine(const std::string& app_name); + ~Engine(); + + /** @see Builder::set_action() */ + bool supports_actions() const; + + /** Show a notification. + @return nonzero on success, zero on failure. */ + int show(const Builder& builder); + + /** Close a notification. + @param key the int returned by show() + @return true if the notification was closed. */ + bool close(int key); + + /** Close all remaining notifications. + *@return true if all closed successfully. */ + bool close_all(); + + const std::string& app_name() const; + +private: + class Impl; + std::unique_ptr impl; +}; + +} // namespace notifications +} // namespace indicator +} // namespace unity + +#endif // UNITY_INDICATOR_NOTIFICATIONS_NOTIFICATIONS_H diff --git a/include/notifications/sound.h b/include/notifications/sound.h new file mode 100644 index 0000000..f5f549c --- /dev/null +++ b/include/notifications/sound.h @@ -0,0 +1,60 @@ +/* + * Copyright 2014 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranties of + * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + * + * Authors: + * Charles Kerr + */ + +#ifndef UNITY_INDICATOR_NOTIFICATIONS_SOUND_H +#define UNITY_INDICATOR_NOTIFICATIONS_SOUND_H + +#include +#include + +namespace unity { +namespace indicator { +namespace notifications { + +/*** +**** +***/ + +/** + * Plays a sound, possibly looping. + * + * @param uri the file to play + * @param volume the volume at which to play the sound, [0..100] + * @param loop if true, loop the sound for the lifespan of the object + */ +class Sound +{ +public: + Sound(const std::string& uri, unsigned int volume, bool loop); + ~Sound(); + +private: + class Impl; + std::unique_ptr impl; +}; + +/*** +**** +***/ + +} // namespace notifications +} // namespace indicator +} // namespace unity + +#endif // UNITY_INDICATOR_NOTIFICATIONS_SOUND_H diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index af09c71..754d537 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -11,6 +11,7 @@ set (SERVICE_CXX_SOURCES actions.cpp actions-live.cpp alarm-queue-simple.cpp + awake.cpp appointment.cpp clock.cpp clock-live.cpp @@ -22,11 +23,13 @@ set (SERVICE_CXX_SOURCES locations.cpp locations-settings.cpp menu.cpp + notifications.cpp planner-month.cpp planner-range.cpp planner-upcoming.cpp settings-live.cpp snap.cpp + sound.cpp timezone-file.cpp timezone-geoclue.cpp timezones-live.cpp diff --git a/src/awake.cpp b/src/awake.cpp new file mode 100644 index 0000000..19826ae --- /dev/null +++ b/src/awake.cpp @@ -0,0 +1,250 @@ +/* + * Copyright 2014 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranties of + * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + * + * Authors: + * Charles Kerr + */ + +#include +#include + +#include + +#include + +namespace unity { +namespace indicator { +namespace notifications { + +/*** +**** +***/ + +class Awake::Impl +{ +public: + + Impl(const std::string& app_name): + m_app_name(app_name), + m_cancellable(g_cancellable_new()) + { + g_bus_get(G_BUS_TYPE_SYSTEM, m_cancellable, on_system_bus_ready, this); + } + + ~Impl() + { + g_cancellable_cancel (m_cancellable); + g_object_unref (m_cancellable); + + if (m_system_bus != nullptr) + { + unforce_awake (); + unforce_screen (); + g_object_unref (m_system_bus); + } + } + +private: + + static void on_system_bus_ready (GObject *, GAsyncResult *res, gpointer gself) + { + GError * error; + GDBusConnection * system_bus; + + error = nullptr; + system_bus = g_bus_get_finish (res, &error); + if (error != nullptr) + { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_warning ("Unable to get bus: %s", error->message); + + g_error_free (error); + } + else if (system_bus != nullptr) + { + auto self = static_cast(gself); + + self->m_system_bus = G_DBUS_CONNECTION (g_object_ref (system_bus)); + + // ask powerd to keep the system awake + static constexpr int32_t POWERD_SYS_STATE_ACTIVE = 1; + g_dbus_connection_call (system_bus, + BUS_POWERD_NAME, + BUS_POWERD_PATH, + BUS_POWERD_INTERFACE, + "requestSysState", + g_variant_new("(si)", self->m_app_name.c_str(), POWERD_SYS_STATE_ACTIVE), + G_VARIANT_TYPE("(s)"), + G_DBUS_CALL_FLAGS_NONE, + -1, + self->m_cancellable, + on_force_awake_response, + self); + + // ask unity-system-compositor to turn on the screen + g_dbus_connection_call (system_bus, + BUS_SCREEN_NAME, + BUS_SCREEN_PATH, + BUS_SCREEN_INTERFACE, + "keepDisplayOn", + nullptr, + G_VARIANT_TYPE("(i)"), + G_DBUS_CALL_FLAGS_NONE, + -1, + self->m_cancellable, + on_force_screen_response, + self); + + g_object_unref (system_bus); + } + } + + static void on_force_awake_response (GObject * connection, + GAsyncResult * res, + gpointer gself) + { + GError * error; + GVariant * args; + + error = nullptr; + args = g_dbus_connection_call_finish (G_DBUS_CONNECTION(connection), res, &error); + if (error != nullptr) + { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED) && + !g_error_matches (error, G_DBUS_ERROR, G_DBUS_ERROR_SERVICE_UNKNOWN)) + { + g_warning ("Unable to inhibit sleep: %s", error->message); + } + + g_error_free (error); + } + else + { + auto self = static_cast(gself); + + g_clear_pointer (&self->m_awake_cookie, g_free); + g_variant_get (args, "(s)", &self->m_awake_cookie); + g_debug ("m_awake_cookie is now '%s'", self->m_awake_cookie); + + g_variant_unref (args); + } + } + + static void on_force_screen_response (GObject * connection, + GAsyncResult * res, + gpointer gself) + { + GError * error; + GVariant * args; + + error = nullptr; + args = g_dbus_connection_call_finish (G_DBUS_CONNECTION(connection), res, &error); + if (error != nullptr) + { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED) && + !g_error_matches (error, G_DBUS_ERROR, G_DBUS_ERROR_SERVICE_UNKNOWN)) + { + g_warning ("Unable to turn on the screen: %s", error->message); + } + + g_error_free (error); + } + else + { + auto self = static_cast(gself); + + self->m_screen_cookie = NO_SCREEN_COOKIE; + g_variant_get (args, "(i)", &self->m_screen_cookie); + g_debug ("m_screen_cookie is now '%d'", self->m_screen_cookie); + + g_variant_unref (args); + } + } + + void unforce_awake () + { + g_return_if_fail (G_IS_DBUS_CONNECTION(m_system_bus)); + + if (m_awake_cookie != nullptr) + { + g_dbus_connection_call (m_system_bus, + BUS_POWERD_NAME, + BUS_POWERD_PATH, + BUS_POWERD_INTERFACE, + "clearSysState", + g_variant_new("(s)", m_awake_cookie), + nullptr, + G_DBUS_CALL_FLAGS_NONE, + -1, + nullptr, + nullptr, + nullptr); + + g_clear_pointer (&m_awake_cookie, g_free); + } + } + + void unforce_screen () + { + g_return_if_fail (G_IS_DBUS_CONNECTION(m_system_bus)); + + if (m_screen_cookie != NO_SCREEN_COOKIE) + { + g_dbus_connection_call (m_system_bus, + BUS_SCREEN_NAME, + BUS_SCREEN_PATH, + BUS_SCREEN_INTERFACE, + "removeDisplayOnRequest", + g_variant_new("(i)", m_screen_cookie), + nullptr, + G_DBUS_CALL_FLAGS_NONE, + -1, + nullptr, + nullptr, + nullptr); + + m_screen_cookie = NO_SCREEN_COOKIE; + } + } + + const std::string m_app_name; + GCancellable * m_cancellable = nullptr; + GDBusConnection * m_system_bus = nullptr; + char * m_awake_cookie = nullptr; + int32_t m_screen_cookie = NO_SCREEN_COOKIE; + + static constexpr int32_t NO_SCREEN_COOKIE { std::numeric_limits::min() }; +}; + +/*** +**** +***/ + +Awake::Awake(const std::string& app_name): + impl(new Impl (app_name)) +{ +} + +Awake::~Awake() +{ +} + +/*** +**** +***/ + +} // namespace datetime +} // namespace indicator +} // namespace unity diff --git a/src/main.cpp b/src/main.cpp index cc81cd7..eb90020 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -32,6 +32,7 @@ #include #include #include +#include #ifdef HAVE_UBUNTU_HW_ALARM_H #include @@ -45,6 +46,8 @@ #include #include // exit() +namespace uin = unity::indicator::notifications; + using namespace unity::indicator::datetime; namespace @@ -141,7 +144,8 @@ main(int /*argc*/, char** /*argv*/) MenuFactory factory(actions, state); // set up the snap decisions - Snap snap (state->clock, state->settings); + auto notification_engine = std::make_shared("indicator-datetime-service"); + std::unique_ptr snap (new Snap(notification_engine, 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){ @@ -153,7 +157,7 @@ main(int /*argc*/, char** /*argv*/) url_dispatch_send(url, nullptr, nullptr); }; auto snap_dismiss = [](const Appointment&){}; - snap(appt, snap_show, snap_dismiss); + (*snap)(appt, snap_show, snap_dismiss); }); // create the menus @@ -170,6 +174,7 @@ main(int /*argc*/, char** /*argv*/) }); exporter.publish(actions, menus); g_main_loop_run(loop); + g_main_loop_unref(loop); return 0; } diff --git a/src/notifications.cpp b/src/notifications.cpp new file mode 100644 index 0000000..41ced42 --- /dev/null +++ b/src/notifications.cpp @@ -0,0 +1,367 @@ +/* + * Copyright 2014 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranties of + * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + * + * Authors: + * Charles Kerr + */ + +#include + +#include + +#include +#include +#include +#include + +namespace unity { +namespace indicator { +namespace notifications { + +static G_DEFINE_QUARK(NotificationKey, notification_key) + +static G_DEFINE_QUARK(NotificationAction, notification_action) + +/*** +**** +***/ + +class Builder::Impl +{ +public: + std::string m_title; + std::string m_body; + std::string m_icon_name; + std::chrono::seconds m_duration; + std::set m_string_hints; + std::vector> m_actions; + std::function m_closed_callback; +}; + +Builder::Builder(): + impl(new Impl()) +{ +} + +Builder::~Builder() +{ +} + +void +Builder::set_title (const std::string& title) +{ + impl->m_title = title; +} + +void +Builder::set_body (const std::string& body) +{ + impl->m_body = body; +} + +void +Builder::set_icon_name (const std::string& icon_name) +{ + impl->m_icon_name = icon_name; +} + +void +Builder::set_timeout (const std::chrono::seconds& duration) +{ + impl->m_duration = duration; +} + +void +Builder::add_hint (const std::string& name) +{ + impl->m_string_hints.insert (name); +} + +void +Builder::add_action (const std::string& action, const std::string& label) +{ + impl->m_actions.push_back(std::pair(action,label)); +} + +void +Builder::set_closed_callback (std::function cb) +{ + impl->m_closed_callback.swap (cb); +} + +/*** +**** +***/ + +class Engine::Impl +{ + struct notification_data + { + std::shared_ptr nn; + std::function closed_callback; + }; + +public: + + Impl(const std::string& app_name): + m_app_name(app_name) + { + if (!notify_init(app_name.c_str())) + g_critical("Unable to initialize libnotify!"); + + // put the server capabilities into m_caps + auto caps_gl = notify_get_server_caps(); + std::string caps_str; + for(auto l=caps_gl; l!=nullptr; l=l->next) + { + m_caps.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); + } + + ~Impl() + { + close_all (); + + notify_uninit (); + } + + const std::string& app_name() const + { + return m_app_name; + } + + bool supports_actions() const + { + return m_caps.count("actions") != 0; + } + + bool close_all () + { + bool all_closed = true; + + // close() removes the item from m_notifications, + // so increment the iterator before it gets invalidated + for (auto it=m_notifications.begin(), end=m_notifications.end(); it!=end; ) + { + const int key = it->first; + ++it; + if (!close (key)) + all_closed = false; + } + + return all_closed; + } + + bool close (int key) + { + bool is_closed = true; + + // if we've got this one... + auto it = m_notifications.find(key); + if (it != m_notifications.end()) + { + GError * error = nullptr; + is_closed = notify_notification_close (it->second.nn.get(), &error); + if (!is_closed) + { + g_warning ("Unable to close notification %d: %s", key, error->message); + g_error_free (error); + } + on_closed (key); + } + + return is_closed; + } + + int show (const Builder& builder) + { + int ret = -1; + const auto& info = *builder.impl; + + auto * nn = notify_notification_new (info.m_title.c_str(), + info.m_body.c_str(), + info.m_icon_name.c_str()); + + if (info.m_duration.count() != 0) + { + const auto& d= info.m_duration; + auto ms = std::chrono::duration_cast(d); + + notify_notification_set_hint (nn, + HINT_TIMEOUT, + g_variant_new_int32(ms.count())); + } + + for (const auto& hint : info.m_string_hints) + { + notify_notification_set_hint (nn, + hint.c_str(), + g_variant_new_boolean(true)); + } + + for (const auto& action : info.m_actions) + { + notify_notification_add_action (nn, + action.first.c_str(), + action.second.c_str(), + on_notification_clicked, + nullptr, + nullptr); + } + + // if we can show it, keep it + GError * error = nullptr; + if (notify_notification_show(nn, &error)) + { + static int next_key = 1; + const int key = next_key++; + + g_signal_connect (nn, "closed", + G_CALLBACK(on_notification_closed), this); + g_object_set_qdata (G_OBJECT(nn), + notification_key_quark(), + GINT_TO_POINTER(key)); + + notification_data ndata; + ndata.closed_callback = info.m_closed_callback; + ndata.nn.reset(nn, [this](NotifyNotification * n) { + g_signal_handlers_disconnect_by_data(n, this); + g_object_unref (G_OBJECT(n)); + }); + + m_notifications[key] = ndata; + ret = key; + } + else + { + g_critical ("Unable to show notification for '%s': %s", info.m_title.c_str(), error->message); + g_error_free (error); + g_object_unref (nn); + } + + return ret; + } + +private: + + static void on_notification_clicked (NotifyNotification * nn, + char * action, + gpointer) + { + g_object_set_qdata_full (G_OBJECT(nn), + notification_action_quark(), + g_strdup(action), + g_free); + } + + static void on_notification_closed (NotifyNotification * nn, gpointer gself) + { + const GQuark q = notification_key_quark(); + const int key = GPOINTER_TO_INT(g_object_get_qdata(G_OBJECT(nn), q)); + static_cast(gself)->on_closed (key); + } + + void on_closed (int key) + { + auto it = m_notifications.find(key); + g_return_if_fail (it != m_notifications.end()); + + const auto& ndata = it->second; + auto nn = ndata.nn.get(); + if (ndata.closed_callback) + { + std::string action; + + const auto q = notification_action_quark(); + const auto p = g_object_get_qdata (G_OBJECT(nn), q); + if (p != nullptr) + action = static_cast(p); + + ndata.closed_callback (action); + } + + g_signal_handlers_disconnect_by_data(nn, this); + m_notifications.erase(it); + } + + /*** + **** + ***/ + + const std::string m_app_name; + std::map m_notifications; + std::set m_caps; + + static constexpr char const * HINT_TIMEOUT {"x-canonical-snap-decisions-timeout"}; +}; + +/*** +**** +***/ + +Engine::Engine(const std::string& app_name): + impl(new Impl(app_name)) +{ +} + +Engine::~Engine() +{ +} + +bool +Engine::supports_actions() const +{ + return impl->supports_actions(); +} + +int +Engine::show(const Builder& builder) +{ + return impl->show(builder); +} + +bool +Engine::close_all() +{ + return impl->close_all(); +} + +bool +Engine::close(int key) +{ + return impl->close(key); +} + +const std::string& +Engine::app_name() const +{ + return impl->app_name(); +} + +/*** +**** +***/ + +} // namespace notifications +} // namespace indicator +} // namespace unity + diff --git a/src/snap.cpp b/src/snap.cpp index c21a398..4da4b45 100644 --- a/src/snap.cpp +++ b/src/snap.cpp @@ -17,25 +17,20 @@ * Charles Kerr */ -#include -#include -#include #include -#include +#include +#include #include -#include #include -#include #include -#include -#include // std::call_once() -#include #include +namespace uin = unity::indicator::notifications; + namespace unity { namespace indicator { namespace datetime { @@ -44,175 +39,8 @@ namespace datetime { **** ***/ -namespace -{ - -static constexpr char const * APP_NAME = {"indicator-datetime-service"}; - -/** - * Plays a sound, possibly looping. - */ -class Sound -{ - typedef Sound Self; - -public: - - Sound(const std::shared_ptr& 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)) - { - // 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_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()); - } - - m_play = gst_element_factory_make("playbin", "play"); - - 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); - - play(); - } - - ~Sound() - { - stop(); - - g_source_remove(m_watch_source); - - if (m_play != nullptr) - { - gst_element_set_state (m_play, GST_STATE_NULL); - g_clear_pointer (&m_play, gst_object_unref); - } - } - -private: - - void stop() - { - if (m_play != nullptr) - { - gst_element_set_state (m_play, GST_STATE_PAUSED); - } - } - - void play() - { - g_return_if_fail(m_play != nullptr); - - 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); - } - - // 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)); - } - - static gboolean bus_callback(GstBus*, GstMessage* msg, gpointer gself) - { - auto self = static_cast(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); - } - - return G_SOURCE_CONTINUE; // keep listening - } - - /*** - **** - ***/ - - const std::shared_ptr 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; -}; - -class SoundBuilder -{ -public: - void set_clock(const std::shared_ptr& 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); - } - -private: - std::shared_ptr m_clock; - std::string m_uri; - unsigned int m_volume = 50; - unsigned int m_duration_minutes = 30; - bool m_looping = true; -}; - -/** -*** libnotify -- snap decisions -**/ - -std::string get_alarm_uri(const Appointment& appointment, - const std::shared_ptr& settings) +static std::string get_alarm_uri(const Appointment& appointment, + const std::shared_ptr& settings) { const char* FALLBACK {"/usr/share/sounds/ubuntu/ringtones/Suru arpeggio.ogg"}; @@ -244,384 +72,21 @@ std::string get_alarm_uri(const Appointment& appointment, return uri; } -int32_t n_existing_snaps = 0; - -} // unnamed namespace - -/** - * A popup notification (with optional sound) - * that emits a Response signal when done. - */ -class Snap::Popup -{ -public: - - Popup(const Appointment& appointment, const SoundBuilder& sound_builder): - m_appointment(appointment), - m_interactive(get_interactive()), - m_sound_builder(sound_builder), - m_cancellable(g_cancellable_new()) - { - g_bus_get (G_BUS_TYPE_SYSTEM, m_cancellable, on_system_bus_ready, this); - - show(); - } - - ~Popup() - { - if (m_cancellable != nullptr) - { - g_cancellable_cancel (m_cancellable); - g_clear_object (&m_cancellable); - } - - if (m_system_bus != nullptr) - { - unforce_awake (); - unforce_screen (); - g_clear_object (&m_system_bus); - } - - if (m_nn != nullptr) - { - notify_notification_clear_actions(m_nn); - g_signal_handlers_disconnect_by_data(m_nn, this); - g_clear_object(&m_nn); - } - } - - typedef enum - { - RESPONSE_SHOW, - RESPONSE_DISMISS, - RESPONSE_CLOSE - } - Response; - - core::Signal& response() { return m_response; } - -private: - - void show() - { - const Appointment& appointment = m_appointment; - - /// 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(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); - } - - 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); - } - - // user clicked 'show' - static void on_snap_show(NotifyNotification*, gchar*, gpointer gself) - { - auto self = static_cast(gself); - self->m_response_value = RESPONSE_SHOW; - self->m_sound.reset(); - } - - // user clicked 'dismiss' - static void on_snap_dismiss(NotifyNotification*, gchar*, gpointer gself) - { - auto self = static_cast(gself); - self->m_response_value = RESPONSE_DISMISS; - self->m_sound.reset(); - } - - // 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); - } - - /*** - **** Interactive - ***/ - - 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); - - 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; - } - - static bool get_interactive() - { - static bool interactive; - - static std::once_flag once; - std::call_once(once, [](){ - interactive = get_server_caps().count("actions") != 0; - }); - - return interactive; - } - - /*** - **** - ***/ - - static void on_system_bus_ready (GObject *, GAsyncResult *res, gpointer gself) - { - GError * error; - GDBusConnection * system_bus; - - error = nullptr; - system_bus = g_bus_get_finish (res, &error); - if (error != nullptr) - { - if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) - g_warning ("Unable to get bus: %s", error->message); - - g_error_free (error); - } - else if (system_bus != nullptr) - { - auto self = static_cast(gself); - - self->m_system_bus = G_DBUS_CONNECTION (g_object_ref (system_bus)); - - // ask powerd to keep the system awake - static constexpr int32_t POWERD_SYS_STATE_ACTIVE = 1; - g_dbus_connection_call (system_bus, - BUS_POWERD_NAME, - BUS_POWERD_PATH, - BUS_POWERD_INTERFACE, - "requestSysState", - g_variant_new("(si)", APP_NAME, POWERD_SYS_STATE_ACTIVE), - G_VARIANT_TYPE("(s)"), - G_DBUS_CALL_FLAGS_NONE, - -1, - self->m_cancellable, - on_force_awake_response, - self); - - // ask unity-system-compositor to turn on the screen - g_dbus_connection_call (system_bus, - BUS_SCREEN_NAME, - BUS_SCREEN_PATH, - BUS_SCREEN_INTERFACE, - "keepDisplayOn", - nullptr, - G_VARIANT_TYPE("(i)"), - G_DBUS_CALL_FLAGS_NONE, - -1, - self->m_cancellable, - on_force_screen_response, - self); - - g_object_unref (system_bus); - } - } - - static void on_force_awake_response (GObject * connection, - GAsyncResult * res, - gpointer gself) - { - GError * error; - GVariant * args; - - error = nullptr; - args = g_dbus_connection_call_finish (G_DBUS_CONNECTION(connection), res, &error); - if (error != nullptr) - { - if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) - g_warning ("Unable to inhibit sleep: %s", error->message); - - g_error_free (error); - } - else - { - auto self = static_cast(gself); - - g_clear_pointer (&self->m_awake_cookie, g_free); - g_variant_get (args, "(s)", &self->m_awake_cookie); - g_debug ("m_awake_cookie is now '%s'", self->m_awake_cookie); - - g_variant_unref (args); - } - } - - static void on_force_screen_response (GObject * connection, - GAsyncResult * res, - gpointer gself) - { - GError * error; - GVariant * args; - - error = nullptr; - args = g_dbus_connection_call_finish (G_DBUS_CONNECTION(connection), res, &error); - if (error != nullptr) - { - if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) - g_warning ("Unable to turn on the screen: %s", error->message); - - g_error_free (error); - } - else - { - auto self = static_cast(gself); - - self->m_screen_cookie = NO_SCREEN_COOKIE; - g_variant_get (args, "(i)", &self->m_screen_cookie); - g_debug ("m_screen_cookie is now '%d'", self->m_screen_cookie); - - g_variant_unref (args); - } - } - - void unforce_awake () - { - g_return_if_fail (G_IS_DBUS_CONNECTION(m_system_bus)); - - if (m_awake_cookie != nullptr) - { - g_dbus_connection_call (m_system_bus, - BUS_POWERD_NAME, - BUS_POWERD_PATH, - BUS_POWERD_INTERFACE, - "clearSysState", - g_variant_new("(s)", m_awake_cookie), - nullptr, - G_DBUS_CALL_FLAGS_NONE, - -1, - nullptr, - nullptr, - nullptr); - - g_clear_pointer (&m_awake_cookie, g_free); - } - } - - void unforce_screen () - { - g_return_if_fail (G_IS_DBUS_CONNECTION(m_system_bus)); - - if (m_screen_cookie != NO_SCREEN_COOKIE) - { - g_dbus_connection_call (m_system_bus, - BUS_SCREEN_NAME, - BUS_SCREEN_PATH, - BUS_SCREEN_INTERFACE, - "removeDisplayOnRequest", - g_variant_new("(i)", m_screen_cookie), - nullptr, - G_DBUS_CALL_FLAGS_NONE, - -1, - nullptr, - nullptr, - nullptr); - - m_screen_cookie = NO_SCREEN_COOKIE; - } - } - - /*** - **** - ***/ - - typedef Popup Self; - - const Appointment m_appointment; - const bool m_interactive; - SoundBuilder m_sound_builder; - std::unique_ptr m_sound; - core::Signal m_response; - Response m_response_value = RESPONSE_CLOSE; - NotifyNotification* m_nn = nullptr; - GCancellable * m_cancellable = nullptr; - GDBusConnection * m_system_bus = nullptr; - char * m_awake_cookie = nullptr; - int32_t m_screen_cookie = NO_SCREEN_COOKIE; - - static constexpr int32_t NO_SCREEN_COOKIE { std::numeric_limits::min() }; - - 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"}; -}; - /*** **** ***/ -Snap::Snap(const std::shared_ptr& clock, +Snap::Snap(const std::shared_ptr& engine, const std::shared_ptr& settings): - m_clock(clock), + m_engine(engine), m_settings(settings) { - if (!n_existing_snaps++ && !notify_init(APP_NAME)) - g_critical("libnotify initialization failed"); } Snap::~Snap() { - for (auto popup : m_pending) - delete popup; - - if (!--n_existing_snaps) - notify_uninit(); + for (const auto& key : m_notifications) + m_engine->close (key); } void Snap::operator()(const Appointment& appointment, @@ -634,38 +99,52 @@ void Snap::operator()(const Appointment& 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); - - m_pending.insert(popup); - - // listen for it to finish... - popup->response().connect([this, - appointment, - show, - dismiss, - popup](Popup::Response response){ - - m_pending.erase(popup); - - // we can't delete the Popup inside its response() signal handler - // because core::signal deadlocks, 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) + // force the system to stay awake + auto awake = std::make_shared(m_engine->app_name()); + + // create the sound..., + const auto uri = get_alarm_uri(appointment, m_settings); + const auto volume = m_settings->alarm_volume.get(); + const bool loop = m_engine->supports_actions(); + auto sound = std::make_shared(uri, volume, loop); + + // show a notification... + const auto minutes = m_settings->alarm_duration.get(); + const bool interactive = m_engine->supports_actions(); + uin::Builder notification_builder; + notification_builder.set_body (appointment.summary); + notification_builder.set_icon_name ("alarm-clock"); + notification_builder.add_hint (uin::Builder::HINT_SNAP); + notification_builder.add_hint (uin::Builder::HINT_TINT); + const auto timestr = appointment.begin.format (_("%a, %X")); + auto title = g_strdup_printf (_("Alarm %s"), timestr.c_str()); + notification_builder.set_title (title); + g_free (title); + notification_builder.set_timeout (std::chrono::duration_cast(std::chrono::minutes(minutes))); + if (interactive) { + notification_builder.add_action ("show", _("Show")); + notification_builder.add_action ("dismiss", _("Dismiss")); + } + + // add the 'sound' and 'awake' objects to the capture so that + // they stay alive until the closed callback is called; i.e., + // for the lifespan of the notficiation + notification_builder.set_closed_callback([appointment, + show, + dismiss, + sound, + awake](const std::string& action){ + if (action == "show") show(appointment); - else if (response == Popup::RESPONSE_DISMISS) + else dismiss(appointment); }); + + const auto key = m_engine->show (notification_builder); + if (key) + m_notifications.insert (key); + else + show(appointment); } /*** diff --git a/src/sound.cpp b/src/sound.cpp new file mode 100644 index 0000000..bf2284e --- /dev/null +++ b/src/sound.cpp @@ -0,0 +1,161 @@ +/* + * Copyright 2014 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranties of + * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + * + * Authors: + * Charles Kerr + */ + +#include + +#include + +#include // std::call_once() + +namespace unity { +namespace indicator { +namespace notifications { + +/*** +**** +***/ + +/** + * Plays a sound, possibly looping. + */ +class Sound::Impl +{ +public: + + Impl(const std::string& uri, + unsigned int volume, + bool loop): + m_uri(uri), + m_volume(volume), + m_loop(loop) + { + // 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); + } + }); + + m_play = gst_element_factory_make("playbin", "play"); + + 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); + + g_debug("Playing '%s'", m_uri.c_str()); + play(); + } + + ~Impl() + { + stop(); + + g_source_remove(m_watch_source); + + if (m_play != nullptr) + { + gst_element_set_state (m_play, GST_STATE_NULL); + g_clear_pointer (&m_play, gst_object_unref); + } + } + +private: + + void stop() + { + if (m_play != nullptr) + { + gst_element_set_state (m_play, GST_STATE_PAUSED); + } + } + + void play() + { + g_return_if_fail(m_play != nullptr); + + 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); + } + + // 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)); + } + + static gboolean bus_callback(GstBus*, GstMessage* msg, gpointer gself) + { + auto self = static_cast(gself); + + if ((GST_MESSAGE_TYPE(msg) == GST_MESSAGE_EOS) && (self->m_loop)) + { + 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); + } + + return G_SOURCE_CONTINUE; // keep listening + } + + /*** + **** + ***/ + + const std::string m_uri; + const unsigned int m_volume; + const bool m_loop; + guint m_watch_source = 0; + GstElement* m_play = nullptr; +}; + +Sound::Sound(const std::string& uri, unsigned int volume, bool loop): + impl (new Impl(uri, volume, loop)) +{ +} + +Sound::~Sound() +{ +} + +/*** +**** +***/ + +} // namespace notifications +} // namespace indicator +} // namespace unity diff --git a/tests/manual-test-snap.cpp b/tests/manual-test-snap.cpp index cc24a67..7d4403d 100644 --- a/tests/manual-test-snap.cpp +++ b/tests/manual-test-snap.cpp @@ -22,13 +22,13 @@ #include #include #include +#include #include using namespace unity::indicator::datetime; -#define TIMEZONE_FILE ("/etc/timezone") - +namespace uin = unity::indicator::notifications; /*** **** @@ -94,11 +94,12 @@ int main(int argc, const char* argv[]) auto settings = std::make_shared(); settings->alarm_volume.set(volume); - auto timezones = std::make_shared(settings, TIMEZONE_FILE); - auto clock = std::make_shared(timezones); - Snap snap (clock, settings); + + auto notification_engine = std::make_shared("indicator-datetime-service"); + Snap snap (notification_engine, settings); snap(a, show, dismiss); g_main_loop_run(loop); + g_main_loop_unref(loop); return 0; } diff --git a/tests/test-snap.cpp b/tests/test-snap.cpp index f9e7a66..9a049fa 100644 --- a/tests/test-snap.cpp +++ b/tests/test-snap.cpp @@ -21,11 +21,11 @@ #include #include #include -#include -#include +#include +#include -#include +#include #include @@ -37,6 +37,11 @@ using namespace unity::indicator::datetime; **** ***/ +namespace +{ + static constexpr char const * APP_NAME {"indicator-datetime-service"}; +} + using namespace unity::indicator::datetime; class SnapFixture: public GlibFixture @@ -60,18 +65,19 @@ protected: static constexpr char const * POWERD_METHOD_REQUEST_SYS_STATE {"requestSysState"}; static constexpr char const * POWERD_METHOD_CLEAR_SYS_STATE {"clearSysState"}; - static constexpr int NOTIFY_ID {1234}; + static constexpr int FIRST_NOTIFY_ID {1000}; static constexpr int NOTIFICATION_CLOSED_EXPIRED {1}; static constexpr int NOTIFICATION_CLOSED_DISMISSED {2}; static constexpr int NOTIFICATION_CLOSED_API {3}; static constexpr int NOTIFICATION_CLOSED_UNDEFINED {4}; - static constexpr char const * APP_NAME {"indicator-datetime-service"}; - - static constexpr char const * METHOD_NOTIFY {"Notify"}; + static constexpr char const * METHOD_CLOSE {"CloseNotification"}; static constexpr char const * METHOD_GET_CAPS {"GetCapabilities"}; static constexpr char const * METHOD_GET_INFO {"GetServerInformation"}; + static constexpr char const * METHOD_NOTIFY {"Notify"}; + + static constexpr char const * SIGNAL_CLOSED {"NotificationClosed"}; static constexpr char const * HINT_TIMEOUT {"x-canonical-snap-decisions-timeout"}; @@ -118,7 +124,8 @@ protected: NOTIFY_INTERFACE, &error); g_assert_no_error(error); - + + // METHOD_GET_INFO str = g_strdup("ret = ('mock-notify', 'test vendor', '1.0', '1.1')"); dbus_test_dbus_mock_object_add_method(notify_mock, notify_obj, @@ -130,7 +137,14 @@ protected: g_assert_no_error (error); g_free (str); - str = g_strdup_printf ("ret = %d", NOTIFY_ID); + // METHOD_NOTIFY + str = g_strdup_printf("try:\n" + " self.NextNotifyId\n" + "except AttributeError:\n" + " self.NextNotifyId = %d\n" + "ret = self.NextNotifyId\n" + "self.NextNotifyId += 1\n", + FIRST_NOTIFY_ID); dbus_test_dbus_mock_object_add_method(notify_mock, notify_obj, METHOD_NOTIFY, @@ -141,6 +155,21 @@ protected: g_assert_no_error (error); g_free (str); + // METHOD_CLOSE + str = g_strdup_printf("self.EmitSignal('%s', '%s', 'uu', [ args[0], %d ])", + NOTIFY_INTERFACE, + SIGNAL_CLOSED, + NOTIFICATION_CLOSED_API); + dbus_test_dbus_mock_object_add_method(notify_mock, + notify_obj, + METHOD_CLOSE, + G_VARIANT_TYPE("(u)"), + nullptr, + str, + &error); + g_assert_no_error (error); + g_free (str); + dbus_test_service_add_task(service, DBUS_TEST_TASK(notify_mock)); /// @@ -226,14 +255,10 @@ protected: ASSERT_NE(nullptr, system_bus); g_dbus_connection_set_exit_on_close(system_bus, FALSE); g_object_add_weak_pointer(G_OBJECT(system_bus), (gpointer *)&system_bus); - - notify_init(APP_NAME); } virtual void TearDown() { - notify_uninit(); - g_clear_object(&screen_mock); g_clear_object(&powerd_mock); g_clear_object(¬ify_mock); @@ -291,10 +316,8 @@ TEST_F(SnapFixture, InteractiveDuration) static constexpr int duration_minutes = 120; auto settings = std::make_shared(); settings->alarm_duration.set(duration_minutes); - auto timezones = std::make_shared(); - auto clock = std::make_shared(timezones); - - Snap snap (clock, settings); + auto ne = std::make_shared(APP_NAME); + Snap snap (ne, settings); make_interactive(); @@ -333,6 +356,7 @@ TEST_F(SnapFixture, InteractiveDuration) const auto duration = std::chrono::minutes(duration_minutes); EXPECT_EQ(std::chrono::duration_cast(duration).count(), i32); g_variant_unref(hints); + ne.reset(); } /*** @@ -341,12 +365,9 @@ TEST_F(SnapFixture, InteractiveDuration) TEST_F(SnapFixture, InhibitSleep) { - //static constexpr int duration_minutes = 120; auto settings = std::make_shared(); - //settings->alarm_duration.set(duration_minutes); - auto timezones = std::make_shared(); - auto clock = std::make_shared(timezones); - auto snap = new Snap (clock, settings); + auto ne = std::make_shared(APP_NAME); + auto snap = new Snap (ne, settings); make_interactive(); @@ -372,6 +393,7 @@ TEST_F(SnapFixture, InhibitSleep) &error)); // force-close the snap + wait_msec(100); delete snap; wait_msec(100); @@ -398,12 +420,9 @@ TEST_F(SnapFixture, InhibitSleep) TEST_F(SnapFixture, ForceScreen) { - //static constexpr int duration_minutes = 120; auto settings = std::make_shared(); - //settings->alarm_duration.set(duration_minutes); - auto timezones = std::make_shared(); - auto clock = std::make_shared(timezones); - auto snap = new Snap (clock, settings); + auto ne = std::make_shared(APP_NAME); + auto snap = new Snap (ne, settings); make_interactive(); @@ -423,6 +442,7 @@ TEST_F(SnapFixture, ForceScreen) g_assert_no_error(error); // force-close the snap + wait_msec(100); delete snap; wait_msec(100); -- cgit v1.2.3 From a1e8addae51d3f8a026630b47b0c42c11cd52507 Mon Sep 17 00:00:00 2001 From: Charles Kerr Date: Sun, 27 Jul 2014 00:08:29 -0500 Subject: copyediting --- src/notifications.cpp | 2 +- src/snap.cpp | 29 +++++++++++++---------------- 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/src/notifications.cpp b/src/notifications.cpp index 41ced42..486a910 100644 --- a/src/notifications.cpp +++ b/src/notifications.cpp @@ -160,7 +160,7 @@ public: // close() removes the item from m_notifications, // so increment the iterator before it gets invalidated - for (auto it=m_notifications.begin(), end=m_notifications.end(); it!=end; ) + for (auto it=m_notifications.begin(), e=m_notifications.end(); it!=e; ) { const int key = it->first; ++it; diff --git a/src/snap.cpp b/src/snap.cpp index 4da4b45..d99e5ef 100644 --- a/src/snap.cpp +++ b/src/snap.cpp @@ -109,38 +109,35 @@ void Snap::operator()(const Appointment& appointment, auto sound = std::make_shared(uri, volume, loop); // show a notification... - const auto minutes = m_settings->alarm_duration.get(); + const auto minutes = std::chrono::minutes(m_settings->alarm_duration.get()); const bool interactive = m_engine->supports_actions(); - uin::Builder notification_builder; - notification_builder.set_body (appointment.summary); - notification_builder.set_icon_name ("alarm-clock"); - notification_builder.add_hint (uin::Builder::HINT_SNAP); - notification_builder.add_hint (uin::Builder::HINT_TINT); + uin::Builder b; + b.set_body (appointment.summary); + b.set_icon_name ("alarm-clock"); + b.add_hint (uin::Builder::HINT_SNAP); + b.add_hint (uin::Builder::HINT_TINT); const auto timestr = appointment.begin.format (_("%a, %X")); auto title = g_strdup_printf (_("Alarm %s"), timestr.c_str()); - notification_builder.set_title (title); + b.set_title (title); g_free (title); - notification_builder.set_timeout (std::chrono::duration_cast(std::chrono::minutes(minutes))); + b.set_timeout (std::chrono::duration_cast(minutes)); if (interactive) { - notification_builder.add_action ("show", _("Show")); - notification_builder.add_action ("dismiss", _("Dismiss")); + b.add_action ("show", _("Show")); + b.add_action ("dismiss", _("Dismiss")); } // add the 'sound' and 'awake' objects to the capture so that // they stay alive until the closed callback is called; i.e., // for the lifespan of the notficiation - notification_builder.set_closed_callback([appointment, - show, - dismiss, - sound, - awake](const std::string& action){ + b.set_closed_callback([appointment, show, dismiss, sound, awake] + (const std::string& action){ if (action == "show") show(appointment); else dismiss(appointment); }); - const auto key = m_engine->show (notification_builder); + const auto key = m_engine->show(b); if (key) m_notifications.insert (key); else -- cgit v1.2.3 From 31d63005e2937f7712a515f56645fe3c34628ca8 Mon Sep 17 00:00:00 2001 From: Charles Kerr Date: Sun, 27 Jul 2014 00:08:34 -0500 Subject: in sound.cpp, check the return value of gst_init_check() --- src/sound.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/sound.cpp b/src/sound.cpp index bf2284e..052b168 100644 --- a/src/sound.cpp +++ b/src/sound.cpp @@ -49,8 +49,7 @@ public: static std::once_flag once; std::call_once(once, [](){ GError* error = nullptr; - gst_init_check (nullptr, nullptr, &error); - if (error) + if (!gst_init_check (nullptr, nullptr, &error)) { g_critical("Unable to play alarm sound: %s", error->message); g_error_free(error); -- cgit v1.2.3 From 7271b2139a5c600a2c3cdb4e552e05ddb0f374dd Mon Sep 17 00:00:00 2001 From: Charles Kerr Date: Sun, 27 Jul 2014 10:51:54 -0500 Subject: make close return void instead of bool, because after all what more can you do if the call fails? What's the point? --- include/notifications/notifications.h | 4 ++-- src/notifications.cpp | 26 ++++++++------------------ 2 files changed, 10 insertions(+), 20 deletions(-) diff --git a/include/notifications/notifications.h b/include/notifications/notifications.h index b4c88b4..43442d3 100644 --- a/include/notifications/notifications.h +++ b/include/notifications/notifications.h @@ -96,11 +96,11 @@ public: /** Close a notification. @param key the int returned by show() @return true if the notification was closed. */ - bool close(int key); + void close(int key); /** Close all remaining notifications. *@return true if all closed successfully. */ - bool close_all(); + void close_all(); const std::string& app_name() const; diff --git a/src/notifications.cpp b/src/notifications.cpp index 486a910..6d993df 100644 --- a/src/notifications.cpp +++ b/src/notifications.cpp @@ -154,42 +154,32 @@ public: return m_caps.count("actions") != 0; } - bool close_all () + void close_all () { - bool all_closed = true; - // close() removes the item from m_notifications, // so increment the iterator before it gets invalidated for (auto it=m_notifications.begin(), e=m_notifications.end(); it!=e; ) { const int key = it->first; ++it; - if (!close (key)) - all_closed = false; + close (key); } - - return all_closed; } - bool close (int key) + void close (int key) { - bool is_closed = true; - // if we've got this one... auto it = m_notifications.find(key); if (it != m_notifications.end()) { GError * error = nullptr; - is_closed = notify_notification_close (it->second.nn.get(), &error); - if (!is_closed) + if (!notify_notification_close (it->second.nn.get(), &error)) { g_warning ("Unable to close notification %d: %s", key, error->message); g_error_free (error); } on_closed (key); } - - return is_closed; } int show (const Builder& builder) @@ -339,16 +329,16 @@ Engine::show(const Builder& builder) return impl->show(builder); } -bool +void Engine::close_all() { - return impl->close_all(); + impl->close_all(); } -bool +void Engine::close(int key) { - return impl->close(key); + impl->close(key); } const std::string& -- cgit v1.2.3 From b0936139bfef6fe169b5c17be4b2dafa3c2e2c3a Mon Sep 17 00:00:00 2001 From: Charles Kerr Date: Sun, 27 Jul 2014 11:01:30 -0500 Subject: copyediting: comments, use anonymous namespace --- include/notifications/notifications.h | 5 +++-- src/awake.cpp | 14 ++++++++++---- src/notifications.cpp | 27 +++++++++++++++++---------- src/snap.cpp | 11 ++++++++--- src/sound.cpp | 25 ++++--------------------- 5 files changed, 42 insertions(+), 40 deletions(-) diff --git a/include/notifications/notifications.h b/include/notifications/notifications.h index 43442d3..c2e2d85 100644 --- a/include/notifications/notifications.h +++ b/include/notifications/notifications.h @@ -34,8 +34,7 @@ class Engine; /** * Helper class for showing notifications. * - * Populate the builder, with the relevant properites, - * then pass it to Engine::show(). + * Populate the builder, then pass it to Engine::show(). * * @see Engine::show(Builder) */ @@ -46,7 +45,9 @@ public: ~Builder(); void set_title (const std::string& title); + void set_body (const std::string& body); + void set_icon_name (const std::string& icon_name); /* Set an interval, after which the notification will automatically diff --git a/src/awake.cpp b/src/awake.cpp index 19826ae..57358ab 100644 --- a/src/awake.cpp +++ b/src/awake.cpp @@ -58,7 +58,9 @@ public: private: - static void on_system_bus_ready (GObject *, GAsyncResult *res, gpointer gself) + static void on_system_bus_ready (GObject *, + GAsyncResult *res, + gpointer gself) { GError * error; GDBusConnection * system_bus; @@ -119,7 +121,9 @@ private: GVariant * args; error = nullptr; - args = g_dbus_connection_call_finish (G_DBUS_CONNECTION(connection), res, &error); + args = g_dbus_connection_call_finish (G_DBUS_CONNECTION(connection), + res, + &error); if (error != nullptr) { if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED) && @@ -150,7 +154,9 @@ private: GVariant * args; error = nullptr; - args = g_dbus_connection_call_finish (G_DBUS_CONNECTION(connection), res, &error); + args = g_dbus_connection_call_finish (G_DBUS_CONNECTION(connection), + res, + &error); if (error != nullptr) { if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED) && @@ -233,7 +239,7 @@ private: ***/ Awake::Awake(const std::string& app_name): - impl(new Impl (app_name)) + impl(new Impl (app_name)) { } diff --git a/src/notifications.cpp b/src/notifications.cpp index 6d993df..bad4b1a 100644 --- a/src/notifications.cpp +++ b/src/notifications.cpp @@ -168,10 +168,11 @@ public: void close (int key) { - // if we've got this one... auto it = m_notifications.find(key); if (it != m_notifications.end()) { + // tell the server to close, call the close() callback, + // and immediately forget about the nn. GError * error = nullptr; if (!notify_notification_close (it->second.nn.get(), &error)) { @@ -187,9 +188,9 @@ public: int ret = -1; const auto& info = *builder.impl; - auto * nn = notify_notification_new (info.m_title.c_str(), - info.m_body.c_str(), - info.m_icon_name.c_str()); + auto nn = notify_notification_new (info.m_title.c_str(), + info.m_body.c_str(), + info.m_icon_name.c_str()); if (info.m_duration.count() != 0) { @@ -243,7 +244,9 @@ public: } else { - g_critical ("Unable to show notification for '%s': %s", info.m_title.c_str(), error->message); + g_critical ("Unable to show notification for '%s': %s", + info.m_title.c_str(), + error->message); g_error_free (error); g_object_unref (nn); } @@ -266,8 +269,8 @@ private: static void on_notification_closed (NotifyNotification * nn, gpointer gself) { const GQuark q = notification_key_quark(); - const int key = GPOINTER_TO_INT(g_object_get_qdata(G_OBJECT(nn), q)); - static_cast(gself)->on_closed (key); + const gpointer gkey = g_object_get_qdata(G_OBJECT(nn), q); + static_cast(gself)->on_closed(GPOINTER_TO_INT(gkey)); } void on_closed (int key) @@ -281,8 +284,8 @@ private: { std::string action; - const auto q = notification_action_quark(); - const auto p = g_object_get_qdata (G_OBJECT(nn), q); + const GQuark q = notification_action_quark(); + const gpointer p = g_object_get_qdata(G_OBJECT(nn), q); if (p != nullptr) action = static_cast(p); @@ -298,7 +301,11 @@ private: ***/ const std::string m_app_name; + + // key-to-data std::map m_notifications; + + // server capabilities std::set m_caps; static constexpr char const * HINT_TIMEOUT {"x-canonical-snap-decisions-timeout"}; @@ -320,7 +327,7 @@ Engine::~Engine() bool Engine::supports_actions() const { - return impl->supports_actions(); + return true;//impl->supports_actions(); } int diff --git a/src/snap.cpp b/src/snap.cpp index d99e5ef..e9df256 100644 --- a/src/snap.cpp +++ b/src/snap.cpp @@ -39,8 +39,11 @@ namespace datetime { **** ***/ -static std::string get_alarm_uri(const Appointment& appointment, - const std::shared_ptr& settings) +namespace // unnamed namespace +{ + +std::string get_alarm_uri(const Appointment& appointment, + const std::shared_ptr& settings) { const char* FALLBACK {"/usr/share/sounds/ubuntu/ringtones/Suru arpeggio.ogg"}; @@ -72,6 +75,8 @@ static std::string get_alarm_uri(const Appointment& appointment, return uri; } +} // unnamed namespace + /*** **** ***/ @@ -102,7 +107,7 @@ void Snap::operator()(const Appointment& appointment, // force the system to stay awake auto awake = std::make_shared(m_engine->app_name()); - // create the sound..., + // create the sound... const auto uri = get_alarm_uri(appointment, m_settings); const auto volume = m_settings->alarm_volume.get(); const bool loop = m_engine->supports_actions(); diff --git a/src/sound.cpp b/src/sound.cpp index 052b168..7658658 100644 --- a/src/sound.cpp +++ b/src/sound.cpp @@ -63,13 +63,14 @@ public: gst_object_unref(bus); g_debug("Playing '%s'", m_uri.c_str()); - play(); + 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); } ~Impl() { - stop(); - g_source_remove(m_watch_source); if (m_play != nullptr) @@ -81,24 +82,6 @@ public: private: - void stop() - { - if (m_play != nullptr) - { - gst_element_set_state (m_play, GST_STATE_PAUSED); - } - } - - void play() - { - g_return_if_fail(m_play != nullptr); - - 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); - } - // convert settings range [1..100] to gst playbin's range is [0...1.0] gdouble get_volume() const { -- cgit v1.2.3 From f6df9730bd512982850b7f234f883eabbd925e7d Mon Sep 17 00:00:00 2001 From: Charles Kerr Date: Wed, 30 Jul 2014 14:53:10 -0500 Subject: remove testing stub --- src/notifications.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/notifications.cpp b/src/notifications.cpp index bad4b1a..da7351b 100644 --- a/src/notifications.cpp +++ b/src/notifications.cpp @@ -327,7 +327,7 @@ Engine::~Engine() bool Engine::supports_actions() const { - return true;//impl->supports_actions(); + return impl->supports_actions(); } int -- cgit v1.2.3 From d6b290fda978379fb07285aaddfeb31686735667 Mon Sep 17 00:00:00 2001 From: Charles Kerr Date: Wed, 30 Jul 2014 14:54:37 -0500 Subject: move Snap's guts into an Impl class --- include/datetime/snap.h | 6 +- src/snap.cpp | 185 +++++++++++++++++++++++++++--------------------- 2 files changed, 107 insertions(+), 84 deletions(-) diff --git a/include/datetime/snap.h b/include/datetime/snap.h index 78d9f65..ef5c868 100644 --- a/include/datetime/snap.h +++ b/include/datetime/snap.h @@ -27,7 +27,6 @@ #include #include -#include namespace unity { namespace indicator { @@ -49,9 +48,8 @@ public: appointment_func dismiss); private: - const std::shared_ptr m_engine; - const std::shared_ptr m_settings; - std::set m_notifications; + class Impl; + std::unique_ptr impl; }; } // namespace datetime diff --git a/src/snap.cpp b/src/snap.cpp index e9df256..0eb176c 100644 --- a/src/snap.cpp +++ b/src/snap.cpp @@ -27,6 +27,7 @@ #include #include +#include #include namespace uin = unity::indicator::notifications; @@ -39,114 +40,138 @@ namespace datetime { **** ***/ -namespace // unnamed namespace +class Snap::Impl { +public: -std::string get_alarm_uri(const Appointment& appointment, - const std::shared_ptr& settings) -{ - const char* FALLBACK {"/usr/share/sounds/ubuntu/ringtones/Suru arpeggio.ogg"}; - - const std::string candidates[] = { appointment.audio_url, - settings->alarm_sound.get(), - FALLBACK }; + Impl(const std::shared_ptr& engine, + const std::shared_ptr& settings): + m_engine(engine), + m_settings(settings) + { + } - std::string uri; + ~Impl() + { + for (const auto& key : m_notifications) + m_engine->close (key); + } - for(const auto& candidate : candidates) + void operator()(const Appointment& appointment, + appointment_func show, + appointment_func dismiss) { - if (gst_uri_is_valid (candidate.c_str())) + if (!appointment.has_alarms) { - uri = candidate; - break; + dismiss(appointment); + return; + } + + // force the system to stay awake + auto awake = std::make_shared(m_engine->app_name()); + + // create the sound... + const auto uri = get_alarm_uri(appointment, m_settings); + const auto volume = m_settings->alarm_volume.get(); + const bool loop = m_engine->supports_actions(); + auto sound = std::make_shared(uri, volume, loop); + + // show a notification... + const auto minutes = std::chrono::minutes(m_settings->alarm_duration.get()); + const bool interactive = m_engine->supports_actions(); + uin::Builder b; + b.set_body (appointment.summary); + b.set_icon_name ("alarm-clock"); + b.add_hint (uin::Builder::HINT_SNAP); + b.add_hint (uin::Builder::HINT_TINT); + const auto timestr = appointment.begin.format (_("%a, %X")); + auto title = g_strdup_printf (_("Alarm %s"), timestr.c_str()); + b.set_title (title); + g_free (title); + b.set_timeout (std::chrono::duration_cast(minutes)); + if (interactive) { + b.add_action ("show", _("Show")); + b.add_action ("dismiss", _("Dismiss")); } - else if (g_file_test(candidate.c_str(), G_FILE_TEST_EXISTS)) + + // add the 'sound' and 'awake' objects to the capture so that + // they stay alive until the closed callback is called; i.e., + // for the lifespan of the notficiation + b.set_closed_callback([appointment, show, dismiss, sound, awake] + (const std::string& action){ + if (action == "show") + show(appointment); + else + dismiss(appointment); + }); + + const auto key = m_engine->show(b); + if (key) + m_notifications.insert (key); + else + show(appointment); + } + +private: + + std::string get_alarm_uri(const Appointment& appointment, + const std::shared_ptr& settings) const + { + const char* FALLBACK {"/usr/share/sounds/ubuntu/ringtones/Suru arpeggio.ogg"}; + + const std::string candidates[] = { appointment.audio_url, + settings->alarm_sound.get(), + FALLBACK }; + + std::string uri; + + for(const auto& candidate : candidates) { - gchar* tmp = gst_filename_to_uri(candidate.c_str(), nullptr); - if (tmp != nullptr) + if (gst_uri_is_valid (candidate.c_str())) { - uri = tmp; - g_free (tmp); + 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; + } + } } - } - return uri; -} + return uri; + } -} // unnamed namespace + const std::shared_ptr m_engine; + const std::shared_ptr m_settings; + std::set m_notifications; +}; /*** **** ***/ -Snap::Snap(const std::shared_ptr& engine, +Snap::Snap(const std::shared_ptr& engine, const std::shared_ptr& settings): - m_engine(engine), - m_settings(settings) + impl(new Impl(engine, settings)) { } Snap::~Snap() { - for (const auto& key : m_notifications) - m_engine->close (key); } -void Snap::operator()(const Appointment& appointment, - appointment_func show, - appointment_func dismiss) +void +Snap::operator()(const Appointment& appointment, + appointment_func show, + appointment_func dismiss) { - if (!appointment.has_alarms) - { - dismiss(appointment); - return; - } - - // force the system to stay awake - auto awake = std::make_shared(m_engine->app_name()); - - // create the sound... - const auto uri = get_alarm_uri(appointment, m_settings); - const auto volume = m_settings->alarm_volume.get(); - const bool loop = m_engine->supports_actions(); - auto sound = std::make_shared(uri, volume, loop); - - // show a notification... - const auto minutes = std::chrono::minutes(m_settings->alarm_duration.get()); - const bool interactive = m_engine->supports_actions(); - uin::Builder b; - b.set_body (appointment.summary); - b.set_icon_name ("alarm-clock"); - b.add_hint (uin::Builder::HINT_SNAP); - b.add_hint (uin::Builder::HINT_TINT); - const auto timestr = appointment.begin.format (_("%a, %X")); - auto title = g_strdup_printf (_("Alarm %s"), timestr.c_str()); - b.set_title (title); - g_free (title); - b.set_timeout (std::chrono::duration_cast(minutes)); - if (interactive) { - b.add_action ("show", _("Show")); - b.add_action ("dismiss", _("Dismiss")); - } - - // add the 'sound' and 'awake' objects to the capture so that - // they stay alive until the closed callback is called; i.e., - // for the lifespan of the notficiation - b.set_closed_callback([appointment, show, dismiss, sound, awake] - (const std::string& action){ - if (action == "show") - show(appointment); - else - dismiss(appointment); - }); - - const auto key = m_engine->show(b); - if (key) - m_notifications.insert (key); - else - show(appointment); + (*impl)(appointment, show, dismiss); } /*** -- cgit v1.2.3 From 559d185dd7d51e56fbd8246970ef520d3edd18ae Mon Sep 17 00:00:00 2001 From: Charles Kerr Date: Wed, 30 Jul 2014 16:01:03 -0500 Subject: initial draft of haptic feedback when alarms play --- CMakeLists.txt | 3 +- debian/control | 1 + include/notifications/haptic.h | 57 ++++++++++++++++++++++++++ src/CMakeLists.txt | 1 + src/haptic.cpp | 92 ++++++++++++++++++++++++++++++++++++++++++ src/notifications.cpp | 2 +- src/snap.cpp | 8 +++- 7 files changed, 160 insertions(+), 4 deletions(-) create mode 100644 include/notifications/haptic.h create mode 100644 src/haptic.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 9b4987e..3bc7e81 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -41,7 +41,8 @@ pkg_check_modules (SERVICE_DEPS REQUIRED gstreamer-1.0>=1.2 libnotify>=0.7.6 url-dispatcher-1>=1 - properties-cpp>=0.0.1) + properties-cpp>=0.0.1 + ubuntu-platform-api>=2.2.0) include_directories (SYSTEM ${SERVICE_DEPS_INCLUDE_DIRS}) CHECK_INCLUDE_FILE(ubuntu/hardware/alarm.h HAVE_UBUNTU_HW_ALARM_H) diff --git a/debian/control b/debian/control index 77e9241..dab9096 100644 --- a/debian/control +++ b/debian/control @@ -21,6 +21,7 @@ Build-Depends: cmake, libproperties-cpp-dev, libubuntu-platform-hardware-api-headers [armhf i386 amd64], libubuntu-platform-hardware-api-dev [armhf i386 amd64], + libubuntu-application-api-dev [armhf i386 amd64], libdbustest1-dev, locales, Standards-Version: 3.9.3 diff --git a/include/notifications/haptic.h b/include/notifications/haptic.h new file mode 100644 index 0000000..3036485 --- /dev/null +++ b/include/notifications/haptic.h @@ -0,0 +1,57 @@ +/* + * Copyright 2014 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranties of + * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + * + * Authors: + * Charles Kerr + */ + +#ifndef UNITY_INDICATOR_NOTIFICATIONS_HAPTIC_H +#define UNITY_INDICATOR_NOTIFICATIONS_HAPTIC_H + +#include + +namespace unity { +namespace indicator { +namespace notifications { + +/*** +**** +***/ + +/** + * A class that forces the screen display on and inhibits sleep + */ +class Haptic +{ +public: + enum Mode { VIBRATE_DEFAULT }; + + Haptic(const Mode& mode = VIBRATE_DEFAULT); + ~Haptic(); + +private: + class Impl; + std::unique_ptr impl; +}; + +/*** +**** +***/ + +} // namespace notifications +} // namespace indicator +} // namespace unity + +#endif // UNITY_INDICATOR_NOTIFICATIONS_HAPTIC_H diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 754d537..a466a48 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -20,6 +20,7 @@ set (SERVICE_CXX_SOURCES exporter.cpp formatter.cpp formatter-desktop.cpp + haptic.cpp locations.cpp locations-settings.cpp menu.cpp diff --git a/src/haptic.cpp b/src/haptic.cpp new file mode 100644 index 0000000..c5a20df --- /dev/null +++ b/src/haptic.cpp @@ -0,0 +1,92 @@ +/* + * Copyright 2014 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranties of + * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + * + * Authors: + * Charles Kerr + */ + +#include + +#include + +#include + +namespace unity { +namespace indicator { +namespace notifications { + +/*** +**** +***/ + +class Haptic::Impl +{ +public: + + Impl(const Mode& mode): + m_mode(mode), + m_sensor(ua_sensors_haptic_new()) + { + if (m_sensor == nullptr) + g_warning ("Haptic device unavailable"); + else + m_tag = g_timeout_add_seconds (1, on_timeout, this); + } + + ~Impl() + { + if (m_tag) + g_source_remove(m_tag); + } + +private: + + static gboolean on_timeout (gpointer gself) + { + static_cast(gself)->vibrate_now(); + return G_SOURCE_CONTINUE; + } + + void vibrate_now() + { + const uint32_t msec = 1500; + ua_sensors_haptic_vibrate_once (m_sensor, msec); + } + + const Mode m_mode; + UASensorsHaptic * m_sensor = nullptr; + guint m_tag = 0; +}; + +/*** +**** +***/ + +Haptic::Haptic(const Mode& mode): + impl(new Impl (mode)) +{ +} + +Haptic::~Haptic() +{ +} + +/*** +**** +***/ + +} // namespace datetime +} // namespace indicator +} // namespace unity diff --git a/src/notifications.cpp b/src/notifications.cpp index da7351b..c66f634 100644 --- a/src/notifications.cpp +++ b/src/notifications.cpp @@ -327,7 +327,7 @@ Engine::~Engine() bool Engine::supports_actions() const { - return impl->supports_actions(); + return true; //impl->supports_actions(); } int diff --git a/src/snap.cpp b/src/snap.cpp index 0eb176c..f3e0f20 100644 --- a/src/snap.cpp +++ b/src/snap.cpp @@ -20,6 +20,7 @@ #include #include +#include #include #include @@ -76,6 +77,9 @@ public: const bool loop = m_engine->supports_actions(); auto sound = std::make_shared(uri, volume, loop); + // create the haptic feedback... + auto haptic = std::make_shared(); + // show a notification... const auto minutes = std::chrono::minutes(m_settings->alarm_duration.get()); const bool interactive = m_engine->supports_actions(); @@ -94,10 +98,10 @@ public: b.add_action ("dismiss", _("Dismiss")); } - // add the 'sound' and 'awake' objects to the capture so that + // add 'sound', 'haptic', and 'awake' objects to the capture so // they stay alive until the closed callback is called; i.e., // for the lifespan of the notficiation - b.set_closed_callback([appointment, show, dismiss, sound, awake] + b.set_closed_callback([appointment, show, dismiss, sound, awake, haptic] (const std::string& action){ if (action == "show") show(appointment); -- cgit v1.2.3 From e4b663ebd5be78bd9fb9802e54b050a7b2a984bf Mon Sep 17 00:00:00 2001 From: Charles Kerr Date: Wed, 30 Jul 2014 16:34:02 -0500 Subject: in haptic.cpp, make sure to enable the sensor by calling ua_sensors_haptic_enable() --- src/haptic.cpp | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/haptic.cpp b/src/haptic.cpp index c5a20df..663e919 100644 --- a/src/haptic.cpp +++ b/src/haptic.cpp @@ -40,13 +40,21 @@ public: m_sensor(ua_sensors_haptic_new()) { if (m_sensor == nullptr) + { g_warning ("Haptic device unavailable"); + } else - m_tag = g_timeout_add_seconds (1, on_timeout, this); + { + ua_sensors_haptic_enable(m_sensor); + m_tag = g_timeout_add_seconds (2, on_timeout, this); + } } ~Impl() { + if (m_sensor != nullptr) + ua_sensors_haptic_enable(m_sensor); + if (m_tag) g_source_remove(m_tag); } @@ -61,8 +69,8 @@ private: void vibrate_now() { - const uint32_t msec = 1500; - ua_sensors_haptic_vibrate_once (m_sensor, msec); + const uint32_t msec = 1000; + (void) ua_sensors_haptic_vibrate_once (m_sensor, msec); } const Mode m_mode; -- cgit v1.2.3 From cbb4e95519f5127b152483b00731c260dc1a7670 Mon Sep 17 00:00:00 2001 From: Charles Kerr Date: Wed, 30 Jul 2014 16:35:49 -0500 Subject: add haptic feedback to the manual tests --- tests/manual | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/manual b/tests/manual index be64863..c932004 100644 --- a/tests/manual +++ b/tests/manual @@ -30,6 +30,7 @@ Test-case indicator-datetime/new-alarm-wakeup (Note: if in doubt about sleep you can see in the syslog whether the device actually suspended or whether the suspend was aborted)
Confirm that the screen comes on when the alarm is triggered.
+
If the device supports haptic feedback, confirm the alarm vibrates.
Test-case indicator-datetime/edited-alarm-wakeup @@ -40,6 +41,7 @@ Test-case indicator-datetime/edited-alarm-wakeup (Note: if in doubt about sleep you can see in the syslog whether the device actually suspended or whether the suspend was aborted)
Confirm that the screen comes on when the alarm is triggered.
+
If the device supports haptic feedback, confirm the alarm vibrates.
Test-case indicator-datetime/tell-snap-decision-to-dismiss -- cgit v1.2.3 From 5d6108ee4440881b08b419a1c454b1db57e1d6ce Mon Sep 17 00:00:00 2001 From: Charles Kerr Date: Wed, 30 Jul 2014 17:03:08 -0500 Subject: fix doxygen comments --- include/notifications/notifications.h | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/include/notifications/notifications.h b/include/notifications/notifications.h index c2e2d85..d876a57 100644 --- a/include/notifications/notifications.h +++ b/include/notifications/notifications.h @@ -91,16 +91,14 @@ public: bool supports_actions() const; /** Show a notification. - @return nonzero on success, zero on failure. */ + @return zero on failure, or a key that can be passed to close() */ int show(const Builder& builder); /** Close a notification. - @param key the int returned by show() - @return true if the notification was closed. */ + @param key the int returned by show() */ void close(int key); - /** Close all remaining notifications. - *@return true if all closed successfully. */ + /** Close all remaining notifications. */ void close_all(); const std::string& app_name() const; -- cgit v1.2.3 From 97e7d1b81965388d66ae6aca0b1c19dc87d241e7 Mon Sep 17 00:00:00 2001 From: Charles Kerr Date: Thu, 31 Jul 2014 09:55:38 -0500 Subject: in sound.cpp, fix tab damage --- src/sound.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sound.cpp b/src/sound.cpp index 7658658..d13c854 100644 --- a/src/sound.cpp +++ b/src/sound.cpp @@ -46,7 +46,7 @@ public: m_loop(loop) { // init GST once - static std::once_flag once; + static std::once_flag once; std::call_once(once, [](){ GError* error = nullptr; if (!gst_init_check (nullptr, nullptr, &error)) -- cgit v1.2.3 From e2e259126c0d5c834135014d85430ffcfc885588 Mon Sep 17 00:00:00 2001 From: Charles Kerr Date: Thu, 31 Jul 2014 09:59:44 -0500 Subject: remove testing stub used by development --- src/notifications.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/notifications.cpp b/src/notifications.cpp index c66f634..da7351b 100644 --- a/src/notifications.cpp +++ b/src/notifications.cpp @@ -327,7 +327,7 @@ Engine::~Engine() bool Engine::supports_actions() const { - return true; //impl->supports_actions(); + return impl->supports_actions(); } int -- cgit v1.2.3 From 9d32289feda6d72e53bcf7e77e2cbef09f88165e Mon Sep 17 00:00:00 2001 From: Charles Kerr Date: Thu, 31 Jul 2014 10:33:14 -0500 Subject: in indicator::noficiations::Haptic, better naming of the Mode enum --- include/notifications/haptic.h | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/include/notifications/haptic.h b/include/notifications/haptic.h index 3036485..d82d1c9 100644 --- a/include/notifications/haptic.h +++ b/include/notifications/haptic.h @@ -36,9 +36,12 @@ namespace notifications { class Haptic { public: - enum Mode { VIBRATE_DEFAULT }; + enum Mode + { + MODE_PULSE + }; - Haptic(const Mode& mode = VIBRATE_DEFAULT); + Haptic(const Mode& mode = MODE_PULSE); ~Haptic(); private: -- cgit v1.2.3 From 5809e107390f59ad11565e8b8644c191f8430dd3 Mon Sep 17 00:00:00 2001 From: Charles Kerr Date: Thu, 31 Jul 2014 11:44:16 -0500 Subject: in haptic.cpp, start vibrating immediately --- src/haptic.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/src/haptic.cpp b/src/haptic.cpp index 663e919..c7517d1 100644 --- a/src/haptic.cpp +++ b/src/haptic.cpp @@ -47,6 +47,7 @@ public: { ua_sensors_haptic_enable(m_sensor); m_tag = g_timeout_add_seconds (2, on_timeout, this); + on_timeout (this); } } -- cgit v1.2.3 From ad789d53f4c4bb059eb52c97c53d720c7ccb7a3e Mon Sep 17 00:00:00 2001 From: Charles Kerr Date: Thu, 31 Jul 2014 11:59:12 -0500 Subject: in Haptic::Impl::~Impl(), call ua_sensors_haptic_disable(). h/t Antti --- src/haptic.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/haptic.cpp b/src/haptic.cpp index c7517d1..637c6f5 100644 --- a/src/haptic.cpp +++ b/src/haptic.cpp @@ -54,7 +54,7 @@ public: ~Impl() { if (m_sensor != nullptr) - ua_sensors_haptic_enable(m_sensor); + ua_sensors_haptic_disable(m_sensor); if (m_tag) g_source_remove(m_tag); -- cgit v1.2.3 From 0c8faf27ea83c3996c68b0ab02c0b4ed824b4129 Mon Sep 17 00:00:00 2001 From: Charles Kerr Date: Thu, 31 Jul 2014 12:01:04 -0500 Subject: in haptic.cpp, better comments --- src/haptic.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/haptic.cpp b/src/haptic.cpp index 637c6f5..90c75f7 100644 --- a/src/haptic.cpp +++ b/src/haptic.cpp @@ -45,6 +45,9 @@ public: } else { + /* We only support one vibrate mode for now: an on/off pulse at + one-second intervals. So, set a timer to go off every 2 seconds + that vibrates for one second. */ ua_sensors_haptic_enable(m_sensor); m_tag = g_timeout_add_seconds (2, on_timeout, this); on_timeout (this); -- cgit v1.2.3 From af1b645de9b8116ead5ad72583f51afe28350818 Mon Sep 17 00:00:00 2001 From: Charles Kerr Date: Thu, 31 Jul 2014 15:22:47 -0500 Subject: drop the ubuntu-application-api middleman and call usensorsd directly: ua_sensors_haptic_new() crashes on desktop and ua_sensors_haptic_vibrate_once() makes blocking dbus calls. --- CMakeLists.txt | 3 +- debian/control | 1 - include/notifications/dbus-shared.h | 4 ++ src/haptic.cpp | 80 ++++++++++++++++++++++++++----------- 4 files changed, 62 insertions(+), 26 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 3bc7e81..9b4987e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -41,8 +41,7 @@ pkg_check_modules (SERVICE_DEPS REQUIRED gstreamer-1.0>=1.2 libnotify>=0.7.6 url-dispatcher-1>=1 - properties-cpp>=0.0.1 - ubuntu-platform-api>=2.2.0) + properties-cpp>=0.0.1) include_directories (SYSTEM ${SERVICE_DEPS_INCLUDE_DIRS}) CHECK_INCLUDE_FILE(ubuntu/hardware/alarm.h HAVE_UBUNTU_HW_ALARM_H) diff --git a/debian/control b/debian/control index 15b981a..9e7133e 100644 --- a/debian/control +++ b/debian/control @@ -24,7 +24,6 @@ Build-Depends: cmake, libproperties-cpp-dev, libubuntu-platform-hardware-api-headers [armhf i386 amd64], libubuntu-platform-hardware-api-dev [armhf i386 amd64], - libubuntu-application-api-dev [armhf i386 amd64], libdbustest1-dev, locales, Standards-Version: 3.9.3 diff --git a/include/notifications/dbus-shared.h b/include/notifications/dbus-shared.h index 7738cb7..af714e7 100644 --- a/include/notifications/dbus-shared.h +++ b/include/notifications/dbus-shared.h @@ -29,4 +29,8 @@ #define BUS_POWERD_PATH "/com/canonical/powerd" #define BUS_POWERD_INTERFACE "com.canonical.powerd" +#define BUS_HAPTIC_NAME "com.canonical.usensord" +#define BUS_HAPTIC_PATH "/com/canonical/usensord/haptic" +#define BUS_HAPTIC_INTERFACE "com.canonical.usensord.haptic" + #endif /* INDICATOR_NOTIFICATIONS_DBUS_SHARED_H */ diff --git a/src/haptic.cpp b/src/haptic.cpp index 90c75f7..2b1ee21 100644 --- a/src/haptic.cpp +++ b/src/haptic.cpp @@ -17,11 +17,10 @@ * Charles Kerr */ +#include #include -#include - -#include +#include namespace unity { namespace indicator { @@ -37,34 +36,58 @@ public: Impl(const Mode& mode): m_mode(mode), - m_sensor(ua_sensors_haptic_new()) + m_cancellable(g_cancellable_new()) { - if (m_sensor == nullptr) - { - g_warning ("Haptic device unavailable"); - } - else - { - /* We only support one vibrate mode for now: an on/off pulse at - one-second intervals. So, set a timer to go off every 2 seconds - that vibrates for one second. */ - ua_sensors_haptic_enable(m_sensor); - m_tag = g_timeout_add_seconds (2, on_timeout, this); - on_timeout (this); - } + g_bus_get (G_BUS_TYPE_SESSION, m_cancellable, on_bus_ready, this); } ~Impl() { - if (m_sensor != nullptr) - ua_sensors_haptic_disable(m_sensor); - if (m_tag) g_source_remove(m_tag); + + g_cancellable_cancel (m_cancellable); + g_object_unref (m_cancellable); + + g_clear_object (&m_bus); } private: + static void on_bus_ready (GObject*, GAsyncResult* res, gpointer gself) + { + GError * error; + GDBusConnection * bus; + + error = nullptr; + bus = g_bus_get_finish (res, &error); + if (error != nullptr) + { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_warning ("Unable to get bus: %s", error->message); + + g_error_free (error); + } + else if (bus != nullptr) + { + auto self = static_cast(gself); + + self->m_bus = G_DBUS_CONNECTION (g_object_ref (bus)); + self->start_vibrating(); + + g_object_unref (bus); + } + } + + void start_vibrating() + { + /* We only support one vibrate mode for now: an on/off pulse at + one-second intervals. So set a looping 2-second timer that asks + the phone to vibrate for one second. */ + m_tag = g_timeout_add_seconds (2, on_timeout, this); + on_timeout (this); + } + static gboolean on_timeout (gpointer gself) { static_cast(gself)->vibrate_now(); @@ -73,12 +96,23 @@ private: void vibrate_now() { - const uint32_t msec = 1000; - (void) ua_sensors_haptic_vibrate_once (m_sensor, msec); + g_dbus_connection_call (m_bus, + BUS_HAPTIC_NAME, + BUS_HAPTIC_PATH, + BUS_HAPTIC_INTERFACE, + "Vibrate", + g_variant_new("(u)", 1000u), + nullptr, + G_DBUS_CALL_FLAGS_NONE, + -1, + m_cancellable, + nullptr, + nullptr); } const Mode m_mode; - UASensorsHaptic * m_sensor = nullptr; + GCancellable * m_cancellable = nullptr; + GDBusConnection * m_bus = nullptr; guint m_tag = 0; }; -- cgit v1.2.3 From e789710c2888b79e3abfbdcdda50d8334848ebc3 Mon Sep 17 00:00:00 2001 From: Charles Kerr Date: Thu, 31 Jul 2014 15:25:15 -0500 Subject: add haptic feedback unit tests --- tests/test-snap.cpp | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/tests/test-snap.cpp b/tests/test-snap.cpp index 9a049fa..018d6f9 100644 --- a/tests/test-snap.cpp +++ b/tests/test-snap.cpp @@ -56,6 +56,8 @@ private: protected: + static constexpr char const * HAPTIC_METHOD_VIBRATE {"Vibrate"}; + static constexpr int SCREEN_COOKIE {8675309}; static constexpr char const * SCREEN_METHOD_KEEP_DISPLAY_ON {"keepDisplayOn"}; static constexpr char const * SCREEN_METHOD_REMOVE_DISPLAY_ON_REQUEST {"removeDisplayOnRequest"}; @@ -88,9 +90,11 @@ protected: DbusTestDbusMock * notify_mock = nullptr; DbusTestDbusMock * powerd_mock = nullptr; DbusTestDbusMock * screen_mock = nullptr; + DbusTestDbusMock * haptic_mock = nullptr; DbusTestDbusMockObject * notify_obj = nullptr; DbusTestDbusMockObject * powerd_obj = nullptr; DbusTestDbusMockObject * screen_obj = nullptr; + DbusTestDbusMockObject * haptic_obj = nullptr; void SetUp() { @@ -235,9 +239,28 @@ protected: "", &error); g_assert_no_error (error); - dbus_test_service_add_task(service, DBUS_TEST_TASK(screen_mock)); + /// + /// Add the haptic mock + /// + + haptic_mock = dbus_test_dbus_mock_new(BUS_HAPTIC_NAME); + haptic_obj = dbus_test_dbus_mock_get_object(haptic_mock, + BUS_HAPTIC_PATH, + BUS_HAPTIC_INTERFACE, + &error); + + dbus_test_dbus_mock_object_add_method(haptic_mock, + haptic_obj, + HAPTIC_METHOD_VIBRATE, + G_VARIANT_TYPE("(u)"), + nullptr, + "", + &error); + g_assert_no_error (error); + dbus_test_service_add_task(service, DBUS_TEST_TASK(haptic_mock)); + // start 'em up. // make the system bus work off the mock bus too, since that's @@ -259,6 +282,7 @@ protected: virtual void TearDown() { + g_clear_object(&haptic_mock); g_clear_object(&screen_mock); g_clear_object(&powerd_mock); g_clear_object(¬ify_mock); @@ -392,6 +416,13 @@ TEST_F(SnapFixture, InhibitSleep) nullptr, &error)); + // confirm that haptic feedback got called + EXPECT_TRUE (dbus_test_dbus_mock_object_check_method_call (haptic_mock, + haptic_obj, + HAPTIC_METHOD_VIBRATE, + nullptr, + &error)); + // force-close the snap wait_msec(100); delete snap; -- cgit v1.2.3 From 43e0fb826aa3a5acfd8937438989231804b4c400 Mon Sep 17 00:00:00 2001 From: Charles Kerr Date: Thu, 31 Jul 2014 16:25:26 -0500 Subject: in haptic.cpp, better support for vibration patterns --- src/haptic.cpp | 57 ++++++++++++++++++++++++++++++++++++++++------------- tests/test-snap.cpp | 8 ++++---- 2 files changed, 47 insertions(+), 18 deletions(-) diff --git a/src/haptic.cpp b/src/haptic.cpp index 2b1ee21..a27c334 100644 --- a/src/haptic.cpp +++ b/src/haptic.cpp @@ -22,6 +22,8 @@ #include +#include + namespace unity { namespace indicator { namespace notifications { @@ -81,38 +83,65 @@ private: void start_vibrating() { - /* We only support one vibrate mode for now: an on/off pulse at - one-second intervals. So set a looping 2-second timer that asks - the phone to vibrate for one second. */ - m_tag = g_timeout_add_seconds (2, on_timeout, this); + g_return_if_fail (m_tag == 0); + + switch (m_mode) + { + case MODE_PULSE: // the only mode currently supported... :) + // one second on, one second off. + m_vibrate_pattern_msec = std::vector({1000, 1000}); + break; + } + + // Set up a loop so that the pattern keeps repeating. + // NB: VibratePattern takes a repeat arg, but we avoid it because + // there's no way to cancel a pattern once it's started... + // The phone would keep vibrating long after the alarm was dismissed! + // So we stick to a short pattern and handle the repeat loop manually. + guint interval_msec = 0; + for (const auto& msec : m_vibrate_pattern_msec) + interval_msec += msec; + m_tag = g_timeout_add (interval_msec, on_timeout, this); on_timeout (this); } static gboolean on_timeout (gpointer gself) { - static_cast(gself)->vibrate_now(); - return G_SOURCE_CONTINUE; - } - - void vibrate_now() - { - g_dbus_connection_call (m_bus, + auto self = static_cast(gself); + + // build the pattern array of uint32s + GVariantBuilder builder; + g_variant_builder_init (&builder, G_VARIANT_TYPE_ARRAY); + for (const auto& msec : self->m_vibrate_pattern_msec) + g_variant_builder_add_value (&builder, g_variant_new_uint32(msec)); + auto pattern_array = g_variant_builder_end (&builder); + + // build the argument list + g_variant_builder_init (&builder, G_VARIANT_TYPE_TUPLE); + g_variant_builder_add_value (&builder, pattern_array); + g_variant_builder_add_value (&builder, g_variant_new_uint32(1u)); + auto args = g_variant_builder_end (&builder); + + g_dbus_connection_call (self->m_bus, BUS_HAPTIC_NAME, BUS_HAPTIC_PATH, BUS_HAPTIC_INTERFACE, - "Vibrate", - g_variant_new("(u)", 1000u), + "VibratePattern", + args, nullptr, G_DBUS_CALL_FLAGS_NONE, -1, - m_cancellable, + self->m_cancellable, nullptr, nullptr); + + return G_SOURCE_CONTINUE; } const Mode m_mode; GCancellable * m_cancellable = nullptr; GDBusConnection * m_bus = nullptr; + std::vector m_vibrate_pattern_msec; guint m_tag = 0; }; diff --git a/tests/test-snap.cpp b/tests/test-snap.cpp index 018d6f9..06e0a80 100644 --- a/tests/test-snap.cpp +++ b/tests/test-snap.cpp @@ -56,7 +56,7 @@ private: protected: - static constexpr char const * HAPTIC_METHOD_VIBRATE {"Vibrate"}; + static constexpr char const * HAPTIC_METHOD_VIBRATE_PATTERN {"VibratePattern"}; static constexpr int SCREEN_COOKIE {8675309}; static constexpr char const * SCREEN_METHOD_KEEP_DISPLAY_ON {"keepDisplayOn"}; @@ -253,8 +253,8 @@ protected: dbus_test_dbus_mock_object_add_method(haptic_mock, haptic_obj, - HAPTIC_METHOD_VIBRATE, - G_VARIANT_TYPE("(u)"), + HAPTIC_METHOD_VIBRATE_PATTERN, + G_VARIANT_TYPE("(auu)"), nullptr, "", &error); @@ -419,7 +419,7 @@ TEST_F(SnapFixture, InhibitSleep) // confirm that haptic feedback got called EXPECT_TRUE (dbus_test_dbus_mock_object_check_method_call (haptic_mock, haptic_obj, - HAPTIC_METHOD_VIBRATE, + HAPTIC_METHOD_VIBRATE_PATTERN, nullptr, &error)); -- cgit v1.2.3 From 4148750ea93e67e5d9aaa15ebc8e3c7a5a5554f1 Mon Sep 17 00:00:00 2001 From: Charles Kerr Date: Thu, 31 Jul 2014 16:48:00 -0500 Subject: configurable haptic mode, part 1 of 3: add haptic feedback mode to the GSettings schema and to our Settings object --- data/com.canonical.indicator.datetime.gschema.xml.in | 8 ++++++++ include/datetime/settings-live.h | 1 + include/datetime/settings-shared.h | 1 + include/datetime/settings.h | 1 + src/settings-live.cpp | 14 ++++++++++++++ tests/test-settings.cpp | 1 + 6 files changed, 26 insertions(+) diff --git a/data/com.canonical.indicator.datetime.gschema.xml.in b/data/com.canonical.indicator.datetime.gschema.xml.in index 62b42c1..4e4acd8 100644 --- a/data/com.canonical.indicator.datetime.gschema.xml.in +++ b/data/com.canonical.indicator.datetime.gschema.xml.in @@ -123,6 +123,14 @@ 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). + + 'pulse' + <_summary>What kind of haptic feedback, if any, to trigger with an alarm. + <_description> + What kind of haptic feedback, if any, to trigger with an alarm. + Two modes are currently supported: 'pulse', 'none'. + + '/usr/share/sounds/ubuntu/ringtones/Suru arpeggio.ogg' <_summary>The alarm's default sound file. diff --git a/include/datetime/settings-live.h b/include/datetime/settings-live.h index 4db2d40..4850e69 100644 --- a/include/datetime/settings-live.h +++ b/include/datetime/settings-live.h @@ -58,6 +58,7 @@ private: void update_alarm_sound(); void update_alarm_volume(); void update_alarm_duration(); + void update_alarm_haptic(); GSettings* m_settings; diff --git a/include/datetime/settings-shared.h b/include/datetime/settings-shared.h index 23d2e1c..a211821 100644 --- a/include/datetime/settings-shared.h +++ b/include/datetime/settings-shared.h @@ -48,5 +48,6 @@ TimeFormatMode; #define SETTINGS_ALARM_SOUND_S "alarm-default-sound" #define SETTINGS_ALARM_VOLUME_S "alarm-default-volume" #define SETTINGS_ALARM_DURATION_S "alarm-duration-minutes" +#define SETTINGS_ALARM_HAPTIC_S "alarm-haptic-feedback" #endif // INDICATOR_DATETIME_SETTINGS_SHARED diff --git a/include/datetime/settings.h b/include/datetime/settings.h index e5f885e..c6fe13b 100644 --- a/include/datetime/settings.h +++ b/include/datetime/settings.h @@ -57,6 +57,7 @@ public: core::Property time_format_mode; core::Property timezone_name; core::Property alarm_sound; + core::Property alarm_haptic; core::Property alarm_volume; core::Property alarm_duration; }; diff --git a/src/settings-live.cpp b/src/settings-live.cpp index 71bbd96..a8338ed 100644 --- a/src/settings-live.cpp +++ b/src/settings-live.cpp @@ -55,6 +55,7 @@ LiveSettings::LiveSettings(): update_alarm_sound(); update_alarm_volume(); update_alarm_duration(); + update_alarm_haptic(); // now listen for clients to change the properties s.t. we can sync update GSettings @@ -130,6 +131,10 @@ LiveSettings::LiveSettings(): alarm_duration.changed().connect([this](unsigned int value){ g_settings_set_uint(m_settings, SETTINGS_ALARM_DURATION_S, value); }); + + alarm_haptic.changed().connect([this](const std::string& value){ + g_settings_set_string(m_settings, SETTINGS_ALARM_HAPTIC_S, value.c_str()); + }); } /*** @@ -237,6 +242,13 @@ void LiveSettings::update_alarm_duration() alarm_duration.set(g_settings_get_uint(m_settings, SETTINGS_ALARM_DURATION_S)); } +void LiveSettings::update_alarm_haptic() +{ + auto val = g_settings_get_string(m_settings, SETTINGS_ALARM_HAPTIC_S); + alarm_haptic.set(val); + g_free(val); +} + /*** **** ***/ @@ -284,6 +296,8 @@ void LiveSettings::update_key(const std::string& key) update_alarm_volume(); else if (key == SETTINGS_ALARM_DURATION_S) update_alarm_duration(); + else if (key == SETTINGS_ALARM_HAPTIC_S) + update_alarm_haptic(); } /*** diff --git a/tests/test-settings.cpp b/tests/test-settings.cpp index 44a0252..4fb0a08 100644 --- a/tests/test-settings.cpp +++ b/tests/test-settings.cpp @@ -159,6 +159,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); + TestStringProperty(m_settings->alarm_haptic, SETTINGS_ALARM_HAPTIC_S); } TEST_F(SettingsFixture, TimeFormatMode) -- cgit v1.2.3 From 8afa4ba3102138f245fba704eba03b907edefa2b Mon Sep 17 00:00:00 2001 From: Charles Kerr Date: Thu, 31 Jul 2014 17:06:50 -0500 Subject: configurable haptic mode, part 2 of 3: use the new haptic mode setting when popping up notifications; sync notification tests --- src/snap.cpp | 5 ++++- tests/test-snap.cpp | 49 ++++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 46 insertions(+), 8 deletions(-) diff --git a/src/snap.cpp b/src/snap.cpp index beefa94..0b2322a 100644 --- a/src/snap.cpp +++ b/src/snap.cpp @@ -78,7 +78,10 @@ public: auto sound = std::make_shared(uri, volume, loop); // create the haptic feedback... - auto haptic = std::make_shared(); + const auto haptic_mode = m_settings->alarm_haptic.get(); + std::shared_ptr haptic; + if (haptic_mode == "pulse") + haptic = std::make_shared(uin::Haptic::MODE_PULSE); // show a notification... const auto minutes = std::chrono::minutes(m_settings->alarm_duration.get()); diff --git a/tests/test-snap.cpp b/tests/test-snap.cpp index 06e0a80..5afaab1 100644 --- a/tests/test-snap.cpp +++ b/tests/test-snap.cpp @@ -416,13 +416,6 @@ TEST_F(SnapFixture, InhibitSleep) nullptr, &error)); - // confirm that haptic feedback got called - EXPECT_TRUE (dbus_test_dbus_mock_object_check_method_call (haptic_mock, - haptic_obj, - HAPTIC_METHOD_VIBRATE_PATTERN, - nullptr, - &error)); - // force-close the snap wait_msec(100); delete snap; @@ -485,3 +478,45 @@ TEST_F(SnapFixture, ForceScreen) &error)); g_assert_no_error(error); } + +/*** +**** +***/ + +TEST_F(SnapFixture, HapticModes) +{ + auto settings = std::make_shared(); + auto ne = std::make_shared(APP_NAME); + auto func = [this](const Appointment&){g_idle_add(quit_idle, loop);}; + GError * error = nullptr; + + // invoke a snap decision while haptic feedback is set to "pulse", + // confirm that VibratePattern got called + settings->alarm_haptic.set("pulse"); + auto snap = new Snap (ne, settings); + (*snap)(appt, func, func); + wait_msec(100); + EXPECT_TRUE (dbus_test_dbus_mock_object_check_method_call (haptic_mock, + haptic_obj, + HAPTIC_METHOD_VIBRATE_PATTERN, + nullptr, + &error)); + delete snap; + + // invoke a snap decision while haptic feedback is set to "none", + // confirm that VibratePattern =didn't= get called + wait_msec(100); + dbus_test_dbus_mock_object_clear_method_calls (haptic_mock, haptic_obj, &error); + settings->alarm_haptic.set("none"); + snap = new Snap (ne, settings); + (*snap)(appt, func, func); + wait_msec(100); + EXPECT_FALSE (dbus_test_dbus_mock_object_check_method_call (haptic_mock, + haptic_obj, + HAPTIC_METHOD_VIBRATE_PATTERN, + nullptr, + &error)); + delete snap; + + g_assert_no_error (error); +} -- cgit v1.2.3 From 56d881413ffaa171d4367bb968f5454988e2737e Mon Sep 17 00:00:00 2001 From: Charles Kerr Date: Thu, 31 Jul 2014 17:17:53 -0500 Subject: configurable haptic mode, part 3 of 3: expose the new haptic mode setting as a DBus property; sync exporter tests --- data/com.canonical.indicator.datetime.AlarmProperties.xml | 9 +++++++++ src/exporter.cpp | 1 + tests/test-exporter.cpp | 12 ++++++++++++ 3 files changed, 22 insertions(+) diff --git a/data/com.canonical.indicator.datetime.AlarmProperties.xml b/data/com.canonical.indicator.datetime.AlarmProperties.xml index d25fa82..9b38af9 100644 --- a/data/com.canonical.indicator.datetime.AlarmProperties.xml +++ b/data/com.canonical.indicator.datetime.AlarmProperties.xml @@ -11,6 +11,15 @@ + + + + What kind of haptic feedback, if any, to trigger with an alarm. + Two modes are currently supported: 'pulse', 'none'. + + + + diff --git a/src/exporter.cpp b/src/exporter.cpp index 88aee2f..1d45705 100644 --- a/src/exporter.cpp +++ b/src/exporter.cpp @@ -144,6 +144,7 @@ private: 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); + bind_string_property(m_alarm_props, "haptic-feedback", m_settings->alarm_haptic); } /*** diff --git a/tests/test-exporter.cpp b/tests/test-exporter.cpp index 2e3411a..96665cf 100644 --- a/tests/test-exporter.cpp +++ b/tests/test-exporter.cpp @@ -186,26 +186,35 @@ TEST_F(ExporterFixture, AlarmProperties) auto expected_volume = 1; int expected_duration = 60; const char * expected_sound = "/tmp/foo.wav"; + const char * expected_haptic = "pulse"; settings->alarm_volume.set(expected_volume); settings->alarm_duration.set(expected_duration); settings->alarm_sound.set(expected_sound); + settings->alarm_haptic.set(expected_haptic); wait_msec(); static constexpr const char* const SOUND_PROP {"default-sound"}; static constexpr const char* const VOLUME_PROP {"default-volume"}; static constexpr const char* const DURATION_PROP {"duration"}; + static constexpr const char* const HAPTIC_PROP {"haptic-feedback"}; char* sound = nullptr; + char* haptic = nullptr; int volume = -1; int duration = -1; g_object_get(proxy, SOUND_PROP, &sound, + HAPTIC_PROP, &haptic, VOLUME_PROP, &volume, DURATION_PROP, &duration, nullptr); EXPECT_STREQ(expected_sound, sound); + EXPECT_STREQ(expected_haptic, haptic); EXPECT_EQ(expected_volume, volume); EXPECT_EQ(expected_duration, duration); + g_clear_pointer (&sound, g_free); + g_clear_pointer (&haptic, g_free); + /*** **** Try chaning the DBus properties -- do the Settings change to match it? ***/ @@ -213,13 +222,16 @@ TEST_F(ExporterFixture, AlarmProperties) expected_volume = 100; expected_duration = 30; expected_sound = "/tmp/bar.wav"; + expected_haptic = "none"; g_object_set(proxy, SOUND_PROP, expected_sound, + HAPTIC_PROP, expected_haptic, VOLUME_PROP, expected_volume, DURATION_PROP, expected_duration, nullptr); wait_msec(); EXPECT_STREQ(expected_sound, settings->alarm_sound.get().c_str()); + EXPECT_STREQ(expected_haptic, settings->alarm_haptic.get().c_str()); EXPECT_EQ(expected_volume, settings->alarm_volume.get()); EXPECT_EQ(expected_duration, settings->alarm_duration.get()); -- cgit v1.2.3 From d129112ad9ce04fad979af95b5b5cd4fd87f5b2b Mon Sep 17 00:00:00 2001 From: Charles Kerr Date: Thu, 31 Jul 2014 19:52:42 -0500 Subject: in Haptic, make the looping logic easier to read. --- include/notifications/haptic.h | 2 +- src/haptic.cpp | 51 ++++++++++++++++++++++-------------------- 2 files changed, 28 insertions(+), 25 deletions(-) diff --git a/include/notifications/haptic.h b/include/notifications/haptic.h index d82d1c9..bfb5679 100644 --- a/include/notifications/haptic.h +++ b/include/notifications/haptic.h @@ -31,7 +31,7 @@ namespace notifications { ***/ /** - * A class that forces the screen display on and inhibits sleep + * Tries to emit haptic feedback to match the user-specified mode. */ class Haptic { diff --git a/src/haptic.cpp b/src/haptic.cpp index a27c334..54a7d49 100644 --- a/src/haptic.cpp +++ b/src/haptic.cpp @@ -22,6 +22,7 @@ #include +#include #include namespace unity { @@ -88,60 +89,62 @@ private: switch (m_mode) { case MODE_PULSE: // the only mode currently supported... :) + // one second on, one second off. - m_vibrate_pattern_msec = std::vector({1000, 1000}); + m_pattern = std::vector({1000u, 1000u}); break; } - // Set up a loop so that the pattern keeps repeating. - // NB: VibratePattern takes a repeat arg, but we avoid it because - // there's no way to cancel a pattern once it's started... - // The phone would keep vibrating long after the alarm was dismissed! - // So we stick to a short pattern and handle the repeat loop manually. - guint interval_msec = 0; - for (const auto& msec : m_vibrate_pattern_msec) - interval_msec += msec; - m_tag = g_timeout_add (interval_msec, on_timeout, this); - on_timeout (this); + // Set up a loop to keep repeating the pattern + auto msec = std::accumulate(m_pattern.begin(), m_pattern.end(), 0u); + m_tag = g_timeout_add(msec, call_vibrate_pattern_static, this); + call_vibrate_pattern(); } - static gboolean on_timeout (gpointer gself) + static gboolean call_vibrate_pattern_static (gpointer gself) { - auto self = static_cast(gself); + static_cast(gself)->call_vibrate_pattern(); + return G_SOURCE_CONTINUE; + } - // build the pattern array of uint32s + void call_vibrate_pattern() + { + // build the vibrate pattern GVariantBuilder builder; g_variant_builder_init (&builder, G_VARIANT_TYPE_ARRAY); - for (const auto& msec : self->m_vibrate_pattern_msec) + for (const auto& msec : m_pattern) g_variant_builder_add_value (&builder, g_variant_new_uint32(msec)); auto pattern_array = g_variant_builder_end (&builder); - // build the argument list + /* Use a repeat_count of 1 here because we handle looping ourselves. + NB: VibratePattern could do it for us, but doesn't let us cancel + a running loop -- we could keep vibrating even after "this" was + destructed */ + auto repeat_count = g_variant_new_uint32 (1u); + g_variant_builder_init (&builder, G_VARIANT_TYPE_TUPLE); g_variant_builder_add_value (&builder, pattern_array); - g_variant_builder_add_value (&builder, g_variant_new_uint32(1u)); - auto args = g_variant_builder_end (&builder); + g_variant_builder_add_value (&builder, repeat_count); + auto vibrate_pattern_args = g_variant_builder_end (&builder); - g_dbus_connection_call (self->m_bus, + g_dbus_connection_call (m_bus, BUS_HAPTIC_NAME, BUS_HAPTIC_PATH, BUS_HAPTIC_INTERFACE, "VibratePattern", - args, + vibrate_pattern_args, nullptr, G_DBUS_CALL_FLAGS_NONE, -1, - self->m_cancellable, + m_cancellable, nullptr, nullptr); - - return G_SOURCE_CONTINUE; } const Mode m_mode; GCancellable * m_cancellable = nullptr; GDBusConnection * m_bus = nullptr; - std::vector m_vibrate_pattern_msec; + std::vector m_pattern; guint m_tag = 0; }; -- cgit v1.2.3 From 6da58726eb18e7205845ec1af4cb43d08d988e31 Mon Sep 17 00:00:00 2001 From: Charles Kerr Date: Fri, 1 Aug 2014 11:42:43 -0500 Subject: refactor changes based on Antti's feedback --- include/notifications/CMakeLists.txt | 2 -- src/notifications.cpp | 27 ++++++++++++++------------- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/include/notifications/CMakeLists.txt b/include/notifications/CMakeLists.txt index 139597f..e69de29 100644 --- a/include/notifications/CMakeLists.txt +++ b/include/notifications/CMakeLists.txt @@ -1,2 +0,0 @@ - - diff --git a/src/notifications.cpp b/src/notifications.cpp index da7351b..dfb1dc6 100644 --- a/src/notifications.cpp +++ b/src/notifications.cpp @@ -156,14 +156,14 @@ public: void close_all () { - // close() removes the item from m_notifications, - // so increment the iterator before it gets invalidated - for (auto it=m_notifications.begin(), e=m_notifications.end(); it!=e; ) - { - const int key = it->first; - ++it; - close (key); - } + // call close() on all our keys + + std::set keys; + for (const auto& it : m_notifications) + keys.insert (it.first); + + for (const int key : keys) + close (key); } void close (int key) @@ -171,15 +171,16 @@ public: auto it = m_notifications.find(key); if (it != m_notifications.end()) { - // tell the server to close, call the close() callback, - // and immediately forget about the nn. + // tell the server to close the notification GError * error = nullptr; if (!notify_notification_close (it->second.nn.get(), &error)) { g_warning ("Unable to close notification %d: %s", key, error->message); g_error_free (error); } - on_closed (key); + + // call the user callback and remove it from our bookkeeping + remove_closed_notification (key); } } @@ -270,10 +271,10 @@ private: { const GQuark q = notification_key_quark(); const gpointer gkey = g_object_get_qdata(G_OBJECT(nn), q); - static_cast(gself)->on_closed(GPOINTER_TO_INT(gkey)); + static_cast(gself)->remove_closed_notification(GPOINTER_TO_INT(gkey)); } - void on_closed (int key) + void remove_closed_notification (int key) { auto it = m_notifications.find(key); g_return_if_fail (it != m_notifications.end()); -- cgit v1.2.3 From 91c7ce04f148bbeed31ac65d41a6e20b2c080014 Mon Sep 17 00:00:00 2001 From: Charles Kerr Date: Fri, 8 Aug 2014 15:24:25 -0500 Subject: in notifications.cpp, register for the 'closed' signal before calling notification_notify(). --- src/notifications.cpp | 51 ++++++++++++++++++++++++--------------------------- 1 file changed, 24 insertions(+), 27 deletions(-) diff --git a/src/notifications.cpp b/src/notifications.cpp index dfb1dc6..b19fa48 100644 --- a/src/notifications.cpp +++ b/src/notifications.cpp @@ -189,30 +189,36 @@ public: int ret = -1; const auto& info = *builder.impl; - auto nn = notify_notification_new (info.m_title.c_str(), - info.m_body.c_str(), - info.m_icon_name.c_str()); + std::shared_ptr nn ( + notify_notification_new(info.m_title.c_str(), + info.m_body.c_str(), + info.m_icon_name.c_str()), + [this](NotifyNotification * n) { + g_signal_handlers_disconnect_by_data(n, this); + g_object_unref (G_OBJECT(n)); + } + ); if (info.m_duration.count() != 0) { const auto& d= info.m_duration; auto ms = std::chrono::duration_cast(d); - notify_notification_set_hint (nn, + notify_notification_set_hint (nn.get(), HINT_TIMEOUT, g_variant_new_int32(ms.count())); } for (const auto& hint : info.m_string_hints) { - notify_notification_set_hint (nn, + notify_notification_set_hint (nn.get(), hint.c_str(), g_variant_new_boolean(true)); } for (const auto& action : info.m_actions) { - notify_notification_add_action (nn, + notify_notification_add_action (nn.get(), action.first.c_str(), action.second.c_str(), on_notification_clicked, @@ -220,27 +226,19 @@ public: nullptr); } - // if we can show it, keep it + static int next_key = 1; + const int key = next_key++; + g_object_set_qdata (G_OBJECT(nn.get()), + notification_key_quark(), + GINT_TO_POINTER(key)); + + m_notifications[key] = { nn, info.m_closed_callback }; + g_signal_connect (nn.get(), "closed", + G_CALLBACK(on_notification_closed), this); + GError * error = nullptr; - if (notify_notification_show(nn, &error)) + if (notify_notification_show(nn.get(), &error)) { - static int next_key = 1; - const int key = next_key++; - - g_signal_connect (nn, "closed", - G_CALLBACK(on_notification_closed), this); - g_object_set_qdata (G_OBJECT(nn), - notification_key_quark(), - GINT_TO_POINTER(key)); - - notification_data ndata; - ndata.closed_callback = info.m_closed_callback; - ndata.nn.reset(nn, [this](NotifyNotification * n) { - g_signal_handlers_disconnect_by_data(n, this); - g_object_unref (G_OBJECT(n)); - }); - - m_notifications[key] = ndata; ret = key; } else @@ -249,7 +247,7 @@ public: info.m_title.c_str(), error->message); g_error_free (error); - g_object_unref (nn); + m_notifications.erase(key); } return ret; @@ -293,7 +291,6 @@ private: ndata.closed_callback (action); } - g_signal_handlers_disconnect_by_data(nn, this); m_notifications.erase(it); } -- cgit v1.2.3 From 9cc6380c1cb0c4e96a893a1b9d2720d2ed3181bd Mon Sep 17 00:00:00 2001 From: Charles Kerr Date: Fri, 8 Aug 2014 15:31:49 -0500 Subject: in notifications, don't ask the notification server for its capabilities until we need them. --- src/notifications.cpp | 44 ++++++++++++++++++++++++++------------------ 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/src/notifications.cpp b/src/notifications.cpp index b19fa48..18f15d9 100644 --- a/src/notifications.cpp +++ b/src/notifications.cpp @@ -120,21 +120,6 @@ public: { if (!notify_init(app_name.c_str())) g_critical("Unable to initialize libnotify!"); - - // put the server capabilities into m_caps - auto caps_gl = notify_get_server_caps(); - std::string caps_str; - for(auto l=caps_gl; l!=nullptr; l=l->next) - { - m_caps.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); } ~Impl() @@ -151,7 +136,7 @@ public: bool supports_actions() const { - return m_caps.count("actions") != 0; + return server_caps().count("actions") != 0; } void close_all () @@ -255,6 +240,28 @@ public: private: + const std::set& server_caps() const + { + if (G_UNLIKELY(m_lazy_caps.empty())) + { + auto caps_gl = notify_get_server_caps(); + std::string caps_str; + for(auto l=caps_gl; l!=nullptr; l=l->next) + { + m_lazy_caps.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 m_lazy_caps; + } + static void on_notification_clicked (NotifyNotification * nn, char * action, gpointer) @@ -303,8 +310,9 @@ private: // key-to-data std::map m_notifications; - // server capabilities - std::set m_caps; + // server capabilities. + // as the name indicates, don't use this directly: use server_caps() instead + mutable std::set m_lazy_caps; static constexpr char const * HINT_TIMEOUT {"x-canonical-snap-decisions-timeout"}; }; -- cgit v1.2.3