aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CMakeLists.txt4
-rw-r--r--debian/control2
-rw-r--r--include/datetime/clock-watcher.h72
-rw-r--r--include/datetime/date-time.h3
-rw-r--r--include/datetime/planner-eds.h5
-rw-r--r--include/datetime/snap.h51
-rw-r--r--include/datetime/timezone-file.h4
-rw-r--r--po/POTFILES.in2
-rw-r--r--src/CMakeLists.txt2
-rw-r--r--src/actions-live.cpp2
-rw-r--r--src/clock-watcher.cpp71
-rw-r--r--src/date-time.cpp18
-rw-r--r--src/main.cpp31
-rw-r--r--src/menu.cpp103
-rw-r--r--src/planner-eds.cpp293
-rw-r--r--src/snap.cpp256
-rw-r--r--src/timezone-file.cpp27
-rw-r--r--tests/CMakeLists.txt5
-rw-r--r--tests/geoclue-fixture.h2
-rw-r--r--tests/manual-test-snap.cpp63
-rw-r--r--tests/test-clock-watcher.cpp166
-rw-r--r--tests/test-clock.cpp4
-rw-r--r--tests/test-planner.cpp14
-rw-r--r--tests/test-settings.cpp4
-rw-r--r--tests/test-utils.cpp2
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" },