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',