diff options
-rw-r--r-- | include/CMakeLists.txt | 1 | ||||
-rw-r--r-- | include/datetime/dbus-shared.h | 14 | ||||
-rw-r--r-- | include/datetime/snap.h | 14 | ||||
-rw-r--r-- | include/notifications/CMakeLists.txt | 2 | ||||
-rw-r--r-- | include/notifications/awake.h | 55 | ||||
-rw-r--r-- | include/notifications/dbus-shared.h | 32 | ||||
-rw-r--r-- | include/notifications/notifications.h | 116 | ||||
-rw-r--r-- | include/notifications/sound.h | 60 | ||||
-rw-r--r-- | src/CMakeLists.txt | 3 | ||||
-rw-r--r-- | src/awake.cpp | 256 | ||||
-rw-r--r-- | src/main.cpp | 9 | ||||
-rw-r--r-- | src/notifications.cpp | 364 | ||||
-rw-r--r-- | src/snap.cpp | 686 | ||||
-rw-r--r-- | src/sound.cpp | 143 | ||||
-rw-r--r-- | tests/manual-test-snap.cpp | 11 | ||||
-rw-r--r-- | tests/test-snap.cpp | 74 |
16 files changed, 1195 insertions, 645 deletions
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 <charles.kerr@canonical.com> */ -#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..ef5c868 100644 --- a/include/datetime/snap.h +++ b/include/datetime/snap.h @@ -21,12 +21,12 @@ #define INDICATOR_DATETIME_SNAP_H #include <datetime/appointment.h> -#include <datetime/clock.h> #include <datetime/settings.h> +#include <notifications/notifications.h> + #include <functional> #include <memory> -#include <set> namespace unity { namespace indicator { @@ -38,7 +38,7 @@ namespace datetime { class Snap { public: - Snap(const std::shared_ptr<Clock>& clock, + Snap(const std::shared_ptr<unity::indicator::notifications::Engine>& engine, const std::shared_ptr<const Settings>& settings); virtual ~Snap(); @@ -48,12 +48,8 @@ public: appointment_func dismiss); private: - const std::shared_ptr<Clock> m_clock; - const std::shared_ptr<const Settings> m_settings; - - class Popup; - friend class Popup; - std::set<Popup*> m_pending; + class Impl; + std::unique_ptr<Impl> impl; }; } // 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 <http://www.gnu.org/licenses/>. + * + * Authors: + * Charles Kerr <charles.kerr@canonical.com> + */ + +#ifndef UNITY_INDICATOR_NOTIFICATIONS_AWAKE_H +#define UNITY_INDICATOR_NOTIFICATIONS_AWAKE_H + +#include <memory> + +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> 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 <http://www.gnu.org/licenses/>. + * + * Authors: + * Ted Gould <ted@canonical.com> + * Charles Kerr <charles.kerr@canonical.com> + */ + +#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..260b466 --- /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 <http://www.gnu.org/licenses/>. + * + * Authors: + * Charles Kerr <charles.kerr@canonical.com> + */ + +#ifndef UNITY_INDICATOR_NOTIFICATIONS_NOTIFICATIONS_H +#define UNITY_INDICATOR_NOTIFICATIONS_NOTIFICATIONS_H + +#include <chrono> +#include <functional> +#include <memory> +#include <string> + +namespace unity { +namespace indicator { +namespace notifications { + +class Engine; + +/** + * Helper class for showing notifications. + * + * Populate the builder, 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"}; + static constexpr char const * HINT_NONSHAPEDICON {"x-canonical-non-shaped-icon"}; + + /* 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<void(const std::string& action)>); + +private: + friend class Engine; + class Impl; + std::unique_ptr<Impl> 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 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() */ + void close(int key); + + /** Close all remaining notifications. */ + void close_all(); + + const std::string& app_name() const; + +private: + class Impl; + std::unique_ptr<Impl> 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 <http://www.gnu.org/licenses/>. + * + * Authors: + * Charles Kerr <charles.kerr@canonical.com> + */ + +#ifndef UNITY_INDICATOR_NOTIFICATIONS_SOUND_H +#define UNITY_INDICATOR_NOTIFICATIONS_SOUND_H + +#include <memory> +#include <string> + +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> 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..57358ab --- /dev/null +++ b/src/awake.cpp @@ -0,0 +1,256 @@ +/* + * 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 <http://www.gnu.org/licenses/>. + * + * Authors: + * Charles Kerr <charles.kerr@canonical.com> + */ + +#include <notifications/awake.h> +#include <notifications/dbus-shared.h> + +#include <gio/gio.h> + +#include <limits> + +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<Impl*>(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<Impl*>(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<Impl*>(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<int32_t>::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 <datetime/timezone-file.h> #include <datetime/timezones-live.h> #include <datetime/wakeup-timer-mainloop.h> +#include <notifications/notifications.h> #ifdef HAVE_UBUNTU_HW_ALARM_H #include <datetime/wakeup-timer-uha.h> @@ -45,6 +46,8 @@ #include <locale.h> #include <cstdlib> // 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<uin::Engine>("indicator-datetime-service"); + std::unique_ptr<Snap> 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..da7351b --- /dev/null +++ b/src/notifications.cpp @@ -0,0 +1,364 @@ +/* + * 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 <http://www.gnu.org/licenses/>. + * + * Authors: + * Charles Kerr <charles.kerr@canonical.com> + */ + +#include <notifications/notifications.h> + +#include <libnotify/notify.h> + +#include <map> +#include <set> +#include <string> +#include <vector> + +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<std::string> m_string_hints; + std::vector<std::pair<std::string,std::string>> m_actions; + std::function<void(const std::string&)> 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<std::string,std::string>(action,label)); +} + +void +Builder::set_closed_callback (std::function<void (const std::string&)> cb) +{ + impl->m_closed_callback.swap (cb); +} + +/*** +**** +***/ + +class Engine::Impl +{ + struct notification_data + { + std::shared_ptr<NotifyNotification> nn; + std::function<void(const std::string&)> 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; + } + + 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); + } + } + + void close (int key) + { + 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)) + { + g_warning ("Unable to close notification %d: %s", key, error->message); + g_error_free (error); + } + on_closed (key); + } + } + + 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<std::chrono::milliseconds>(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 gpointer gkey = g_object_get_qdata(G_OBJECT(nn), q); + static_cast<Impl*>(gself)->on_closed(GPOINTER_TO_INT(gkey)); + } + + 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 GQuark q = notification_action_quark(); + const gpointer p = g_object_get_qdata(G_OBJECT(nn), q); + if (p != nullptr) + action = static_cast<const char*>(p); + + ndata.closed_callback (action); + } + + g_signal_handlers_disconnect_by_data(nn, this); + m_notifications.erase(it); + } + + /*** + **** + ***/ + + const std::string m_app_name; + + // key-to-data + std::map<int,notification_data> m_notifications; + + // server capabilities + std::set<std::string> 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); +} + +void +Engine::close_all() +{ + impl->close_all(); +} + +void +Engine::close(int key) +{ + 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 af39ec6..1506008 100644 --- a/src/snap.cpp +++ b/src/snap.cpp @@ -17,25 +17,21 @@ * Charles Kerr <charles.kerr@canonical.com> */ -#include <datetime/appointment.h> -#include <datetime/dbus-shared.h> -#include <datetime/formatter.h> #include <datetime/snap.h> -#include <core/signal.h> +#include <notifications/awake.h> +#include <notifications/sound.h> #include <gst/gst.h> -#include <libnotify/notify.h> #include <glib/gi18n.h> -#include <glib.h> #include <chrono> -#include <limits> -#include <mutex> // std::call_once() #include <set> #include <string> +namespace uin = unity::indicator::notifications; + namespace unity { namespace indicator { namespace datetime { @@ -44,631 +40,139 @@ namespace datetime { **** ***/ -namespace -{ - -static constexpr char const * APP_NAME = {"indicator-datetime-service"}; - -/** - * Plays a sound, possibly looping. - */ -class Sound +class Snap::Impl { - typedef Sound Self; - public: - Sound(const std::shared_ptr<Clock>& clock, - const std::string& uri, - unsigned int volume, - unsigned int duration_minutes, - bool loop): - m_clock(clock), - m_uri(uri), - m_volume(volume), - m_loop(loop), - m_loop_end_time(clock->localtime().add_full(0, 0, 0, 0, (int)duration_minutes, 0.0)) - { - // 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() + Impl(const std::shared_ptr<unity::indicator::notifications::Engine>& engine, + const std::shared_ptr<const Settings>& settings): + m_engine(engine), + m_settings(settings) { - 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 + ~Impl() { - 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)); + for (const auto& key : m_notifications) + m_engine->close (key); } - static gboolean bus_callback(GstBus*, GstMessage* msg, gpointer gself) + void operator()(const Appointment& appointment, + appointment_func show, + appointment_func dismiss) { - auto self = static_cast<Sound*>(gself); - - if ((GST_MESSAGE_TYPE(msg) == GST_MESSAGE_EOS) && - (self->m_loop) && - (self->m_clock->localtime() < self->m_loop_end_time)) + if (!appointment.has_alarms) { - 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<Clock> m_clock; - const std::string m_uri; - const unsigned int m_volume; - const bool m_loop; - const DateTime m_loop_end_time; - guint m_watch_source = 0; - GstElement* m_play = nullptr; -}; - -class SoundBuilder -{ -public: - void set_clock(const std::shared_ptr<Clock>& c) {m_clock = c;} - void set_uri(const std::string& uri) {m_uri = uri;} - void set_volume(const unsigned int v) {m_volume = v;} - void set_duration_minutes(unsigned int i) {m_duration_minutes=i;} - unsigned int duration_minutes() const {return m_duration_minutes;} - void set_looping(bool b) {m_looping=b;} + dismiss(appointment); + return; + } + + // force the system to stay awake + auto awake = std::make_shared<uin::Awake>(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<uin::Sound>(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); + b.add_hint (uin::Builder::HINT_NONSHAPEDICON); + 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<std::chrono::seconds>(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); + }); - Sound* operator()() { - return new Sound (m_clock, - m_uri, - m_volume, - m_duration_minutes, - m_looping); + const auto key = m_engine->show(b); + if (key) + m_notifications.insert (key); + else + show(appointment); } private: - std::shared_ptr<Clock> m_clock; - std::string m_uri; - unsigned int m_volume = 50; - unsigned int m_duration_minutes = 30; - bool m_looping = true; -}; - -/** -*** libnotify -- snap decisions -**/ -std::string get_alarm_uri(const Appointment& appointment, - const std::shared_ptr<const Settings>& settings) -{ - const char* FALLBACK {"/usr/share/sounds/ubuntu/ringtones/Suru arpeggio.ogg"}; + std::string get_alarm_uri(const Appointment& appointment, + const std::shared_ptr<const Settings>& 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 }; + const std::string candidates[] = { appointment.audio_url, + settings->alarm_sound.get(), + FALLBACK }; - std::string uri; + std::string uri; - for(const auto& candidate : candidates) - { - if (gst_uri_is_valid (candidate.c_str())) - { - uri = candidate; - break; - } - else if (g_file_test(candidate.c_str(), G_FILE_TEST_EXISTS)) + 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; -} - -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>& 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<std::chrono::milliseconds>(duration).count())); - notify_notification_set_hint(m_nn, HINT_NONSHAPEDICON, - g_variant_new_boolean(true)); - - /// alarm popup dialog's button to show the active alarm - notify_notification_add_action(m_nn, "show", _("Show"), - on_snap_show, this, nullptr); - /// alarm popup dialog's button to shut up the alarm - notify_notification_add_action(m_nn, "dismiss", _("Dismiss"), - on_snap_dismiss, this, nullptr); - g_signal_connect(m_nn, "closed", G_CALLBACK(on_snap_closed), this); - } - - 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<Self*>(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<Self*>(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<Self*>(gself); - self->m_sound.reset(); - self->m_response(self->m_response_value); - } - - /*** - **** Interactive - ***/ - - static std::set<std::string> get_server_caps() - { - std::set<std::string> caps_set; - auto caps_gl = notify_get_server_caps(); - std::string caps_str; - for(auto l=caps_gl; l!=nullptr; l=l->next) - { - caps_set.insert((const char*)l->data); - - 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<Popup*>(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<Popup*>(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<Popup*>(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; - } + return uri; } - /*** - **** - ***/ - - typedef Popup Self; - - const Appointment m_appointment; - const bool m_interactive; - SoundBuilder m_sound_builder; - std::unique_ptr<Sound> m_sound; - core::Signal<Response> m_response; - Response m_response_value = RESPONSE_CLOSE; - NotifyNotification* m_nn = nullptr; - 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<int32_t>::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"}; - static constexpr char const * HINT_NONSHAPEDICON {"x-canonical-non-shaped-icon"}; + const std::shared_ptr<unity::indicator::notifications::Engine> m_engine; + const std::shared_ptr<const Settings> m_settings; + std::set<int> m_notifications; }; /*** **** ***/ -Snap::Snap(const std::shared_ptr<Clock>& clock, +Snap::Snap(const std::shared_ptr<unity::indicator::notifications::Engine>& engine, const std::shared_ptr<const Settings>& settings): - m_clock(clock), - m_settings(settings) + impl(new Impl(engine, 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(); } -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; - } - - // 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<Popup*>(gdata); - return G_SOURCE_REMOVE; - }, popup); - - // maybe notify the client code that the popup's done - if (response == Popup::RESPONSE_SHOW) - show(appointment); - else if (response == Popup::RESPONSE_DISMISS) - dismiss(appointment); - }); + (*impl)(appointment, show, dismiss); } /*** diff --git a/src/sound.cpp b/src/sound.cpp new file mode 100644 index 0000000..d13c854 --- /dev/null +++ b/src/sound.cpp @@ -0,0 +1,143 @@ +/* + * 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 <http://www.gnu.org/licenses/>. + * + * Authors: + * Charles Kerr <charles.kerr@canonical.com> + */ + +#include <notifications/sound.h> + +#include <gst/gst.h> + +#include <mutex> // 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; + if (!gst_init_check (nullptr, nullptr, &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()); + 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() + { + 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: + + // 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<Impl*>(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 <datetime/settings-live.h> #include <datetime/snap.h> #include <datetime/timezones-live.h> +#include <notifications/notifications.h> #include <glib.h> 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<LiveSettings>(); settings->alarm_volume.set(volume); - auto timezones = std::make_shared<LiveTimezones>(settings, TIMEZONE_FILE); - auto clock = std::make_shared<LiveClock>(timezones); - Snap snap (clock, settings); + + auto notification_engine = std::make_shared<uin::Engine>("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 <datetime/dbus-shared.h> #include <datetime/settings.h> #include <datetime/snap.h> -#include <datetime/timezones.h> -#include <libdbustest/dbus-test.h> +#include <notifications/dbus-shared.h> +#include <notifications/notifications.h> -#include <libnotify/notify.h> +#include <libdbustest/dbus-test.h> #include <glib.h> @@ -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>(); settings->alarm_duration.set(duration_minutes); - auto timezones = std::make_shared<Timezones>(); - auto clock = std::make_shared<LiveClock>(timezones); - - Snap snap (clock, settings); + auto ne = std::make_shared<unity::indicator::notifications::Engine>(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<std::chrono::milliseconds>(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>(); - //settings->alarm_duration.set(duration_minutes); - auto timezones = std::make_shared<Timezones>(); - auto clock = std::make_shared<LiveClock>(timezones); - auto snap = new Snap (clock, settings); + auto ne = std::make_shared<unity::indicator::notifications::Engine>(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>(); - //settings->alarm_duration.set(duration_minutes); - auto timezones = std::make_shared<Timezones>(); - auto clock = std::make_shared<LiveClock>(timezones); - auto snap = new Snap (clock, settings); + auto ne = std::make_shared<unity::indicator::notifications::Engine>(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); |