/* * Copyright 2014 Canonical Ltd. * Copyright 2021-2024 Robert Tari * * 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 * Robert Tari */ #ifndef ALARM_DEFAULT_SOUND #define ALARM_DEFAULT_SOUND "dummy" #endif #include #include #include #include #include #include #include // std::sort() #include #include // time() #include // strstr(), strlen() #include #include namespace ayatana { namespace indicator { namespace datetime { static constexpr char const * TAG_ALARM {"x-lomiri-alarm"}; static constexpr char const * TAG_DISABLED {"x-lomiri-disabled"}; /**** ***** ****/ class EdsEngine::Impl { public: explicit Impl(const std::shared_ptr &myself) : m_myself(myself) { auto cancellable_deleter = [](GCancellable * c) { g_cancellable_cancel(c); g_clear_object(&c); }; m_cancellable = std::shared_ptr(g_cancellable_new(), cancellable_deleter); e_source_registry_new(m_cancellable.get(), on_source_registry_ready, this); m_myself->emails().changed().connect([this](const std::set &) { set_dirty_soon(); }); } ~Impl() { m_cancellable.reset(); 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); } core::Signal<>& changed() { return m_changed; } void get_appointments(const DateTime& begin, const DateTime& end, const Timezone& timezone, std::function&)> func) { const auto b_str = begin.format("%F %T"); const auto e_str = end.format("%F %T"); g_debug("getting all appointments from [%s ... %s]", b_str.c_str(), e_str.c_str()); /** *** init the default timezone **/ ICalTimezone * default_timezone = nullptr; const auto tz = timezone.timezone.get().c_str(); auto gtz = timezone_from_name(tz, nullptr, nullptr, &default_timezone); if (gtz == nullptr) { gtz = g_time_zone_new_local(); } g_debug("default_timezone is %s", default_timezone ? i_cal_timezone_get_display_name(default_timezone) : "null"); /** *** walk through the sources to build the appointment list **/ auto main_task = std::make_shared(this, func, default_timezone, gtz, begin, end); for (auto& kv : m_clients) { auto& client = kv.second; if (default_timezone != nullptr) e_cal_client_set_default_timezone(client, default_timezone); g_debug("calling e_cal_client_generate_instances for %p", (void*)client); auto& source = kv.first; auto extension = e_source_get_extension(source, E_SOURCE_EXTENSION_CALENDAR); // check source is selected if (!e_source_selectable_get_selected(E_SOURCE_SELECTABLE(extension))) { g_debug("Source is not selected, ignore it: %s", e_source_get_display_name(source)); continue; } const auto color = e_source_selectable_get_color(E_SOURCE_SELECTABLE(extension)); e_cal_client_generate_instances( client, begin.to_unix(), end.to_unix(), m_cancellable.get(), on_event_generated, new ClientSubtask(main_task, client, m_cancellable, color), on_event_generated_list_ready); } } void disable_alarm(const Appointment& appointment) { if (appointment.is_alarm()) { for (auto& kv : m_clients) // find the matching icalcomponent { e_cal_client_get_object(kv.second, appointment.uid.c_str(), nullptr, m_cancellable.get(), on_object_ready_for_disable, this); } } } private: void set_dirty_now() { m_changed(); } static gboolean set_dirty_now_static (gpointer gself) { auto self = static_cast(gself); self->m_rebuild_tag = 0; self->m_rebuild_deadline = 0; self->set_dirty_now(); return G_SOURCE_REMOVE; } void set_dirty_soon() { static constexpr int MIN_BATCH_SEC = 1; static constexpr int MAX_BATCH_SEC = 60; static_assert(MIN_BATCH_SEC <= MAX_BATCH_SEC, "bad boundaries"); const auto now = time(nullptr); if (m_rebuild_deadline == 0) // first pass { m_rebuild_deadline = now + MAX_BATCH_SEC; m_rebuild_tag = g_timeout_add_seconds(MIN_BATCH_SEC, set_dirty_now_static, this); } else if (now < m_rebuild_deadline) { g_source_remove (m_rebuild_tag); m_rebuild_tag = g_timeout_add_seconds(MIN_BATCH_SEC, set_dirty_now_static, this); } } 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("ayatana-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, #if EDS_CHECK_VERSION(3,13,90) -1, #endif self->m_cancellable.get(), 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("ayatana-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))); auto source = e_client_get_source(client); auto ecc = E_CAL_CLIENT(client); self->m_clients[source] = ecc; self->ensure_client_alarms_have_triggers(ecc); // now create a view for it so that we can listen for changes e_cal_client_get_view (ecc, "#t", // match all self->m_cancellable.get(), on_client_view_ready, self); g_debug("client connected; calling set_dirty_soon()"); self->set_dirty_soon(); } } 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_set_flags(view, E_CAL_CLIENT_VIEW_FLAGS_NONE, nullptr); 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 set_dirty_soon()"); self->set_dirty_soon(); } else if(error != nullptr) { if (!g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) g_warning("ayatana-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)->set_dirty_soon(); } static void on_view_objects_modified(ECalClientView* /*view*/, gpointer /*objects*/, gpointer gself) { g_debug("%s", G_STRFUNC); static_cast(gself)->set_dirty_soon(); } static void on_view_objects_removed(ECalClientView* /*view*/, gpointer /*objects*/, gpointer gself) { g_debug("%s", G_STRFUNC); static_cast(gself)->set_dirty_soon(); } 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); set_dirty_soon(); } // 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); set_dirty_soon(); } } 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); set_dirty_soon(); } } static void on_source_changed(ESourceRegistry* /*registry*/, ESource* /*source*/, gpointer gself) { g_debug("source changed; calling set_dirty_soon()"); static_cast(gself)->set_dirty_soon(); } void ensure_client_alarms_have_triggers(ECalClient* client) { auto sexp = g_strdup_printf("has-categories? '%s'", TAG_ALARM); e_cal_client_get_object_list_as_comps( client, sexp, m_cancellable.get(), ensure_client_alarms_have_triggers_async_cb, this); g_clear_pointer(&sexp, g_free); } static void ensure_client_alarms_have_triggers_async_cb( GObject * oclient, GAsyncResult * res, gpointer gself) { ECalClient * client = E_CAL_CLIENT(oclient); GError * error = nullptr; GSList * components = nullptr; if (e_cal_client_get_object_list_as_comps_finish(client, res, &components, &error)) { auto self = static_cast(gself); self->ensure_ayatana_alarms_have_triggers(client, components); e_client_util_free_object_slist (components); } else if (error != nullptr) { if (!g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) g_warning("can't get clock-app alarm list: %s", error->message); g_error_free(error); } } static ECalComponentAlarm * ensure_alarm_has_trigger(ECalComponentAlarm *alarm) { ECalComponentAlarm * ret = nullptr; auto trigger = e_cal_component_alarm_get_trigger(alarm); if (trigger == nullptr || (e_cal_component_alarm_trigger_get_kind (trigger) == E_CAL_COMPONENT_ALARM_TRIGGER_NONE)) { auto copy = e_cal_component_alarm_copy (alarm); auto null_duration = i_cal_duration_new_null_duration(); trigger = e_cal_component_alarm_trigger_new_relative(E_CAL_COMPONENT_ALARM_TRIGGER_RELATIVE_START, null_duration); e_cal_component_alarm_trigger_set_duration(trigger, null_duration); e_cal_component_alarm_set_trigger(copy, trigger); ret = copy; e_cal_component_alarm_trigger_free(trigger); g_clear_object(&null_duration); } return ret; } void ensure_ayatana_alarms_have_triggers(ECalClient * client, GSList * components) { GSList * modify_slist = nullptr; // for each component.. for (auto l=components; l!=nullptr; l=l->next) { bool bChanged = false; // for each alarm... auto component = E_CAL_COMPONENT(l->data); auto auids = e_cal_component_get_alarm_uids(component); for(auto lAlarms=auids; lAlarms!=nullptr; lAlarms=lAlarms->next) { auto auid = static_cast(lAlarms->data); auto alarm = e_cal_component_get_alarm(component, auid); if (alarm == nullptr) continue; auto new_alarm = ensure_alarm_has_trigger (alarm); if (new_alarm != nullptr) { e_cal_component_remove_alarm (component, auid); e_cal_component_add_alarm (component, new_alarm); bChanged = true; g_clear_pointer (&new_alarm, e_cal_component_alarm_free); } } g_slist_free_full (auids, g_free); if (bChanged) { auto icc = e_cal_component_get_icalcomponent(component); // icc owned by ecc modify_slist = g_slist_prepend(modify_slist, icc); } } if (modify_slist != nullptr) { e_cal_client_modify_objects(client, modify_slist, E_CAL_OBJ_MOD_ALL, E_CAL_OPERATION_FLAG_NONE, m_cancellable.get(), ensure_ayatana_alarms_have_triggers_async_cb, this); g_clear_pointer(&modify_slist, g_slist_free); } } // log a warning if e_cal_client_modify_objects() failed static void ensure_ayatana_alarms_have_triggers_async_cb( GObject * oclient, GAsyncResult * res, gpointer /*gself*/) { GError * error = nullptr; e_cal_client_modify_objects_finish (E_CAL_CLIENT(oclient), res, &error); if (error != nullptr) { if (!g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) g_warning("couldn't add alarm triggers: %s", error->message); g_error_free(error); } } /*** **** ***/ typedef std::function&)> appointment_func; struct Task { Impl* p; appointment_func func; ICalTimezone* default_timezone; // pointer owned by libical GTimeZone* gtz; std::vector appointments; const DateTime begin; const DateTime end; Task(Impl* p_in, appointment_func func_in, ICalTimezone* tz_in, GTimeZone* gtz_in, const DateTime& begin_in, const DateTime& end_in): p{p_in}, func{func_in}, default_timezone{tz_in}, gtz{gtz_in}, begin{begin_in}, end{end_in} {} ~Task() { g_clear_pointer(>z, g_time_zone_unref); // give the caller the sorted finished product auto& a = appointments; std::sort(a.begin(), a.end(), [](const Appointment& a, const Appointment& b){return a.begin < b.begin;}); func(a); }; }; struct ClientSubtask { std::shared_ptr task; ECalClient* client; std::shared_ptr cancellable; std::string color; GList *components; GList *instance_components; std::set parent_components; ClientSubtask(const std::shared_ptr& task_in, ECalClient* client_in, const std::shared_ptr& cancellable_in, const char* color_in): task(task_in), client(client_in), cancellable(cancellable_in), components(nullptr), instance_components(nullptr) { if (color_in) color = color_in; } }; static std::string get_alarm_text(ECalComponentAlarm * alarm) { std::string ret; auto action = e_cal_component_alarm_get_action(alarm); if (action == E_CAL_COMPONENT_ALARM_DISPLAY) { auto text = e_cal_component_alarm_get_description(alarm); if (text != nullptr) { auto value = e_cal_component_text_get_value(text); if (value != nullptr) { ret = value; } } } return ret; } static std::string get_alarm_sound_url(ECalComponentAlarm * alarm, const std::string & default_sound) { std::string ret; auto action = e_cal_component_alarm_get_action(alarm); if (action == E_CAL_COMPONENT_ALARM_AUDIO) { auto attachments = e_cal_component_alarm_get_attachments(alarm); for (; attachments != nullptr; attachments = attachments->next) { ICalAttach *attach = I_CAL_ATTACH (attachments->data); if (attach != nullptr && i_cal_attach_get_is_url (attach)) { const char* url = i_cal_attach_get_url(attach); if (url != nullptr) { ret = url; break; } } } if (ret.empty()) ret = default_sound; } return ret; } static gboolean on_event_generated(ICalComponent *comp, ICalTime *start, ICalTime *end, gpointer gsubtask, GCancellable *cancellable, GError **error) { auto subtask = static_cast(gsubtask); const auto uid = i_cal_component_get_uid (comp); auto ecomp = e_cal_component_new_from_icalcomponent (i_cal_component_clone (comp)); auto auids = e_cal_component_get_alarm_uids(ecomp); for(auto l=auids; l!=nullptr; l=l->next) { auto auid = static_cast(l->data); auto alarm = e_cal_component_get_alarm(ecomp, auid); if (alarm == nullptr) continue; auto new_alarm = ensure_alarm_has_trigger (alarm); if (new_alarm != nullptr) { e_cal_component_remove_alarm (ecomp, auid); e_cal_component_add_alarm (ecomp, new_alarm); g_clear_pointer (&new_alarm, e_cal_component_alarm_free); } } if (e_cal_component_is_instance(ecomp) && (uid != nullptr)) { subtask->parent_components.insert(std::string(uid)); subtask->instance_components = g_list_append(subtask->instance_components, ecomp); } else { subtask->components = g_list_append(subtask->components, ecomp); } return TRUE; } static void merge_detached_instances(ClientSubtask *subtask, GSList *instances) { for (GSList *i=instances; i!=nullptr; i=i->next) { auto instance = static_cast(i->data); auto instance_id = e_cal_component_get_id(instance); for (GList *c=subtask->instance_components ; c!= nullptr; c=c->next) { auto component = static_cast(c->data); auto component_id = e_cal_component_get_id(component); bool found = false; if (e_cal_component_id_equal(instance_id, component_id)) { // replaces virtual instance with the real one g_object_unref(component); c->data = g_object_ref(instance); found = true; } e_cal_component_id_free(component_id); if (found) break; } e_cal_component_id_free(instance_id); } } static void fetch_detached_instances(GObject *, GAsyncResult *res, gpointer gsubtask) { auto subtask = static_cast(gsubtask); if (res) { GError *error = nullptr; GSList *comps = nullptr; e_cal_client_get_objects_for_uid_finish(subtask->client, res, &comps, &error); if (error) { g_warning("Fail to retrieve detached instances: %s", error->message); g_error_free(error); } else { merge_detached_instances(subtask, comps); e_client_util_free_object_slist(comps); } } if (subtask->parent_components.empty()) { on_event_fetch_list_done(gsubtask); return; } // continue fetch detached instances auto i_begin = subtask->parent_components.begin(); std::string id = *i_begin; subtask->parent_components.erase(i_begin); e_cal_client_get_objects_for_uid(subtask->client, id.c_str(), subtask->cancellable.get(), (GAsyncReadyCallback) fetch_detached_instances, gsubtask); } static void on_event_generated_list_ready(gpointer gsubtask) { fetch_detached_instances(nullptr, nullptr, gsubtask); } static gint sort_events_by_start_date(ECalComponent *eventA, ECalComponent *eventB) { auto start_date_a = e_cal_component_get_dtstart(eventA); auto start_date_b = e_cal_component_get_dtstart(eventB); auto icaltime_a = e_cal_component_datetime_get_value(start_date_a); auto icaltime_b = e_cal_component_datetime_get_value(start_date_b); auto time_a = i_cal_time_as_timet(icaltime_a); auto time_b = i_cal_time_as_timet(icaltime_b); if (time_a == time_b) return 0; if (time_a < time_b) return -1; return 1; } static bool is_alarm_interesting(ECalComponentAlarm *alarm) { if (alarm) { auto action = e_cal_component_alarm_get_action(alarm); if ((action == E_CAL_COMPONENT_ALARM_AUDIO) || (action == E_CAL_COMPONENT_ALARM_DISPLAY)) { return true; } } return false; } // we only care about AUDIO or DISPLAY alarms, other kind of alarm will not generate a notification static bool event_has_valid_alarms(ECalComponent *event) { if (!e_cal_component_has_alarms(event)) return false; // check alarms bool valid = false; auto uids = e_cal_component_get_alarm_uids(event); for (auto l=uids; l!=nullptr; l=uids->next) { auto auid = static_cast(l->data); auto alarm = e_cal_component_get_alarm(event, auid); valid = is_alarm_interesting(alarm); if (valid) { break; } } g_slist_free_full(uids, g_free); return valid; } static void on_event_fetch_list_done(gpointer gsubtask) { auto subtask = static_cast(gsubtask); // generate alarms constexpr std::array omit = { (ECalComponentAlarmAction)-1 }; // list of action types to omit, terminated with -1 GSList * comp_alarms = nullptr; // we do not need translate tz for instance events, // they are aredy in the correct time e_cal_util_generate_alarms_for_list( subtask->instance_components, subtask->task->begin.to_unix(), subtask->task->end.to_unix(), const_cast(omit.data()), &comp_alarms, e_cal_client_tzlookup_cb, subtask->client, nullptr); // convert timezone for non-instance events e_cal_util_generate_alarms_for_list( subtask->components, subtask->task->begin.to_unix(), subtask->task->end.to_unix(), const_cast(omit.data()), &comp_alarms, e_cal_client_tzlookup_cb, subtask->client, subtask->task->default_timezone); // walk the alarms & add them for (auto l=comp_alarms; l!=nullptr; l=l->next) add_alarms_to_subtask(static_cast(l->data), subtask, subtask->task->gtz); subtask->components = g_list_concat(subtask->components, subtask->instance_components); subtask->components = g_list_sort(subtask->components, (GCompareFunc) sort_events_by_start_date); // add events without alarm for (auto l=subtask->components; l!=nullptr; l=l->next) { auto component = static_cast(l->data); if (!event_has_valid_alarms(component)) add_event_to_subtask(component, subtask, subtask->task->gtz); } g_list_free_full(subtask->components, g_object_unref); g_slist_free_full(comp_alarms, e_cal_component_alarms_free); delete subtask; } static GTimeZone * timezone_from_name (const char * tzid, ECalClient * client, GCancellable * cancellable, ICalTimezone **itimezone) { if (tzid == nullptr) return nullptr; auto itz = i_cal_timezone_get_builtin_timezone_from_tzid(tzid); // usually works if (itz == nullptr) // fallback itz = i_cal_timezone_get_builtin_timezone(tzid); if (client && (itz == nullptr)) // ok we have a strange tzid... ask EDS to look it up in VTIMEZONES e_cal_client_get_timezone_sync(client, tzid, &itz, cancellable, nullptr); const char* identifier {}; if (itimezone) *itimezone = itz; if (itz != nullptr) { identifier = i_cal_timezone_get_display_name(itz); if (identifier == nullptr) identifier = i_cal_timezone_get_location(itz); } // handle the TZID /freeassociation.sourceforge.net/Tzfile/[Location] case if (identifier != nullptr) { const char* pch; const char* key = "/freeassociation.sourceforge.net/"; if ((pch = strstr(identifier, key))) { identifier = pch + strlen(key); key = "Tzfile/"; // some don't have this, so check for it separately if ((pch = strstr(identifier, key))) identifier = pch + strlen(key); } } if (identifier == nullptr) g_warning("Unrecognized TZID: '%s'", tzid); else { #if GLIB_CHECK_VERSION(2, 68, 0) auto pZone = g_time_zone_new_identifier(identifier); if (pZone == NULL) { pZone = g_time_zone_new_utc(); } #else auto pZone = g_time_zone_new(identifier); #endif return pZone; } return nullptr; } static DateTime datetime_from_component_date_time(ECalClient * client, std::shared_ptr & cancellable, const ECalComponentDateTime * in, GTimeZone * default_timezone) { DateTime out; if (in == nullptr) return out; auto value = e_cal_component_datetime_get_value(in); if (value == nullptr) return out; auto tz = i_cal_time_get_timezone (value); GTimeZone * gtz = timezone_from_name(i_cal_timezone_get_tzid (tz), client, cancellable.get(), nullptr); if (gtz == nullptr) gtz = g_time_zone_ref(default_timezone); out = DateTime(gtz, i_cal_time_get_year(value), i_cal_time_get_month(value), i_cal_time_get_day(value), i_cal_time_get_hour(value), i_cal_time_get_minute(value), i_cal_time_get_second(value)); g_time_zone_unref(gtz); return out; } bool is_component_interesting(ECalComponent * component) { // we only want calendar events and vtodos const auto vtype = e_cal_component_get_vtype(component); if ((vtype != E_CAL_COMPONENT_EVENT) && (vtype != E_CAL_COMPONENT_TODO)) return false; // we're not interested in completed or cancelled components auto status = e_cal_component_get_status(component); if ((status == I_CAL_STATUS_COMPLETED) || (status == I_CAL_STATUS_CANCELLED)) return false; // we don't want disabled alarms bool disabled = false; GSList * categ_list = e_cal_component_get_categories_list (component); for (GSList * l=categ_list; l!=nullptr; l=l->next) { auto tag = static_cast(l->data); if (!g_strcmp0(tag, TAG_DISABLED)) disabled = true; } g_slist_free (categ_list); if (!disabled) { // we don't want not attending alarms // check if the user is part of attendee list if we found it check the status auto attendeeList = e_cal_component_get_attendees(component); for (GSList *attendeeIter=attendeeList; attendeeIter != nullptr; attendeeIter = attendeeIter->next) { ECalComponentAttendee *attendee = static_cast(attendeeIter->data); auto value = e_cal_component_attendee_get_value(attendee); if (value != nullptr) { if (strncmp(value, "mailto:", 7) == 0) { if (m_myself->isMyEmail(value+7)) { disabled = (e_cal_component_attendee_get_partstat(attendee) == I_CAL_PARTSTAT_DECLINED); break; } } } } if (attendeeList != nullptr) g_slist_free(attendeeList); } if (disabled) return false; return true; } static Appointment get_appointment(ECalClient * client, std::shared_ptr & cancellable, ECalComponent * component, GTimeZone * gtz) { Appointment baseline; // get appointment.uid const auto uid = e_cal_component_get_uid(component); if (uid != nullptr) baseline.uid = uid; // get source uid ESource *source = nullptr; g_object_get(G_OBJECT(client), "source", &source, nullptr); if (source != nullptr) { baseline.source_uid = e_source_get_uid(source); g_object_unref(source); } // get appointment.summary auto text = e_cal_component_get_summary(component); auto value = e_cal_component_text_get_value(text); if (value != nullptr) baseline.summary = value; // get appointment.begin auto eccdt_tmp = e_cal_component_get_dtstart(component); baseline.begin = datetime_from_component_date_time(client, cancellable, eccdt_tmp, gtz); e_cal_component_datetime_free(eccdt_tmp); // get appointment.end eccdt_tmp = e_cal_component_get_dtend(component); if (eccdt_tmp != nullptr) { auto dt_value = e_cal_component_datetime_get_value(eccdt_tmp); baseline.end = dt_value != nullptr ? datetime_from_component_date_time(client, cancellable, eccdt_tmp, gtz) : baseline.begin; } else { baseline.end = baseline.begin; } g_clear_pointer (&eccdt_tmp, e_cal_component_datetime_free); // get appointment.type baseline.type = Appointment::EVENT; auto icc = e_cal_component_get_icalcomponent(component); // icc owned by component auto categ_list = e_cal_component_get_categories_list (component); for (GSList * l=categ_list; l!=nullptr; l=l->next) { auto tag = static_cast(l->data); if (!g_strcmp0(tag, TAG_ALARM)) baseline.type = Appointment::ALARM; } g_slist_free_full(categ_list, g_free); // Get the colour - we might need this override in the future /*if (icc) { ECalComponentPropertyBag *pBag = e_cal_component_property_bag_new_from_component (icc, NULL, NULL); if (pBag) { guint nProperties = e_cal_component_property_bag_get_count (pBag); for (guint nProperty = 0; nProperty < nProperties; nProperty++) { ICalProperty *pProperty = e_cal_component_property_bag_get (pBag, nProperty); gchar *sName = i_cal_property_get_property_name (pProperty); gboolean bColour = !g_strcmp0 (sName, "COLOR"); if (bColour) { baseline.color = i_cal_property_get_value_as_string (pProperty); break; } } e_cal_component_property_bag_free (pBag); } }*/ g_debug("%s got appointment from %s to %s: %s", G_STRLOC, baseline.begin.format("%F %T %z").c_str(), baseline.end.format("%F %T %z").c_str(), i_cal_component_as_ical_string(icc) /* string owned by ical */); return baseline; } static void add_alarms_to_subtask(ECalComponentAlarms * comp_alarms, ClientSubtask * subtask, GTimeZone * gtz) { auto component = e_cal_component_alarms_get_component(comp_alarms); if (!subtask->task->p->is_component_interesting(component)) return; Appointment baseline = get_appointment(subtask->client, subtask->cancellable, component, gtz); baseline.color = subtask->color; /** *** Now loop through comp_alarms to get information that we need *** to build the instance appointments and their alarms. *** *** Outer map key is the instance component's start + end time. *** We build Appointment.begin and .end from that. *** *** inner map key is the alarm trigger, we build Alarm.time from that. *** *** inner map value is the Alarm. *** *** We map the alarms based on their trigger time so that we *** can fold together multiple valarms that trigger for the *** same componeng at the same time. This is commonplace; *** e.g. one valarm will have a display action and another *** will specify a sound to be played. */ std::map,std::map> alarms; for (auto l=e_cal_component_alarms_get_instances(comp_alarms); l!=nullptr; l=l->next) { auto ai = static_cast(l->data); auto a = e_cal_component_get_alarm(component, e_cal_component_alarm_instance_get_uid(ai)); if (!is_alarm_interesting(a)) { continue; } auto instance_time = std::make_pair(DateTime{gtz, e_cal_component_alarm_instance_get_occur_start(ai)}, DateTime{gtz, e_cal_component_alarm_instance_get_occur_end(ai)}); auto trigger_time = DateTime{gtz, e_cal_component_alarm_instance_get_time(ai)}; auto& alarm = alarms[instance_time][trigger_time]; if (alarm.text.empty()) alarm.text = get_alarm_text(a); if (alarm.audio_url.empty()) { alarm.audio_url = get_alarm_sound_url(a, (baseline.is_alarm() ? "file://" ALARM_DEFAULT_SOUND : "file://" CALENDAR_DEFAULT_SOUND)); } if (!alarm.time.is_set()) alarm.time = trigger_time; e_cal_component_alarm_free(a); } for (auto& i : alarms) { Appointment appointment = baseline; appointment.begin = i.first.first; appointment.end = i.first.second; appointment.alarms.reserve(i.second.size()); for (auto& j : i.second) { if (j.second.has_text() || j.second.has_sound()) appointment.alarms.push_back(j.second); } subtask->task->appointments.push_back(appointment); } } static void add_event_to_subtask(ECalComponent * component, ClientSubtask * subtask, GTimeZone * gtz) { // add it. simple, eh? if (subtask->task->p->is_component_interesting(component)) { Appointment appointment = get_appointment(subtask->client, subtask->cancellable, component, gtz); appointment.color = subtask->color; subtask->task->appointments.push_back(appointment); } } /*** **** ***/ static void on_object_ready_for_disable(GObject * client, GAsyncResult * result, gpointer gself) { ICalComponent * icc = nullptr; if (e_cal_client_get_object_finish (E_CAL_CLIENT(client), result, &icc, nullptr)) { auto rrule_property = i_cal_component_get_first_property (icc, I_CAL_RRULE_PROPERTY); // transfer none auto rdate_property = i_cal_component_get_first_property (icc, I_CAL_RDATE_PROPERTY); // transfer none const bool is_nonrepeating = (rrule_property == nullptr) && (rdate_property == nullptr); if (is_nonrepeating) { g_debug("'%s' appears to be a one-time alarm... adding 'disabled' tag.", i_cal_component_as_ical_string(icc)); auto ecc = e_cal_component_new_from_icalcomponent (icc); // takes ownership of icc icc = nullptr; if (ecc != nullptr) { // add TAG_DISABLED to the list of categories auto old_categories = e_cal_component_get_categories_list(ecc); auto new_categories = g_slist_copy(old_categories); new_categories = g_slist_append(new_categories, const_cast(TAG_DISABLED)); e_cal_component_set_categories_list(ecc, new_categories); g_slist_free(new_categories); g_slist_free_full (old_categories, g_free); e_cal_client_modify_object(E_CAL_CLIENT(client), e_cal_component_get_icalcomponent(ecc), E_CAL_OBJ_MOD_THIS, E_CAL_OPERATION_FLAG_NONE, static_cast(gself)->m_cancellable.get(), on_disable_done, nullptr); g_clear_object(&ecc); } } g_clear_pointer(&icc, i_cal_component_free); } } static void on_disable_done (GObject* gclient, GAsyncResult *res, gpointer) { GError * error = nullptr; if (!e_cal_client_modify_object_finish (E_CAL_CLIENT(gclient), res, &error)) { if (!g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) g_warning("ayatana-indicator-datetime cannot mark one-time alarm as disabled: %s", error->message); g_error_free(error); } } /*** **** ***/ core::Signal<> m_changed; std::set m_sources; std::map m_clients; std::map m_views; std::shared_ptr m_cancellable; ESourceRegistry* m_source_registry {}; guint m_rebuild_tag {}; time_t m_rebuild_deadline {}; std::shared_ptr m_myself; }; /*** **** ***/ EdsEngine::EdsEngine(const std::shared_ptr &myself): p(new Impl(myself)) { } EdsEngine::~EdsEngine() =default; core::Signal<>& EdsEngine::changed() { return p->changed(); } void EdsEngine::get_appointments(const DateTime& begin, const DateTime& end, const Timezone& tz, std::function&)> func) { p->get_appointments(begin, end, tz, func); } void EdsEngine::disable_alarm(const Appointment& appointment) { p->disable_alarm(appointment); } /*** **** ***/ } // namespace datetime } // namespace indicator } // namespace ayatana