/* * Copyright 2013 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 . * * Authors: * Charles Kerr */ #include #include #include #include #include #include #include // std::sort() #include #include namespace unity { namespace indicator { namespace datetime { /**** ***** ****/ class PlannerEds::Impl { public: Impl(PlannerEds& owner, const std::shared_ptr& 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 rebuild_soon()", dt.format("%F %T").c_str()); rebuild_soon(MONTH); }); rebuild_soon(ALL); } ~Impl() { 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); if (m_source_registry) g_signal_handlers_disconnect_by_data(m_source_registry, this); g_clear_object(&m_source_registry); } private: static void on_source_registry_ready(GObject* /*source*/, GAsyncResult* res, gpointer gself) { GError * error = nullptr; auto r = e_source_registry_new_finish(res, &error); if (error != nullptr) { if (!g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) g_warning("indicator-datetime cannot show EDS appointments: %s", error->message); g_error_free(error); } else { 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(gself); self->m_source_registry = r; 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(gself); self->m_sources.insert(E_SOURCE(g_object_ref(source))); if (e_source_get_enabled(source)) on_source_enabled(registry, source, gself); } static void on_source_enabled(ESourceRegistry* /*registry*/, ESource* source, gpointer gself) { auto self = static_cast(gself); ECalClientSourceType source_type; bool client_wanted = false; if (e_source_has_extension(source, E_SOURCE_EXTENSION_CALENDAR)) { source_type = E_CAL_CLIENT_SOURCE_TYPE_EVENTS; client_wanted = true; } else if (e_source_has_extension(source, E_SOURCE_EXTENSION_TASK_LIST)) { source_type = E_CAL_CLIENT_SOURCE_TYPE_TASKS; client_wanted = true; } const auto source_uid = e_source_get_uid(source); if (client_wanted) { g_debug("%s connecting a client to source %s", G_STRFUNC, source_uid); e_cal_client_connect(source, source_type, self->m_cancellable, on_client_connected, gself); } else { g_debug("%s not using source %s -- no tasks/calendar", G_STRFUNC, source_uid); } } static void on_client_connected(GObject* /*source*/, GAsyncResult * res, gpointer gself) { GError * error = nullptr; EClient * client = e_cal_client_connect_finish(res, &error); if (error) { if (!g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) g_warning("indicator-datetime cannot connect to EDS source: %s", error->message); g_error_free(error); } else { // add the client to our collection auto self = static_cast(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_client_view_ready (GObject* client, GAsyncResult* res, gpointer gself) { 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(gself); self->m_views[e_client_get_source(E_CLIENT(client))] = view; 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) { 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_error_free(error); } } static void on_view_objects_added(ECalClientView* /*view*/, gpointer /*objects*/, gpointer gself) { g_debug("%s", G_STRFUNC); static_cast(gself)->rebuild_soon(ALL); } static void on_view_objects_modified(ECalClientView* /*view*/, gpointer /*objects*/, gpointer gself) { g_debug("%s", G_STRFUNC); static_cast(gself)->rebuild_soon(ALL); } static void on_view_objects_removed(ECalClientView* /*view*/, gpointer /*objects*/, gpointer gself) { g_debug("%s", G_STRFUNC); static_cast(gself)->rebuild_soon(ALL); } static void on_source_disabled(ESourceRegistry* /*registry*/, ESource* source, gpointer gself) { static_cast(gself)->disable_source(source); } void disable_source(ESource* source) { // 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); } } static void on_source_removed(ESourceRegistry* /*registry*/, ESource* source, gpointer gself) { static_cast(gself)->remove_source(source); } void remove_source(ESource* source) { disable_source(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 rebuild_soon()"); static_cast(gself)->rebuild_soon(ALL); } private: typedef std::function&)> appointment_func; struct Task { Impl* p; appointment_func func; std::vector appointments; Task(Impl* p_in, const appointment_func& func_in): p(p_in), func(func_in) {} }; struct AppointmentSubtask { std::shared_ptr task; ECalClient* client; std::string color; AppointmentSubtask(const std::shared_ptr& task_in, ECalClient* client_in, const char* color_in): task(task_in), client(client_in) { if (color_in) color = color_in; } }; void rebuild_soon(int rebuild_flags) { 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, rebuild_now_static, this); } static gboolean rebuild_now_static(gpointer gself) { auto self = static_cast(gself); const auto flags = self->m_rebuild_flags; self->m_rebuild_tag = 0; self->m_rebuild_flags = 0; self->rebuild_now(flags); return G_SOURCE_REMOVE; } void rebuild_now(int rebuild_flags) { if (rebuild_flags & UPCOMING) rebuild_upcoming(); 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& 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& 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 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); auto begin_str = g_date_time_format(begin_dt, "%F %T"); auto end_str = g_date_time_format(end_dt, "%F %T"); g_debug("getting all appointments from [%s ... %s]", begin_str, end_str); g_free(begin_str); g_free(end_str); /** *** init the default timezone **/ icaltimezone * default_timezone = nullptr; const auto tz = g_date_time_get_timezone_abbreviation(m_owner.time.get().get()); g_debug("%s tz is %s", G_STRLOC, tz); if (tz && *tz) { default_timezone = icaltimezone_get_builtin_timezone(tz); if (default_timezone == nullptr) // maybe str is a tzid? default_timezone = icaltimezone_get_builtin_timezone_from_tzid(tz); g_debug("default_timezone is %p", (void*)default_timezone); } /** *** walk through the sources to build the appointment list **/ auto task_deleter = [](Task* task){ // give the caller the (sorted) finished product auto& a = task->appointments; std::sort(a.begin(), a.end(), [](const Appointment& a, const Appointment& b){return a.begin < b.begin;}); task->func(a); // we're done; delete the task g_debug("time to delete task %p", (void*)task); delete task; }; std::shared_ptr main_task(new Task(this, func), task_deleter); for (auto& kv : m_clients) { 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); e_cal_client_generate_instances(client, begin, end, m_cancellable, my_get_appointments_foreach, new AppointmentSubtask (main_task, client, color), [](gpointer g){delete static_cast(g);}); } } struct UrlSubtask { std::shared_ptr task; Appointment appointment; UrlSubtask(const std::shared_ptr& task_in, const Appointment& appointment_in): task(task_in), appointment(appointment_in) {} }; static gboolean my_get_appointments_foreach(ECalComponent* component, time_t begin, time_t end, gpointer gsubtask) { const auto vtype = e_cal_component_get_vtype(component); auto subtask = static_cast(gsubtask); if ((vtype == E_CAL_COMPONENT_EVENT) || (vtype == E_CAL_COMPONENT_TODO)) { const gchar* uid = nullptr; e_cal_component_get_uid(component, &uid); auto status = ICAL_STATUS_NONE; e_cal_component_get_status(component, &status); if ((uid != nullptr) && (status != ICAL_STATUS_COMPLETED) && (status != ICAL_STATUS_CANCELLED)) { Appointment appointment; /* Determine whether this is a recurring event. NB: icalrecurrencetype supports complex recurrence patterns; however, since design only allows daily recurrence, that's all we support here. */ GSList * recur_list; e_cal_component_get_rrule_list(component, &recur_list); for (auto l=recur_list; l!=nullptr; l=l->next) { const auto recur = static_cast(l->data); appointment.is_daily |= ((recur->freq == ICAL_DAILY_RECURRENCE) && (recur->interval == 1)); } e_cal_component_free_recur_list(recur_list); ECalComponentText text; text.value = nullptr; e_cal_component_get_summary(component, &text); if (text.value) appointment.summary = text.value; appointment.begin = DateTime(begin); appointment.end = DateTime(end); appointment.color = subtask->color; appointment.is_event = vtype == E_CAL_COMPONENT_EVENT; appointment.uid = uid; GList * alarm_uids = e_cal_component_get_alarm_uids(component); appointment.has_alarms = alarm_uids != nullptr; cal_obj_uid_list_free(alarm_uids); e_cal_client_get_attachment_uris(subtask->client, uid, nullptr, subtask->task->p->m_cancellable, on_appointment_uris_ready, new UrlSubtask(subtask->task, appointment)); } } return G_SOURCE_CONTINUE; } static void on_appointment_uris_ready(GObject* client, GAsyncResult* res, gpointer gsubtask) { auto subtask = static_cast(gsubtask); GSList * uris = nullptr; GError * error = nullptr; e_cal_client_get_attachment_uris_finish(E_CAL_CLIENT(client), res, &uris, &error); if (error != nullptr) { if (!g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED) && !g_error_matches(error, E_CLIENT_ERROR, E_CLIENT_ERROR_NOT_SUPPORTED)) { g_warning("Error getting appointment uris: %s", error->message); } g_error_free(error); } else if (uris != nullptr) { subtask->appointment.url = (const char*) uris->data; // copy the first URL g_debug("found url '%s' for appointment '%s'", subtask->appointment.url.c_str(), subtask->appointment.summary.c_str()); e_client_util_free_string_slist(uris); } g_debug("adding appointment '%s' '%s'", subtask->appointment.summary.c_str(), subtask->appointment.url.c_str()); subtask->task->appointments.push_back(subtask->appointment); delete subtask; } PlannerEds& m_owner; std::shared_ptr m_clock; std::set m_sources; std::map m_clients; std::map 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(const std::shared_ptr& clock): p(new Impl(*this, clock)) {} PlannerEds::~PlannerEds() =default; } // namespace datetime } // namespace indicator } // namespace unity