diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/CMakeLists.txt | 8 | ||||
-rw-r--r-- | src/actions-live.cpp | 138 | ||||
-rw-r--r-- | src/actions.cpp | 73 | ||||
-rw-r--r-- | src/awake.cpp | 184 | ||||
-rw-r--r-- | src/clock.cpp | 2 | ||||
-rw-r--r-- | src/date-time.cpp | 10 | ||||
-rw-r--r-- | src/engine-eds.cpp | 5 | ||||
-rw-r--r-- | src/haptic.cpp | 20 | ||||
-rw-r--r-- | src/main.cpp | 38 | ||||
-rw-r--r-- | src/menu.cpp | 116 | ||||
-rw-r--r-- | src/myself.cpp | 2 | ||||
-rw-r--r-- | src/notifications.cpp | 207 | ||||
-rw-r--r-- | src/planner-snooze.cpp | 4 | ||||
-rw-r--r-- | src/planner-upcoming.cpp | 2 | ||||
-rw-r--r-- | src/settings-live.cpp | 135 | ||||
-rw-r--r-- | src/snap.cpp | 142 | ||||
-rw-r--r-- | src/timezone-timedated.cpp | 236 | ||||
-rw-r--r-- | src/timezones-live.cpp | 11 |
18 files changed, 831 insertions, 502 deletions
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 96284fb..a64a50e 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -57,9 +57,9 @@ endif() # add warnings/coverage info on handwritten files # but not the autogenerated ones... set_source_files_properties(${SERVICE_CXX_SOURCES} - PROPERTIES COMPILE_FLAGS "${COMPILE_FLAGS} ${GCOV_FLAGS} -g -std=c++11") + PROPERTIES COMPILE_FLAGS "${COMPILE_FLAGS} -std=c++11") set_source_files_properties(${SERVICE_C_SOURCES} - PROPERTIES COMPILE_FLAGS "${COMPILE_FLAGS} ${GCOV_FLAGS} -g -std=c99") + PROPERTIES COMPILE_FLAGS "${COMPILE_FLAGS} -std=c99") # add the bin dir to our include path so our code can find the generated header files include_directories (${CMAKE_CURRENT_BINARY_DIR}) @@ -69,6 +69,6 @@ include_directories (${CMAKE_SOURCE_DIR}) link_directories (${SERVICE_DEPS_LIBRARY_DIRS}) add_executable (${SERVICE_EXEC} main.cpp) -set_source_files_properties(${SERVICE_SOURCES} main.cpp PROPERTIES COMPILE_FLAGS "${COMPILE_FLAGS} -g -std=c++11") -target_link_libraries (${SERVICE_EXEC} ${SERVICE_LIB} ${SERVICE_DEPS_LIBRARIES} ${GCOV_LIBS} ${URLDISPATCHER_LIBRARIES}) +set_source_files_properties(${SERVICE_SOURCES} main.cpp PROPERTIES COMPILE_FLAGS "${COMPILE_FLAGS} -std=c++11") +target_link_libraries (${SERVICE_EXEC} ${SERVICE_LIB} ${SERVICE_DEPS_LIBRARIES}) install (TARGETS ${SERVICE_EXEC} RUNTIME DESTINATION ${CMAKE_INSTALL_FULL_PKGLIBEXECDIR}) diff --git a/src/actions-live.cpp b/src/actions-live.cpp index 271d2f3..5c49bc4 100644 --- a/src/actions-live.cpp +++ b/src/actions-live.cpp @@ -17,14 +17,18 @@ * Charles Kerr <charles.kerr@canonical.com> */ +#include <datetime/dbus-shared.h> #include <datetime/actions-live.h> -#ifdef HAS_URLDISPATCHER -#include <lomiri-url-dispatcher.h> -#endif - #include <glib.h> +#include <sstream> + +extern "C" +{ + #include <ayatana/common/utils.h> +} + namespace ayatana { namespace indicator { namespace datetime { @@ -38,53 +42,55 @@ LiveActions::LiveActions(const std::shared_ptr<State>& state_in): { } -void LiveActions::execute_command(const std::string& cmdstr) -{ - const auto cmd = cmdstr.c_str(); - g_debug("Issuing command '%s'", cmd); +/*** +**** +***/ - GError* error = nullptr; - if (!g_spawn_command_line_async(cmd, &error)) +void LiveActions::open_alarm_app() +{ + if (ayatana_common_utils_is_lomiri()) { - g_warning("Unable to start \"%s\": %s", cmd, error->message); - g_error_free(error); + ayatana_common_utils_open_url("alarm://"); + } + else + { + ayatana_common_utils_execute_command("evolution -c calendar"); } } -void LiveActions::dispatch_url(const std::string& url) +void LiveActions::open_calendar_app(const DateTime& dt) { - g_debug("Dispatching url '%s'", url.c_str()); -#ifdef HAS_URLDISPATCHER - lomiri_url_dispatch_send(url.c_str(), nullptr, nullptr); -#else - // FIXME: Deal with this, if we build without liburl-dispatcher... -#endif + if (ayatana_common_utils_is_lomiri()) + { + const auto utc = dt.to_timezone("UTC"); + auto cmd = utc.format("calendar://startdate=%Y-%m-%dT%H:%M:%S+00:00"); + ayatana_common_utils_open_url(cmd.c_str()); + } + else + { + const auto utc = dt.start_of_day().to_timezone("UTC"); + auto cmd = utc.format("evolution \"calendar:///?startdate=%Y%m%dT%H%M%SZ\""); + ayatana_common_utils_execute_command(cmd.c_str()); + } } -/*** -**** -***/ - -void LiveActions::desktop_open_settings_app() +void LiveActions::open_settings_app() { - if (g_getenv ("MIR_SOCKET") != nullptr) + if (ayatana_common_utils_is_lomiri()) + { + ayatana_common_utils_open_url("settings:///system/time-date"); + } + else if (ayatana_common_utils_is_unity()) { - dispatch_url("settings:///system/time-date"); + ayatana_common_utils_execute_command("unity-control-center datetime"); + } + else if (ayatana_common_utils_is_mate()) + { + ayatana_common_utils_execute_command("mate-time-admin"); } else { - if ((g_strcmp0 (g_getenv ("XDG_CURRENT_DESKTOP"), "Unity") == 0)) - { - execute_command("unity-control-center datetime"); - } - else if ((g_strcmp0 (g_getenv ("XDG_CURRENT_DESKTOP"), "MATE") == 0)) - { - execute_command("mate-time-admin"); - } - else - { - execute_command("gnome-control-center datetime"); - } + ayatana_common_utils_execute_command("gnome-control-center datetime"); } } @@ -120,61 +126,25 @@ bool LiveActions::desktop_has_calendar_app() const return have_calendar; } -void LiveActions::desktop_open_alarm_app() -{ - execute_command("evolution -c calendar"); -} - -void LiveActions::desktop_open_appointment(const Appointment& appt) +void LiveActions::open_appointment(const Appointment& appt, const DateTime& date) { - desktop_open_calendar_app(appt.begin); -} - -void LiveActions::desktop_open_calendar_app(const DateTime& dt) -{ - const auto utc = dt.start_of_day().to_timezone("UTC"); - auto cmd = utc.format("evolution \"calendar:///?startdate=%Y%m%dT%H%M%SZ\""); - execute_command(cmd.c_str()); -} - -/*** -**** -***/ - -void LiveActions::phone_open_alarm_app() -{ - dispatch_url("appid://com.ubuntu.clock/clock/current-user-version"); -} - -void LiveActions::phone_open_appointment(const Appointment& appt) -{ - if (!appt.activation_url.empty()) { - dispatch_url(appt.activation_url); + ayatana_common_utils_open_url(appt.activation_url.c_str()); } else switch (appt.type) { case Appointment::UBUNTU_ALARM: - phone_open_alarm_app(); + open_alarm_app(); break; + case Appointment::EVENT: default: - phone_open_calendar_app(appt.begin); + open_calendar_app(date); + break; } } -void LiveActions::phone_open_calendar_app(const DateTime&) -{ - // does calendar app have a mechanism for specifying dates? - dispatch_url("appid://com.ubuntu.calendar/calendar/current-user-version"); -} - -void LiveActions::phone_open_settings_app() -{ - dispatch_url("settings:///system/time-date"); -} - /*** **** ***/ @@ -235,7 +205,7 @@ on_datetime1_proxy_ready (GObject * object G_GNUC_UNUSED, else { g_dbus_proxy_call(proxy, - "SetTimezone", + Bus::Timedate1::Methods::SET_TIMEZONE, g_variant_new ("(sb)", data->tzid.c_str(), TRUE), G_DBUS_CALL_FLAGS_NONE, -1, @@ -263,9 +233,9 @@ void LiveActions::set_location(const std::string& tzid, const std::string& name) g_dbus_proxy_new_for_bus (G_BUS_TYPE_SYSTEM, G_DBUS_PROXY_FLAGS_NONE, nullptr, - "org.freedesktop.timedate1", - "/org/freedesktop/timedate1", - "org.freedesktop.timedate1", + Bus::Timedate1::BUSNAME, + Bus::Timedate1::ADDR, + Bus::Timedate1::IFACE, nullptr, on_datetime1_proxy_ready, data); diff --git a/src/actions.cpp b/src/actions.cpp index 93629a0..315340a 100644 --- a/src/actions.cpp +++ b/src/actions.cpp @@ -48,13 +48,8 @@ DateTime datetime_from_timet_variant(GVariant* v) return DateTime::NowLocal(); } -bool lookup_appointment_by_uid_variant(const std::shared_ptr<State>& state, GVariant* vuid, Appointment& setme) +bool lookup_appointment_by_uid(const std::shared_ptr<State>& state, const gchar* uid, Appointment& setme) { - g_return_val_if_fail(vuid != nullptr, false); - g_return_val_if_fail(g_variant_type_equal(G_VARIANT_TYPE_STRING,g_variant_get_type(vuid)), false); - const auto uid = g_variant_get_string(vuid, nullptr); - g_return_val_if_fail(uid && *uid, false); - for(const auto& appt : state->calendar_upcoming->appointments().get()) { if (appt.uid == uid) @@ -67,46 +62,28 @@ bool lookup_appointment_by_uid_variant(const std::shared_ptr<State>& state, GVar return false; } -void on_desktop_appointment_activated (GSimpleAction*, GVariant *vuid, gpointer gself) +void on_appointment_activated (GSimpleAction*, GVariant *vdata, gpointer gself) { auto self = static_cast<Actions*>(gself); Appointment appt; - if (lookup_appointment_by_uid_variant(self->state(), vuid, appt)) - self->desktop_open_appointment(appt); + const gchar* uid = nullptr; + gint64 time = 0; + g_variant_get(vdata, "(&sx)", &uid, &time); + if (lookup_appointment_by_uid(self->state(), uid, appt)) + self->open_appointment(appt, DateTime::Local(time)); } -void on_desktop_alarm_activated (GSimpleAction*, GVariant*, gpointer gself) +void on_alarm_activated (GSimpleAction*, GVariant*, gpointer gself) { - static_cast<Actions*>(gself)->desktop_open_alarm_app(); + static_cast<Actions*>(gself)->open_alarm_app(); } -void on_desktop_calendar_activated (GSimpleAction*, GVariant* vt, gpointer gself) +void on_calendar_activated (GSimpleAction*, GVariant* vt, gpointer gself) { const auto dt = datetime_from_timet_variant(vt); - static_cast<Actions*>(gself)->desktop_open_calendar_app(dt); + static_cast<Actions*>(gself)->open_calendar_app(dt); } -void on_desktop_settings_activated (GSimpleAction*, GVariant*, gpointer gself) +void on_settings_activated (GSimpleAction*, GVariant*, gpointer gself) { - static_cast<Actions*>(gself)->desktop_open_settings_app(); -} - -void on_phone_appointment_activated (GSimpleAction*, GVariant *vuid, gpointer gself) -{ - auto self = static_cast<Actions*>(gself); - Appointment appt; - if (lookup_appointment_by_uid_variant(self->state(), vuid, appt)) - self->phone_open_appointment(appt); -} -void on_phone_alarm_activated (GSimpleAction*, GVariant*, gpointer gself) -{ - static_cast<Actions*>(gself)->phone_open_alarm_app(); -} -void on_phone_calendar_activated (GSimpleAction*, GVariant* vt, gpointer gself) -{ - const auto dt = datetime_from_timet_variant(vt); - static_cast<Actions*>(gself)->phone_open_calendar_app(dt); -} -void on_phone_settings_activated (GSimpleAction*, GVariant*, gpointer gself) -{ - static_cast<Actions*>(gself)->phone_open_settings_app(); + static_cast<Actions*>(gself)->open_settings_app(); } void on_set_location(GSimpleAction * /*action*/, @@ -134,9 +111,9 @@ void on_calendar_active_changed(GSimpleAction * /*action*/, } } -void on_calendar_activated(GSimpleAction * /*action*/, - GVariant * state, - gpointer gself) +void on_calendar_date_activated(GSimpleAction * /*action*/, + GVariant * state, + gpointer gself) { const time_t t = g_variant_get_int64(state); @@ -198,15 +175,15 @@ Actions::Actions(const std::shared_ptr<State>& state): { GActionEntry entries[] = { - { "desktop.open-appointment", on_desktop_appointment_activated, "s", nullptr }, - { "desktop.open-alarm-app", on_desktop_alarm_activated }, - { "desktop.open-calendar-app", on_desktop_calendar_activated, "x", nullptr }, - { "desktop.open-settings-app", on_desktop_settings_activated }, + { "desktop.open-appointment", on_appointment_activated, "(sx)", nullptr }, + { "desktop.open-alarm-app", on_alarm_activated }, + { "desktop.open-calendar-app", on_calendar_activated, "x", nullptr }, + { "desktop.open-settings-app", on_settings_activated }, - { "phone.open-appointment", on_phone_appointment_activated, "s", nullptr }, - { "phone.open-alarm-app", on_phone_alarm_activated }, - { "phone.open-calendar-app", on_phone_calendar_activated, "x", nullptr }, - { "phone.open-settings-app", on_phone_settings_activated }, + { "phone.open-appointment", on_appointment_activated, "(sx)", nullptr }, + { "phone.open-alarm-app", on_alarm_activated }, + { "phone.open-calendar-app", on_calendar_activated, "x", nullptr }, + { "phone.open-settings-app", on_settings_activated }, { "calendar-active", nullptr, nullptr, "false", on_calendar_active_changed }, { "set-location", on_set_location, "s" } @@ -240,7 +217,7 @@ Actions::Actions(const std::shared_ptr<State>& state): v = create_calendar_state(state); a = g_simple_action_new_stateful("calendar", G_VARIANT_TYPE_INT64, v); g_action_map_add_action(gam, G_ACTION(a)); - g_signal_connect(a, "activate", G_CALLBACK(on_calendar_activated), this); + g_signal_connect(a, "activate", G_CALLBACK(on_calendar_date_activated), this); g_object_unref(a); /// diff --git a/src/awake.cpp b/src/awake.cpp index dd41c35..9ee337e 100644 --- a/src/awake.cpp +++ b/src/awake.cpp @@ -36,89 +36,38 @@ class Awake::Impl { public: - Impl(const std::string& app_name): + Impl(GDBusConnection* bus, const std::string& app_name): m_app_name(app_name), - m_cancellable(g_cancellable_new()) + m_cancellable(g_cancellable_new()), + m_system_bus{G_DBUS_CONNECTION(g_object_ref(bus))} { - g_bus_get(G_BUS_TYPE_SYSTEM, m_cancellable, on_system_bus_ready, this); + // ask repowerd to keep the system awake + static constexpr int32_t POWERD_SYS_STATE_ACTIVE = 1; + g_dbus_connection_call (m_system_bus, + BUS_POWERD_NAME, + BUS_POWERD_PATH, + BUS_POWERD_INTERFACE, + "requestSysState", + g_variant_new("(si)", m_app_name.c_str(), POWERD_SYS_STATE_ACTIVE), + G_VARIANT_TYPE("(s)"), + G_DBUS_CALL_FLAGS_NONE, + -1, + m_cancellable, + on_force_awake_response, + this); + } ~Impl() { g_cancellable_cancel (m_cancellable); g_object_unref (m_cancellable); - - if (m_display_on_timer) - { - g_source_remove (m_display_on_timer); - m_display_on_timer = 0; - } - - if (m_system_bus != nullptr) - { - unforce_awake (); - remove_display_on_request (); - g_object_unref (m_system_bus); - } + unforce_awake (); + g_clear_object (&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_keep_display_on_response, - self); - - g_object_unref (system_bus); - } - } - static void on_force_awake_response (GObject * connection, GAsyncResult * res, gpointer gself) @@ -146,60 +95,11 @@ private: 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_keep_display_on_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_display_on_cookie = NO_DISPLAY_ON_COOKIE; - g_variant_get (args, "(i)", &self->m_display_on_cookie); - g_debug ("m_display_on_cookie is now '%d'", self->m_display_on_cookie); - - self->m_display_on_timer = g_timeout_add_seconds (self->m_display_on_seconds, - on_display_on_timer, - gself); g_variant_unref (args); } } - static gboolean on_display_on_timer (gpointer gself) - { - auto self = static_cast<Impl*>(gself); - - self->m_display_on_timer = 0; - self->remove_display_on_request(); - - return G_SOURCE_REMOVE; - } - - void unforce_awake () { g_return_if_fail (G_IS_DBUS_CONNECTION(m_system_bus)); @@ -215,7 +115,7 @@ private: nullptr, G_DBUS_CALL_FLAGS_NONE, -1, - nullptr, + m_cancellable, nullptr, nullptr); @@ -223,56 +123,18 @@ private: } } - void remove_display_on_request () - { - g_return_if_fail (G_IS_DBUS_CONNECTION(m_system_bus)); - - if (m_display_on_cookie != NO_DISPLAY_ON_COOKIE) - { - g_dbus_connection_call (m_system_bus, - BUS_SCREEN_NAME, - BUS_SCREEN_PATH, - BUS_SCREEN_INTERFACE, - "removeDisplayOnRequest", - g_variant_new("(i)", m_display_on_cookie), - nullptr, - G_DBUS_CALL_FLAGS_NONE, - -1, - nullptr, - nullptr, - nullptr); - - m_display_on_cookie = NO_DISPLAY_ON_COOKIE; - } - } - const std::string m_app_name; GCancellable * m_cancellable = nullptr; GDBusConnection * m_system_bus = nullptr; char * m_awake_cookie = nullptr; - - /** - * As described by bug #1434637, alarms should have the display turn on, - * dim, and turn off "just like it would if you'd woken it up yourself". - * USC may be adding an intent-based bus API to handle this use case, - * e.g. turnDisplayOnTemporarily(intent), but there's no timeframe for it. - * - * Until that's avaialble, we can get close to Design's specs by - * requesting a display-on cookie and then releasing the cookie - * a moment later. */ - const guint m_display_on_seconds = 1; - guint m_display_on_timer = 0; - int32_t m_display_on_cookie = NO_DISPLAY_ON_COOKIE; - - static constexpr int32_t NO_DISPLAY_ON_COOKIE { std::numeric_limits<int32_t>::min() }; }; /*** **** ***/ -Awake::Awake(const std::string& app_name): - impl(new Impl(app_name)) +Awake::Awake(GDBusConnection* system_bus, const std::string& app_name): + impl{new Impl{system_bus, app_name}} { } diff --git a/src/clock.cpp b/src/clock.cpp index a2ef387..47eebe6 100644 --- a/src/clock.cpp +++ b/src/clock.cpp @@ -155,7 +155,7 @@ private: } /** - *** DBus Chatter: com.canonical.powerd + *** DBus Chatter: com.lomiri.Repowerd *** *** Fire Clock::minute_changed() signal when powerd says the system's *** has awoken from sleep -- the old timestamp is likely out-of-date diff --git a/src/date-time.cpp b/src/date-time.cpp index 169426c..911fb7a 100644 --- a/src/date-time.cpp +++ b/src/date-time.cpp @@ -245,11 +245,21 @@ bool DateTime::operator<(const DateTime& that) const return g_date_time_compare(get(), that.get()) < 0; } +bool DateTime::operator>(const DateTime& that) const +{ + return g_date_time_compare(get(), that.get()) > 0; +} + bool DateTime::operator<=(const DateTime& that) const { return g_date_time_compare(get(), that.get()) <= 0; } +bool DateTime::operator>=(const DateTime& that) const +{ + return g_date_time_compare(get(), that.get()) >= 0; +} + bool DateTime::operator!=(const DateTime& that) const { // return true if this isn't set, or if it's not equal diff --git a/src/engine-eds.cpp b/src/engine-eds.cpp index fc6a45b..4228258 100644 --- a/src/engine-eds.cpp +++ b/src/engine-eds.cpp @@ -1260,11 +1260,6 @@ private: **** ***/ -EdsEngine::EdsEngine(): - p(new Impl(std::shared_ptr<Myself>(new Myself))) -{ -} - EdsEngine::EdsEngine(const std::shared_ptr<Myself> &myself): p(new Impl(myself)) { diff --git a/src/haptic.cpp b/src/haptic.cpp index 7430c04..dc2cb82 100644 --- a/src/haptic.cpp +++ b/src/haptic.cpp @@ -37,9 +37,10 @@ class Haptic::Impl { public: - Impl(const Mode& mode): + Impl(const Mode& mode, bool repeat): m_mode(mode), - m_cancellable(g_cancellable_new()) + m_cancellable(g_cancellable_new()), + m_repeat(repeat) { g_bus_get (G_BUS_TYPE_SESSION, m_cancellable, on_bus_ready, this); } @@ -93,11 +94,15 @@ private: // one second on, one second off. m_pattern = std::vector<uint32_t>({1000u, 1000u}); break; + } - // 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); + if (m_repeat) + { + // 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(); } @@ -146,14 +151,15 @@ private: GDBusConnection * m_bus = nullptr; std::vector<uint32_t> m_pattern; guint m_tag = 0; + bool m_repeat = false; }; /*** **** ***/ -Haptic::Haptic(const Mode& mode): - impl(new Impl (mode)) +Haptic::Haptic(const Mode& mode, bool repeat): + impl(new Impl (mode, repeat)) { } diff --git a/src/main.cpp b/src/main.cpp index fdd84b5..6cad190 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -25,6 +25,7 @@ #include <datetime/exporter.h> #include <datetime/locations-settings.h> #include <datetime/menu.h> +#include <datetime/myself.h> #include <datetime/planner-aggregate.h> #include <datetime/planner-snooze.h> #include <datetime/planner-range.h> @@ -60,7 +61,7 @@ namespace if (!g_strcmp0("lightdm", g_get_user_name())) engine.reset(new MockEngine); else - engine.reset(new EdsEngine); + engine.reset(new EdsEngine(std::shared_ptr<Myself>(new Myself))); return engine; } @@ -70,7 +71,7 @@ namespace { // create the live objects auto live_settings = std::make_shared<LiveSettings>(); - auto live_timezones = std::make_shared<LiveTimezones>(live_settings); + auto live_timezones = std::make_shared<LiveTimezones>(live_settings, timezone_); auto live_clock = std::make_shared<LiveClock>(timezone_); // create a full-month planner currently pointing to the current month @@ -131,8 +132,17 @@ main(int /*argc*/, char** /*argv*/) bindtextdomain(GETTEXT_PACKAGE, LOCALEDIR); textdomain(GETTEXT_PACKAGE); + // get the system bus + GError* error {}; + auto system_bus = g_bus_get_sync(G_BUS_TYPE_SYSTEM, nullptr, &error); + if (error != nullptr) { + g_critical("Unable to get system bus: %s", error->message); + g_clear_error(&error); + return 0; + } + auto engine = create_engine(); - auto timezone_ = std::make_shared<TimedatedTimezone>(); + auto timezone_ = std::make_shared<TimedatedTimezone>(system_bus); auto state = create_state(engine, timezone_); auto actions = std::make_shared<LiveActions>(state); MenuFactory factory(actions, state); @@ -141,14 +151,23 @@ main(int /*argc*/, char** /*argv*/) // set up the snap decisions auto snooze_planner = std::make_shared<SnoozePlanner>(state->settings, state->clock); auto notification_engine = std::make_shared<ain::Engine>("ayatana-indicator-datetime-service"); - std::unique_ptr<Snap> snap (new Snap(notification_engine, state->settings)); + auto sound_builder = std::make_shared<uin::DefaultSoundBuilder>(); + std::unique_ptr<Snap> snap (new Snap(notification_engine, sound_builder, state->settings, system_bus)); auto alarm_queue = create_simple_alarm_queue(state->clock, snooze_planner, engine, timezone_); - auto on_snooze = [snooze_planner](const Appointment& appointment, const Alarm& alarm) { - snooze_planner->add(appointment, alarm); + auto on_response = [snooze_planner, actions](const Appointment& appointment, const Alarm& alarm, const Snap::Response& response) { + switch(response) { + case Snap::Response::Snooze: + snooze_planner->add(appointment, alarm); + break; + case Snap::Response::ShowApp: + actions->open_appointment(appointment, appointment.begin); + break; + case Snap::Response::None: + break; + } }; - auto on_ok = [](const Appointment&, const Alarm&){}; - auto on_alarm_reached = [&engine, &snap, &on_snooze, &on_ok](const Appointment& appointment, const Alarm& alarm) { - (*snap)(appointment, alarm, on_snooze, on_ok); + auto on_alarm_reached = [&engine, &snap, &on_response](const Appointment& appointment, const Alarm& alarm) { + (*snap)(appointment, alarm, on_response); engine->disable_ubuntu_alarm(appointment); }; alarm_queue->alarm_reached().connect(on_alarm_reached); @@ -170,5 +189,6 @@ main(int /*argc*/, char** /*argv*/) g_main_loop_run(loop); g_main_loop_unref(loop); + g_clear_object(&system_bus); return 0; } diff --git a/src/menu.cpp b/src/menu.cpp index d19ad73..b1ac75c 100644 --- a/src/menu.cpp +++ b/src/menu.cpp @@ -22,6 +22,8 @@ #include <datetime/state.h> #include <glib/gi18n.h> #include <gio/gio.h> +#include <algorithm> +#include <iterator> #include <vector> extern "C" @@ -58,11 +60,90 @@ GMenuModel* Menu::menu_model() return G_MENU_MODEL(m_menu); } +/** + * To avoid a giant menu on the PC, and to avoid pushing lower menu items + * off-screen on the phone, the menu should show the + * next five calendar events, if any. + * + * The list might include multiple occurrences of the same event (bug 1515821). + */ +std::vector<Appointment> +Menu::get_display_appointments(const std::vector<Appointment>& appointments_in, + const DateTime& now, + unsigned int max_items) +{ + std::vector<Appointment> appointments; + std::copy_if(appointments_in.begin(), + appointments_in.end(), + std::back_inserter(appointments), + [now](const Appointment& a){return a.end >= now;}); + + if (appointments.size() > max_items) + { + const auto next_minute = now.add_full(0,0,0,0,1,-now.seconds()); + const auto start_of_day = now.start_of_day(); + const auto end_of_day = now.end_of_day(); + + /* + * If there are more than five, the events shown should be, in order of priority: + * 1. any events that start or end (bug 1329048) after the current minute today; + * 2. any full-day events that span all of today (bug 1302004); + * 3. any events that start or end tomorrow; + * 4. any events that start or end the day after tomorrow; and so on. + */ + auto compare = [next_minute, start_of_day, end_of_day]( + const Appointment& a, + const Appointment& b) + { + const bool a_later_today = (a.begin >= next_minute) || (a.end <= end_of_day); + const bool b_later_today = (b.begin >= next_minute) || (b.end <= end_of_day); + if (a_later_today != b_later_today) + return a_later_today; + + const bool a_full_day_today = (a.begin <= start_of_day) && (end_of_day <= a.end); + const bool b_full_day_today = (b.begin <= start_of_day) && (end_of_day <= b.end); + if (a_full_day_today != b_full_day_today) + return a_full_day_today; + + const bool a_after_today = (a.begin > end_of_day) || (a.end > end_of_day); + const bool b_after_today = (a.begin > end_of_day) || (a.end > end_of_day); + if (a_after_today != b_after_today) + return a_after_today; + if (a.begin != b.begin) + return a.begin < b.begin; + if (b.end != b.end) + return a.end < b.end; + + return false; + }; + std::sort(appointments.begin(), appointments.end(), compare); + appointments.resize(max_items); + } + + /* + * However, the display order should be the reverse: full-day events + * first (since they start first), part-day events afterward in + * chronological order. If multiple events have exactly the same start+end + * time, they should be sorted alphabetically. + */ + auto compare = [](const Appointment& a, const Appointment& b) + { + if (a.begin != b.begin) + return a.begin < b.begin; + + if (a.end != b.end) + return a.end < b.end; + + return a.summary < b.summary; + }; + std::sort(appointments.begin(), appointments.end(), compare); + return appointments; +} + /**** ***** ****/ - #define ALARM_ICON_NAME "alarm-clock" #define CALENDAR_ICON_NAME "calendar" @@ -150,25 +231,19 @@ protected: void update_upcoming() { - // The usual case is on desktop (and /only/ case on phone) - // is that we're looking at the current date and want to see - // "the next five calendar events, if any." - // - // However on the Desktop when the user clicks onto a different - // calendar date, show the next five calendar events starting - // from the beginning of that clicked day. - DateTime begin; + // The usual case is to show events germane to the current time. + // However when the user clicks onto a different calendar date, + // we pick events starting from the beginning of that clicked day. const auto now = m_state->clock->localtime(); const auto calendar_day = m_state->calendar_month->month().get(); - if ((profile() == Desktop) && !DateTime::is_same_day(now, calendar_day)) - begin = calendar_day.start_of_day(); - else - begin = now.start_of_minute(); + const auto begin = DateTime::is_same_day(now, calendar_day) + ? now.start_of_minute() + : calendar_day.start_of_day(); - std::vector<Appointment> upcoming; - for(const auto& a : m_state->calendar_upcoming->appointments().get()) - if (begin <= a.begin) - upcoming.push_back(a); + auto upcoming = get_display_appointments( + m_state->calendar_upcoming->appointments().get(), + begin + ); if (m_upcoming != upcoming) { @@ -339,9 +414,12 @@ private: if (!appt.color.empty()) g_menu_item_set_attribute (menu_item, "x-ayatana-color", "s", appt.color.c_str()); - if (action_name != nullptr) + if (action_name != nullptr) { g_menu_item_set_action_and_target_value (menu_item, action_name, - g_variant_new_string (appt.uid.c_str())); + g_variant_new ("(sx)", + appt.uid.c_str(), + unix_time)); + } g_menu_append_item (menu, menu_item); g_object_unref (menu_item); diff --git a/src/myself.cpp b/src/myself.cpp index 9c02054..ae2f061 100644 --- a/src/myself.cpp +++ b/src/myself.cpp @@ -25,6 +25,8 @@ #include <libaccounts-glib/accounts-glib.h> #endif +#include <libaccounts-glib/ag-account.h> + #include <algorithm> namespace ayatana { diff --git a/src/notifications.cpp b/src/notifications.cpp index 051653d..b36227b 100644 --- a/src/notifications.cpp +++ b/src/notifications.cpp @@ -21,10 +21,23 @@ #include <libnotify/notify.h> +#include <messaging-menu/messaging-menu-app.h> +#include <messaging-menu/messaging-menu-message.h> + +#ifdef HAS_URLDISPATCHER +#include <lomiri-url-dispatcher.h> +#endif + +#include <uuid/uuid.h> + +#include <gio/gdesktopappinfo.h> + #include <map> #include <set> #include <string> #include <vector> +#include <memory> + namespace ayatana { namespace indicator { @@ -45,9 +58,13 @@ public: std::string m_body; std::string m_icon_name; std::chrono::seconds m_duration; + gint64 m_start_time {}; 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; + std::function<void()> m_timeout_callback; + bool m_show_notification_bubble; + bool m_post_to_messaging_menu; }; Builder::Builder(): @@ -101,6 +118,30 @@ Builder::set_closed_callback (std::function<void (const std::string&)> cb) impl->m_closed_callback.swap (cb); } +void +Builder::set_timeout_callback (std::function<void()> cb) +{ + impl->m_timeout_callback.swap (cb); +} + +void +Builder::set_start_time (uint64_t time) +{ + impl->m_start_time = time; +} + +void +Builder::set_show_notification_bubble (bool show) +{ + impl->m_show_notification_bubble = show; +} + +void +Builder::set_post_to_messaging_menu (bool post) +{ + impl->m_post_to_messaging_menu = post; +} + /*** **** ***/ @@ -110,7 +151,14 @@ class Engine::Impl struct notification_data { std::shared_ptr<NotifyNotification> nn; - std::function<void(const std::string&)> closed_callback; + Builder::Impl data; + }; + + struct messaging_menu_data + { + std::string msg_id; + std::function<void()> callback; + Engine::Impl *self; }; public: @@ -120,13 +168,23 @@ public: { if (!notify_init(app_name.c_str())) g_critical("Unable to initialize libnotify!"); + + // messaging menu + auto app_id = calendar_app_id(); + if (!app_id.empty()) { + m_messaging_app.reset(messaging_menu_app_new(app_id.c_str()), g_object_unref); + messaging_menu_app_register(m_messaging_app.get()); + } } ~Impl() { close_all (); + remove_all (); notify_uninit (); + if (m_messaging_app) + messaging_menu_app_unregister (m_messaging_app.get()); } const std::string& app_name() const @@ -217,10 +275,15 @@ public: notification_key_quark(), GINT_TO_POINTER(key)); - m_notifications[key] = { nn, info.m_closed_callback }; + m_notifications[key] = { nn, info }; g_signal_connect (nn.get(), "closed", G_CALLBACK(on_notification_closed), this); + if (!info.m_show_notification_bubble) { + post(info); + return ret; + } + GError * error = nullptr; if (notify_notification_show(nn.get(), &error)) { @@ -238,6 +301,76 @@ public: return ret; } + std::string post(const Builder::Impl& data) + { + if (!data.m_post_to_messaging_menu) { + return ""; + } + + if (!m_messaging_app) { + return std::string(); + } + uuid_t message_uuid; + uuid_generate(message_uuid); + + char uuid_buf[37]; + uuid_unparse(message_uuid, uuid_buf); + const std::string message_id(uuid_buf); + + // use full icon path name, "calendar-app" does not work with themed icons + auto icon_file = g_file_new_for_path(calendar_app_icon().c_str()); + // messaging_menu_message_new: will take control of icon object + GIcon *icon = g_file_icon_new(icon_file); + g_object_unref(icon_file); + + // check if source exists + if (!messaging_menu_app_has_source(m_messaging_app.get(), m_app_name.c_str())) + messaging_menu_app_append_source(m_messaging_app.get(), m_app_name.c_str(), nullptr, "Calendar"); + + auto msg = messaging_menu_message_new(message_id.c_str(), + icon, + data.m_title.c_str(), + nullptr, + data.m_body.c_str(), + data.m_start_time * G_USEC_PER_SEC); // secs -> microsecs + if (msg) + { + std::shared_ptr<messaging_menu_data> msg_data(new messaging_menu_data{message_id, data.m_timeout_callback, this}); + m_messaging_messages[message_id] = msg_data; + g_signal_connect(G_OBJECT(msg), "activate", + G_CALLBACK(on_message_activated), msg_data.get()); + messaging_menu_app_append_message(m_messaging_app.get(), msg, m_app_name.c_str(), false); + + // we use that to keep track of messaging, in case of message get cleared from menu + g_object_set_data_full(G_OBJECT(msg), "destroy-notify", msg_data.get(), on_message_destroyed); + // keep the message control with message_menu + g_object_unref(msg); + + return message_id; + } else { + g_warning("Fail to create messaging menu message"); + } + return ""; + } + + void remove (const std::string &key) + { + auto it = m_messaging_messages.find(key); + if (it != m_messaging_messages.end()) + { + // tell the server to remove message + messaging_menu_app_remove_message_by_id(m_messaging_app.get(), it->second->msg_id.c_str()); + // message will be remove by on_message_destroyed cb. + } + } + + void remove_all () + { + // call remove() on all our keys + while (!m_messaging_messages.empty()) + remove(m_messaging_messages.begin()->first); + } + private: const std::set<std::string>& server_caps() const @@ -279,6 +412,28 @@ private: static_cast<Impl*>(gself)->remove_closed_notification(GPOINTER_TO_INT(gkey)); } + static void on_message_activated (MessagingMenuMessage *, + const char *, + GVariant *, + gpointer data) + { + auto msg_data = static_cast<messaging_menu_data*>(data); + auto it = msg_data->self->m_messaging_messages.find(msg_data->msg_id); + g_return_if_fail (it != msg_data->self->m_messaging_messages.end()); + const auto& ndata = it->second; + + if (ndata->callback) + ndata->callback(); + } + + static void on_message_destroyed(gpointer data) + { + auto msg_data = static_cast<messaging_menu_data*>(data); + auto it = msg_data->self->m_messaging_messages.find(msg_data->msg_id); + if (it != msg_data->self->m_messaging_messages.end()) + msg_data->self->m_messaging_messages.erase(it); + } + void remove_closed_notification (int key) { auto it = m_notifications.find(key); @@ -286,25 +441,67 @@ private: const auto& ndata = it->second; auto nn = ndata.nn.get(); - if (ndata.closed_callback) + + if (ndata.data.m_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); + ndata.data.m_closed_callback (action); + // empty action means that the notification got timeout + // post a message on messaging menu + if (action.empty()) + post(ndata.data); } m_notifications.erase(it); } + static std::string calendar_app_id() + { +#ifdef HAS_URLDISPATCHER + auto urls = g_strsplit("calendar://", ",", 0); + auto appids = lomiri_url_dispatch_url_appid(const_cast<const gchar**>(urls)); + g_strfreev(urls); + std::string result; + if (appids != nullptr) { + // Due the use of old API by messaging_menu we need append a extra ".desktop" to the app_id. + result = std::string(appids[0]) + ".desktop"; + g_strfreev(appids); + } + return result; +#else + return std::string(); +#endif + } + + static std::string calendar_app_icon() + { + auto app_desktop = g_desktop_app_info_new(calendar_app_id().c_str()); + if (app_desktop != nullptr) { + auto icon_name = g_desktop_app_info_get_string(app_desktop, "Icon"); + g_object_unref(app_desktop); + if (icon_name) { + std::string result(icon_name); + g_free(icon_name); + return result; + } + } + g_warning("Fail to get calendar icon"); + return std::string(); + } + /*** **** ***/ + // messaging menu + std::shared_ptr<MessagingMenuApp> m_messaging_app; + std::map<std::string, std::shared_ptr<messaging_menu_data> > m_messaging_messages; + const std::string m_app_name; // key-to-data diff --git a/src/planner-snooze.cpp b/src/planner-snooze.cpp index cb365ca..b81c912 100644 --- a/src/planner-snooze.cpp +++ b/src/planner-snooze.cpp @@ -59,7 +59,9 @@ public: appt.alarms.push_back(alarm); // reschedule the alarm to go off N minutes from now - const auto offset = std::chrono::minutes(m_settings->snooze_duration.get()); + // also take into count every whole minute since the alarm went off + const auto offset_to_now = std::chrono::duration_cast<std::chrono::minutes>(std::chrono::microseconds(DateTime::NowLocal() - appt.begin)); + const auto offset = offset_to_now + std::chrono::minutes(m_settings->snooze_duration.get()); appt.begin += offset; appt.end += offset; appt.alarms[0].time += offset; diff --git a/src/planner-upcoming.cpp b/src/planner-upcoming.cpp index 918ebbe..149ac09 100644 --- a/src/planner-upcoming.cpp +++ b/src/planner-upcoming.cpp @@ -33,7 +33,7 @@ UpcomingPlanner::UpcomingPlanner(const std::shared_ptr<RangePlanner>& range_plan { date().changed().connect([this](const DateTime& dt){ // set the range to the upcoming month - const auto b = dt.add_days(-1).start_of_day(); + const auto b = dt.start_of_day(); const auto e = b.add_full(0, 1, 0, 0, 0, 0); g_debug("%p setting date range to [%s..%s]", this, b.format("%F %T").c_str(), e.format("%F %T").c_str()); m_range_planner->range().set(std::pair<DateTime,DateTime>(b,e)); diff --git a/src/settings-live.cpp b/src/settings-live.cpp index 5c2addb..dc90d0e 100644 --- a/src/settings-live.cpp +++ b/src/settings-live.cpp @@ -19,6 +19,11 @@ #include <datetime/settings-live.h> +extern "C" +{ + #include <ayatana/common/utils.h> +} + namespace ayatana { namespace indicator { namespace datetime { @@ -29,13 +34,15 @@ namespace datetime { LiveSettings::~LiveSettings() { + g_clear_object(&m_settings_general_notification); + g_clear_object(&m_settings_cal_notification); g_clear_object(&m_settings); } LiveSettings::LiveSettings(): m_settings(g_settings_new(SETTINGS_INTERFACE)) { - g_signal_connect (m_settings, "changed", G_CALLBACK(on_changed), this); + g_signal_connect (m_settings, "changed", G_CALLBACK(on_changed_ccid), this); // init the Properties from the GSettings backend update_custom_time_format(); @@ -58,6 +65,25 @@ LiveSettings::LiveSettings(): update_alarm_haptic(); update_snooze_duration(); + if (ayatana_common_utils_is_lomiri()) + { + m_settings_cal_notification = g_settings_new_with_path(SETTINGS_NOTIFY_SCHEMA_ID, SETTINGS_NOTIFY_CALENDAR_PATH); + m_settings_general_notification = g_settings_new(SETTINGS_NOTIFY_APPS_SCHEMA_ID); + g_signal_connect (m_settings_cal_notification, "changed", G_CALLBACK(on_changed_cal_notification), this); + g_signal_connect (m_settings_general_notification, "changed", G_CALLBACK(on_changed_general_notification), this); + update_cal_notification_enabled(); + update_cal_notification_sounds(); + update_cal_notification_vibrations(); + update_cal_notification_bubbles(); + update_cal_notification_list(); + update_vibrate_silent_mode(); + } + else + { + m_settings_cal_notification = NULL; + m_settings_general_notification = NULL; + } + // now listen for clients to change the properties s.t. we can sync update GSettings custom_time_format.changed().connect([this](const std::string& value){ @@ -140,6 +166,30 @@ LiveSettings::LiveSettings(): snooze_duration.changed().connect([this](unsigned int value){ g_settings_set_uint(m_settings, SETTINGS_SNOOZE_DURATION_S, value); }); + + cal_notification_enabled.changed().connect([this](bool value){ + g_settings_set_boolean(m_settings_cal_notification, SETTINGS_NOTIFY_ENABLED_KEY, value); + }); + + cal_notification_sounds.changed().connect([this](bool value){ + g_settings_set_boolean(m_settings_cal_notification, SETTINGS_NOTIFY_SOUNDS_KEY, value); + }); + + cal_notification_vibrations.changed().connect([this](bool value){ + g_settings_set_boolean(m_settings_cal_notification, SETTINGS_NOTIFY_VIBRATIONS_KEY, value); + }); + + cal_notification_bubbles.changed().connect([this](bool value){ + g_settings_set_boolean(m_settings_cal_notification, SETTINGS_NOTIFY_BUBBLES_KEY, value); + }); + + cal_notification_list.changed().connect([this](bool value){ + g_settings_set_boolean(m_settings_cal_notification, SETTINGS_NOTIFY_LIST_KEY, value); + }); + + vibrate_silent_mode.changed().connect([this](bool value){ + g_settings_set_boolean(m_settings_general_notification, SETTINGS_VIBRATE_SILENT_KEY, value); + }); } /*** @@ -261,18 +311,91 @@ void LiveSettings::update_snooze_duration() snooze_duration.set(g_settings_get_uint(m_settings, SETTINGS_SNOOZE_DURATION_S)); } +void LiveSettings::update_cal_notification_enabled() +{ + cal_notification_enabled.set(g_settings_get_boolean(m_settings_cal_notification, SETTINGS_NOTIFY_ENABLED_KEY)); +} + +void LiveSettings::update_cal_notification_sounds() +{ + cal_notification_sounds.set(g_settings_get_boolean(m_settings_cal_notification, SETTINGS_NOTIFY_SOUNDS_KEY)); +} + +void LiveSettings::update_cal_notification_vibrations() +{ + cal_notification_vibrations.set(g_settings_get_boolean(m_settings_cal_notification, SETTINGS_NOTIFY_VIBRATIONS_KEY)); +} + +void LiveSettings::update_cal_notification_bubbles() +{ + cal_notification_bubbles.set(g_settings_get_boolean(m_settings_cal_notification, SETTINGS_NOTIFY_BUBBLES_KEY)); +} + +void LiveSettings::update_cal_notification_list() +{ + cal_notification_list.set(g_settings_get_boolean(m_settings_cal_notification, SETTINGS_NOTIFY_LIST_KEY)); +} + +void LiveSettings::update_vibrate_silent_mode() +{ + vibrate_silent_mode.set(g_settings_get_boolean(m_settings_general_notification, SETTINGS_VIBRATE_SILENT_KEY)); +} + +/*** +**** +***/ + +void LiveSettings::on_changed_cal_notification(GSettings* /*settings*/, + gchar* key, + gpointer gself) +{ + static_cast<LiveSettings*>(gself)->update_key_cal_notification(key); +} + + +void LiveSettings::update_key_cal_notification(const std::string& key) +{ + if (key == SETTINGS_NOTIFY_ENABLED_KEY) + update_cal_notification_enabled(); + else if (key == SETTINGS_NOTIFY_SOUNDS_KEY) + update_cal_notification_sounds(); + else if (key == SETTINGS_NOTIFY_VIBRATIONS_KEY) + update_cal_notification_vibrations(); + else if (key == SETTINGS_NOTIFY_BUBBLES_KEY) + update_cal_notification_bubbles(); + else if (key == SETTINGS_NOTIFY_LIST_KEY) + update_cal_notification_list(); +} + +/*** +**** +***/ + +void LiveSettings::on_changed_general_notification(GSettings* /*settings*/, + gchar* key, + gpointer gself) +{ + static_cast<LiveSettings*>(gself)->update_key_general_notification(key); +} + +void LiveSettings::update_key_general_notification(const std::string& key) +{ + if (key == SETTINGS_VIBRATE_SILENT_KEY) + update_vibrate_silent_mode(); +} + /*** **** ***/ -void LiveSettings::on_changed(GSettings* /*settings*/, - gchar* key, - gpointer gself) +void LiveSettings::on_changed_ccid(GSettings* /*settings*/, + gchar* key, + gpointer gself) { - static_cast<LiveSettings*>(gself)->update_key(key); + static_cast<LiveSettings*>(gself)->update_key_ccid(key); } -void LiveSettings::update_key(const std::string& key) +void LiveSettings::update_key_ccid(const std::string& key) { if (key == SETTINGS_LOCATIONS_S) update_locations(); diff --git a/src/snap.cpp b/src/snap.cpp index f0300af..00c4743 100644 --- a/src/snap.cpp +++ b/src/snap.cpp @@ -53,19 +53,25 @@ class Snap::Impl public: Impl(const std::shared_ptr<ayatana::indicator::notifications::Engine>& engine, - const std::shared_ptr<const Settings>& settings): + const std::shared_ptr<ayatana::indicator::notifications::SoundBuilder>& sound_builder, + const std::shared_ptr<const Settings>& settings, + GDBusConnection* system_bus): m_engine(engine), + m_sound_builder(sound_builder), m_settings(settings), - m_cancellable(g_cancellable_new()) + m_cancellable(g_cancellable_new()), + m_system_bus{G_DBUS_CONNECTION(g_object_ref(system_bus))} { auto object_path = g_strdup_printf("/org/freedesktop/Accounts/User%lu", (gulong)getuid()); - accounts_service_sound_proxy_new_for_bus(G_BUS_TYPE_SYSTEM, - G_DBUS_PROXY_FLAGS_GET_INVALIDATED_PROPERTIES, - "org.freedesktop.Accounts", - object_path, - m_cancellable, - on_sound_proxy_ready, - this); + + + accounts_service_sound_proxy_new(m_system_bus, + G_DBUS_PROXY_FLAGS_GET_INVALIDATED_PROPERTIES, + "org.freedesktop.Accounts", + object_path, + m_cancellable, + on_sound_proxy_ready, + this); g_free(object_path); } @@ -74,6 +80,7 @@ public: g_cancellable_cancel(m_cancellable); g_clear_object(&m_cancellable); g_clear_object(&m_accounts_service_sound_proxy); + g_clear_object(&m_system_bus); for (const auto& key : m_notifications) m_engine->close (key); @@ -81,9 +88,14 @@ public: void operator()(const Appointment& appointment, const Alarm& alarm, - appointment_func snooze, - appointment_func ok) + response_func on_response) { + // If calendar notifications are disabled, don't show them + if (!appointment.is_ubuntu_alarm() && !calendar_notifications_are_enabled()) { + g_debug("Skipping disabled calendar event '%s' notification", appointment.summary.c_str()); + return; + } + /* Alarms and calendar events are treated differently. Alarms should require manual intervention to dismiss. Calendar events are less urgent and shouldn't require manual @@ -91,11 +103,14 @@ public: const bool interactive = appointment.is_ubuntu_alarm() && m_engine->supports_actions(); // force the system to stay awake - auto awake = std::make_shared<ain::Awake>(m_engine->app_name()); + std::shared_ptr<ain::Awake> awake; + if (appointment.is_ubuntu_alarm() || calendar_bubbles_enabled() || calendar_list_enabled()) { + awake = std::make_shared<ain::Awake>(m_system_bus, m_engine->app_name()); + } // calendar events are muted in silent mode; alarm clocks never are std::shared_ptr<ain::Sound> sound; - if (appointment.is_ubuntu_alarm() || !silent_mode()) { + if (appointment.is_ubuntu_alarm() || (calendar_sounds_enabled() && !silent_mode())) { // create the sound. const auto role = appointment.is_ubuntu_alarm() ? "alarm" : "alert"; const auto uri = get_alarm_uri(appointment, alarm, m_settings); @@ -106,18 +121,22 @@ public: // create the haptic feedback... std::shared_ptr<ain::Haptic> haptic; - if (should_vibrate()) { - const auto haptic_mode = m_settings->alarm_haptic.get(); - if (haptic_mode == "pulse") - haptic = std::make_shared<ain::Haptic>(ain::Haptic::MODE_PULSE); + if (should_vibrate() && (appointment.is_ubuntu_alarm() || calendar_vibrations_enabled())) { + // when in silent mode should only vibrate if user defined so + if (!silent_mode() || vibrate_in_silent_mode_enabled()) { + const auto haptic_mode = m_settings->alarm_haptic.get(); + if (haptic_mode == "pulse") + haptic = std::make_shared<ain::Haptic>(ain::Haptic::MODE_PULSE, appointment.is_ubuntu_alarm()); + } } // show a notification... const auto minutes = std::chrono::minutes(m_settings->alarm_duration.get()); ain::Builder b; b.set_body (appointment.summary); - b.set_icon_name ("alarm-clock"); + b.set_icon_name (appointment.is_ubuntu_alarm() ? "alarm-clock" : "calendar-app"); b.add_hint (ain::Builder::HINT_NONSHAPED_ICON); + b.set_start_time (appointment.begin.to_unix()); const char * timefmt; if (is_locale_12h()) { @@ -130,28 +149,53 @@ public: timefmt = _("%a, %H:%M"); } const auto timestr = appointment.begin.format(timefmt); - auto title = g_strdup_printf(_("Alarm %s"), timestr.c_str()); + + const char * titlefmt; + if (appointment.is_ubuntu_alarm()) { + titlefmt = _("Alarm %s"); + } else { + titlefmt = _("Event %s"); + } + auto title = g_strdup_printf(titlefmt, 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_hint (ain::Builder::HINT_SNAP); b.add_hint (ain::Builder::HINT_AFFIRMATIVE_HINT); - b.add_action ("ok", _("OK")); - b.add_action ("snooze", _("Snooze")); + b.add_action (ACTION_NONE, _("OK")); + b.add_action (ACTION_SNOOZE, _("Snooze")); + } else { + b.add_hint (ain::Builder::HINT_INTERACTIVE); + b.add_action (ACTION_SHOW_APP, _("OK")); } // 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, alarm, snooze, ok, sound, awake, haptic] + b.set_closed_callback([appointment, alarm, on_response, sound, awake, haptic] (const std::string& action){ - if (action == "snooze") - snooze(appointment, alarm); + Snap::Response response; + if ((action == ACTION_SNOOZE) || (appointment.is_ubuntu_alarm() && action.empty())) + response = Snap::Response::Snooze; + else if (action == ACTION_SHOW_APP) + response = Snap::Response::ShowApp; else - ok(appointment, alarm); + response = Snap::Response::None; + + on_response(appointment, alarm, response); }); + //TODO: we need to extend it to support alarms appointments + if (!appointment.is_ubuntu_alarm()) { + b.set_timeout_callback([appointment, alarm, on_response](){ + on_response(appointment, alarm, Snap::Response::ShowApp); + }); + } + + b.set_show_notification_bubble(appointment.is_ubuntu_alarm() || calendar_bubbles_enabled()); + b.set_post_to_messaging_menu(appointment.is_ubuntu_alarm() || calendar_list_enabled()); + const auto key = m_engine->show(b); if (key) m_notifications.insert (key); @@ -159,12 +203,42 @@ public: private: + bool calendar_notifications_are_enabled() const + { + return m_settings->cal_notification_enabled.get(); + } + + bool calendar_sounds_enabled() const + { + return m_settings->cal_notification_sounds.get(); + } + + bool calendar_vibrations_enabled() const + { + return m_settings->cal_notification_vibrations.get(); + } + + bool calendar_bubbles_enabled() const + { + return m_settings->cal_notification_bubbles.get(); + } + + bool calendar_list_enabled() const + { + return m_settings->cal_notification_list.get(); + } + + bool vibrate_in_silent_mode_enabled() const + { + return m_settings->vibrate_silent_mode.get(); + } + static void on_sound_proxy_ready(GObject* /*source_object*/, GAsyncResult* res, gpointer gself) { GError * error; error = nullptr; - auto proxy = accounts_service_sound_proxy_new_for_bus_finish (res, &error); + auto proxy = accounts_service_sound_proxy_new_finish (res, &error); if (error != nullptr) { if (!g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) @@ -230,6 +304,11 @@ private: std::set<int> m_notifications; GCancellable * m_cancellable {nullptr}; AccountsServiceSound * m_accounts_service_sound_proxy {nullptr}; + GDBusConnection * m_system_bus {nullptr}; + + static constexpr char const * ACTION_NONE {"none"}; + static constexpr char const * ACTION_SNOOZE {"snooze"}; + static constexpr char const * ACTION_SHOW_APP {"show-app"}; }; /*** @@ -237,8 +316,10 @@ private: ***/ Snap::Snap(const std::shared_ptr<ayatana::indicator::notifications::Engine>& engine, - const std::shared_ptr<const Settings>& settings): - impl(new Impl(engine, settings)) + const std::shared_ptr<ayatana::indicator::notifications::SoundBuilder>& sound_builder, + const std::shared_ptr<const Settings>& settings, + GDBusConnection* system_bus): + impl(new Impl(engine, sound_builder, settings, system_bus)) { } @@ -249,10 +330,9 @@ Snap::~Snap() void Snap::operator()(const Appointment& appointment, const Alarm& alarm, - appointment_func show, - appointment_func ok) + response_func on_response) { - (*impl)(appointment, alarm, show, ok); + (*impl)(appointment, alarm, on_response); } /*** diff --git a/src/timezone-timedated.cpp b/src/timezone-timedated.cpp index d38557b..1b6497e 100644 --- a/src/timezone-timedated.cpp +++ b/src/timezone-timedated.cpp @@ -17,6 +17,7 @@ * Charles Kerr <charles.kerr@canonical.com> */ +#include <datetime/dbus-shared.h> #include <datetime/timezone-timedated.h> #include <gio/gio.h> @@ -36,166 +37,169 @@ class TimedatedTimezone::Impl { public: - Impl(TimedatedTimezone& owner, std::string filename): - m_owner(owner), - m_filename(filename) + Impl(TimedatedTimezone& owner, GDBusConnection* connection): + m_owner{owner}, + m_connection{G_DBUS_CONNECTION(g_object_ref(G_OBJECT(connection)))}, + m_cancellable{g_cancellable_new()} { - g_debug("Filename is '%s'", filename.c_str()); - monitor_timezone_property(); + // set the fallback value + m_owner.timezone.set("Etc/Utc"); + + // watch for timedate1 on the bus + m_watcher_id = g_bus_watch_name_on_connection( + m_connection, + Bus::Timedate1::BUSNAME, + G_BUS_NAME_WATCHER_FLAGS_AUTO_START, + on_timedate1_appeared, + on_timedate1_vanished, + this, + nullptr); + + // listen for changed properties + m_signal_subscription_id = g_dbus_connection_signal_subscribe( + m_connection, + Bus::Timedate1::IFACE, + Bus::Properties::IFACE, + Bus::Properties::Signals::PROPERTIES_CHANGED, + Bus::Timedate1::ADDR, + nullptr, + G_DBUS_SIGNAL_FLAGS_NONE, + on_properties_changed, + this, + nullptr); } ~Impl() { - clear(); - } + g_cancellable_cancel(m_cancellable); + g_clear_object(&m_cancellable); -private: + g_bus_unwatch_name(m_watcher_id); - void clear() - { - if (m_connection && m_signal_subscription_id) - { - g_dbus_connection_signal_unsubscribe (m_connection, m_signal_subscription_id); - m_signal_subscription_id = 0; - } + g_dbus_connection_signal_unsubscribe(m_connection, m_signal_subscription_id); g_clear_object(&m_connection); } - static void on_properties_changed (GDBusConnection *connection G_GNUC_UNUSED, - const gchar *sender_name G_GNUC_UNUSED, - const gchar *object_path G_GNUC_UNUSED, - const gchar *interface_name G_GNUC_UNUSED, - const gchar *signal_name G_GNUC_UNUSED, - GVariant *parameters, - gpointer gself) - { - auto self = static_cast<Impl*>(gself); - const char *tz; - GVariant *changed_properties; - gchar **invalidated_properties; +private: - g_variant_get (parameters, "(s@a{sv}^as)", NULL, &changed_properties, &invalidated_properties); + static void on_timedate1_appeared(GDBusConnection * /*connection*/, + const gchar * name, + const gchar * /*name_owner*/, + gpointer gself) + { + g_debug("%s appeared on bus", name); - if (g_variant_lookup(changed_properties, "Timezone", "&s", &tz, NULL)) - self->notify_timezone(tz); - else if (g_strv_contains (invalidated_properties, "Timezone")) - self->notify_timezone(self->get_timezone_from_file(self->m_filename)); + static_cast<Impl*>(gself)->ask_for_timezone(); + } - g_variant_unref (changed_properties); - g_strfreev (invalidated_properties); + static void on_timedate1_vanished(GDBusConnection * /*connection*/, + const gchar * name, + gpointer /*gself*/) + { + g_debug("%s not present on bus", name); } - void monitor_timezone_property() + static void on_properties_changed(GDBusConnection * /*connection*/, + const gchar * /*sender_name*/, + const gchar * /*object_path*/, + const gchar * /*interface_name*/, + const gchar * /*signal_name*/, + GVariant * parameters, + gpointer gself) { - GError *err = nullptr; - - /* - * There is an unlikely race which happens if there is an activation - * and timezone change before our match rule is added. - */ - notify_timezone(get_timezone_from_file(m_filename)); - - /* - * Make sure the bus is around at least until we add the match rules, - * otherwise things (tests) are sad. - */ - m_connection = g_bus_get_sync(G_BUS_TYPE_SYSTEM, - nullptr, - &err); - - if (err) + auto self = static_cast<Impl*>(gself); + + GVariant* changed_properties {}; + gchar** invalidated_properties {}; + g_variant_get(parameters, "(s@a{sv}^as)", NULL, &changed_properties, &invalidated_properties); + + const char* tz {}; + if (g_variant_lookup(changed_properties, Bus::Timedate1::Properties::TIMEZONE, "&s", &tz, NULL)) + { + if (tz != nullptr) + self->set_timezone(tz); + else + g_warning("%s no timezone found", G_STRLOC); + } + else if (g_strv_contains(invalidated_properties, Bus::Timedate1::Properties::TIMEZONE)) { - g_warning("Couldn't get bus connection: '%s'", err->message); - g_error_free(err); - return; + self->ask_for_timezone(); } - m_signal_subscription_id = g_dbus_connection_signal_subscribe(m_connection, - "org.freedesktop.timedate1", - "org.freedesktop.DBus.Properties", - "PropertiesChanged", - "/org/freedesktop/timedate1", - NULL, G_DBUS_SIGNAL_FLAGS_NONE, - on_properties_changed, - this, nullptr); + g_variant_unref(changed_properties); + g_strfreev(invalidated_properties); } - void notify_timezone(std::string new_timezone) + void ask_for_timezone() { - g_debug("notify_timezone '%s'", new_timezone.c_str()); - if (!new_timezone.empty()) - m_owner.timezone.set(new_timezone); + g_dbus_connection_call( + m_connection, + Bus::Timedate1::BUSNAME, + Bus::Timedate1::ADDR, + Bus::Properties::IFACE, + Bus::Properties::Methods::GET, + g_variant_new("(ss)", Bus::Timedate1::IFACE, Bus::Timedate1::Properties::TIMEZONE), + G_VARIANT_TYPE("(v)"), + G_DBUS_CALL_FLAGS_NONE, + -1, + m_cancellable, + on_get_timezone_ready, + this); } - std::string get_timezone_from_file(const std::string& filename) + static void on_get_timezone_ready(GObject * connection, + GAsyncResult * res, + gpointer gself) { - GError * error; - GIOChannel * io_channel; - std::string ret; - - // read through filename line-by-line until we fine a nonempty non-comment line - error = nullptr; - io_channel = g_io_channel_new_file(filename.c_str(), "r", &error); - if (error == nullptr) + GError* error {}; + GVariant* v = g_dbus_connection_call_finish(G_DBUS_CONNECTION(connection), res, &error); + if (error != nullptr) { - auto line = g_string_new(nullptr); - - while(ret.empty()) - { - const auto io_status = g_io_channel_read_line_string(io_channel, line, nullptr, &error); - if ((io_status == G_IO_STATUS_EOF) || (io_status == G_IO_STATUS_ERROR)) - break; - if (error != nullptr) - break; - - g_strstrip(line->str); - - if (!line->len) // skip empty lines - continue; - - if (*line->str=='#') // skip comments - continue; - - ret = line->str; - } + if (!g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_warning("%s Couldn't get timezone: %s", G_STRLOC, error->message); + } + else if (v != nullptr) + { + GVariant* tzv {}; + g_variant_get(v, "(v)", &tzv); + const char* tz = g_variant_get_string(tzv, nullptr); - g_string_free(line, true); - } else - /* Default to UTC */ - ret = "Etc/Utc"; + if (tz != nullptr) + static_cast<Impl*>(gself)->set_timezone(tz); + else + g_warning("%s no timezone found", G_STRLOC); - if (io_channel != nullptr) - { - g_io_channel_shutdown(io_channel, false, nullptr); - g_io_channel_unref(io_channel); + g_clear_pointer(&tzv, g_variant_unref); + g_clear_pointer(&v, g_variant_unref); } + } - if (error != nullptr) - { - g_warning("%s Unable to read timezone file '%s': %s", G_STRLOC, filename.c_str(), error->message); - g_error_free(error); - } + void set_timezone(const std::string& tz) + { + g_return_if_fail(!tz.empty()); - return ret; + g_debug("set timezone: '%s'", tz.c_str()); + m_owner.timezone.set(tz); } /*** **** ***/ - TimedatedTimezone & m_owner; - GDBusConnection *m_connection = nullptr; - unsigned long m_signal_subscription_id = 0; - std::string m_filename; + TimedatedTimezone& m_owner; + GDBusConnection* m_connection {}; + GCancellable* m_cancellable {}; + unsigned long m_signal_subscription_id {}; + unsigned int m_watcher_id {}; }; /*** **** ***/ -TimedatedTimezone::TimedatedTimezone(std::string filename): - impl(new Impl{*this, filename}) +TimedatedTimezone::TimedatedTimezone(GDBusConnection* connection): + impl{new Impl{*this, connection}} { } diff --git a/src/timezones-live.cpp b/src/timezones-live.cpp index 2979036..f3bd02d 100644 --- a/src/timezones-live.cpp +++ b/src/timezones-live.cpp @@ -25,11 +25,14 @@ namespace ayatana { namespace indicator { namespace datetime { -LiveTimezones::LiveTimezones(const std::shared_ptr<const Settings>& settings): - m_file(), +LiveTimezones::LiveTimezones( + const std::shared_ptr<const Settings>& settings, + const std::shared_ptr<Timezone>& primary_timezone +): + m_primary_timezone(primary_timezone), m_settings(settings) { - m_file.timezone.changed().connect([this](const std::string&){update_timezones();}); + m_primary_timezone->timezone.changed().connect([this](const std::string&){update_timezones();}); m_settings->show_detected_location.changed().connect([this](bool){update_geolocation();}); update_geolocation(); @@ -53,7 +56,7 @@ void LiveTimezones::update_geolocation() void LiveTimezones::update_timezones() { - const auto a = m_file.timezone.get(); + const auto a = m_primary_timezone->timezone.get(); const auto b = m_geo ? m_geo->timezone.get() : ""; timezone.set(a.empty() ? b : a); |