diff options
author | Charles Kerr <charles.kerr@canonical.com> | 2014-02-19 18:02:22 +0000 |
---|---|---|
committer | CI bot <ps-jenkins@lists.canonical.com> | 2014-02-19 18:02:22 +0000 |
commit | 359764b5ea8926ae372844155b1394ebe06e3174 (patch) | |
tree | ae60e379a905cc017bc323e79cdc62d2d37c1177 /src | |
parent | 372b0a77f8840a35bb131ecf507313056170c403 (diff) | |
parent | 29ca2a552d7a7dcb6487c27ae4d036608aa68320 (diff) | |
download | ayatana-indicator-datetime-359764b5ea8926ae372844155b1394ebe06e3174.tar.gz ayatana-indicator-datetime-359764b5ea8926ae372844155b1394ebe06e3174.tar.bz2 ayatana-indicator-datetime-359764b5ea8926ae372844155b1394ebe06e3174.zip |
support for ubuntu-clock-app's alarms Fixes: 1233176, 1237752, 1255716
Diffstat (limited to 'src')
-rw-r--r-- | src/CMakeLists.txt | 2 | ||||
-rw-r--r-- | src/actions-live.cpp | 2 | ||||
-rw-r--r-- | src/clock-watcher.cpp | 71 | ||||
-rw-r--r-- | src/date-time.cpp | 18 | ||||
-rw-r--r-- | src/main.cpp | 31 | ||||
-rw-r--r-- | src/menu.cpp | 103 | ||||
-rw-r--r-- | src/planner-eds.cpp | 293 | ||||
-rw-r--r-- | src/snap.cpp | 256 | ||||
-rw-r--r-- | src/timezone-file.cpp | 27 |
9 files changed, 629 insertions, 174 deletions
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 2cea786..7b1d7df 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -13,6 +13,7 @@ add_library (${SERVICE_LIB} STATIC appointment.cpp clock.cpp clock-live.cpp + clock-watcher.cpp date-time.cpp exporter.cpp formatter.cpp @@ -22,6 +23,7 @@ add_library (${SERVICE_LIB} STATIC menu.cpp planner-eds.cpp settings-live.cpp + snap.cpp timezone-file.cpp timezone-geoclue.cpp timezones-live.cpp diff --git a/src/actions-live.cpp b/src/actions-live.cpp index f510ed1..ccc7fcf 100644 --- a/src/actions-live.cpp +++ b/src/actions-live.cpp @@ -156,7 +156,7 @@ on_datetime1_proxy_ready (GObject * object G_GNUC_UNUSED, GError * err = nullptr; auto proxy = g_dbus_proxy_new_for_bus_finish(res, &err); - if (err != NULL) + if (err != nullptr) { if (!g_error_matches(err, G_IO_ERROR, G_IO_ERROR_CANCELLED)) g_warning("Could not grab DBus proxy for timedated: %s", err->message); diff --git a/src/clock-watcher.cpp b/src/clock-watcher.cpp new file mode 100644 index 0000000..a2e700d --- /dev/null +++ b/src/clock-watcher.cpp @@ -0,0 +1,71 @@ +/* + * Copyright 2014 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranties of + * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see <http://www.gnu.org/licenses/>. + * + * Authors: + * Charles Kerr <charles.kerr@canonical.com> + */ + +#include <datetime/clock-watcher.h> + +namespace unity { +namespace indicator { +namespace datetime { + +/*** +**** +***/ + +ClockWatcherImpl::ClockWatcherImpl(const std::shared_ptr<const State>& state): + m_state(state) +{ + m_state->planner->upcoming.changed().connect([this](const std::vector<Appointment>&){ + g_debug("ClockWatcher pulse because upcoming appointments changed"); + pulse(); + }); + m_state->clock->minute_changed.connect([this](){ + g_debug("ClockWatcher pulse because clock minute_changed"); + pulse(); + }); + pulse(); +} + +core::Signal<const Appointment&>& ClockWatcherImpl::alarm_reached() +{ + return m_alarm_reached; +} + +void ClockWatcherImpl::pulse() +{ + const auto now = m_state->clock->localtime(); + + for(const auto& appointment : m_state->planner->upcoming.get()) + { + if (m_triggered.count(appointment.uid)) + continue; + if (!DateTime::is_same_minute(now, appointment.begin)) + continue; + + m_triggered.insert(appointment.uid); + m_alarm_reached(appointment); + } +} + +/*** +**** +***/ + +} // namespace datetime +} // namespace indicator +} // namespace unity diff --git a/src/date-time.cpp b/src/date-time.cpp index a634c5e..e6d99cd 100644 --- a/src/date-time.cpp +++ b/src/date-time.cpp @@ -69,6 +69,14 @@ DateTime DateTime::to_timezone(const std::string& zone) const return dt; } +DateTime DateTime::add_full(int years, int months, int days, int hours, int minutes, double seconds) const +{ + auto gdt = g_date_time_add_full(get(), years, months, days, hours, minutes, seconds); + DateTime dt(gdt); + g_date_time_unref(gdt); + return dt; +} + GDateTime* DateTime::get() const { g_assert(m_dt); @@ -88,6 +96,11 @@ int DateTime::day_of_month() const return g_date_time_get_day_of_month(get()); } +double DateTime::seconds() const +{ + return g_date_time_get_seconds(get()); +} + int64_t DateTime::to_unix() const { return g_date_time_to_unix(get()); @@ -112,6 +125,11 @@ 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/main.cpp b/src/main.cpp index 1534777..31d9db6 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -17,24 +17,25 @@ * with this program. If not, see <http://www.gnu.org/licenses/>. */ - - #include <datetime/actions-live.h> #include <datetime/clock.h> +#include <datetime/clock-watcher.h> #include <datetime/exporter.h> #include <datetime/locations-settings.h> #include <datetime/menu.h> #include <datetime/planner-eds.h> #include <datetime/settings-live.h> +#include <datetime/snap.h> #include <datetime/state.h> #include <datetime/timezones-live.h> #include <glib/gi18n.h> // bindtextdomain() #include <gio/gio.h> -#include <libnotify/notify.h> + +#include <url-dispatcher.h> #include <locale.h> -#include <stdlib.h> // exit() +#include <cstdlib> // exit() using namespace unity::indicator::datetime; @@ -50,10 +51,6 @@ main(int /*argc*/, char** /*argv*/) bindtextdomain(GETTEXT_PACKAGE, GNOMELOCALEDIR); textdomain(GETTEXT_PACKAGE); - // init libnotify - if(!notify_init("indicator-datetime-service")) - g_critical("libnotify initialization failed"); - // build the state, actions, and menufactory std::shared_ptr<State> state(new State); std::shared_ptr<Settings> live_settings(new LiveSettings); @@ -62,11 +59,27 @@ main(int /*argc*/, char** /*argv*/) state->settings = live_settings; state->clock = live_clock; state->locations.reset(new SettingsLocations(live_settings, live_timezones)); - state->planner.reset(new PlannerEds); + state->planner.reset(new PlannerEds(live_clock)); state->planner->time = live_clock->localtime(); std::shared_ptr<Actions> actions(new LiveActions(state)); MenuFactory factory(actions, state); + // snap decisions + ClockWatcherImpl clock_watcher(state); + Snap snap; + clock_watcher.alarm_reached().connect([&snap](const Appointment& appt){ + auto snap_show = [](const Appointment& a){ + const char* url; + if(!a.url.empty()) + url = a.url.c_str(); + else // alarm doesn't have a URl associated with it; use a fallback + url = "appid://com.ubuntu.clock/clock/current-user-version"; + url_dispatch_send(url, nullptr, nullptr); + }; + auto snap_dismiss = [](const Appointment&){}; + snap(appt, snap_show, snap_dismiss); + }); + // create the menus std::vector<std::shared_ptr<Menu>> menus; for(int i=0, n=Menu::NUM_PROFILES; i<n; i++) diff --git a/src/menu.cpp b/src/menu.cpp index bdf92c3..b2562db 100644 --- a/src/menu.cpp +++ b/src/menu.cpp @@ -22,11 +22,11 @@ #include <datetime/formatter.h> #include <datetime/state.h> -#include <json-glib/json-glib.h> - #include <glib/gi18n.h> #include <gio/gio.h> +#include <vector> + namespace unity { namespace indicator { namespace datetime { @@ -62,7 +62,7 @@ GMenuModel* Menu::menu_model() ****/ -#define FALLBACK_ALARM_CLOCK_ICON_NAME "clock" +#define ALARM_ICON_NAME "alarm-clock" #define CALENDAR_ICON_NAME "calendar" class MenuImpl: public Menu @@ -105,12 +105,15 @@ protected: update_section(Appointments); // showing events got toggled }); m_state->planner->upcoming.changed().connect([this](const std::vector<Appointment>&){ - update_section(Appointments); // "upcoming" is the list of Appointments we show + update_upcoming(); // our m_upcoming is planner->upcoming() filtered by time }); m_state->clock->date_changed.connect([this](){ update_section(Calendar); // need to update the Date menuitem update_section(Locations); // locations' relative time may have changed }); + m_state->clock->minute_changed.connect([this](){ + update_upcoming(); // our m_upcoming is planner->upcoming() filtered by time + }); m_state->locations->locations.changed().connect([this](const std::vector<Location>&) { update_section(Locations); // "locations" is the list of Locations we show }); @@ -133,6 +136,24 @@ protected: g_action_group_change_action_state(action_group, action_name.c_str(), state); } + void update_upcoming() + { + const auto now = m_state->clock->localtime(); + const auto next_minute = now.add_full(0,0,0,0,1,-now.seconds()); + + std::vector<Appointment> upcoming; + for(const auto& a : m_state->planner->upcoming.get()) + if (next_minute <= a.begin) + upcoming.push_back(a); + + if (m_upcoming != upcoming) + { + m_upcoming.swap(upcoming); + update_header(); // show an 'alarm' icon if there are upcoming alarms + update_section(Appointments); // "upcoming" is the list of Appointments we show + } + } + std::shared_ptr<const State> m_state; std::shared_ptr<Actions> m_actions; std::shared_ptr<const Formatter> m_formatter; @@ -141,71 +162,19 @@ protected: GVariant* get_serialized_alarm_icon() { if (G_UNLIKELY(m_serialized_alarm_icon == nullptr)) - m_serialized_alarm_icon = create_alarm_icon(); - - return m_serialized_alarm_icon; - } - -private: - - /* try to get the clock app's filename from click. (/$pkgdir/$icon) */ - static GVariant* create_alarm_icon() - { - GVariant* serialized = nullptr; - gchar* icon_filename = nullptr; - gchar* standard_error = nullptr; - gchar* pkgdir = nullptr; - - g_spawn_command_line_sync("click pkgdir com.ubuntu.clock", &pkgdir, &standard_error, nullptr, nullptr); - g_clear_pointer(&standard_error, g_free); - if (pkgdir != nullptr) - { - gchar* manifest = nullptr; - g_strstrip(pkgdir); - g_spawn_command_line_sync("click info com.ubuntu.clock", &manifest, &standard_error, nullptr, nullptr); - g_clear_pointer(&standard_error, g_free); - if (manifest != nullptr) - { - JsonParser* parser = json_parser_new(); - if (json_parser_load_from_data(parser, manifest, -1, nullptr)) - { - JsonNode* root = json_parser_get_root(parser); /* transfer-none */ - if ((root != nullptr) && (JSON_NODE_TYPE(root) == JSON_NODE_OBJECT)) - { - JsonObject* o = json_node_get_object(root); /* transfer-none */ - const gchar* icon_name = json_object_get_string_member(o, "icon"); - if (icon_name != nullptr) - icon_filename = g_build_filename(pkgdir, icon_name, nullptr); - } - } - g_object_unref(parser); - g_free(manifest); - } - g_free(pkgdir); - } - - if (icon_filename != nullptr) { - GFile* file = g_file_new_for_path(icon_filename); - GIcon* icon = g_file_icon_new(file); - - serialized = g_icon_serialize(icon); - - g_object_unref(icon); - g_object_unref(file); - g_free(icon_filename); - } - - if (serialized == nullptr) - { - auto i = g_themed_icon_new_with_default_fallbacks(FALLBACK_ALARM_CLOCK_ICON_NAME); - serialized = g_icon_serialize(i); + auto i = g_themed_icon_new_with_default_fallbacks(ALARM_ICON_NAME); + m_serialized_alarm_icon = g_icon_serialize(i); g_object_unref(i); } - return serialized; + return m_serialized_alarm_icon; } + std::vector<Appointment> m_upcoming; + +private: + GVariant* get_serialized_calendar_icon() { if (G_UNLIKELY(m_serialized_calendar_icon == nullptr)) @@ -273,7 +242,7 @@ private: // add calendar if (show_calendar) { - item = g_menu_item_new ("[calendar]", NULL); + item = g_menu_item_new ("[calendar]", nullptr); v = g_variant_new_int64(0); g_menu_item_set_action_and_target_value (item, "indicator.calendar", v); g_menu_item_set_attribute (item, "x-canonical-type", @@ -296,11 +265,13 @@ private: const int MAX_APPTS = 5; std::set<std::string> added; - for (const auto& appt : m_state->planner->upcoming.get()) + for (const auto& appt : m_upcoming) { + // don't show too many if (n++ >= MAX_APPTS) break; + // don't show duplicates if (added.count(appt.uid)) continue; @@ -508,7 +479,7 @@ protected: { // are there alarms? bool has_alarms = false; - for(const auto& appointment : m_state->planner->upcoming.get()) + for(const auto& appointment : m_upcoming) if((has_alarms = appointment.has_alarms)) break; diff --git a/src/planner-eds.cpp b/src/planner-eds.cpp index cb42d6e..7d9416c 100644 --- a/src/planner-eds.cpp +++ b/src/planner-eds.cpp @@ -26,6 +26,9 @@ #include <libecal/libecal.h> #include <libedataserver/libedataserver.h> +#include <map> +#include <set> + namespace unity { namespace indicator { namespace datetime { @@ -34,25 +37,28 @@ namespace datetime { ***** ****/ -G_DEFINE_QUARK("source-client", source_client) - - class PlannerEds::Impl { public: - Impl(PlannerEds& owner): + Impl(PlannerEds& owner, const std::shared_ptr<Clock>& clock): m_owner(owner), + m_clock(clock), m_cancellable(g_cancellable_new()) { e_source_registry_new(m_cancellable, on_source_registry_ready, this); + m_clock->minute_changed.connect([this](){ + g_debug("rebuilding upcoming because the clock's minute_changed"); + rebuild_soon(UPCOMING); + }); + m_owner.time.changed().connect([this](const DateTime& dt) { - g_debug("planner's datetime property changed to %s; calling rebuildSoon()", dt.format("%F %T").c_str()); - rebuildSoon(); + g_debug("planner's datetime property changed to %s; calling rebuild_soon()", dt.format("%F %T").c_str()); + rebuild_soon(MONTH); }); - rebuildSoon(); + rebuild_soon(ALL); } ~Impl() @@ -60,6 +66,9 @@ public: g_cancellable_cancel(m_cancellable); g_clear_object(&m_cancellable); + while(!m_sources.empty()) + remove_source(*m_sources.begin()); + if (m_rebuild_tag) g_source_remove(m_rebuild_tag); @@ -83,26 +92,31 @@ private: } else { - auto self = static_cast<Impl*>(gself); - - g_signal_connect(r, "source-added", G_CALLBACK(on_source_added), self); - g_signal_connect(r, "source-removed", G_CALLBACK(on_source_removed), self); - g_signal_connect(r, "source-changed", G_CALLBACK(on_source_changed), self); - g_signal_connect(r, "source-disabled", G_CALLBACK(on_source_disabled), self); - g_signal_connect(r, "source-enabled", G_CALLBACK(on_source_enabled), self); + g_signal_connect(r, "source-added", G_CALLBACK(on_source_added), gself); + g_signal_connect(r, "source-removed", G_CALLBACK(on_source_removed), gself); + g_signal_connect(r, "source-changed", G_CALLBACK(on_source_changed), gself); + g_signal_connect(r, "source-disabled", G_CALLBACK(on_source_disabled), gself); + g_signal_connect(r, "source-enabled", G_CALLBACK(on_source_enabled), gself); + auto self = static_cast<Impl*>(gself); self->m_source_registry = r; - - GList* sources = e_source_registry_list_sources(r, E_SOURCE_EXTENSION_CALENDAR); - for (auto l=sources; l!=nullptr; l=l->next) - on_source_added(r, E_SOURCE(l->data), gself); - g_list_free_full(sources, g_object_unref); + self->add_sources_by_extension(E_SOURCE_EXTENSION_CALENDAR); + self->add_sources_by_extension(E_SOURCE_EXTENSION_TASK_LIST); } } + void add_sources_by_extension(const char* extension) + { + auto& r = m_source_registry; + auto sources = e_source_registry_list_sources(r, extension); + for (auto l=sources; l!=nullptr; l=l->next) + on_source_added(r, E_SOURCE(l->data), this); + g_list_free_full(sources, g_object_unref); + } + static void on_source_added(ESourceRegistry* registry, ESource* source, gpointer gself) { - auto self = static_cast<PlannerEds::Impl*>(gself); + auto self = static_cast<Impl*>(gself); self->m_sources.insert(E_SOURCE(g_object_ref(source))); @@ -112,10 +126,19 @@ private: static void on_source_enabled(ESourceRegistry* /*registry*/, ESource* source, gpointer gself) { - auto self = static_cast<PlannerEds::Impl*>(gself); + auto self = static_cast<Impl*>(gself); + ECalClientSourceType source_type; + + if (e_source_has_extension(source, E_SOURCE_EXTENSION_CALENDAR)) + source_type = E_CAL_CLIENT_SOURCE_TYPE_EVENTS; + else if (e_source_has_extension(source, E_SOURCE_EXTENSION_TASK_LIST)) + source_type = E_CAL_CLIENT_SOURCE_TYPE_TASKS; + else + g_assert_not_reached(); + g_debug("connecting a client to source %s", e_source_get_uid(source)); e_cal_client_connect(source, - E_CAL_CLIENT_SOURCE_TYPE_EVENTS, + source_type, self->m_cancellable, on_client_connected, gself); @@ -134,45 +157,118 @@ private: } else { - // we've got a new connected ECalClient, so store it & notify clients - g_object_set_qdata_full(G_OBJECT(e_client_get_source(client)), - source_client_quark(), - client, - g_object_unref); - - g_debug("client connected; calling rebuildSoon()"); - static_cast<Impl*>(gself)->rebuildSoon(); + // add the client to our collection + auto self = static_cast<Impl*>(gself); + g_debug("got a client for %s", e_cal_client_get_local_attachment_store(E_CAL_CLIENT(client))); + self->m_clients[e_client_get_source(client)] = E_CAL_CLIENT(client); + + // now create a view for it so that we can listen for changes + e_cal_client_get_view (E_CAL_CLIENT(client), + "#t", // match all + self->m_cancellable, + on_client_view_ready, + self); + + g_debug("client connected; calling rebuild_soon()"); + self->rebuild_soon(ALL); } } - static void on_source_disabled(ESourceRegistry* /*registry*/, ESource* source, gpointer gself) + static void on_client_view_ready (GObject* client, GAsyncResult* res, gpointer gself) { - gpointer e_cal_client; + GError* error = nullptr; + ECalClientView* view = nullptr; + + if (e_cal_client_get_view_finish (E_CAL_CLIENT(client), res, &view, &error)) + { + // add the view to our collection + e_cal_client_view_start(view, &error); + g_debug("got a view for %s", e_cal_client_get_local_attachment_store(E_CAL_CLIENT(client))); + auto self = static_cast<Impl*>(gself); + self->m_views[e_client_get_source(E_CLIENT(client))] = view; - // if this source has a connected ECalClient, remove it & notify clients - if ((e_cal_client = g_object_steal_qdata(G_OBJECT(source), source_client_quark()))) + g_signal_connect(view, "objects-added", G_CALLBACK(on_view_objects_added), self); + g_signal_connect(view, "objects-modified", G_CALLBACK(on_view_objects_modified), self); + g_signal_connect(view, "objects-removed", G_CALLBACK(on_view_objects_removed), self); + g_debug("view connected; calling rebuild_soon()"); + self->rebuild_soon(ALL); + } + else if(error != nullptr) { - g_object_unref(e_cal_client); + if (!g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + g_warning("indicator-datetime cannot get View to EDS client: %s", error->message); - g_debug("source disabled; calling rebuildSoon()"); - static_cast<Impl*>(gself)->rebuildSoon(); + g_error_free(error); } } - static void on_source_removed(ESourceRegistry* registry, ESource* source, gpointer gself) + static void on_view_objects_added(ECalClientView* /*view*/, gpointer /*objects*/, gpointer gself) + { + g_debug("%s", G_STRFUNC); + static_cast<Impl*>(gself)->rebuild_soon(ALL); + } + static void on_view_objects_modified(ECalClientView* /*view*/, gpointer /*objects*/, gpointer gself) + { + g_debug("%s", G_STRFUNC); + static_cast<Impl*>(gself)->rebuild_soon(ALL); + } + static void on_view_objects_removed(ECalClientView* /*view*/, gpointer /*objects*/, gpointer gself) + { + g_debug("%s", G_STRFUNC); + static_cast<Impl*>(gself)->rebuild_soon(ALL); + } + + static void on_source_disabled(ESourceRegistry* /*registry*/, ESource* source, gpointer gself) + { + static_cast<Impl*>(gself)->disable_source(source); + } + void disable_source(ESource* source) { - auto self = static_cast<PlannerEds::Impl*>(gself); + // if an ECalClientView is associated with this source, remove it + auto vit = m_views.find(source); + if (vit != m_views.end()) + { + auto& view = vit->second; + e_cal_client_view_stop(view, nullptr); + const auto n_disconnected = g_signal_handlers_disconnect_by_data(view, this); + g_warn_if_fail(n_disconnected == 3); + g_object_unref(view); + m_views.erase(vit); + rebuild_soon(ALL); + } + + // if an ECalClient is associated with this source, remove it + auto cit = m_clients.find(source); + if (cit != m_clients.end()) + { + auto& client = cit->second; + g_object_unref(client); + m_clients.erase(cit); + rebuild_soon(ALL); + } + } - on_source_disabled(registry, source, gself); + static void on_source_removed(ESourceRegistry* /*registry*/, ESource* source, gpointer gself) + { + static_cast<Impl*>(gself)->remove_source(source); + } + void remove_source(ESource* source) + { + disable_source(source); - self->m_sources.erase(source); - g_object_unref(source); + auto sit = m_sources.find(source); + if (sit != m_sources.end()) + { + g_object_unref(*sit); + m_sources.erase(sit); + rebuild_soon(ALL); + } } static void on_source_changed(ESourceRegistry* /*registry*/, ESource* /*source*/, gpointer gself) { - g_debug("source changed; calling rebuildSoon()"); - static_cast<Impl*>(gself)->rebuildSoon(); + g_debug("source changed; calling rebuild_soon()"); + static_cast<Impl*>(gself)->rebuild_soon(ALL); } private: @@ -196,58 +292,72 @@ private: task(task_in), client(client_in), color(color_in) {} }; - void rebuildSoon() + void rebuild_soon(int rebuild_flags) { - const static guint ARBITRARY_INTERVAL_SECS = 2; + static const guint ARBITRARY_INTERVAL_SECS = 2; + + m_rebuild_flags |= rebuild_flags; if (m_rebuild_tag == 0) - m_rebuild_tag = g_timeout_add_seconds(ARBITRARY_INTERVAL_SECS, rebuildNowStatic, this); + m_rebuild_tag = g_timeout_add_seconds(ARBITRARY_INTERVAL_SECS, rebuild_now_static, this); } - static gboolean rebuildNowStatic(gpointer gself) + static gboolean rebuild_now_static(gpointer gself) { auto self = static_cast<Impl*>(gself); + const auto flags = self->m_rebuild_flags; self->m_rebuild_tag = 0; - self->rebuildNow(); + self->m_rebuild_flags = 0; + self->rebuild_now(flags); return G_SOURCE_REMOVE; } - void rebuildNow() + void rebuild_now(int rebuild_flags) { - const auto calendar_date = m_owner.time.get().get(); - GDateTime* begin; - GDateTime* end; - int y, m, d; - - // get all the appointments in the calendar month - g_date_time_get_ymd(calendar_date, &y, &m, &d); - begin = g_date_time_new_local(y, m, 1, 0, 0, 0.1); - end = g_date_time_new_local(y, m, g_date_get_days_in_month(GDateMonth(m),GDateYear(y)), 23, 59, 59.9); - if (begin && end) - { - getAppointments(begin, end, [this](const std::vector<Appointment>& appointments) { - g_debug("got %d appointments in this calendar month", (int)appointments.size()); - m_owner.this_month.set(appointments); - }); - } - g_clear_pointer(&begin, g_date_time_unref); - g_clear_pointer(&end, g_date_time_unref); + if (rebuild_flags & UPCOMING) + rebuild_upcoming(); - // get the upcoming appointments - begin = g_date_time_ref(calendar_date); - end = g_date_time_add_months(begin, 1); - if (begin && end) - { - getAppointments(begin, end, [this](const std::vector<Appointment>& appointments) { - g_debug("got %d upcoming appointments", (int)appointments.size()); - m_owner.upcoming.set(appointments); - }); - } - g_clear_pointer(&begin, g_date_time_unref); - g_clear_pointer(&end, g_date_time_unref); + if (rebuild_flags & MONTH) + rebuild_month(); + } + + void rebuild_month() + { + const auto ref = m_owner.time.get().get(); + auto month_begin = g_date_time_add_full(ref, + 0, // subtract no years + 0, // subtract no months + -(g_date_time_get_day_of_month(ref)-1), + -g_date_time_get_hour(ref), + -g_date_time_get_minute(ref), + -g_date_time_get_seconds(ref)); + auto month_end = g_date_time_add_full(month_begin, 0, 1, 0, 0, 0, -0.1); + + get_appointments(month_begin, month_end, [this](const std::vector<Appointment>& appointments) { + g_debug("got %d appointments in this calendar month", (int)appointments.size()); + m_owner.this_month.set(appointments); + }); + + g_date_time_unref(month_end); + g_date_time_unref(month_begin); + } + + void rebuild_upcoming() + { + const auto ref = m_clock->localtime(); + const auto begin = g_date_time_add_minutes(ref.get(),-10); + const auto end = g_date_time_add_months(begin,1); + + get_appointments(begin, end, [this](const std::vector<Appointment>& appointments) { + g_debug("got %d upcoming appointments", (int)appointments.size()); + m_owner.upcoming.set(appointments); + }); + + g_date_time_unref(end); + g_date_time_unref(begin); } - void getAppointments(GDateTime* begin_dt, GDateTime* end_dt, appointment_func func) + void get_appointments(GDateTime* begin_dt, GDateTime* end_dt, appointment_func func) { const auto begin = g_date_time_to_unix(begin_dt); const auto end = g_date_time_to_unix(end_dt); @@ -286,16 +396,14 @@ private: delete task; }); - for (auto& source : m_sources) + for (auto& kv : m_clients) { - auto client = E_CAL_CLIENT(g_object_get_qdata(G_OBJECT(source), source_client_quark())); - if (client == nullptr) - continue; - + auto& client = kv.second; if (default_timezone != nullptr) e_cal_client_set_default_timezone(client, default_timezone); // start a new subtask to enumerate all the components in this client. + auto& source = kv.first; auto extension = e_source_get_extension(source, E_SOURCE_EXTENSION_CALENDAR); const auto color = e_source_selectable_get_color(E_SOURCE_SELECTABLE(extension)); g_debug("calling e_cal_client_generate_instances for %p", (void*)client); @@ -407,16 +515,19 @@ private: delete subtask; } -private: - PlannerEds& m_owner; + std::shared_ptr<Clock> m_clock; std::set<ESource*> m_sources; - GCancellable * m_cancellable = nullptr; - ESourceRegistry * m_source_registry = nullptr; + std::map<ESource*,ECalClient*> m_clients; + std::map<ESource*,ECalClientView*> m_views; + GCancellable* m_cancellable = nullptr; + ESourceRegistry* m_source_registry = nullptr; guint m_rebuild_tag = 0; + guint m_rebuild_flags = 0; + enum { UPCOMING=(1<<0), MONTH=(1<<1), ALL=UPCOMING|MONTH }; }; -PlannerEds::PlannerEds(): p(new Impl(*this)) {} +PlannerEds::PlannerEds(const std::shared_ptr<Clock>& clock): p(new Impl(*this, clock)) {} PlannerEds::~PlannerEds() =default; diff --git a/src/snap.cpp b/src/snap.cpp new file mode 100644 index 0000000..f2d075a --- /dev/null +++ b/src/snap.cpp @@ -0,0 +1,256 @@ +/* + * Copyright 2014 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranties of + * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see <http://www.gnu.org/licenses/>. + * + * Authors: + * Charles Kerr <charles.kerr@canonical.com> + */ + +#include <datetime/appointment.h> +#include <datetime/formatter.h> +#include <datetime/snap.h> + +#include <canberra.h> +#include <libnotify/notify.h> + +#include <glib/gi18n.h> +#include <glib.h> + +#define ALARM_SOUND_FILENAME "/usr/share/sounds/ubuntu/stereo/phone-incoming-call.ogg" + +namespace unity { +namespace indicator { +namespace datetime { + +/*** +**** +***/ + +namespace +{ + +/** +*** libcanberra -- play sounds +**/ + +// arbitrary number, but we need a consistent id for play/cancel +const int32_t alarm_ca_id = 1; + +gboolean media_cached = FALSE; +ca_context *c_context = nullptr; +guint timeout_tag = 0; + +ca_context* get_ca_context() +{ + if (G_UNLIKELY(c_context == nullptr)) + { + int rv; + + if ((rv = ca_context_create(&c_context)) != CA_SUCCESS) + { + g_warning("Failed to create canberra context: %s\n", ca_strerror(rv)); + c_context = nullptr; + } + else + { + const char* filename = ALARM_SOUND_FILENAME; + rv = ca_context_cache(c_context, + CA_PROP_EVENT_ID, "alarm", + CA_PROP_MEDIA_FILENAME, filename, + CA_PROP_CANBERRA_CACHE_CONTROL, "permanent", + NULL); + media_cached = rv == CA_SUCCESS; + if (rv != CA_SUCCESS) + g_warning("Couldn't add '%s' to canberra cache: %s", filename, ca_strerror(rv)); + } + } + + return c_context; +} + +void play_alarm_sound(); + +gboolean play_alarm_sound_idle (gpointer) +{ + timeout_tag = 0; + play_alarm_sound(); + return G_SOURCE_REMOVE; +} + +void on_alarm_play_done (ca_context* /*context*/, uint32_t /*id*/, int rv, void* /*user_data*/) +{ + // wait one second, then play it again + if ((rv == CA_SUCCESS) && (timeout_tag == 0)) + timeout_tag = g_timeout_add_seconds (1, play_alarm_sound_idle, nullptr); +} + +void play_alarm_sound() +{ + const gchar* filename = ALARM_SOUND_FILENAME; + auto context = get_ca_context(); + g_return_if_fail(context != nullptr); + + ca_proplist* props = nullptr; + ca_proplist_create(&props); + if (media_cached) + ca_proplist_sets(props, CA_PROP_EVENT_ID, "alarm"); + ca_proplist_sets(props, CA_PROP_MEDIA_FILENAME, filename); + + const auto rv = ca_context_play_full(context, alarm_ca_id, props, on_alarm_play_done, nullptr); + if (rv != CA_SUCCESS) + g_warning("Failed to play file '%s': %s", filename, ca_strerror(rv)); + + g_clear_pointer(&props, ca_proplist_destroy); +} + +void stop_alarm_sound() +{ + auto context = get_ca_context(); + if (context != nullptr) + { + const auto rv = ca_context_cancel(context, alarm_ca_id); + if (rv != CA_SUCCESS) + g_warning("Failed to cancel alarm sound: %s", ca_strerror(rv)); + } + + if (timeout_tag != 0) + { + g_source_remove(timeout_tag); + timeout_tag = 0; + } +} + +/** +*** libnotify -- snap decisions +**/ + +void first_time_init() +{ + static bool inited = false; + + if (G_UNLIKELY(!inited)) + { + inited = true; + + if(!notify_init("indicator-datetime-service")) + g_critical("libnotify initialization failed"); + } +} + +struct SnapData +{ + Snap::appointment_func show; + Snap::appointment_func dismiss; + Appointment appointment; +}; + +void on_snap_show(NotifyNotification*, gchar* /*action*/, gpointer gdata) +{ + stop_alarm_sound(); + auto data = static_cast<SnapData*>(gdata); + data->show(data->appointment); +} + +void on_snap_dismiss(NotifyNotification*, gchar* /*action*/, gpointer gdata) +{ + stop_alarm_sound(); + auto data = static_cast<SnapData*>(gdata); + data->dismiss(data->appointment); +} + +void snap_data_destroy_notify(gpointer gdata) +{ + delete static_cast<SnapData*>(gdata); +} + +void show_snap_decision(SnapData* data) +{ + const Appointment& appointment = data->appointment; + + const auto timestr = appointment.begin.format("%a, %X"); + auto title = g_strdup_printf(_("Alarm %s"), timestr.c_str()); + const auto body = appointment.summary; + const gchar* icon_name = "alarm-clock"; + + auto nn = notify_notification_new(title, body.c_str(), icon_name); + notify_notification_set_hint_string(nn, "x-canonical-snap-decisions", "true"); + notify_notification_set_hint_string(nn, "x-canonical-private-button-tint", "true"); + notify_notification_add_action(nn, "show", _("Show"), on_snap_show, data, nullptr); + notify_notification_add_action(nn, "dismiss", _("Dismiss"), on_snap_dismiss, data, nullptr); + g_object_set_data_full(G_OBJECT(nn), "snap-data", data, snap_data_destroy_notify); + + GError * error = nullptr; + notify_notification_show(nn, &error); + if (error != NULL) + { + g_warning("Unable to show snap decision for '%s': %s", body.c_str(), error->message); + g_error_free(error); + data->show(data->appointment); + } + + g_free(title); +} + +/** +*** +**/ + +void notify(const Appointment& appointment, + Snap::appointment_func show, + Snap::appointment_func dismiss) +{ + auto data = new SnapData; + data->appointment = appointment; + data->show = show; + data->dismiss = dismiss; + + play_alarm_sound(); + show_snap_decision(data); +} + +} // unnamed namespace + + +/*** +**** +***/ + +Snap::Snap() +{ + first_time_init(); +} + +Snap::~Snap() +{ + media_cached = false; + g_clear_pointer(&c_context, ca_context_destroy); +} + +void Snap::operator()(const Appointment& appointment, + appointment_func show, + appointment_func dismiss) +{ + if (appointment.has_alarms) + notify(appointment, show, dismiss); + else + dismiss(appointment); +} + +/*** +**** +***/ + +} // namespace datetime +} // namespace indicator +} // namespace unity diff --git a/src/timezone-file.cpp b/src/timezone-file.cpp index 76737b4..c99897a 100644 --- a/src/timezone-file.cpp +++ b/src/timezone-file.cpp @@ -19,6 +19,9 @@ #include <datetime/timezone-file.h> +#include <cerrno> +#include <cstdlib> + namespace unity { namespace indicator { namespace datetime { @@ -29,7 +32,7 @@ FileTimezone::FileTimezone() FileTimezone::FileTimezone(const std::string& filename) { - setFilename(filename); + set_filename(filename); } FileTimezone::~FileTimezone() @@ -49,13 +52,23 @@ FileTimezone::clear() } void -FileTimezone::setFilename(const std::string& filename) +FileTimezone::set_filename(const std::string& filename) { clear(); - m_filename = filename; + auto tmp = realpath(filename.c_str(), nullptr); + if(tmp != nullptr) + { + m_filename = tmp; + free(tmp); + } + else + { + g_warning("Unable to resolve path '%s': %s", filename.c_str(), g_strerror(errno)); + m_filename = filename; // better than nothing? + } - auto file = g_file_new_for_path(filename.c_str()); + auto file = g_file_new_for_path(m_filename.c_str()); GError * err = nullptr; m_monitor = g_file_monitor_file(file, G_FILE_MONITOR_NONE, nullptr, &err); g_object_unref(file); @@ -66,15 +79,15 @@ FileTimezone::setFilename(const std::string& filename) } else { - m_monitor_handler_id = g_signal_connect_swapped(m_monitor, "changed", G_CALLBACK(onFileChanged), this); - g_debug("%s Monitoring timezone file '%s'", G_STRLOC, filename.c_str()); + m_monitor_handler_id = g_signal_connect_swapped(m_monitor, "changed", G_CALLBACK(on_file_changed), this); + g_debug("%s Monitoring timezone file '%s'", G_STRLOC, m_filename.c_str()); } reload(); } void -FileTimezone::onFileChanged(gpointer gself) +FileTimezone::on_file_changed(gpointer gself) { static_cast<FileTimezone*>(gself)->reload(); } |