/* * 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 #include #include #include #include #include #include // std::call_once() #include #include namespace unity { namespace indicator { 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) { 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) { if (gst_uri_is_valid (candidate.c_str())) { uri = candidate; break; } else if (g_file_test(candidate.c_str(), G_FILE_TEST_EXISTS)) { gchar* tmp = gst_filename_to_uri(candidate.c_str(), nullptr); if (tmp != nullptr) { uri = tmp; g_free (tmp); break; } } } 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())); 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(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"}; static constexpr char const * HINT_NONSHAPEDICON {"x-canonical-non-shaped-icon"}; }; /*** **** ***/ Snap::Snap(const std::shared_ptr& clock, const std::shared_ptr& settings): m_clock(clock), 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(); } 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(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); }); } /*** **** ***/ } // namespace datetime } // namespace indicator } // namespace unity