aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/CMakeLists.txt8
-rw-r--r--src/actions-live.cpp138
-rw-r--r--src/actions.cpp73
-rw-r--r--src/awake.cpp184
-rw-r--r--src/clock.cpp2
-rw-r--r--src/date-time.cpp10
-rw-r--r--src/engine-eds.cpp5
-rw-r--r--src/haptic.cpp20
-rw-r--r--src/main.cpp38
-rw-r--r--src/menu.cpp116
-rw-r--r--src/myself.cpp2
-rw-r--r--src/notifications.cpp207
-rw-r--r--src/planner-snooze.cpp4
-rw-r--r--src/planner-upcoming.cpp2
-rw-r--r--src/settings-live.cpp135
-rw-r--r--src/snap.cpp142
-rw-r--r--src/timezone-timedated.cpp236
-rw-r--r--src/timezones-live.cpp11
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);