/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- * * Copyright (C) 2020 Canonical Ltd. * * This program 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 2 of the License, or * (at your option) any later version. * * This program 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 this program; if not, see . * * SPDX-License-Identifier: GPL-3.0-or-later * * Authors: Marco Trevisan */ #include #include #include "cc-fingerprint-dialog.h" #include "cc-fingerprint-manager.h" #include "cc-fprintd-generated.h" #include "cc-list-row.h" #include "config.h" #define CC_FPRINTD_NAME "net.reactivated.Fprint" /* Translate fprintd strings */ #define TR(s) dgettext ("fprintd", s) #include "fingerprint-strings.h" typedef enum { DIALOG_STATE_NONE = 0, DIALOG_STATE_DEVICES_LISTING = (1 << 0), DIALOG_STATE_DEVICE_CLAIMING = (1 << 1), DIALOG_STATE_DEVICE_CLAIMED = (1 << 2), DIALOG_STATE_DEVICE_PRINTS_LISTING = (1 << 3), DIALOG_STATE_DEVICE_RELEASING = (1 << 4), DIALOG_STATE_DEVICE_ENROLL_STARTING = (1 << 5), DIALOG_STATE_DEVICE_ENROLLING = (1 << 6), DIALOG_STATE_DEVICE_ENROLL_STOPPING = (1 << 7), DIALOG_STATE_DEVICE_DELETING = (1 << 8), DIALOG_STATE_IDLE = DIALOG_STATE_DEVICE_CLAIMED | DIALOG_STATE_DEVICE_ENROLLING, } DialogState; struct _CcFingerprintDialog { GtkWindow parent_instance; GtkButton *back_button; GtkButton *cancel_button; GtkButton *delete_prints_button; GtkButton *done_button; GtkBox *add_print_popover_box; GtkEntry *enroll_print_entry; GtkFlowBox *prints_gallery; GtkHeaderBar *titlebar; GtkImage *enroll_result_image; GtkLabel *enroll_message; GtkLabel *enroll_result_message; GtkLabel *infobar_error; GtkLabel *title; GtkListBox *devices_list; GtkPopover *add_print_popover; GtkSpinner *spinner; GtkStack *stack; GtkWidget *add_print_icon; GtkWidget *delete_confirmation_infobar; GtkWidget *device_selector; GtkWidget *enroll_print_bin; GtkWidget *enroll_result_icon; GtkWidget *enrollment_view; GtkWidget *error_infobar; GtkWidget *no_devices_found; GtkWidget *prints_manager; CcFingerprintManager *manager; DialogState dialog_state; CcFprintdDevice *device; gulong device_signal_id; gulong device_name_owner_id; GCancellable *cancellable; GStrv enrolled_fingers; guint enroll_stages_passed; guint enroll_stage_passed_id; gdouble enroll_progress; }; /* TODO - fprintd and API changes required: - Identify the finger when the enroll dialog is visible + Only if device supports identification · And only in such case support enrolling more than one finger - Delete a single fingerprint | and remove the "Delete all" button - Highlight the finger when the sensor is touched during enrollment - Add customized labels to fingerprints - Devices hotplug (object manager) */ G_DEFINE_TYPE (CcFingerprintDialog, cc_fingerprint_dialog, GTK_TYPE_WINDOW) enum { PROP_0, PROP_MANAGER, N_PROPS }; #define N_VALID_FINGERS G_N_ELEMENTS (FINGER_IDS) - 1 /* The order of the fingers here will affect the UI order */ const char * FINGER_IDS[] = { "right-index-finger", "left-index-finger", "right-thumb", "right-middle-finger", "right-ring-finger", "right-little-finger", "left-thumb", "left-middle-finger", "left-ring-finger", "left-little-finger", "any", }; typedef enum { ENROLL_STATE_NORMAL, ENROLL_STATE_RETRY, ENROLL_STATE_SUCCESS, ENROLL_STATE_WARNING, ENROLL_STATE_ERROR, ENROLL_STATE_COMPLETED, N_ENROLL_STATES, } EnrollState; const char * ENROLL_STATE_CLASSES[N_ENROLL_STATES] = { "", "retry", "success", "warning", "error", "completed", }; static GParamSpec *properties[N_PROPS]; CcFingerprintDialog * cc_fingerprint_dialog_new (CcFingerprintManager *manager) { return g_object_new (CC_TYPE_FINGERPRINT_DIALOG, "fingerprint-manager", manager, NULL); } static gboolean update_dialog_state (CcFingerprintDialog *self, DialogState state) { if (self->dialog_state == state) return FALSE; self->dialog_state = state; if (self->dialog_state == DIALOG_STATE_NONE || self->dialog_state == (self->dialog_state & DIALOG_STATE_IDLE)) { gtk_spinner_stop (self->spinner); } else { gtk_spinner_start (self->spinner); } return TRUE; } static gboolean add_dialog_state (CcFingerprintDialog *self, DialogState state) { return update_dialog_state (self, (self->dialog_state | state)); } static gboolean remove_dialog_state (CcFingerprintDialog *self, DialogState state) { return update_dialog_state (self, (self->dialog_state & ~state)); } typedef struct { CcFingerprintDialog *dialog; DialogState state; } DialogStateRemover; static DialogStateRemover * auto_state_remover (CcFingerprintDialog *self, DialogState state) { DialogStateRemover *state_remover; state_remover = g_new0 (DialogStateRemover, 1); state_remover->dialog = g_object_ref (self); state_remover->state = state; return state_remover; } static void auto_state_remover_cleanup (DialogStateRemover *state_remover) { remove_dialog_state (state_remover->dialog, state_remover->state); g_clear_object (&state_remover->dialog); g_free (state_remover); } G_DEFINE_AUTOPTR_CLEANUP_FUNC (DialogStateRemover, auto_state_remover_cleanup); static const char * dbus_error_to_human (CcFingerprintDialog *self, GError *error) { g_autofree char *dbus_error = g_dbus_error_get_remote_error (error); if (dbus_error == NULL) { /* Fallback to generic */ } else if (g_str_equal (dbus_error, CC_FPRINTD_NAME ".Error.ClaimDevice")) return _("the device needs to be claimed to perform this action"); else if (g_str_equal (dbus_error, CC_FPRINTD_NAME ".Error.AlreadyInUse")) return _("the device is already claimed by another process"); else if (g_str_equal (dbus_error, CC_FPRINTD_NAME ".Error.PermissionDenied")) return _("you do not have permission to perform the action"); else if (g_str_equal (dbus_error, CC_FPRINTD_NAME ".Error.NoEnrolledPrints")) return _("no prints have been enrolled"); else if (g_str_equal (dbus_error, CC_FPRINTD_NAME ".Error.NoActionInProgress")) { /* Fallback to generic */ } else if (g_str_equal (dbus_error, CC_FPRINTD_NAME ".Error.InvalidFingername")) { /* Fallback to generic */ } else if (g_str_equal (dbus_error, CC_FPRINTD_NAME ".Error.Internal")) { /* Fallback to generic */ } if (self->dialog_state & DIALOG_STATE_DEVICE_ENROLLING) return _("Failed to communicate with the device during enrollment"); if (self->dialog_state & DIALOG_STATE_DEVICE_CLAIMED || self->dialog_state & DIALOG_STATE_DEVICE_CLAIMING) return _("Failed to communicate with the fingerprint reader"); return _("Failed to communicate with the fingerprint daemon"); } static void disconnect_device_signals (CcFingerprintDialog *self) { if (!self->device) return; if (self->device_signal_id) { g_signal_handler_disconnect (self->device, self->device_signal_id); self->device_signal_id = 0; } if (self->device_name_owner_id) { g_signal_handler_disconnect (self->device, self->device_name_owner_id); self->device_name_owner_id = 0; } } static void cc_fingerprint_dialog_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { CcFingerprintDialog *self = CC_FINGERPRINT_DIALOG (object); switch (prop_id) { case PROP_MANAGER: g_value_set_object (value, self->manager); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); } } static void cc_fingerprint_dialog_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { CcFingerprintDialog *self = CC_FINGERPRINT_DIALOG (object); switch (prop_id) { case PROP_MANAGER: g_set_object (&self->manager, g_value_get_object (value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); } } static void notify_error (CcFingerprintDialog *self, const char *error_message) { if (error_message) gtk_label_set_label (self->infobar_error, error_message); gtk_widget_set_visible (self->error_infobar, error_message != NULL); } static GtkWidget * fingerprint_icon_new (const char *icon_name, const char *label_text, GType icon_widget_type, gpointer progress_data, GtkWidget **out_icon, GtkWidget **out_label) { GtkStyleContext *context; GtkWidget *box; GtkWidget *label; GtkWidget *image; GtkWidget *icon_widget; g_return_val_if_fail (g_type_is_a (icon_widget_type, GTK_TYPE_WIDGET), NULL); box = gtk_box_new (GTK_ORIENTATION_VERTICAL, 10); gtk_widget_set_name (box, "fingerprint-box"); gtk_widget_set_hexpand (box, TRUE); image = gtk_image_new_from_icon_name (icon_name); if (icon_widget_type == GTK_TYPE_IMAGE) icon_widget = image; else icon_widget = g_object_new (icon_widget_type, NULL); if (g_type_is_a (icon_widget_type, GTK_TYPE_MENU_BUTTON)) { gtk_menu_button_set_child (GTK_MENU_BUTTON (icon_widget), image); gtk_widget_set_can_focus (icon_widget, FALSE); } gtk_widget_set_halign (icon_widget, GTK_ALIGN_CENTER); gtk_widget_set_valign (icon_widget, GTK_ALIGN_CENTER); gtk_widget_set_name (icon_widget, "fingerprint-image"); gtk_box_append (GTK_BOX (box), icon_widget); context = gtk_widget_get_style_context (icon_widget); gtk_style_context_add_class (context, "circular"); label = gtk_label_new_with_mnemonic (label_text); gtk_box_append (GTK_BOX (box), label); context = gtk_widget_get_style_context (box); gtk_style_context_add_class (context, "fingerprint-icon"); if (out_icon) *out_icon = icon_widget; if (out_label) *out_label = label; return box; } static GtkWidget * fingerprint_menu_button (const char *icon_name, const char *label_text) { GtkWidget *flowbox_child; GtkWidget *button; GtkWidget *label; GtkWidget *box; box = fingerprint_icon_new (icon_name, label_text, GTK_TYPE_MENU_BUTTON, NULL, &button, &label); flowbox_child = gtk_flow_box_child_new (); gtk_widget_set_focus_on_click (flowbox_child, FALSE); gtk_widget_set_name (flowbox_child, "fingerprint-flowbox"); gtk_flow_box_child_set_child (GTK_FLOW_BOX_CHILD (flowbox_child), box); g_object_set_data (G_OBJECT (flowbox_child), "button", button); g_object_set_data (G_OBJECT (flowbox_child), "icon", GTK_IMAGE (gtk_menu_button_get_child (GTK_MENU_BUTTON (button)))); g_object_set_data (G_OBJECT (flowbox_child), "label", label); g_object_set_data (G_OBJECT (button), "flowbox-child", flowbox_child); return flowbox_child; } static gboolean prints_visibility_filter (GtkFlowBoxChild *child, gpointer user_data) { CcFingerprintDialog *self = user_data; const char *finger_id; if (gtk_stack_get_visible_child (self->stack) != self->prints_manager) return FALSE; finger_id = g_object_get_data (G_OBJECT (child), "finger-id"); if (!finger_id) return TRUE; if (!self->enrolled_fingers) return FALSE; return g_strv_contains ((const gchar **) self->enrolled_fingers, finger_id); } static GList * get_container_children (GtkWidget *container) { GtkWidget *child; GList *list = NULL; child = gtk_widget_get_first_child (container); while (child) { GtkWidget *next = gtk_widget_get_next_sibling (child); list = g_list_append (list, child); child = next; } return list; } static void update_prints_to_add_visibility (CcFingerprintDialog *self) { g_autoptr(GList) print_buttons = NULL; GList *l; guint i; print_buttons = get_container_children (GTK_WIDGET (self->add_print_popover_box)); for (i = 0, l = print_buttons; i < N_VALID_FINGERS && l; ++i, l = l->next) { GtkWidget *button = l->data; gboolean enrolled; enrolled = self->enrolled_fingers && g_strv_contains ((const gchar **) self->enrolled_fingers, FINGER_IDS[i]); gtk_widget_set_visible (button, !enrolled); } } static void update_prints_visibility (CcFingerprintDialog *self) { update_prints_to_add_visibility (self); gtk_flow_box_invalidate_filter (self->prints_gallery); } static void list_enrolled_cb (GObject *object, GAsyncResult *res, gpointer user_data) { g_auto(GStrv) enrolled_fingers = NULL; g_autoptr(GError) error = NULL; g_autoptr(DialogStateRemover) state_remover = NULL; CcFprintdDevice *fprintd_device = CC_FPRINTD_DEVICE (object); CcFingerprintDialog *self = user_data; guint n_enrolled_fingers = 0; cc_fprintd_device_call_list_enrolled_fingers_finish (fprintd_device, &enrolled_fingers, res, &error); if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) return; state_remover = auto_state_remover (self, DIALOG_STATE_DEVICE_PRINTS_LISTING); gtk_widget_set_sensitive (GTK_WIDGET (self->add_print_icon), TRUE); if (self->dialog_state & DIALOG_STATE_DEVICE_CLAIMED) gtk_widget_set_sensitive (GTK_WIDGET (self->prints_manager), TRUE); if (error) { g_autofree char *dbus_error = g_dbus_error_get_remote_error (error); if (!dbus_error || !g_str_equal (dbus_error, CC_FPRINTD_NAME ".Error.NoEnrolledPrints")) { g_autofree char *error_message = NULL; error_message = g_strdup_printf (_("Failed to list fingerprints: %s"), dbus_error_to_human (self, error)); g_warning ("Listing of fingerprints on device %s failed: %s", cc_fprintd_device_get_name (self->device), error->message); notify_error (self, error_message); return; } } else { n_enrolled_fingers = g_strv_length (enrolled_fingers); } self->enrolled_fingers = g_steal_pointer (&enrolled_fingers); gtk_flow_box_set_max_children_per_line (self->prints_gallery, MIN (3, n_enrolled_fingers + 1)); update_prints_visibility (self); if (n_enrolled_fingers == N_VALID_FINGERS) gtk_widget_set_sensitive (self->add_print_icon, FALSE); if (n_enrolled_fingers > 0) gtk_widget_show (GTK_WIDGET (self->delete_prints_button)); } static void update_prints_store (CcFingerprintDialog *self) { ActUser *user; g_assert_true (CC_FPRINTD_IS_DEVICE (self->device)); if (!add_dialog_state (self, DIALOG_STATE_DEVICE_PRINTS_LISTING)) return; gtk_widget_set_sensitive (GTK_WIDGET (self->add_print_icon), FALSE); gtk_widget_hide (GTK_WIDGET (self->delete_prints_button)); g_clear_pointer (&self->enrolled_fingers, g_strfreev); user = cc_fingerprint_manager_get_user (self->manager); cc_fprintd_device_call_list_enrolled_fingers (self->device, act_user_get_user_name (user), self->cancellable, list_enrolled_cb, self); } static void delete_prints_cb (GObject *object, GAsyncResult *res, gpointer user_data) { g_autoptr(GError) error = NULL; CcFprintdDevice *fprintd_device = CC_FPRINTD_DEVICE (object); CcFingerprintDialog *self = user_data; cc_fprintd_device_call_delete_enrolled_fingers2_finish (fprintd_device, res, &error); if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) return; if (error) { g_autofree char *error_message = NULL; error_message = g_strdup_printf (_("Failed to delete saved fingerprints: %s"), dbus_error_to_human (self, error)); g_warning ("Deletion of fingerprints on device %s failed: %s", cc_fprintd_device_get_name (self->device), error->message); notify_error (self, error_message); } update_prints_store (self); cc_fingerprint_manager_update_state (self->manager, NULL, NULL); } static void delete_enrolled_prints (CcFingerprintDialog *self) { g_return_if_fail (self->dialog_state & DIALOG_STATE_DEVICE_CLAIMED); if (!add_dialog_state (self, DIALOG_STATE_DEVICE_DELETING)) return; gtk_widget_set_sensitive (GTK_WIDGET (self->prints_manager), FALSE); cc_fprintd_device_call_delete_enrolled_fingers2 (self->device, self->cancellable, delete_prints_cb, self); } static const char * get_finger_name (const char *finger_id) { if (g_str_equal (finger_id, "left-thumb")) return _("Left thumb"); if (g_str_equal (finger_id, "left-middle-finger")) return _("Left middle finger"); if (g_str_equal (finger_id, "left-index-finger")) return _("_Left index finger"); if (g_str_equal (finger_id, "left-ring-finger")) return _("Left ring finger"); if (g_str_equal (finger_id, "left-little-finger")) return _("Left little finger"); if (g_str_equal (finger_id, "right-thumb")) return _("Right thumb"); if (g_str_equal (finger_id, "right-middle-finger")) return _("Right middle finger"); if (g_str_equal (finger_id, "right-index-finger")) return _("_Right index finger"); if (g_str_equal (finger_id, "right-ring-finger")) return _("Right ring finger"); if (g_str_equal (finger_id, "right-little-finger")) return _("Right little finger"); g_return_val_if_reached (_("Unknown Finger")); } static gboolean have_multiple_devices (CcFingerprintDialog *self) { g_autoptr(GList) devices_rows = NULL; devices_rows = get_container_children (GTK_WIDGET (self->devices_list)); return devices_rows && devices_rows->next; } static void set_enroll_result_message (CcFingerprintDialog *self, EnrollState enroll_state, const char *message) { GtkStyleContext *style_context; const char *icon_name; guint i; g_return_if_fail (enroll_state >= 0 && enroll_state < N_ENROLL_STATES); style_context = gtk_widget_get_style_context (self->enroll_result_icon); switch (enroll_state) { case ENROLL_STATE_WARNING: case ENROLL_STATE_ERROR: icon_name = "fingerprint-detection-warning-symbolic"; break; case ENROLL_STATE_COMPLETED: icon_name = "fingerprint-detection-complete-symbolic"; break; default: icon_name = "fingerprint-detection-symbolic"; } for (i = 0; i < N_ENROLL_STATES; ++i) gtk_style_context_remove_class (style_context, ENROLL_STATE_CLASSES[i]); gtk_style_context_add_class (style_context, ENROLL_STATE_CLASSES[enroll_state]); gtk_image_set_from_icon_name (self->enroll_result_image, icon_name); gtk_label_set_label (self->enroll_result_message, message); } static gboolean stage_passed_timeout_cb (gpointer user_data) { CcFingerprintDialog *self = user_data; const char *current_message; current_message = gtk_label_get_label (self->enroll_result_message); set_enroll_result_message (self, ENROLL_STATE_NORMAL, current_message); self->enroll_stage_passed_id = 0; return FALSE; } static void handle_enroll_signal (CcFingerprintDialog *self, const char *result, gboolean done) { gboolean completed; g_return_if_fail (self->dialog_state & DIALOG_STATE_DEVICE_ENROLLING); g_debug ("Device enroll result message: %s, done: %d", result, done); completed = g_str_equal (result, "enroll-completed"); g_clear_handle_id (&self->enroll_stage_passed_id, g_source_remove); if (g_str_equal (result, "enroll-stage-passed") || completed) { guint enroll_stages; enroll_stages = cc_fprintd_device_get_num_enroll_stages (self->device); self->enroll_stages_passed++; if (enroll_stages > 0) self->enroll_progress = MIN (1.0f, self->enroll_stages_passed / (double) enroll_stages); else g_warning ("The device %s requires an invalid number of enroll stages (%u)", cc_fprintd_device_get_name (self->device), enroll_stages); g_debug ("Enroll state passed, %u/%u (%.2f%%)", self->enroll_stages_passed, (guint) enroll_stages, self->enroll_progress); if (!completed) { set_enroll_result_message (self, ENROLL_STATE_SUCCESS, NULL); self->enroll_stage_passed_id = g_timeout_add (750, stage_passed_timeout_cb, self); } else { if (!G_APPROX_VALUE (self->enroll_progress, 1.0f, FLT_EPSILON)) { g_warning ("Device marked enroll as completed, but progress is at %.2f", self->enroll_progress); self->enroll_progress = 1.0f; } } } else if (!done) { const char *scan_type; const char *message; gboolean is_swipe; scan_type = cc_fprintd_device_get_scan_type (self->device); is_swipe = g_str_equal (scan_type, "swipe"); message = enroll_result_str_to_msg (result, is_swipe); set_enroll_result_message (self, ENROLL_STATE_RETRY, message); self->enroll_stage_passed_id = g_timeout_add (850, stage_passed_timeout_cb, self); } if (done) { if (completed) { /* TRANSLATORS: This is the message shown when the fingerprint * enrollment has been completed successfully */ set_enroll_result_message (self, ENROLL_STATE_COMPLETED, C_("Fingerprint enroll state", "Complete")); gtk_widget_set_sensitive (GTK_WIDGET (self->cancel_button), FALSE); gtk_widget_set_sensitive (GTK_WIDGET (self->done_button), TRUE); gtk_widget_grab_focus (GTK_WIDGET (self->done_button)); } else { const char *message; if (g_str_equal (result, "enroll-disconnected")) { message = _("Fingerprint device disconnected"); remove_dialog_state (self, DIALOG_STATE_DEVICE_CLAIMED | DIALOG_STATE_DEVICE_ENROLLING); } else if (g_str_equal (result, "enroll-data-full")) { message = _("Fingerprint device storage is full"); } else { message = _("Failed to enroll new fingerprint"); } set_enroll_result_message (self, ENROLL_STATE_WARNING, message); } } } static void enroll_start_cb (GObject *object, GAsyncResult *res, gpointer user_data) { g_autoptr(GError) error = NULL; g_autoptr(DialogStateRemover) state_remover = NULL; CcFprintdDevice *fprintd_device = CC_FPRINTD_DEVICE (object); CcFingerprintDialog *self = user_data; cc_fprintd_device_call_enroll_start_finish (fprintd_device, res, &error); if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) return; state_remover = auto_state_remover (self, DIALOG_STATE_DEVICE_ENROLL_STARTING); if (error) { g_autofree char *error_message = NULL; remove_dialog_state (self, DIALOG_STATE_DEVICE_ENROLLING); error_message = g_strdup_printf (_("Failed to start enrollment: %s"), dbus_error_to_human (self, error)); g_warning ("Enrollment on device %s failed: %s", cc_fprintd_device_get_name (self->device), error->message); notify_error (self, error_message); set_enroll_result_message (self, ENROLL_STATE_ERROR, C_("Fingerprint enroll state", "Failed to enroll new fingerprint")); gtk_widget_set_sensitive (self->enrollment_view, FALSE); return; } } static void enroll_stop_cb (GObject *object, GAsyncResult *res, gpointer user_data) { g_autoptr(GError) error = NULL; g_autoptr(DialogStateRemover) state_remover = NULL; CcFprintdDevice *fprintd_device = CC_FPRINTD_DEVICE (object); CcFingerprintDialog *self = user_data; cc_fprintd_device_call_enroll_stop_finish (fprintd_device, res, &error); if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) return; state_remover = auto_state_remover (self, DIALOG_STATE_DEVICE_ENROLLING | DIALOG_STATE_DEVICE_ENROLL_STOPPING); gtk_widget_set_sensitive (self->enrollment_view, TRUE); gtk_stack_set_visible_child (self->stack, self->prints_manager); if (error) { g_autofree char *error_message = NULL; error_message = g_strdup_printf (_("Failed to stop enrollment: %s"), dbus_error_to_human (self, error)); g_warning ("Stopping enrollment on device %s failed: %s", cc_fprintd_device_get_name (self->device), error->message); notify_error (self, error_message); return; } cc_fingerprint_manager_update_state (self->manager, NULL, NULL); } static void enroll_stop (CcFingerprintDialog *self) { g_return_if_fail (self->dialog_state & DIALOG_STATE_DEVICE_ENROLLING); if (!add_dialog_state (self, DIALOG_STATE_DEVICE_ENROLL_STOPPING)) return; gtk_widget_set_sensitive (self->enrollment_view, FALSE); cc_fprintd_device_call_enroll_stop (self->device, self->cancellable, enroll_stop_cb, self); } static char * get_enrollment_string (CcFingerprintDialog *self, const char *finger_id) { char *ret; const char *scan_type; const char *device_name; gboolean is_swipe; device_name = NULL; scan_type = cc_fprintd_device_get_scan_type (self->device); is_swipe = g_str_equal (scan_type, "swipe"); if (have_multiple_devices (self)) device_name = cc_fprintd_device_get_name (self->device); ret = finger_str_to_msg (finger_id, device_name, is_swipe); if (ret) return ret; return g_strdup (_("Repeatedly lift and place your finger on the reader to enroll your fingerprint")); } static void enroll_finger (CcFingerprintDialog *self, const char *finger_id) { g_auto(GStrv) tmp_finger_name = NULL; g_autofree char *finger_name = NULL; g_autofree char *enroll_message = NULL; g_return_if_fail (finger_id); if (!add_dialog_state (self, DIALOG_STATE_DEVICE_ENROLLING | DIALOG_STATE_DEVICE_ENROLL_STARTING)) return; self->enroll_progress = 0; self->enroll_stages_passed = 0; g_debug ("Enrolling finger %s", finger_id); enroll_message = get_enrollment_string (self, finger_id); tmp_finger_name = g_strsplit (get_finger_name (finger_id), "_", -1); finger_name = g_strjoinv ("", tmp_finger_name); set_enroll_result_message (self, ENROLL_STATE_NORMAL, NULL); gtk_stack_set_visible_child (self->stack, self->enrollment_view); gtk_label_set_label (self->enroll_message, enroll_message); gtk_editable_set_text (GTK_EDITABLE (self->enroll_print_entry), finger_name); cc_fprintd_device_call_enroll_start (self->device, finger_id, self->cancellable, enroll_start_cb, self); } static void populate_enrollment_view (CcFingerprintDialog *self) { GtkStyleContext *style_context; self->enroll_result_icon = fingerprint_icon_new ("fingerprint-detection-symbolic", NULL, GTK_TYPE_IMAGE, &self->enroll_progress, (GtkWidget **) &self->enroll_result_image, (GtkWidget **) &self->enroll_result_message); gtk_box_prepend (GTK_BOX (self->enroll_print_bin), self->enroll_result_icon); style_context = gtk_widget_get_style_context (self->enroll_result_icon); gtk_style_context_add_class (style_context, "enroll-status"); } static void on_print_activated_cb (GtkFlowBox *flowbox, GtkFlowBoxChild *child, CcFingerprintDialog *self) { GtkWidget *selected_button; selected_button = g_object_get_data (G_OBJECT (child), "button"); g_signal_emit_by_name (GTK_MENU_BUTTON (selected_button), "activate"); } static void on_enroll_cb (CcFingerprintDialog *self, GtkMenuButton *button) { const char *finger_id; finger_id = g_object_get_data (G_OBJECT (button), "finger-id"); enroll_finger (self, finger_id); } static void populate_add_print_popover (CcFingerprintDialog *self) { guint i; for (i = 0; i < N_VALID_FINGERS; ++i) { GtkWidget *finger_item; finger_item = gtk_button_new (); gtk_button_set_label (GTK_BUTTON (finger_item), get_finger_name (FINGER_IDS[i])); gtk_button_set_use_underline (GTK_BUTTON (finger_item), TRUE); g_object_set_data (G_OBJECT (finger_item), "finger-id", (gpointer) FINGER_IDS[i]); gtk_box_prepend (GTK_BOX (self->add_print_popover_box), finger_item); g_signal_connect_object (finger_item, "clicked", G_CALLBACK (on_enroll_cb), self, G_CONNECT_SWAPPED); } } static void populate_prints_gallery (CcFingerprintDialog *self) { const char *add_print_label; GtkWidget *button; GtkStyleContext *style_context; guint i; g_return_if_fail (!GTK_IS_WIDGET (self->add_print_icon)); for (i = 0; i < N_VALID_FINGERS; ++i) { GtkWidget *flowbox_child; GtkWidget *popover; GtkWidget *reenroll_button; flowbox_child = fingerprint_menu_button ("fingerprint-detection-symbolic", get_finger_name (FINGER_IDS[i])); button = g_object_get_data (G_OBJECT (flowbox_child), "button"); popover = gtk_popover_new (); reenroll_button = gtk_button_new (); gtk_button_set_use_underline (GTK_BUTTON (reenroll_button), TRUE); gtk_button_set_label (GTK_BUTTON (reenroll_button), _("_Re-enroll this finger…")); g_object_set_data (G_OBJECT (reenroll_button), "finger-id", (gpointer) FINGER_IDS[i]); g_signal_connect_object (reenroll_button, "clicked", G_CALLBACK (on_enroll_cb), self, G_CONNECT_SWAPPED); gtk_popover_set_child (GTK_POPOVER (popover), reenroll_button); gtk_menu_button_set_popover (GTK_MENU_BUTTON (button), popover); g_object_set_data (G_OBJECT (flowbox_child), "finger-id", (gpointer) FINGER_IDS[i]); gtk_flow_box_insert (self->prints_gallery, flowbox_child, i); } /* TRANSLATORS: This is the label for the button to enroll a new finger */ add_print_label = _("Scan new fingerprint"); self->add_print_icon = fingerprint_menu_button ("list-add-symbolic", add_print_label); style_context = gtk_widget_get_style_context (self->add_print_icon); gtk_style_context_add_class (style_context, "fingerprint-print-add"); populate_add_print_popover (self); button = g_object_get_data (G_OBJECT (self->add_print_icon), "button"); gtk_menu_button_set_popover (GTK_MENU_BUTTON (button), GTK_WIDGET (self->add_print_popover)); gtk_flow_box_insert (self->prints_gallery, self->add_print_icon, -1); gtk_flow_box_set_max_children_per_line (self->prints_gallery, 1); gtk_flow_box_set_filter_func (self->prints_gallery, prints_visibility_filter, self, NULL); update_prints_visibility (self); } static void release_device_cb (GObject *object, GAsyncResult *res, gpointer user_data) { g_autoptr(GError) error = NULL; CcFprintdDevice *fprintd_device = CC_FPRINTD_DEVICE (object); CcFingerprintDialog *self = user_data; cc_fprintd_device_call_release_finish (fprintd_device, res, &error); if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) return; if (error) { g_autofree char *error_message = NULL; error_message = g_strdup_printf (_("Failed to release fingerprint device %s: %s"), cc_fprintd_device_get_name (fprintd_device), dbus_error_to_human (self, error)); g_warning ("Releasing device %s failed: %s", cc_fprintd_device_get_name (self->device), error->message); notify_error (self, error_message); return; } remove_dialog_state (self, DIALOG_STATE_DEVICE_CLAIMED); } static void release_device (CcFingerprintDialog *self) { if (!self->device || !(self->dialog_state & DIALOG_STATE_DEVICE_CLAIMED)) return; disconnect_device_signals (self); cc_fprintd_device_call_release (self->device, self->cancellable, release_device_cb, self); } static void on_device_signal (CcFingerprintDialog *self, gchar *sender_name, gchar *signal_name, GVariant *parameters, gpointer user_data) { if (g_str_equal (signal_name, "EnrollStatus")) { const char *result; gboolean done; if (!g_variant_is_of_type (parameters, G_VARIANT_TYPE ("(sb)"))) { g_warning ("Unexpected enroll parameters type %s", g_variant_get_type_string (parameters)); return; } g_variant_get (parameters, "(&sb)", &result, &done); handle_enroll_signal (self, result, done); } } static void claim_device (CcFingerprintDialog *self); static void on_device_owner_changed (CcFprintdDevice *device, GParamSpec *spec, CcFingerprintDialog *self) { g_autofree char *name_owner = NULL; name_owner = g_dbus_proxy_get_name_owner (G_DBUS_PROXY (device)); if (!name_owner) { if (self->dialog_state & DIALOG_STATE_DEVICE_CLAIMED) { disconnect_device_signals (self); if (self->dialog_state & DIALOG_STATE_DEVICE_ENROLLING) { set_enroll_result_message (self, ENROLL_STATE_ERROR, C_("Fingerprint enroll state", "Problem Reading Device")); } remove_dialog_state (self, DIALOG_STATE_DEVICE_CLAIMED); claim_device (self); } } } static void claim_device_cb (GObject *object, GAsyncResult *res, gpointer user_data) { g_autoptr(GError) error = NULL; g_autoptr(DialogStateRemover) state_remover = NULL; CcFprintdDevice *fprintd_device = CC_FPRINTD_DEVICE (object); CcFingerprintDialog *self = user_data; cc_fprintd_device_call_claim_finish (fprintd_device, res, &error); if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) return; state_remover = auto_state_remover (self, DIALOG_STATE_DEVICE_CLAIMING); if (error) { g_autofree char *dbus_error = g_dbus_error_get_remote_error (error); g_autofree char *error_message = NULL; if (dbus_error && g_str_equal (dbus_error, CC_FPRINTD_NAME ".Error.AlreadyInUse") && (self->dialog_state & DIALOG_STATE_DEVICE_CLAIMED)) return; error_message = g_strdup_printf (_("Failed to claim fingerprint device %s: %s"), cc_fprintd_device_get_name (self->device), dbus_error_to_human (self, error)); g_warning ("Claiming device %s failed: %s", cc_fprintd_device_get_name (self->device), error->message); notify_error (self, error_message); return; } if (!add_dialog_state (self, DIALOG_STATE_DEVICE_CLAIMED)) return; gtk_widget_set_sensitive (self->prints_manager, TRUE); self->device_signal_id = g_signal_connect_object (self->device, "g-signal", G_CALLBACK (on_device_signal), self, G_CONNECT_SWAPPED); self->device_name_owner_id = g_signal_connect_object (self->device, "notify::g-name-owner", G_CALLBACK (on_device_owner_changed), self, 0); } static void claim_device (CcFingerprintDialog *self) { ActUser *user; g_return_if_fail (!(self->dialog_state & DIALOG_STATE_DEVICE_CLAIMED)); if (!add_dialog_state (self, DIALOG_STATE_DEVICE_CLAIMING)) return; user = cc_fingerprint_manager_get_user (self->manager); gtk_widget_set_sensitive (self->prints_manager, FALSE); cc_fprintd_device_call_claim (self->device, act_user_get_user_name (user), self->cancellable, claim_device_cb, self); } static void on_stack_child_changed (CcFingerprintDialog *self) { GtkWidget *visible_child = gtk_stack_get_visible_child (self->stack); g_debug ("Fingerprint dialog child changed: %s", gtk_stack_get_visible_child_name (self->stack)); gtk_widget_hide (GTK_WIDGET (self->back_button)); gtk_widget_hide (GTK_WIDGET (self->cancel_button)); gtk_widget_hide (GTK_WIDGET (self->done_button)); adw_header_bar_set_show_start_title_buttons (ADW_HEADER_BAR (self->titlebar), TRUE); adw_header_bar_set_show_end_title_buttons (ADW_HEADER_BAR (self->titlebar), TRUE); gtk_flow_box_invalidate_filter (self->prints_gallery); if (visible_child == self->prints_manager) { gtk_widget_set_visible (GTK_WIDGET (self->back_button), have_multiple_devices (self)); notify_error (self, NULL); update_prints_store (self); if (!(self->dialog_state & DIALOG_STATE_DEVICE_CLAIMED)) claim_device (self); } else if (visible_child == self->enrollment_view) { adw_header_bar_set_show_start_title_buttons (ADW_HEADER_BAR (self->titlebar), FALSE); adw_header_bar_set_show_end_title_buttons (ADW_HEADER_BAR (self->titlebar), FALSE); gtk_widget_show (GTK_WIDGET (self->cancel_button)); gtk_widget_set_sensitive (GTK_WIDGET (self->cancel_button), TRUE); gtk_widget_show (GTK_WIDGET (self->done_button)); gtk_widget_set_sensitive (GTK_WIDGET (self->done_button), FALSE); } else { release_device (self); g_clear_object (&self->device); } } static void cc_fingerprint_dialog_init (CcFingerprintDialog *self) { g_autoptr(GtkCssProvider) provider = NULL; self->cancellable = g_cancellable_new (); gtk_widget_init_template (GTK_WIDGET (self)); provider = gtk_css_provider_new (); gtk_css_provider_load_from_resource (provider, "/org/gnome/control-center/user-accounts/cc-fingerprint-dialog.css"); gtk_style_context_add_provider_for_display (gdk_display_get_default (), GTK_STYLE_PROVIDER (provider), GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); on_stack_child_changed (self); g_signal_connect_object (self->stack, "notify::visible-child", G_CALLBACK (on_stack_child_changed), self, G_CONNECT_SWAPPED); g_object_bind_property (self->stack, "visible-child-name", self->title, "label", G_BINDING_SYNC_CREATE); populate_prints_gallery (self); populate_enrollment_view (self); } static void select_device_row (CcFingerprintDialog *self, GtkListBoxRow *row, GtkListBox *listbox) { CcFprintdDevice *device = g_object_get_data (G_OBJECT (row), "device"); g_return_if_fail (CC_FPRINTD_DEVICE (device)); g_set_object (&self->device, device); gtk_stack_set_visible_child (self->stack, self->prints_manager); } static void on_devices_list (GObject *object, GAsyncResult *res, gpointer user_data) { g_autolist (CcFprintdDevice) fprintd_devices = NULL; g_autoptr(DialogStateRemover) state_remover = NULL; g_autoptr(GError) error = NULL; CcFingerprintManager *fingerprint_manager = CC_FINGERPRINT_MANAGER (object); CcFingerprintDialog *self = CC_FINGERPRINT_DIALOG (user_data); fprintd_devices = cc_fingerprint_manager_get_devices_finish (fingerprint_manager, res, &error); if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) return; state_remover = auto_state_remover (self, DIALOG_STATE_DEVICES_LISTING); if (fprintd_devices == NULL) { if (error) { g_autofree char *error_message = NULL; error_message = g_strdup_printf (_("Failed to get fingerprint devices: %s"), dbus_error_to_human (self, error)); g_warning ("Retrieving fingerprint devices failed: %s", error->message); notify_error (self, error_message); } gtk_stack_set_visible_child (self->stack, GTK_WIDGET (self->no_devices_found)); } else if (fprintd_devices->next == NULL) { /* We have just one device... Skip devices selection */ self->device = g_object_ref (fprintd_devices->data); gtk_stack_set_visible_child (self->stack, self->prints_manager); } else { GList *l; for (l = fprintd_devices; l; l = l->next) { CcFprintdDevice *device = l->data; CcListRow *device_row; device_row = g_object_new (CC_TYPE_LIST_ROW, "visible", TRUE, "icon-name", "go-next-symbolic", "title", cc_fprintd_device_get_name (device), NULL); gtk_list_box_insert (self->devices_list, GTK_WIDGET (device_row), -1); g_object_set_data_full (G_OBJECT (device_row), "device", g_object_ref (device), g_object_unref); } gtk_stack_set_visible_child (self->stack, self->device_selector); } } static void cc_fingerprint_dialog_constructed (GObject *object) { CcFingerprintDialog *self = CC_FINGERPRINT_DIALOG (object); bindtextdomain ("fprintd", GNOMELOCALEDIR); bind_textdomain_codeset ("fprintd", "UTF-8"); add_dialog_state (self, DIALOG_STATE_DEVICES_LISTING); cc_fingerprint_manager_get_devices (self->manager, self->cancellable, on_devices_list, self); } static void back_button_clicked_cb (CcFingerprintDialog *self) { if (gtk_stack_get_visible_child (self->stack) == self->prints_manager) { notify_error (self, NULL); gtk_stack_set_visible_child (self->stack, self->device_selector); return; } g_return_if_reached (); } static void confirm_deletion_button_clicked_cb (CcFingerprintDialog *self) { gtk_widget_hide (self->delete_confirmation_infobar); delete_enrolled_prints (self); } static void cancel_deletion_button_clicked_cb (CcFingerprintDialog *self) { gtk_widget_set_sensitive (self->prints_manager, TRUE); gtk_widget_hide (self->delete_confirmation_infobar); } static void delete_prints_button_clicked_cb (CcFingerprintDialog *self) { gtk_widget_set_sensitive (self->prints_manager, FALSE); gtk_widget_show (self->delete_confirmation_infobar); } static void cancel_button_clicked_cb (CcFingerprintDialog *self) { if (self->dialog_state & DIALOG_STATE_DEVICE_ENROLLING) { g_cancellable_cancel (self->cancellable); g_set_object (&self->cancellable, g_cancellable_new ()); g_debug ("Cancelling enroll operation"); enroll_stop (self); } else { gtk_stack_set_visible_child (self->stack, self->prints_manager); } } static void done_button_clicked_cb (CcFingerprintDialog *self) { g_return_if_fail (self->dialog_state & DIALOG_STATE_DEVICE_ENROLLING); g_debug ("Completing enroll operation"); enroll_stop (self); } static gboolean cc_fingerprint_dialog_close_request (GtkWindow *window) { CcFingerprintDialog *self = CC_FINGERPRINT_DIALOG (window); cc_fingerprint_manager_update_state (self->manager, NULL, NULL); g_clear_handle_id (&self->enroll_stage_passed_id, g_source_remove); if (self->device && (self->dialog_state & DIALOG_STATE_DEVICE_CLAIMED)) { disconnect_device_signals (self); if (self->dialog_state & DIALOG_STATE_DEVICE_ENROLLING) cc_fprintd_device_call_enroll_stop_sync (self->device, NULL, NULL); cc_fprintd_device_call_release (self->device, NULL, NULL, NULL); } g_clear_object (&self->manager); g_clear_object (&self->device); g_clear_pointer (&self->enrolled_fingers, g_strfreev); g_cancellable_cancel (self->cancellable); g_clear_object (&self->cancellable); return GTK_WINDOW_CLASS (cc_fingerprint_dialog_parent_class)->close_request (window); } static void cc_fingerprint_dialog_class_init (CcFingerprintDialogClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); GtkWindowClass *window_class = GTK_WINDOW_CLASS (klass); gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/control-center/user-accounts/cc-fingerprint-dialog.ui"); object_class->constructed = cc_fingerprint_dialog_constructed; object_class->get_property = cc_fingerprint_dialog_get_property; object_class->set_property = cc_fingerprint_dialog_set_property; window_class->close_request = cc_fingerprint_dialog_close_request; properties[PROP_MANAGER] = g_param_spec_object ("fingerprint-manager", "FingerprintManager", "The CC fingerprint manager", CC_TYPE_FINGERPRINT_MANAGER, G_PARAM_STATIC_STRINGS | G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY); g_object_class_install_properties (object_class, N_PROPS, properties); gtk_widget_class_bind_template_child (widget_class, CcFingerprintDialog, add_print_popover); gtk_widget_class_bind_template_child (widget_class, CcFingerprintDialog, add_print_popover_box); gtk_widget_class_bind_template_child (widget_class, CcFingerprintDialog, back_button); gtk_widget_class_bind_template_child (widget_class, CcFingerprintDialog, cancel_button); gtk_widget_class_bind_template_child (widget_class, CcFingerprintDialog, delete_confirmation_infobar); gtk_widget_class_bind_template_child (widget_class, CcFingerprintDialog, delete_prints_button); gtk_widget_class_bind_template_child (widget_class, CcFingerprintDialog, device_selector); gtk_widget_class_bind_template_child (widget_class, CcFingerprintDialog, devices_list); gtk_widget_class_bind_template_child (widget_class, CcFingerprintDialog, done_button); gtk_widget_class_bind_template_child (widget_class, CcFingerprintDialog, enroll_message); gtk_widget_class_bind_template_child (widget_class, CcFingerprintDialog, enroll_print_bin); gtk_widget_class_bind_template_child (widget_class, CcFingerprintDialog, enroll_print_entry); gtk_widget_class_bind_template_child (widget_class, CcFingerprintDialog, enrollment_view); gtk_widget_class_bind_template_child (widget_class, CcFingerprintDialog, error_infobar); gtk_widget_class_bind_template_child (widget_class, CcFingerprintDialog, infobar_error); gtk_widget_class_bind_template_child (widget_class, CcFingerprintDialog, no_devices_found); gtk_widget_class_bind_template_child (widget_class, CcFingerprintDialog, prints_gallery); gtk_widget_class_bind_template_child (widget_class, CcFingerprintDialog, prints_manager); gtk_widget_class_bind_template_child (widget_class, CcFingerprintDialog, spinner); gtk_widget_class_bind_template_child (widget_class, CcFingerprintDialog, stack); gtk_widget_class_bind_template_child (widget_class, CcFingerprintDialog, title); gtk_widget_class_bind_template_child (widget_class, CcFingerprintDialog, titlebar); gtk_widget_class_bind_template_callback (widget_class, back_button_clicked_cb); gtk_widget_class_bind_template_callback (widget_class, cancel_button_clicked_cb); gtk_widget_class_bind_template_callback (widget_class, cancel_deletion_button_clicked_cb); gtk_widget_class_bind_template_callback (widget_class, confirm_deletion_button_clicked_cb); gtk_widget_class_bind_template_callback (widget_class, delete_prints_button_clicked_cb); gtk_widget_class_bind_template_callback (widget_class, done_button_clicked_cb); gtk_widget_class_bind_template_callback (widget_class, on_print_activated_cb); gtk_widget_class_bind_template_callback (widget_class, select_device_row); }