diff --git a/po/POTFILES.in b/po/POTFILES.in index 20f5b0b..3191bf5 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -8,6 +8,7 @@ src/calls-call-record.c src/calls-call-record-row.c src/calls-call-selector-item.c src/calls-call-window.c +src/calls-contacts-provider.c src/calls-contacts.c src/calls-encryption-indicator.c src/calls-history-box.c diff --git a/src/calls-contacts-provider.c b/src/calls-contacts-provider.c new file mode 100644 index 0000000..c1fbe34 --- /dev/null +++ b/src/calls-contacts-provider.c @@ -0,0 +1,334 @@ +/* + * Copyright (C) 2021 Purism SPC + * + * This file is part of Calls. + * + * Calls is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Calls is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY 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 Calls. If not, see . + * + * Author(s): + * Bob Ham + * Mohammed Sadiq + * Julian Sparber + * + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include +#include +#include + +#include "calls-contacts-provider.h" +#include "calls-best-match.h" + + +typedef struct +{ + GeeIterator *iter; + IdleCallback callback; + gpointer user_data; +} IdleData; + +struct _CallsContactsProvider +{ + GObject parent_instance; + + FolksIndividualAggregator *folks_aggregator; + + GHashTable *phone_number_best_matches; +}; + +G_DEFINE_TYPE (CallsContactsProvider, calls_contacts_provider, G_TYPE_OBJECT) + +enum { + SIGNAL_ADDED, + SIGNAL_REMOVED, + SIGNAL_LAST_SIGNAL, +}; +static guint signals[SIGNAL_LAST_SIGNAL]; + +static void folks_remove_contact (CallsContactsProvider *self, + FolksIndividual *individual); +static void folks_add_contact (CallsContactsProvider *self, + FolksIndividual *individual); + +static gboolean +folks_individual_has_phone_numbers (FolksIndividual *individual) +{ + g_autoptr (GeeSet) phone_numbers; + + g_object_get (individual, "phone-numbers", &phone_numbers, NULL); + + return !gee_collection_get_is_empty (GEE_COLLECTION (phone_numbers)); +} + +static void +search_view_prepare_cb (FolksSearchView *view, + GAsyncResult *res, + gpointer *user_data) +{ + g_autoptr (GError) error = NULL; + + folks_search_view_prepare_finish (view, res, &error); + + if (error) + g_warning ("Failed to prepare Folks search view: %s", error->message); +} + +static void +folks_individual_property_changed_cb (CallsContactsProvider *self, + GParamSpec *pspec, + FolksIndividual *individual) +{ + if (!folks_individual_has_phone_numbers (individual)) + folks_remove_contact (self, individual); +} + +static int +do_on_idle (IdleData *data) +{ + if (gee_iterator_next (data->iter)) { + data->callback (data->user_data, gee_iterator_get (data->iter)); + + return G_SOURCE_CONTINUE; + } else { + return G_SOURCE_REMOVE; + } +} + +static void +folks_add_contact (CallsContactsProvider *self, + FolksIndividual *individual) +{ + if (individual == NULL) + return; + + if (!folks_individual_has_phone_numbers (individual)) + return; + + g_signal_connect_object (G_OBJECT (individual), + "notify::phone-numbers", + G_CALLBACK (folks_individual_property_changed_cb), + self, G_CONNECT_SWAPPED); + + g_signal_emit (self, signals[SIGNAL_ADDED], 0, individual); +} + +static void +folks_remove_contact (CallsContactsProvider *self, + FolksIndividual *individual) +{ + if (individual == NULL) + return; + + g_signal_handlers_disconnect_by_func (individual, folks_individual_property_changed_cb, self); + g_signal_emit (self, signals[SIGNAL_REMOVED], 0, individual); +} + + +static void +folks_individuals_changed_cb (CallsContactsProvider *self, + GeeMultiMap *changes) +{ + g_autoptr (GeeCollection) removed = NULL; + g_autoptr (GeeCollection) added = NULL; + + removed = GEE_COLLECTION (gee_multi_map_get_keys (changes)); + if (!gee_collection_get_is_empty (removed)) + calls_contacts_provider_consume_iter_on_idle (gee_iterable_iterator (GEE_ITERABLE (removed)), + (IdleCallback) folks_remove_contact, + self); + + added = gee_multi_map_get_values (changes); + if (!gee_collection_get_is_empty (added)) + calls_contacts_provider_consume_iter_on_idle (gee_iterable_iterator (GEE_ITERABLE (added)), + (IdleCallback) folks_add_contact, + self); +} + +static void +folks_prepare_cb (GObject *obj, + GAsyncResult *res, + gpointer user_data) +{ + g_autoptr (GError) error = NULL; + + folks_individual_aggregator_prepare_finish (FOLKS_INDIVIDUAL_AGGREGATOR (obj), res, &error); + + if (error) + g_warning ("Failed to load Folks contacts: %s", error->message); +} + +static void +calls_contacts_provider_finalize (GObject *object) +{ + CallsContactsProvider *self = CALLS_CONTACTS_PROVIDER (object); + + g_clear_object (&self->folks_aggregator); + g_clear_pointer (&self->phone_number_best_matches, g_hash_table_unref); + + G_OBJECT_CLASS (calls_contacts_provider_parent_class)->finalize (object); +} + + +static void +calls_contacts_provider_class_init (CallsContactsProviderClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->finalize = calls_contacts_provider_finalize; + + signals[SIGNAL_ADDED] = + g_signal_new ("added", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, + 1, + FOLKS_TYPE_INDIVIDUAL); + + signals[SIGNAL_REMOVED] = + g_signal_new ("removed", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, + NULL, NULL, NULL, + G_TYPE_NONE, + 1, + FOLKS_TYPE_INDIVIDUAL); +} + + +static void +calls_contacts_provider_init (CallsContactsProvider *self) +{ + g_autoptr (GeeCollection) individuals = NULL; + self->folks_aggregator = folks_individual_aggregator_dup (); + + individuals = calls_contacts_provider_get_individuals (self); + + g_signal_connect_object (self->folks_aggregator, + "individuals-changed-detailed", + G_CALLBACK (folks_individuals_changed_cb), + self, G_CONNECT_SWAPPED); + + if (!gee_collection_get_is_empty (individuals)) + calls_contacts_provider_consume_iter_on_idle (gee_iterable_iterator (GEE_ITERABLE (individuals)), + (IdleCallback) folks_add_contact, + self); + + folks_individual_aggregator_prepare (self->folks_aggregator, folks_prepare_cb, self); + + self->phone_number_best_matches = g_hash_table_new_full (g_str_hash, + g_str_equal, + g_free, + g_object_unref); +} + +CallsContactsProvider * +calls_contacts_provider_new (void) +{ + return g_object_new (CALLS_TYPE_CONTACTS_PROVIDER, NULL); +} + + +/* + * calls_contacts_provider_get_individuals: + * @self: The #CallsContactsProvider + * + * Returns all individuals currrently loaded. + */ +GeeCollection * +calls_contacts_provider_get_individuals (CallsContactsProvider *self) +{ + g_return_val_if_fail (CALLS_IS_CONTACTS_PROVIDER (self), NULL); + + return gee_map_get_values (folks_individual_aggregator_get_individuals (self->folks_aggregator)); +} + +/* + * calls_contacts_provider_lookup_phone_number: + * @self: The #CallsContactsProvider + * @number: The phonenumber + * + * Get a best contact match for a phone number + * + * Returns: (transfer none): The best match as #CallsBestMatch + */ +CallsBestMatch * +calls_contacts_provider_lookup_phone_number (CallsContactsProvider *self, + const gchar *number) +{ + g_autoptr (CallsBestMatch) best_match = NULL; + g_autoptr (GError) error = NULL; + g_autoptr (EPhoneNumber) phone_number = NULL; + g_autoptr (CallsPhoneNumberQuery) query = NULL; + g_autoptr (CallsBestMatchView) view = NULL; + + g_return_val_if_fail (CALLS_IS_CONTACTS_PROVIDER (self), NULL); + + best_match = g_hash_table_lookup (self->phone_number_best_matches, number); + + if (best_match) { + g_object_ref (best_match); + + return g_steal_pointer (&best_match); + } + + /* FIXME: parsing the phone number can add the wrong country code if the default region + * for the app isn't set correctly. + * See https://developer.gnome.org/eds/stable/eds-e-phone-number.html#e-phone-number-get-default-region + */ + phone_number = e_phone_number_from_string (number, NULL, &error); + + if (!phone_number) { + g_warning ("Failed to convert %s to a phone number: %s", number, error->message); + return NULL; + } + + query = calls_phone_number_query_new (phone_number); + + view = calls_best_match_view_new (self->folks_aggregator, FOLKS_QUERY (query)); + + folks_search_view_prepare (FOLKS_SEARCH_VIEW (view), + (GAsyncReadyCallback) search_view_prepare_cb, + NULL); + + best_match = calls_best_match_new (view); + + if (best_match) + g_hash_table_insert (self->phone_number_best_matches, g_strdup (number), g_object_ref (best_match)); + + return g_steal_pointer (&best_match); +} + +void +calls_contacts_provider_consume_iter_on_idle (GeeIterator *iter, + IdleCallback callback, + gpointer user_data) +{ + IdleData *data = g_new (IdleData, 1); + data->iter = iter; + data->user_data = user_data; + data->callback = callback; + + g_idle_add_full (G_PRIORITY_HIGH_IDLE, + G_SOURCE_FUNC (do_on_idle), + data, + g_free); +} diff --git a/src/calls-contacts-provider.h b/src/calls-contacts-provider.h new file mode 100644 index 0000000..2cc8e09 --- /dev/null +++ b/src/calls-contacts-provider.h @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2021 Purism SPC + * + * This file is part of Calls. + * + * Calls is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Calls is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY 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 Calls. If not, see . + * + * Author(s): + * Bob Ham + * Mohammed Sadiq + * Julian Sparber + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + */ + +#pragma once + +#include +#include +#include + +#include "calls-best-match.h" + +G_BEGIN_DECLS + +G_DEFINE_AUTOPTR_CLEANUP_FUNC (GeeMap, g_object_unref) +G_DEFINE_AUTOPTR_CLEANUP_FUNC (GeeSet, g_object_unref) +G_DEFINE_AUTOPTR_CLEANUP_FUNC (GeeCollection, g_object_unref) +G_DEFINE_AUTOPTR_CLEANUP_FUNC (EPhoneNumber, e_phone_number_free) + +typedef void (*IdleCallback) (gpointer user_data, + FolksIndividual *individual); + +#define CALLS_TYPE_CONTACTS_PROVIDER (calls_contacts_provider_get_type ()) + +G_DECLARE_FINAL_TYPE (CallsContactsProvider, calls_contacts_provider, CALLS, CONTACTS_PROVIDER, GObject) + +CallsContactsProvider *calls_contacts_provider_new (void); +GeeCollection *calls_contacts_provider_get_individuals (CallsContactsProvider *self); +CallsBestMatch *calls_contacts_provider_lookup_phone_number (CallsContactsProvider *self, + const gchar *number); +void calls_contacts_provider_consume_iter_on_idle (GeeIterator *iter, + IdleCallback callback, + gpointer user_data); + +G_END_DECLS diff --git a/src/meson.build b/src/meson.build index fdefbe6..dbed8c5 100644 --- a/src/meson.build +++ b/src/meson.build @@ -85,6 +85,7 @@ calls_sources = files(['calls-message-source.c', 'calls-message-source.h', 'calls-record-store.c', 'calls-record-store.h', 'calls-call-record-row.c', 'calls-call-record-row.h', 'calls-contacts.c', 'calls-contacts.h', + 'calls-contacts-provider.c', 'calls-contacts-provider.h', 'calls-best-match.c', 'calls-best-match.h', 'calls-in-app-notification.c', 'calls-in-app-notification.h', 'calls-manager.c', 'calls-manager.h',