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 | |
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
-rw-r--r-- | CMakeLists.txt | 4 | ||||
-rw-r--r-- | debian/control | 2 | ||||
-rw-r--r-- | include/datetime/clock-watcher.h | 72 | ||||
-rw-r--r-- | include/datetime/date-time.h | 3 | ||||
-rw-r--r-- | include/datetime/planner-eds.h | 5 | ||||
-rw-r--r-- | include/datetime/snap.h | 51 | ||||
-rw-r--r-- | include/datetime/timezone-file.h | 4 | ||||
-rw-r--r-- | po/POTFILES.in | 2 | ||||
-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 | ||||
-rw-r--r-- | tests/CMakeLists.txt | 5 | ||||
-rw-r--r-- | tests/geoclue-fixture.h | 2 | ||||
-rw-r--r-- | tests/manual-test-snap.cpp | 63 | ||||
-rw-r--r-- | tests/test-clock-watcher.cpp | 166 | ||||
-rw-r--r-- | tests/test-clock.cpp | 4 | ||||
-rw-r--r-- | tests/test-planner.cpp | 14 | ||||
-rw-r--r-- | tests/test-settings.cpp | 4 | ||||
-rw-r--r-- | tests/test-utils.cpp | 2 |
25 files changed, 1013 insertions, 193 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt index fcee8d5..9b468a5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -38,10 +38,10 @@ pkg_check_modules (SERVICE_DEPS REQUIRED libical>=0.48 libecal-1.2>=3.5 libedataserver-1.2>=3.5 + libcanberra>=0.12 libnotify>=0.7.6 url-dispatcher-1>=1 - properties-cpp>=0.0.1 - json-glib-1.0>=0.16.2) + properties-cpp>=0.0.1) include_directories (SYSTEM ${SERVICE_DEPS_INCLUDE_DIRS}) ## diff --git a/debian/control b/debian/control index 423f8b8..84f76ca 100644 --- a/debian/control +++ b/debian/control @@ -16,13 +16,13 @@ Build-Depends: cmake, libgtest-dev, libglib2.0-dev (>= 2.35.4), libnotify-dev (>= 0.7.6), + libcanberra-dev, libido3-0.1-dev (>= 0.2.90), libgeoclue-dev (>= 0.12.0), libecal1.2-dev (>= 3.5), libical-dev (>= 1.0), libgtk-3-dev (>= 3.1.4), libcairo2-dev (>= 1.10), - libjson-glib-dev, libpolkit-gobject-1-dev, libedataserver1.2-dev (>= 3.5), libgconf2-dev (>= 2.31), diff --git a/include/datetime/clock-watcher.h b/include/datetime/clock-watcher.h new file mode 100644 index 0000000..e93b468 --- /dev/null +++ b/include/datetime/clock-watcher.h @@ -0,0 +1,72 @@ +/* + * Copyright 2014 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranties of + * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see <http://www.gnu.org/licenses/>. + * + * Authors: + * Charles Kerr <charles.kerr@canonical.com> + */ + +#ifndef INDICATOR_DATETIME_CLOCK_WATCHER_H +#define INDICATOR_DATETIME_CLOCK_WATCHER_H + +#include <datetime/state.h> +#include <datetime/appointment.h> + +#include <core/signal.h> + +#include <memory> +#include <set> +#include <string> + +namespace unity { +namespace indicator { +namespace datetime { + + +/** + * \brief Watches the clock and appointments to notify when an + * appointment's time is reached. + */ +class ClockWatcher +{ +public: + ClockWatcher() =default; + virtual ~ClockWatcher() =default; + virtual core::Signal<const Appointment&>& alarm_reached() = 0; +}; + + +/** + * \brief A #ClockWatcher implementation + */ +class ClockWatcherImpl: public ClockWatcher +{ +public: + ClockWatcherImpl(const std::shared_ptr<const State>& state); + ~ClockWatcherImpl() =default; + core::Signal<const Appointment&>& alarm_reached(); + +private: + void pulse(); + std::set<std::string> m_triggered; + std::shared_ptr<const State> m_state; + core::Signal<const Appointment&> m_alarm_reached; +}; + + +} // namespace datetime +} // namespace indicator +} // namespace unity + +#endif // INDICATOR_DATETIME_CLOCK_WATCHER_H diff --git a/include/datetime/date-time.h b/include/datetime/date-time.h index 2ad7856..b054a1f 100644 --- a/include/datetime/date-time.h +++ b/include/datetime/date-time.h @@ -41,6 +41,7 @@ public: DateTime& operator=(GDateTime* in); DateTime& operator=(const DateTime& in); DateTime to_timezone(const std::string& zone) const; + DateTime add_full(int years, int months, int days, int hours, int minutes, double seconds) const; void reset(GDateTime* in=nullptr); GDateTime* get() const; @@ -48,9 +49,11 @@ public: std::string format(const std::string& fmt) const; int day_of_month() const; + double seconds() const; int64_t to_unix() const; bool operator<(const DateTime& that) const; + bool operator<=(const DateTime& that) const; bool operator!=(const DateTime& that) const; bool operator==(const DateTime& that) const; diff --git a/include/datetime/planner-eds.h b/include/datetime/planner-eds.h index f3abce0..a99f611 100644 --- a/include/datetime/planner-eds.h +++ b/include/datetime/planner-eds.h @@ -20,9 +20,10 @@ #ifndef INDICATOR_DATETIME_PLANNER_EDS_H #define INDICATOR_DATETIME_PLANNER_EDS_H +#include <datetime/clock.h> #include <datetime/planner.h> -#include <memory> // unique_ptr +#include <memory> // shared_ptr, unique_ptr namespace unity { namespace indicator { @@ -34,7 +35,7 @@ namespace datetime { class PlannerEds: public Planner { public: - PlannerEds(); + PlannerEds(const std::shared_ptr<Clock>& clock); virtual ~PlannerEds(); private: diff --git a/include/datetime/snap.h b/include/datetime/snap.h new file mode 100644 index 0000000..a493772 --- /dev/null +++ b/include/datetime/snap.h @@ -0,0 +1,51 @@ +/* + * Copyright 2014 Canonical Ltd. + * + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 3, as published + * by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranties of + * MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR + * PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see <http://www.gnu.org/licenses/>. + * + * Authors: + * Charles Kerr <charles.kerr@canonical.com> + */ + +#ifndef INDICATOR_DATETIME_SNAP_H +#define INDICATOR_DATETIME_SNAP_H + +#include <datetime/appointment.h> + +#include <memory> +#include <functional> + +namespace unity { +namespace indicator { +namespace datetime { + +/** + * \brief Pops up Snap Decisions for appointments + */ +class Snap +{ +public: + Snap(); + virtual ~Snap(); + + typedef std::function<void(const Appointment&)> appointment_func; + void operator()(const Appointment& appointment, + appointment_func show, + appointment_func dismiss); +}; + +} // namespace datetime +} // namespace indicator +} // namespace unity + +#endif // INDICATOR_DATETIME_SNAP_H diff --git a/include/datetime/timezone-file.h b/include/datetime/timezone-file.h index d77aaae..a67c01a 100644 --- a/include/datetime/timezone-file.h +++ b/include/datetime/timezone-file.h @@ -42,8 +42,8 @@ public: ~FileTimezone(); private: - void setFilename(const std::string& filename); - static void onFileChanged(gpointer gself); + void set_filename(const std::string& filename); + static void on_file_changed(gpointer gself); void clear(); void reload(); diff --git a/po/POTFILES.in b/po/POTFILES.in index 1b1ee58..3faca0b 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -1,3 +1,5 @@ src/formatter.cpp src/formatter-desktop.cpp src/menu.cpp +src/snap.cpp +src/utils.c 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(); } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 3dcd151..7d590c9 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -42,6 +42,7 @@ function(add_test_by_name name) endfunction() add_test_by_name(test-actions) add_test_by_name(test-clock) +add_test_by_name(test-clock-watcher) add_test_by_name(test-exporter) add_test_by_name(test-formatter) add_test_by_name(test-live-actions) @@ -52,6 +53,10 @@ add_test_by_name(test-settings) add_test_by_name(test-timezone-file) add_test_by_name(test-utils) +set (TEST_NAME manual-test-snap) +add_executable (${TEST_NAME} ${TEST_NAME}.cpp) +add_dependencies (${TEST_NAME} libindicatordatetimeservice) +target_link_libraries (${TEST_NAME} indicatordatetimeservice gtest ${SERVICE_DEPS_LIBRARIES} ${GTEST_LIBS}) # disabling the timezone unit tests because they require # https://code.launchpad.net/~ted/dbus-test-runner/multi-interface-test/+merge/199724 diff --git a/tests/geoclue-fixture.h b/tests/geoclue-fixture.h index 7e29018..0c597d3 100644 --- a/tests/geoclue-fixture.h +++ b/tests/geoclue-fixture.h @@ -95,7 +95,7 @@ class GeoclueFixture : public GlibFixture // I've looked and can't find where this extra ref is coming from. // is there an unbalanced ref to the bus in the test harness?! - while (bus != NULL) + while (bus != nullptr) { g_object_unref (bus); wait_msec (1000); diff --git a/tests/manual-test-snap.cpp b/tests/manual-test-snap.cpp new file mode 100644 index 0000000..51556cd --- /dev/null +++ b/tests/manual-test-snap.cpp @@ -0,0 +1,63 @@ + +/* + * Copyright 2013 Canonical Ltd. + * + * Authors: + * Charles Kerr <charles.kerr@canonical.com> + * + * 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/>. + */ + +#include <datetime/appointment.h> +#include <datetime/snap.h> + +#include <glib.h> + +using namespace unity::indicator::datetime; + +/*** +**** +***/ + +int main() +{ + Appointment a; + a.color = "green"; + a.summary = "Alarm"; + a.url = "alarm:///hello-world"; + a.uid = "D4B57D50247291478ED31DED17FF0A9838DED402"; + a.is_event = false; + a.is_daily = false; + a.has_alarms = true; + auto begin = g_date_time_new_local(2014,12,25,0,0,0); + auto end = g_date_time_add_full(begin,0,0,1,0,0,-1); + a.begin = begin; + a.end = end; + g_date_time_unref(end); + g_date_time_unref(begin); + + auto loop = g_main_loop_new(nullptr, false); + auto show = [loop](const Appointment& appt){ + g_message("You clicked 'show' for appt url '%s'", appt.url.c_str()); + g_main_loop_quit(loop); + }; + auto dismiss = [loop](const Appointment&){ + g_message("You clicked 'dismiss'"); + g_main_loop_quit(loop); + }; + + Snap snap; + snap(a, show, dismiss); + g_main_loop_run(loop); + return 0; +} diff --git a/tests/test-clock-watcher.cpp b/tests/test-clock-watcher.cpp new file mode 100644 index 0000000..79b8485 --- /dev/null +++ b/tests/test-clock-watcher.cpp @@ -0,0 +1,166 @@ +/* + * 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> + +#include <gtest/gtest.h> + +#include "state-fixture.h" + +using namespace unity::indicator::datetime; + +class ClockWatcherFixture: public StateFixture +{ +private: + + typedef StateFixture super; + +protected: + + std::vector<std::string> m_triggered; + std::unique_ptr<ClockWatcher> m_watcher; + + void SetUp() + { + super::SetUp(); + + m_watcher.reset(new ClockWatcherImpl(m_state)); + m_watcher->alarm_reached().connect([this](const Appointment& appt){ + m_triggered.push_back(appt.uid); + }); + + EXPECT_TRUE(m_triggered.empty()); + } + + void TearDown() + { + m_triggered.clear(); + m_watcher.reset(); + + super::TearDown(); + } + + std::vector<Appointment> build_some_appointments() + { + const auto now = m_state->clock->localtime(); + auto tomorrow = g_date_time_add_days (now.get(), 1); + auto tomorrow_begin = g_date_time_add_full (tomorrow, 0, 0, 0, + -g_date_time_get_hour(tomorrow), + -g_date_time_get_minute(tomorrow), + -g_date_time_get_seconds(tomorrow)); + auto tomorrow_end = g_date_time_add_full (tomorrow_begin, 0, 0, 1, 0, 0, -1); + + Appointment a1; // an alarm clock appointment + a1.color = "red"; + a1.summary = "Alarm"; + a1.summary = "http://www.example.com/"; + a1.uid = "example"; + a1.has_alarms = true; + a1.begin = tomorrow_begin; + a1.end = tomorrow_end; + + auto ubermorgen_begin = g_date_time_add_days (tomorrow, 1); + auto ubermorgen_end = g_date_time_add_full (tomorrow_begin, 0, 0, 1, 0, 0, -1); + + Appointment a2; // a non-alarm appointment + a2.color = "green"; + a2.summary = "Other Text"; + a2.summary = "http://www.monkey.com/"; + a2.uid = "monkey"; + a2.has_alarms = false; + a2.begin = ubermorgen_begin; + a2.end = ubermorgen_end; + + // cleanup + g_date_time_unref(ubermorgen_end); + g_date_time_unref(ubermorgen_begin); + g_date_time_unref(tomorrow_end); + g_date_time_unref(tomorrow_begin); + g_date_time_unref(tomorrow); + + return std::vector<Appointment>({a1, a2}); + } +}; + +/*** +**** +***/ + +TEST_F(ClockWatcherFixture, AppointmentsChanged) +{ + // Add some appointments to the planner. + // One of these matches our state's localtime, so that should get triggered. + std::vector<Appointment> a = build_some_appointments(); + a[0].begin = m_state->clock->localtime(); + m_state->planner->upcoming.set(a); + + // Confirm that it got fired + EXPECT_EQ(1, m_triggered.size()); + EXPECT_EQ(a[0].uid, m_triggered[0]); +} + + +TEST_F(ClockWatcherFixture, TimeChanged) +{ + // Add some appointments to the planner. + // Neither of these match the state's localtime, so nothing should be triggered. + std::vector<Appointment> a = build_some_appointments(); + m_state->planner->upcoming.set(a); + EXPECT_TRUE(m_triggered.empty()); + + // Set the state's clock to a time that matches one of the appointments. + // That appointment should get triggered. + m_mock_state->mock_clock->set_localtime(a[1].begin); + EXPECT_EQ(1, m_triggered.size()); + EXPECT_EQ(a[1].uid, m_triggered[0]); +} + + +TEST_F(ClockWatcherFixture, MoreThanOne) +{ + const auto now = m_state->clock->localtime(); + std::vector<Appointment> a = build_some_appointments(); + a[0].begin = a[1].begin = now; + m_state->planner->upcoming.set(a); + + EXPECT_EQ(2, m_triggered.size()); + EXPECT_EQ(a[0].uid, m_triggered[0]); + EXPECT_EQ(a[1].uid, m_triggered[1]); +} + + +TEST_F(ClockWatcherFixture, NoDuplicates) +{ + // Setup: add an appointment that gets triggered. + const auto now = m_state->clock->localtime(); + const std::vector<Appointment> appointments = build_some_appointments(); + std::vector<Appointment> a; + a.push_back(appointments[0]); + a[0].begin = now; + m_state->planner->upcoming.set(a); + EXPECT_EQ(1, m_triggered.size()); + EXPECT_EQ(a[0].uid, m_triggered[0]); + + // Now change the appointment vector by adding one to it. + // Confirm that the ClockWatcher doesn't re-trigger a[0] + a.push_back(appointments[1]); + m_state->planner->upcoming.set(a); + EXPECT_EQ(1, m_triggered.size()); + EXPECT_EQ(a[0].uid, m_triggered[0]); +} diff --git a/tests/test-clock.cpp b/tests/test-clock.cpp index 4287e1c..a4924b3 100644 --- a/tests/test-clock.cpp +++ b/tests/test-clock.cpp @@ -37,12 +37,12 @@ class ClockFixture: public TestDBusFixture void emitPrepareForSleep() { g_dbus_connection_emit_signal(g_bus_get_sync(G_BUS_TYPE_SYSTEM, nullptr, nullptr), - NULL, + nullptr, "/org/freedesktop/login1", // object path "org.freedesktop.login1.Manager", // interface "PrepareForSleep", // signal name g_variant_new("(b)", FALSE), - NULL); + nullptr); } }; diff --git a/tests/test-planner.cpp b/tests/test-planner.cpp index b476ee8..1923ba1 100644 --- a/tests/test-planner.cpp +++ b/tests/test-planner.cpp @@ -28,9 +28,7 @@ #include <langinfo.h> #include <locale.h> -using unity::indicator::datetime::Appointment; -using unity::indicator::datetime::DateTime; -using unity::indicator::datetime::PlannerEds; +using namespace unity::indicator::datetime; /*** **** @@ -40,11 +38,15 @@ typedef GlibFixture PlannerFixture; TEST_F(PlannerFixture, EDS) { - PlannerEds planner; + auto tmp = g_date_time_new_now_local(); + const auto now = DateTime(tmp); + g_date_time_unref(tmp); + + std::shared_ptr<Clock> clock(new MockClock(now)); + PlannerEds planner(clock); wait_msec(100); - auto now = g_date_time_new_now_local(); - planner.time.set(DateTime(now)); + planner.time.set(now); wait_msec(2500); std::vector<Appointment> this_month = planner.this_month.get(); diff --git a/tests/test-settings.cpp b/tests/test-settings.cpp index 980e7fa..707247d 100644 --- a/tests/test-settings.cpp +++ b/tests/test-settings.cpp @@ -167,8 +167,8 @@ TEST_F(SettingsFixture, Locations) { const auto key = SETTINGS_LOCATIONS_S; - const gchar* astrv[] = {"America/Los_Angeles Oakland", "America/Chicago Oklahoma City", "Europe/London London", NULL}; - const gchar* bstrv[] = {"America/Denver", "Europe/London London", "Europe/Berlin Berlin", NULL}; + const gchar* astrv[] = {"America/Los_Angeles Oakland", "America/Chicago Oklahoma City", "Europe/London London", nullptr}; + const gchar* bstrv[] = {"America/Denver", "Europe/London London", "Europe/Berlin Berlin", nullptr}; const std::vector<std::string> av = strv_to_vector(astrv); const std::vector<std::string> bv = strv_to_vector(bstrv); diff --git a/tests/test-utils.cpp b/tests/test-utils.cpp index 036c13f..97f07ed 100644 --- a/tests/test-utils.cpp +++ b/tests/test-utils.cpp @@ -59,7 +59,7 @@ namespace const char* location; const char* expected_name; } beautify_timezone_test_cases[] = { - { "America/Chicago", NULL, "Chicago" }, + { "America/Chicago", nullptr, "Chicago" }, { "America/Chicago", "America/Chicago", "Chicago" }, { "America/Chicago", "America/Chigago Chicago", "Chicago" }, { "America/Chicago", "America/Chicago Oklahoma City", "Oklahoma City" }, |