diff --git a/src/calls-best-match.c b/src/calls-best-match.c new file mode 100644 index 0000000..8f49420 --- /dev/null +++ b/src/calls-best-match.c @@ -0,0 +1,578 @@ +/* + * Copyright (C) 2019 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: Bob Ham + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + */ + +#include "calls-best-match.h" +#include "util.h" + +#include + + +struct _CallsBestMatch +{ + GObject parent_instance; + + CallsBestMatchView *view; + FolksIndividual *best_match; + gulong display_name_notify_handler_id; + gulong avatar_notify_handler_id; + /** All requested gint avatar sizes */ + GList *avatar_sizes; + /** GCancellables for in-progress loads */ + GList *avatar_loads; + /** Map of gint icon size to GdkPixbuf */ + GHashTable *avatars; +}; + +G_DEFINE_TYPE (CallsBestMatch, calls_best_match, G_TYPE_OBJECT); + + +enum { + PROP_0, + PROP_VIEW, + PROP_NAME, + PROP_LAST_PROP, +}; +static GParamSpec *props[PROP_LAST_PROP]; + +enum { + SIGNAL_AVATAR, + SIGNAL_LAST_SIGNAL, +}; +static guint signals [SIGNAL_LAST_SIGNAL]; + + +struct CallsAvatarRequestData +{ + CallsBestMatch *self; + GCancellable *cancellable; + gint size; +}; + + +static void +avatar_request_data_destroy (struct CallsAvatarRequestData *data) +{ + data->self->avatar_loads = + g_list_remove (data->self->avatar_loads, + data->cancellable); + + g_free (data); +} + + +inline static void +add_avatar (CallsBestMatch *self, + gint size, + GdkPixbuf *avatar) +{ + g_hash_table_insert (self->avatars, + GINT_TO_POINTER (size), + avatar); + + g_debug ("Added avatar of size %i for best match `%s'", + size, + folks_individual_get_display_name (self->best_match)); + + g_signal_emit_by_name (self, "avatar", size, avatar); +} + + +static void +request_avatar_pixbuf_new_cb (GInputStream *stream, + GAsyncResult *res, + struct CallsAvatarRequestData *data) +{ + GdkPixbuf *avatar; + GError *error = NULL; + + avatar = gdk_pixbuf_new_from_stream_finish (res, &error); + if (avatar) + { + add_avatar (data->self, data->size, avatar); + } + else + { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + { + g_warning ("Error creating GdkPixbuf from avatar" + " icon stream at size %i for Folks" + " individual `%s': %s", + data->size, + calls_best_match_get_name (data->self), + error->message); + } + g_error_free (error); + } + + avatar_request_data_destroy (data); +} + + +static void +request_avatar_icon_load_cb (GLoadableIcon *icon, + GAsyncResult *res, + struct CallsAvatarRequestData *data) +{ + GInputStream *stream; + GError *error = NULL; + + stream = g_loadable_icon_load_finish (icon, res, NULL, &error); + if (!stream) + { + if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) + { + g_warning ("Error loading avatar icon at size %i" + " for Folks individual `%s': %s", + data->size, + calls_best_match_get_name (data->self), + error->message); + } + g_error_free (error); + + avatar_request_data_destroy (data); + return; + } + + gdk_pixbuf_new_from_stream_at_scale_async + (stream, + -1, + data->size, + TRUE, + data->cancellable, + (GAsyncReadyCallback)request_avatar_pixbuf_new_cb, + data); + + g_object_unref (stream); +} + + +static void +request_avatar (CallsBestMatch *self, + gint size) +{ + GLoadableIcon *icon; + struct CallsAvatarRequestData *data; + + if (!self->best_match) + { + return; + } + + icon = folks_avatar_details_get_avatar + (FOLKS_AVATAR_DETAILS(self->best_match)); + if (!icon) + { + return; + } + + g_debug ("Requesting avatar of size %i for best match `%s'", + size, + folks_individual_get_display_name (self->best_match)); + + data = g_new (struct CallsAvatarRequestData, 1); + data->self = self; + data->size = size; + data->cancellable = g_cancellable_new (); + + self->avatar_loads = g_list_prepend + (self->avatar_loads, data->cancellable); + + g_loadable_icon_load_async + (icon, + size, + data->cancellable, + (GAsyncReadyCallback)request_avatar_icon_load_cb, + data); + + g_object_unref (data->cancellable); +} + + +static void +notify_name (CallsBestMatch *self) +{ + g_object_notify_by_pspec (G_OBJECT (self), + props[PROP_NAME]); +} + + +static void +clear_avatars (CallsBestMatch *self) +{ + GList *node; + + for (node = self->avatar_loads; node; node = node->next) + { + g_cancellable_cancel ((GCancellable *)node->data); + } + + g_list_free (self->avatar_loads); + self->avatar_loads = NULL; + + g_hash_table_remove_all (self->avatars); +} + + +static void +request_avatars (CallsBestMatch *self) +{ + GList *node; + + for (node = self->avatar_sizes; node; node = node->next) + { + request_avatar (self, GPOINTER_TO_INT (node->data)); + } +} + + +static void +change_avatar (CallsBestMatch *self) +{ + g_debug ("Avatar changed for best match `%s'", + folks_individual_get_display_name (self->best_match)); + + clear_avatars (self); + request_avatars (self); +} + + +static void +set_best_match (CallsBestMatch *self, + FolksIndividual *best_match) +{ + g_assert (self->best_match == NULL); + g_assert (self->display_name_notify_handler_id == 0); + g_assert (self->avatar_notify_handler_id == 0); + + self->best_match = best_match; + g_object_ref (best_match); + + self->display_name_notify_handler_id = + g_signal_connect_swapped (self->best_match, + "notify::display-name", + G_CALLBACK (notify_name), + self); + + self->avatar_notify_handler_id = + g_signal_connect_swapped (self->best_match, + "notify::avatar", + G_CALLBACK (change_avatar), + self); +} + + +static void +clear_best_match (CallsBestMatch *self) +{ + calls_clear_signal (self->best_match, + &self->avatar_notify_handler_id); + calls_clear_signal (self->best_match, + &self->display_name_notify_handler_id); + + g_clear_object (&self->best_match); +} + + +static void +new_best_match (CallsBestMatch *self, + FolksIndividual *best_match) +{ + set_best_match (self, best_match); + request_avatars (self); + notify_name (self); +} + + +static void +change_best_match (CallsBestMatch *self, + FolksIndividual *best_match) +{ + clear_best_match (self); + set_best_match (self, best_match); + change_avatar (self); + notify_name (self); +} + + +static void +remove_best_match (CallsBestMatch *self) +{ + GList *node; + + clear_best_match (self); + clear_avatars (self); + + // Emit empty avatars + for (node = self->avatar_sizes; node; node = node->next) + { + g_signal_emit_by_name (self, + "avatar", + GPOINTER_TO_INT (node->data), + NULL); + } + + notify_name (self); +} + + +static void +update_best_match (CallsBestMatch *self) +{ + FolksIndividual *best_match; + + g_debug ("Best match property notified"); + + best_match = calls_best_match_view_get_best_match + (self->view); + + if (best_match) + { + if (self->best_match) + { + if (self->best_match == best_match) + { + // No change + g_debug (" No best match change"); + } + else + { + // Different best match object + change_best_match (self, best_match); + g_debug (" Different best match object"); + } + } + else + { + // New best match + new_best_match (self, best_match); + g_debug (" New best match"); + } + } + else + { + if (self->best_match) + { + // Best match disappeared + remove_best_match (self); + g_debug (" Best match disappeared"); + } + else + { + // No change + g_debug (" No best match change"); + } + } +} + + +static void +set_property (GObject *object, + guint property_id, + const GValue *value, + GParamSpec *pspec) +{ + CallsBestMatch *self = CALLS_BEST_MATCH (object); + + switch (property_id) + { + case PROP_VIEW: + g_set_object (&self->view, + CALLS_BEST_MATCH_VIEW (g_value_get_object (value))); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +constructed (GObject *object) +{ + CallsBestMatch *self = CALLS_BEST_MATCH (object); + + + g_signal_connect_swapped (self->view, + "notify::best-match", + G_CALLBACK (update_best_match), + self); + + G_OBJECT_CLASS (calls_best_match_parent_class)->constructed (object); +} + + +static void +get_property (GObject *object, + guint property_id, + GValue *value, + GParamSpec *pspec) +{ + CallsBestMatch *self = CALLS_BEST_MATCH (object); + + switch (property_id) + { + case PROP_NAME: + g_value_set_string (value, + calls_best_match_get_name (self)); + break; + + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); + break; + } +} + + +static void +dispose (GObject *object) +{ + CallsBestMatch *self = CALLS_BEST_MATCH (object); + + clear_avatars (self); + + g_clear_object (&self->view); + + G_OBJECT_CLASS (calls_best_match_parent_class)->dispose (object); +} + + +static void +finalize (GObject *object) +{ + CallsBestMatch *self = CALLS_BEST_MATCH (object); + + g_list_free (self->avatar_sizes); + g_hash_table_unref (self->avatars); + + G_OBJECT_CLASS (calls_best_match_parent_class)->finalize (object); +} + + +static void +calls_best_match_class_init (CallsBestMatchClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + object_class->set_property = set_property; + object_class->constructed = constructed; + object_class->get_property = get_property; + object_class->dispose = dispose; + object_class->finalize = finalize; + + + props[PROP_VIEW] = + g_param_spec_object ("view", + _("View"), + _("The CallsBestMatchView to monitor"), + CALLS_TYPE_BEST_MATCH_VIEW, + G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY); + + props[PROP_NAME] = + g_param_spec_string ("name", + _("Name"), + _("The display name of the best match"), + NULL, + G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY); + + g_object_class_install_properties (object_class, PROP_LAST_PROP, props); + + + signals[SIGNAL_AVATAR] = + g_signal_new ("avatar", + G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, + 0, NULL, NULL, NULL, + G_TYPE_NONE, 2, + G_TYPE_INT, + GDK_TYPE_PIXBUF); +} + + +static void +calls_best_match_init (CallsBestMatch *self) +{ + self->avatars = g_hash_table_new_full + (g_direct_hash, + g_direct_equal, + NULL, + (GDestroyNotify)g_object_unref); +} + + +CallsBestMatch * +calls_best_match_new (CallsBestMatchView *view) +{ + g_return_val_if_fail (CALLS_IS_BEST_MATCH_VIEW (view), NULL); + + return g_object_new (CALLS_TYPE_BEST_MATCH, + "view", view, + NULL); +} + + +const gchar * +calls_best_match_get_name (CallsBestMatch *self) +{ + g_return_val_if_fail (CALLS_IS_BEST_MATCH (self), NULL); + + if (self->best_match) + { + return folks_individual_get_display_name (self->best_match); + } + else + { + return NULL; + } +} + + +GdkPixbuf * +calls_best_match_request_avatar (CallsBestMatch *self, + gint size) +{ + gpointer sizeptr = GINT_TO_POINTER (size); + GdkPixbuf *avatar; + + g_return_val_if_fail (CALLS_IS_BEST_MATCH (self), NULL); + + avatar = g_hash_table_lookup (self->avatars, sizeptr); + if (avatar) + { + // Already loaded + return avatar; + } + + if (!g_list_find (self->avatar_sizes, sizeptr)) + { + // Not known, do the actual request + request_avatar (self, size); + + // Add the size to the list + self->avatar_sizes = g_list_prepend + (self->avatar_sizes, sizeptr); + } + + return NULL; +} diff --git a/src/calls-best-match.h b/src/calls-best-match.h new file mode 100644 index 0000000..bba0aaa --- /dev/null +++ b/src/calls-best-match.h @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2019 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: Bob Ham + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + */ + +#ifndef CALLS_BEST_MATCH_H__ +#define CALLS_BEST_MATCH_H__ + +#include "calls-vala.h" + +#include + +G_BEGIN_DECLS + +#define CALLS_TYPE_BEST_MATCH (calls_best_match_get_type ()) + +G_DECLARE_FINAL_TYPE (CallsBestMatch, calls_best_match, CALLS, BEST_MATCH, GObject); + +CallsBestMatch *calls_best_match_new (CallsBestMatchView *view); +const gchar * calls_best_match_get_name (CallsBestMatch *self); +GdkPixbuf * calls_best_match_request_avatar (CallsBestMatch *self, + gint size); + +G_END_DECLS + +#endif /* CALLS_BEST_MATCH_H__ */ diff --git a/src/calls-call-record-row.c b/src/calls-call-record-row.c index cffab8c..d668c40 100644 --- a/src/calls-call-record-row.c +++ b/src/calls-call-record-row.c @@ -23,7 +23,7 @@ */ #include "calls-call-record-row.h" -#include "calls-vala.h" +#include "calls-best-match.h" #include "util.h" #include @@ -34,6 +34,9 @@ #include +#define AVATAR_SIZE 32 + + struct _CallsCallRecordRow { GtkOverlay parent_instance; @@ -49,9 +52,7 @@ struct _CallsCallRecordRow guint date_change_timeout; CallsContacts *contacts; - CallsBestMatchView *contact_view; - FolksIndividual *contact; - gulong contact_notify_handler_id; + CallsBestMatch *contact; CallsNewCallBox *new_call; }; @@ -323,13 +324,17 @@ setup_time (CallsCallRecordRow *self, static void -update_target (CallsCallRecordRow *self) +contact_name_cb (CallsCallRecordRow *self) { + const gchar *name = NULL; + if (self->contact) { - const gchar *name = - folks_individual_get_display_name (self->contact); + name = calls_best_match_get_name (self->contact); + } + if (name) + { gtk_label_set_text (self->target, name); } else @@ -347,80 +352,57 @@ update_target (CallsCallRecordRow *self) } -inline static void -clear_contact (CallsCallRecordRow *self) -{ - calls_clear_signal (self->contact, - &self->contact_notify_handler_id); - g_clear_object (&self->contact); -} - - -inline static void -set_contact (CallsCallRecordRow *self, - FolksIndividual *contact) -{ - self->contact = contact; - g_object_ref (contact); - - self->contact_notify_handler_id = - g_signal_connect_swapped (self->contact, - "notify::display-name", - G_CALLBACK (update_target), - self); -} - - static void -update_contact (CallsCallRecordRow *self) +set_avatar (CallsCallRecordRow *self, + GdkPixbuf *avatar) { - FolksIndividual *best_match; - - best_match = calls_best_match_view_get_best_match - (self->contact_view); - - if (best_match) + if (avatar) { - if (self->contact) - { - if (self->contact == best_match) - { - // No change - return; - } - else - { - // Different best match object - clear_contact (self); - set_contact (self, best_match); - } - } - else - { - // New best match - set_contact (self, best_match); - } + gtk_image_set_from_pixbuf (self->avatar, avatar); } else { - if (self->contact) - { - // Best match disappeared - clear_contact (self); - } - else - { - // No change - return; - } + gtk_image_set_from_icon_name (self->avatar, + "avatar-default-symbolic", + GTK_ICON_SIZE_DND); } - - update_target (self); } static void -setup_contact_view (CallsCallRecordRow *self) +contact_avatar_cb (CallsCallRecordRow *self, + gint size, + GdkPixbuf *avatar, + CallsBestMatch *contact) +{ + if (size != AVATAR_SIZE) + { + return; + } + + set_avatar (self, avatar); +} + + +static void +request_contact_avatar (CallsCallRecordRow *self) +{ + GdkPixbuf *avatar; + + if (!self->contact) + { + return; + } + + avatar = calls_best_match_request_avatar + (self->contact, AVATAR_SIZE); + + set_avatar (self, avatar); +} + + +static void +setup_contact (CallsCallRecordRow *self) { g_autofree gchar *target = NULL; EPhoneNumber *phone_number; @@ -440,28 +422,24 @@ setup_contact_view (CallsCallRecordRow *self) g_warning ("Error parsing phone number `%s': %s", target, error->message); g_error_free (error); - update_target (self); return; } - // Look up the search view - self->contact_view = calls_contacts_lookup_phone_number + // Look up the best match object + self->contact = calls_contacts_lookup_phone_number (self->contacts, phone_number); - g_assert (self->contact_view != NULL); + g_assert (self->contact != NULL); g_clear_object (&self->contacts); e_phone_number_free (phone_number); - g_object_ref (self->contact_view); - g_signal_connect_swapped (self->contact_view, - "notify::best-match", - G_CALLBACK (update_contact), + g_signal_connect_swapped (self->contact, + "notify::name", + G_CALLBACK (contact_name_cb), + self); + g_signal_connect_swapped (self->contact, + "avatar", + G_CALLBACK (contact_avatar_cb), self); - - update_contact (self); - if (!self->contact) - { - update_target (self); - } } @@ -515,7 +493,9 @@ constructed (GObject *object) calls_date_time_unref (answered); calls_date_time_unref (end); - setup_contact_view (self); + setup_contact (self); + contact_name_cb (self); + request_contact_avatar (self); obj_class->constructed (object); } @@ -549,9 +529,8 @@ dispose (GObject *object) g_clear_object (&self->new_call); + g_clear_object (&self->contact); g_clear_object (&self->contacts); - g_clear_object (&self->contact_view); - clear_contact (self); calls_clear_source (&self->date_change_timeout); calls_clear_signal (self->record, &self->answered_notify_handler_id); diff --git a/src/calls-contacts.c b/src/calls-contacts.c index e1fe73b..936cd11 100644 --- a/src/calls-contacts.c +++ b/src/calls-contacts.c @@ -32,8 +32,8 @@ struct _CallsContacts GObject parent_instance; FolksIndividualAggregator *big_pile_of_contacts; - /** Map of call target (EPhoneNumber) to CallsBestMatchView */ - GHashTable *phone_number_views; + /** Map of call target (EPhoneNumber) to CallsBestMatch */ + GHashTable *phone_number_best_matches; }; G_DEFINE_TYPE (CallsContacts, calls_contacts, G_TYPE_OBJECT); @@ -56,9 +56,12 @@ static gboolean phone_number_equal (const EPhoneNumber *a, const EPhoneNumber *b) { + EPhoneNumberMatch match = e_phone_number_compare (a, b); + return - e_phone_number_compare (a, b) - == E_PHONE_NUMBER_MATCH_EXACT; + match == E_PHONE_NUMBER_MATCH_EXACT + || + match == E_PHONE_NUMBER_MATCH_NATIONAL; } @@ -97,7 +100,7 @@ constructed (GObject *object) (GAsyncReadyCallback)prepare_cb, self); - self->phone_number_views = g_hash_table_new_full + self->phone_number_best_matches = g_hash_table_new_full ((GHashFunc)phone_number_hash, (GEqualFunc)phone_number_equal, (GDestroyNotify)e_phone_number_free, @@ -112,11 +115,8 @@ dispose (GObject *object) { CallsContacts *self = CALLS_CONTACTS (object); - if (self->phone_number_views) - { - g_hash_table_unref (self->phone_number_views); - self->phone_number_views = NULL; - } + g_clear_pointer (&self->phone_number_best_matches, + g_hash_table_unref); g_clear_object (&self->big_pile_of_contacts); @@ -165,24 +165,21 @@ search_view_prepare_cb (FolksSearchView *view, } -CallsBestMatchView * +CallsBestMatch * calls_contacts_lookup_phone_number (CallsContacts *self, EPhoneNumber *number) { - CallsBestMatchView *view; + CallsBestMatch *best_match; CallsPhoneNumberQuery *query; + CallsBestMatchView *view; - view = g_hash_table_lookup (self->phone_number_views, number); - if (view) + best_match = g_hash_table_lookup (self->phone_number_best_matches, number); + if (best_match) { - return view; + return best_match; } query = calls_phone_number_query_new (number); - if (!query) - { - return NULL; - } view = calls_best_match_view_new (self->big_pile_of_contacts, FOLKS_QUERY (query)); @@ -193,9 +190,13 @@ calls_contacts_lookup_phone_number (CallsContacts *self, (GAsyncReadyCallback)search_view_prepare_cb, self); - g_hash_table_insert (self->phone_number_views, - e_phone_number_copy (number), - view); + best_match = calls_best_match_new (view); + g_assert (best_match != NULL); + g_object_unref (view); - return view; + g_hash_table_insert (self->phone_number_best_matches, + e_phone_number_copy (number), + best_match); + + return best_match; } diff --git a/src/calls-contacts.h b/src/calls-contacts.h index 57fe837..e3081ff 100644 --- a/src/calls-contacts.h +++ b/src/calls-contacts.h @@ -25,7 +25,7 @@ #ifndef CALLS_CONTACTS_H__ #define CALLS_CONTACTS_H__ -#include "calls-vala.h" +#include "calls-best-match.h" #include #include @@ -38,7 +38,7 @@ G_BEGIN_DECLS G_DECLARE_FINAL_TYPE (CallsContacts, calls_contacts, CALLS, CONTACTS, GObject); CallsContacts * calls_contacts_new (); -CallsBestMatchView * calls_contacts_lookup_phone_number (CallsContacts *self, +CallsBestMatch * calls_contacts_lookup_phone_number (CallsContacts *self, EPhoneNumber *number); G_END_DECLS diff --git a/src/meson.build b/src/meson.build index 192af16..24d09f5 100644 --- a/src/meson.build +++ b/src/meson.build @@ -87,6 +87,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-best-match.c', 'calls-best-match.h', ]) calls_config_data = config_data diff --git a/src/util.c b/src/util.c index 994fadf..32f5e57 100644 --- a/src/util.c +++ b/src/util.c @@ -24,6 +24,17 @@ #include "util.h" + +void +calls_object_unref (gpointer object) +{ + if (object) + { + g_object_unref (object); + } +} + + typedef struct { gpointer needle; diff --git a/src/util.h b/src/util.h index 544b81c..a0692e9 100644 --- a/src/util.h +++ b/src/util.h @@ -97,6 +97,10 @@ G_BEGIN_DECLS } +/** If the GObject object is non-NULL, unref it */ +void calls_object_unref (gpointer object); + + /** Find a particular pointer value in a GtkListStore */ gboolean calls_list_store_find (GtkListStore *store,