Blob Blame History Raw
/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*- */
/*
 * Copyright © 2011 – 2017 Red Hat, Inc.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General
 * Public License along with this library; if not, see <http://www.gnu.org/licenses/>.
 */

#include "config.h"

#include <glib/gi18n-lib.h>

#include "goaprovider.h"
#include "goaprovider-priv.h"
#include "goaproviderfactory.h"
#include "goaexchangeprovider.h"
#include "goagoogleprovider.h"
#include "goafacebookprovider.h"
#include "goaimapsmtpprovider.h"
#include "goaowncloudprovider.h"
#include "goaflickrprovider.h"
#include "goafoursquareprovider.h"
#include "goawindowsliveprovider.h"
#include "goatelepathyfactory.h"
#include "goapocketprovider.h"
#include "goamediaserverprovider.h"
#include "goalastfmprovider.h"
#include "goatodoistprovider.h"

#ifdef GOA_KERBEROS_ENABLED
#include "goakerberosprovider.h"
#endif

#include "goautils.h"

/**
 * SECTION:goaprovider
 * @title: GoaProvider
 * @short_description: Abstract base class for providers
 *
 * #GoaProvider is the base type for all providers.
 */

enum {
  PROP_0,
  PROP_PRESEED_DATA,
  NUM_PROPERTIES
};

static GParamSpec *properties[NUM_PROPERTIES] = { NULL, };

static gboolean goa_provider_ensure_credentials_sync_real (GoaProvider   *self,
                                                           GoaObject     *object,
                                                           gint          *out_expires_in,
                                                           GCancellable  *cancellable,
                                                           GError       **error);

static gboolean goa_provider_build_object_real (GoaProvider         *self,
                                                GoaObjectSkeleton   *object,
                                                GKeyFile            *key_file,
                                                const gchar         *group,
                                                GDBusConnection     *connection,
                                                gboolean             just_added,
                                                GError             **error);

static guint goa_provider_get_credentials_generation_real (GoaProvider *self);

static GIcon *goa_provider_get_provider_icon_real (GoaProvider *self,
                                                   GoaObject   *object);

static void goa_provider_initialize_real (GoaProvider *self);

static void goa_provider_remove_account_real (GoaProvider          *self,
                                              GoaObject            *object,
                                              GCancellable         *cancellable,
                                              GAsyncReadyCallback   callback,
                                              gpointer              user_data);

static gboolean goa_provider_remove_account_finish_real (GoaProvider   *self,
                                                         GAsyncResult  *res,
                                                         GError       **error);

static void goa_provider_show_account_real (GoaProvider         *provider,
                                            GoaClient           *client,
                                            GoaObject           *object,
                                            GtkBox              *vbox,
                                            GtkGrid             *dummy1,
                                            GtkGrid             *dummy2);

G_DEFINE_ABSTRACT_TYPE (GoaProvider, goa_provider, G_TYPE_OBJECT);

static struct {
  GoaProviderFeatures feature;
  const gchar *property;
  const gchar *blurb;
} provider_features_info[] = {
  /* The order in which the features are listed is
   * important because it affects the order in which they are
   * displayed in the show_account() UI
   */
  {
    .feature = GOA_PROVIDER_FEATURE_MAIL,
    .property = "mail-disabled",
    .blurb = N_("_Mail"),
  },
  {
    .feature = GOA_PROVIDER_FEATURE_CALENDAR,
    .property = "calendar-disabled",
    .blurb = N_("Cale_ndar"),
  },
  {
    .feature = GOA_PROVIDER_FEATURE_CONTACTS,
    .property = "contacts-disabled",
    .blurb = N_("_Contacts"),
  },
  {
    .feature = GOA_PROVIDER_FEATURE_CHAT,
    .property = "chat-disabled",
    .blurb = N_("C_hat"),
  },
  {
    .feature = GOA_PROVIDER_FEATURE_DOCUMENTS,
    .property = "documents-disabled",
    .blurb = N_("_Documents"),
  },
  {
    .feature = GOA_PROVIDER_FEATURE_MUSIC,
    .property = "music-disabled",
    .blurb = N_("M_usic"),
  },
  {
    .feature = GOA_PROVIDER_FEATURE_PHOTOS,
    .property = "photos-disabled",
    .blurb = N_("_Photos"),
  },
  {
    .feature = GOA_PROVIDER_FEATURE_FILES,
    .property = "files-disabled",
    .blurb = N_("_Files"),
  },
  {
    .feature = GOA_PROVIDER_FEATURE_TICKETING,
    .property = "ticketing-disabled",
    .blurb = N_("Network _Resources"),
  },
  {
    .feature = GOA_PROVIDER_FEATURE_READ_LATER,
    .property = "read-later-disabled",
    .blurb = N_("_Read Later"),
  },
  {
    .feature = GOA_PROVIDER_FEATURE_PRINTERS,
    .property = "printers-disabled",
    .blurb = N_("Prin_ters"),
  },
  {
    .feature = GOA_PROVIDER_FEATURE_MAPS,
    .property = "maps-disabled",
    .blurb = N_("_Maps"),
  },
  {
    .feature = GOA_PROVIDER_FEATURE_TODO,
    .property = "todo-disabled",
    .blurb = N_("T_o Do"),
  },
  {
    .feature = GOA_PROVIDER_FEATURE_INVALID,
    .property = NULL,
    .blurb = NULL,
  }
};

static void
goa_provider_get_property (GObject *object,
                           guint property_id,
                           GValue *value,
                           GParamSpec *pspec)
{
    switch (property_id) {
    case PROP_PRESEED_DATA:
        g_value_set_variant (value, NULL);
        break;
    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
        break;
    }
}

static void
goa_provider_set_property (GObject *object,
                           guint property_id,
                           const GValue *value,
                           GParamSpec *pspec)
{
    switch (property_id) {
    case PROP_PRESEED_DATA:
        break;
    default:
        G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
        break;
    }
}

static void
goa_provider_init (GoaProvider *self)
{
}

static void
goa_provider_class_init (GoaProviderClass *klass)
{
  GObjectClass *object_class = G_OBJECT_CLASS (klass);

  object_class->set_property = goa_provider_set_property;
  object_class->get_property = goa_provider_get_property;

  klass->build_object = goa_provider_build_object_real;
  klass->ensure_credentials_sync = goa_provider_ensure_credentials_sync_real;
  klass->get_credentials_generation = goa_provider_get_credentials_generation_real;
  klass->get_provider_icon = goa_provider_get_provider_icon_real;
  klass->initialize = goa_provider_initialize_real;
  klass->remove_account = goa_provider_remove_account_real;
  klass->remove_account_finish = goa_provider_remove_account_finish_real;
  klass->show_account = goa_provider_show_account_real;

/**
 * GoaProvider:preseed-data
 *
 * An #GVariant of type a{sv} storing any information already collected that
 * can be useful when creating a new account. For instance, this can be useful
 * to reuse the HTTP cookies from an existing browser session to skip the
 * prompt for username and password in the OAuth2-based providers by passing
 * a #GVariant with the following contents:
 *
 * <informalexample>
 * <programlisting>
 * {
 *   "cookies": [
 *     {
 *       "domain": "example.com",
 *       "name": "LSID",
 *       "value": "asdfasdfasdf"
 *     },
 *     {
 *       "domain": "accounts.example.com",
 *       "name": "SSID",
 *       "value": "asdfasdfasdf"
 *     }
 *   ]
 * }
 * </programlisting>
 * </informalexample>
 *
 * Unknown or unsupported keys will be ignored by providers.
 *
 * Deprecated: 3.28: This property does nothing.
 */
  properties[PROP_PRESEED_DATA] =
    g_param_spec_variant ("preseed-data",
        "Collected data to pre-seed account creation",
        "A a{sv} #GVariant containing a provider-type specific set of data that"
        "can be useful during account creation (eg. http cookies from an existing"
        "browser session or the entrypoint url for self-hosted services).",
        G_VARIANT_TYPE_VARDICT,
        NULL,
        G_PARAM_DEPRECATED | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);

  g_object_class_install_properties (object_class, NUM_PROPERTIES, properties);
}

/**
 * goa_provider_get_provider_type:
 * @self: A #GoaProvider.
 *
 * Gets the type of @self.
 *
 * This is a pure virtual method - a subclass must provide an
 * implementation.
 *
 * Returns: (transfer none): A string owned by @self, do not free.
 */
const gchar *
goa_provider_get_provider_type (GoaProvider *self)
{
  g_return_val_if_fail (GOA_IS_PROVIDER (self), NULL);
  return GOA_PROVIDER_GET_CLASS (self)->get_provider_type (self);
}

/**
 * goa_provider_get_provider_name:
 * @self: A #GoaProvider.
 * @object: (allow-none): A #GoaObject for an account.
 *
 * Gets a name for @self and @object that is suitable for display
 * in an user interface. The returned value may depend on @object (if
 * it's not %NULL) - for example, hosted accounts might return a
 * different name.
 *
 * This is a pure virtual method - a subclass must provide an
 * implementation.
 *
 * Returns: (transfer full): A string that should be freed with g_free().
 */
gchar *
goa_provider_get_provider_name (GoaProvider *self,
                                GoaObject   *object)
{
  g_return_val_if_fail (GOA_IS_PROVIDER (self), NULL);
  return GOA_PROVIDER_GET_CLASS (self)->get_provider_name (self, object);
}

/**
 * goa_provider_get_provider_icon:
 * @self: A #GoaProvider.
 * @object: A #GoaObject for an account.
 *
 * Gets an icon for @self and @object that is suitable for display
 * in an user interface. The returned value may depend on @object -
 * for example, hosted accounts might return a different icon.
 *
 * This is a virtual method with a default implementation that returns
 * a #GThemedIcon with fallbacks constructed from the name
 * <literal>goa-account-TYPE</literal> where <literal>TYPE</literal>
 * is the return value of goa_provider_get_provider_type().
 *
 * Returns: (transfer full): An icon that should be freed with g_object_unref().
 */
GIcon *
goa_provider_get_provider_icon (GoaProvider *self,
                                GoaObject   *object)
{
  g_return_val_if_fail (GOA_IS_PROVIDER (self), NULL);
  return GOA_PROVIDER_GET_CLASS (self)->get_provider_icon (self, object);
}

static GIcon *
goa_provider_get_provider_icon_real (GoaProvider *self,
                                     GoaObject   *object)
{
  GIcon *ret;
  gchar *s;
  s = g_strdup_printf ("goa-account-%s", goa_provider_get_provider_type (self));
  ret = g_themed_icon_new_with_default_fallbacks (s);
  g_free (s);
  return ret;
}

/**
 * goa_provider_get_provider_group:
 * @self: A #GoaProvider.
 *
 * Gets the group to which @self belongs that is suitable for
 * organizing the providers while displaying them in an user
 * interface.
 *
 * This is a pure virtual method - a subclass must provide an
 * implementation.
 *
 * Returns: A #GoaProviderGroup.
 *
 * Since: 3.8
 *
 * Deprecated: 3.10: Use goa_provider_get_provider_features() instead.
 */
GoaProviderGroup
goa_provider_get_provider_group (GoaProvider *self)
{
  g_return_val_if_fail (GOA_IS_PROVIDER (self), GOA_PROVIDER_GROUP_INVALID);
  return GOA_PROVIDER_GET_CLASS (self)->get_provider_group (self);
}

/**
 * goa_provider_get_provider_features:
 * @self: A #GoaProvider.
 *
 * Get the features bitmask (eg. %GOA_PROVIDER_FEATURE_CHAT|%GOA_PROVIDER_FEATURE_CONTACTS)
 * supported by the provider.
 *
 * Returns: The #GoaProviderFeatures bitmask with the provided features.
 *
 * Since: 3.10
 */
GoaProviderFeatures
goa_provider_get_provider_features (GoaProvider *self)
{
  g_return_val_if_fail (GOA_IS_PROVIDER (self), GOA_PROVIDER_FEATURE_INVALID);
  g_return_val_if_fail (GOA_PROVIDER_GET_CLASS (self)->get_provider_features != NULL, GOA_PROVIDER_FEATURE_INVALID);
  return GOA_PROVIDER_GET_CLASS (self)->get_provider_features (self);
}

/* ---------------------------------------------------------------------------------------------------- */

void
goa_provider_initialize (GoaProvider *self)
{
  g_return_if_fail (GOA_IS_PROVIDER (self));

  GOA_PROVIDER_GET_CLASS (self)->initialize (self);
}

static void
goa_provider_initialize_real (GoaProvider *self)
{
}

/* ---------------------------------------------------------------------------------------------------- */

/**
 * goa_provider_add_account:
 * @self: A #GoaProvider.
 * @client: A #GoaClient.
 * @dialog: A #GtkDialog.
 * @vbox: A vertically oriented #GtkBox to put content in.
 * @error: Return location for error or %NULL.
 *
 * This method brings up the user interface necessary to create a new
 * account on @client of the type for @self, interacts with the
 * user to get all information needed and creates the account.
 *
 * The passed in @dialog widget is guaranteed to be visible with @vbox
 * being empty and the only visible widget in @dialog's content
 * area. The dialog has exactly one action widget, a cancel button
 * with response id GTK_RESPONSE_CANCEL. Implementations are free to
 * add additional action widgets, as needed.
 *
 * If an account was successfully created, a #GoaObject for the
 * created account is returned. If @dialog is dismissed, %NULL is
 * returned and @error is set to %GOA_ERROR_DIALOG_DISMISSED. If an
 * account couldn't be created then @error is set. In some cases,
 * for example, when the credentials could not be stored in the
 * keyring, a #GoaObject can be returned even if @error is set.
 *
 * The caller will always show an error dialog if @error is set unless
 * the error is %GOA_ERROR_DIALOG_DISMISSED.
 *
 * Implementations should run the <link
 * linkend="g_main_context_default">default main loop</link> while
 * interacting with the user and may do so using e.g. gtk_dialog_run()
 * on @dialog.
 *
 * This is a pure virtual method - a subclass must provide an
 * implementation.
 *
 * Returns: The #GoaObject for the created account (must be relased
 *   with g_object_unref()) or %NULL if @error is set.
 */
GoaObject *
goa_provider_add_account (GoaProvider  *self,
                          GoaClient    *client,
                          GtkDialog    *dialog,
                          GtkBox       *vbox,
                          GError      **error)
{
  GoaObject *ret;

  g_return_val_if_fail (GOA_IS_PROVIDER (self), NULL);
  g_return_val_if_fail (GOA_IS_CLIENT (client), NULL);
  g_return_val_if_fail (GTK_IS_DIALOG (dialog), NULL);
  g_return_val_if_fail (error == NULL || *error == NULL, NULL);

  ret = GOA_PROVIDER_GET_CLASS (self)->add_account (self, client, dialog, vbox, error);

  g_warn_if_fail ((ret == NULL && (error == NULL || *error != NULL)) || GOA_IS_OBJECT (ret));

  return ret;
}

/* ---------------------------------------------------------------------------------------------------- */

/**
 * goa_provider_refresh_account:
 * @self: A #GoaProvider.
 * @client: A #GoaClient.
 * @object: A #GoaObject with a #GoaAccount interface.
 * @parent: (allow-none): Transient parent of dialogs or %NULL.
 * @error: Return location for error or %NULL.
 *
 * This method brings up the user interface necessary for refreshing
 * the credentials for the account specified by @object. This
 * typically involves having the user log in to the account again.
 *
 * Implementations should use @parent (unless %NULL) as the transient
 * parent of any created windows/dialogs.
 *
 * Implementations should run the <link
 * linkend="g_main_context_default">default main loop</link> while
 * interacting with the user.
 *
 * This is a pure virtual method - a subclass must provide an
 * implementation.
 *
 * Returns: %TRUE if the account has been refreshed, %FALSE if @error
 * is set.
 */
gboolean
goa_provider_refresh_account (GoaProvider  *self,
                              GoaClient    *client,
                              GoaObject    *object,
                              GtkWindow    *parent,
                              GError      **error)
{
  g_return_val_if_fail (GOA_IS_PROVIDER (self), FALSE);
  g_return_val_if_fail (GOA_IS_CLIENT (client), FALSE);
  g_return_val_if_fail (GOA_IS_OBJECT (object) && goa_object_peek_account (object) != NULL, FALSE);
  g_return_val_if_fail (parent == NULL || GTK_IS_WINDOW (parent), FALSE);
  g_return_val_if_fail (error == NULL || *error == NULL, FALSE);

  return GOA_PROVIDER_GET_CLASS (self)->refresh_account (self, client, object, parent, error);
}

/* ---------------------------------------------------------------------------------------------------- */

/**
 * goa_provider_show_account:
 * @self: A #GoaProvider.
 * @client: A #GoaClient.
 * @object: A #GoaObject with a #GoaAccount interface.
 * @vbox: A vertically oriented #GtkBox to put content in.
 * @grid: A #GtkGrid to put content in.
 * @dummy: Unused.
 *
 * Method used to add widgets in the control panel for the account
 * represented by @object.
 *
 * This is a virtual method where the default implementation adds
 * one GtkSwitch per service supported by the provider (as reported
 * by goa_provider_get_provider_features()).
 */
void
goa_provider_show_account (GoaProvider         *self,
                           GoaClient           *client,
                           GoaObject           *object,
                           GtkBox              *vbox,
                           GtkGrid             *dummy1,
                           GtkGrid             *dummy2)
{
  g_return_if_fail (GOA_IS_PROVIDER (self));
  g_return_if_fail (GOA_IS_CLIENT (client));
  g_return_if_fail (GOA_IS_OBJECT (object) && goa_object_peek_account (object) != NULL);
  g_return_if_fail (GTK_IS_BOX (vbox));

  GOA_PROVIDER_GET_CLASS (self)->show_account (self, client, object, vbox, dummy1, dummy2);
}

static void
goa_provider_show_account_real (GoaProvider         *provider,
                                GoaClient           *client,
                                GoaObject           *object,
                                GtkBox              *vbox,
                                GtkGrid             *dummy1,
                                GtkGrid             *dummy2)
{
  GoaProviderFeatures features;
  GtkWidget *grid;
  gint row;
  guint i;
  const char *label;

  row = 0;

  goa_utils_account_add_attention_needed (client, object, provider, vbox);

  grid = gtk_grid_new ();
  gtk_widget_set_halign (grid, GTK_ALIGN_CENTER);
  gtk_widget_set_hexpand (grid, TRUE);
  gtk_widget_set_margin_end (grid, 72);
  gtk_widget_set_margin_start (grid, 72);
  gtk_widget_set_margin_top (grid, 24);
  gtk_grid_set_column_spacing (GTK_GRID (grid), 12);
  gtk_grid_set_row_spacing (GTK_GRID (grid), 6);
  gtk_container_add (GTK_CONTAINER (vbox), grid);

  goa_utils_account_add_header (object, GTK_GRID (grid), row++);

  features = goa_provider_get_provider_features (provider);
  /* Translators: This is a label for a series of
   * options switches. For example: “Use for Mail”. */
  label = _("Use for");

  for (i = 0; provider_features_info[i].property != NULL; i++)
    {
      if ((features & provider_features_info[i].feature) != 0)
        {
          goa_util_add_row_switch_from_keyfile_with_blurb (GTK_GRID (grid), row++, object,
                                                           label,
                                                           provider_features_info[i].property,
                                                           _(provider_features_info[i].blurb));
          label = NULL;
        }
    }
}

/* ---------------------------------------------------------------------------------------------------- */

/**
 * goa_provider_build_object:
 * @self: A #GoaProvider.
 * @object: The #GoaObjectSkeleton that is being built.
 * @key_file: The #GKeyFile with configuation data.
 * @group: The group in @key_file to get data from.
 * @connection: The #GDBusConnection used by the daemon to connect to the message bus.
 * @just_added: Whether the account was newly created or being updated.
 * @error: Return location for error or %NULL.
 *
 * This method is called when construction account #GoaObject<!-- -->
 * from configuration data - it basically provides a way to add
 * provider-specific information.
 *
 * The passed in @object will have a #GoaAccount interface
 * set. Implementations should validate and use data from @key_file to
 * add more interfaces to @object.
 *
 * Note that this may be called on already exported objects - for
 * example on configuration files reload.
 *
 * This is a pure virtual method - a subclass must provide an
 * implementation.
 *
 * Returns: %TRUE if data was valid, %FALSE if @error is set.
 */
gboolean
goa_provider_build_object (GoaProvider         *self,
                           GoaObjectSkeleton   *object,
                           GKeyFile            *key_file,
                           const gchar         *group,
                           GDBusConnection     *connection,
                           gboolean             just_added,
                           GError             **error)
{
  g_return_val_if_fail (GOA_IS_PROVIDER (self), FALSE);
  g_return_val_if_fail (GOA_IS_OBJECT_SKELETON (object) && goa_object_peek_account (GOA_OBJECT (object)) != NULL, FALSE);
  g_return_val_if_fail (key_file != NULL, FALSE);
  g_return_val_if_fail (group != NULL, FALSE);
  g_return_val_if_fail (G_IS_DBUS_CONNECTION (connection), FALSE);
  g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
  return GOA_PROVIDER_GET_CLASS (self)->build_object (self,
                                                      object,
                                                      key_file,
                                                      group,
                                                      connection,
                                                      just_added,
                                                      error);
}

/* ---------------------------------------------------------------------------------------------------- */

typedef struct
{
  GoaObject *object;
  gint expires_in;
} EnsureCredentialsData;

static EnsureCredentialsData *
ensure_credentials_data_new (GoaObject *object)
{
  EnsureCredentialsData *data;
  data = g_new0 (EnsureCredentialsData, 1);
  data->object = g_object_ref (object);
  return data;
}

static void
ensure_credentials_data_free (EnsureCredentialsData *data)
{
  g_object_unref (data->object);
  g_free (data);
}

static void
ensure_credentials_in_thread_func (GTask              *task,
                                   gpointer            object,
                                   gpointer            task_data,
                                   GCancellable       *cancellable)
{
  EnsureCredentialsData *data;
  GError *error;

  data = task_data;

  error = NULL;
  if (!goa_provider_ensure_credentials_sync (GOA_PROVIDER (object),
                                             data->object,
                                             &data->expires_in,
                                             cancellable,
                                             &error))
    g_task_return_error (task, error);
  else
    g_task_return_boolean (task, TRUE);
}


/**
 * goa_provider_ensure_credentials:
 * @self: A #GoaProvider.
 * @object: A #GoaObject with a #GoaAccount interface.
 * @cancellable: (allow-none): A #GCancellable or %NULL.
 * @callback: The function to call when the request is satisfied.
 * @user_data: Pointer to pass to @callback.
 *
 * Ensures that credentials for @object are still valid.
 *
 * When the result is ready, @callback will be called in the the <link
 * linkend="g-main-context-push-thread-default">thread-default main
 * loop</link> this function was called from. You can then call
 * goa_provider_ensure_credentials_finish() to get the result
 * of the operation.
 *
 * This is a virtual method where the default implementation simply
 * throws the %GOA_ERROR_NOT_SUPPORTED error. A subclass may provide
 * another implementation.
 */
void
goa_provider_ensure_credentials (GoaProvider          *self,
                                 GoaObject            *object,
                                 GCancellable         *cancellable,
                                 GAsyncReadyCallback   callback,
                                 gpointer              user_data)
{
  GTask *task;

  g_return_if_fail (GOA_IS_PROVIDER (self));
  g_return_if_fail (GOA_IS_OBJECT (object));
  g_return_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable));

  task = g_task_new (self, cancellable, callback, user_data);
  g_task_set_task_data (task, ensure_credentials_data_new (object), (GDestroyNotify) ensure_credentials_data_free);
  g_task_set_source_tag (task, goa_provider_ensure_credentials);
  g_task_run_in_thread (task, ensure_credentials_in_thread_func);

  g_object_unref (task);
}

/**
 * goa_provider_ensure_credentials_finish:
 * @self: A #GoaProvider.
 * @out_expires_in: (out): Return location for how long the expired credentials are good for (0 if unknown) or %NULL.
 * @res: A #GAsyncResult obtained from the #GAsyncReadyCallback passed to goa_provider_ensure_credentials().
 * @error: Return location for error or %NULL.
 *
 * Finishes an operation started with goa_provider_ensure_credentials().
 *
 * Returns: %TRUE if the credentials for the passed #GoaObject are valid, %FALSE if @error is set.
 */
gboolean
goa_provider_ensure_credentials_finish (GoaProvider         *self,
                                        gint                *out_expires_in,
                                        GAsyncResult        *res,
                                        GError             **error)
{
  GTask *task;
  gboolean had_error;

  gboolean ret;
  EnsureCredentialsData *data;

  ret = FALSE;

  g_return_val_if_fail (GOA_IS_PROVIDER (self), FALSE);
  g_return_val_if_fail (error == NULL || *error == NULL, FALSE);

  g_return_val_if_fail (g_task_is_valid (res, self), FALSE);
  task = G_TASK (res);

  g_return_val_if_fail (g_task_get_source_tag (task) == goa_provider_ensure_credentials, FALSE);

  /* Workaround for bgo#764163 */
  had_error = g_task_had_error (task);
  ret = g_task_propagate_boolean (task, error);
  if (had_error)
    goto out;

  data = g_task_get_task_data (task);
  if (out_expires_in != NULL)
    *out_expires_in = data->expires_in;

 out:
  return ret;
}

/* ---------------------------------------------------------------------------------------------------- */

/**
 * goa_provider_ensure_credentials_sync:
 * @self: A #GoaProvider.
 * @object: A #GoaObject with a #GoaAccount interface.
 * @out_expires_in: (out): Return location for how long the expired credentials are good for (0 if unknown) or %NULL.
 * @cancellable: (allow-none): A #GCancellable or %NULL.
 * @error: Return location for error or %NULL.
 *
 * Like goa_provider_ensure_credentials() but blocks the
 * calling thread until an answer is received.
 *
 * Returns: %TRUE if the credentials for the passed #GoaObject are valid, %FALSE if @error is set.
 */
gboolean
goa_provider_ensure_credentials_sync (GoaProvider     *self,
                                      GoaObject       *object,
                                      gint            *out_expires_in,
                                      GCancellable    *cancellable,
                                      GError         **error)
{
  GoaAccount *account = NULL;
  GoaProviderFeatures features;
  gboolean disabled = TRUE;
  gboolean ret = FALSE;
  guint i;

  g_return_val_if_fail (GOA_IS_PROVIDER (self), FALSE);
  g_return_val_if_fail (GOA_IS_OBJECT (object), FALSE);
  g_return_val_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable), FALSE);
  g_return_val_if_fail (error == NULL || *error == NULL, FALSE);

  account = goa_object_get_account (object);
  g_return_val_if_fail (GOA_IS_ACCOUNT (account), FALSE);

  features = goa_provider_get_provider_features (self);

  for (i = 0; provider_features_info[i].property != NULL; i++)
    {
      if ((features & provider_features_info[i].feature) != 0)
        {
          gboolean feature_disabled;

          g_object_get (account, provider_features_info[i].property, &feature_disabled, NULL);
          disabled = disabled && feature_disabled;
          if (!disabled)
            break;
        }
    }

  if (disabled)
    {
      g_set_error_literal (error, GOA_ERROR, GOA_ERROR_NOT_SUPPORTED, _("Account is disabled"));
      goto out;
    }

  ret = GOA_PROVIDER_GET_CLASS (self)->ensure_credentials_sync (self, object, out_expires_in, cancellable, error);

 out:
  if (!ret && error != NULL && *error == NULL)
    {
      const gchar *provider_type;

      provider_type = goa_provider_get_provider_type (self);
      g_critical ("GoaProvider (%s) failed to set error correctly", provider_type);
      g_set_error_literal (error, GOA_ERROR, GOA_ERROR_NOT_AUTHORIZED, _("Unknown error"));
    }

  g_clear_object (&account);
  return ret;
}

/* ---------------------------------------------------------------------------------------------------- */

static gboolean
goa_provider_ensure_credentials_sync_real (GoaProvider   *self,
                                           GoaObject     *object,
                                           gint          *out_expires_in,
                                           GCancellable  *cancellable,
                                           GError       **error)
{
  g_set_error (error,
               GOA_ERROR,
               GOA_ERROR_NOT_SUPPORTED,
               _("ensure_credentials_sync is not implemented on type %s"),
               g_type_name (G_TYPE_FROM_INSTANCE (self)));
  return FALSE;
}

static gboolean
goa_provider_build_object_real (GoaProvider         *self,
                                GoaObjectSkeleton   *object,
                                GKeyFile            *key_file,
                                const gchar         *group,
                                GDBusConnection     *connection,
                                gboolean             just_added,
                                GError             **error)
{
  /* does nothing */
  return TRUE;
}

/* ---------------------------------------------------------------------------------------------------- */

/**
 * goa_provider_get_credentials_generation:
 * @self: A #GoaProvider.
 *
 * Gets the generation of credentials being used for the provider.
 *
 * Implementations should bump this number when changes are introduced
 * that may render existing credentials unusable.
 *
 * For example, if an additional scope is requested (e.g. access to
 * contacts data) while obtaining credentials, then this number needs
 * to be bumped since existing credentials are not good for the added
 * scope.
 *
 * This is a virtual method where the default implementation returns
 * 0.
 *
 * Returns: The current generation of credentials.
 */
guint
goa_provider_get_credentials_generation (GoaProvider *self)
{
  g_return_val_if_fail (GOA_IS_PROVIDER (self), 0);
  return GOA_PROVIDER_GET_CLASS (self)->get_credentials_generation (self);
}

static guint
goa_provider_get_credentials_generation_real (GoaProvider *self)
{
  return 0;
}

/* ---------------------------------------------------------------------------------------------------- */

void
goa_provider_ensure_extension_points_registered (void)
{
  static gsize once_init_value = 0;

  if (g_once_init_enter (&once_init_value))
    {
      GIOExtensionPoint *extension_point;

      extension_point = g_io_extension_point_register (GOA_PROVIDER_EXTENSION_POINT_NAME);
      g_io_extension_point_set_required_type (extension_point, GOA_TYPE_PROVIDER);

      extension_point = g_io_extension_point_register (GOA_PROVIDER_FACTORY_EXTENSION_POINT_NAME);
      g_io_extension_point_set_required_type (extension_point, GOA_TYPE_PROVIDER_FACTORY);

      g_once_init_leave (&once_init_value, 1);
    }
}

/* ---------------------------------------------------------------------------------------------------- */

static struct
{
  const gchar *name;
  GType (*get_type) (void);
} ordered_builtins_map[] = {
  /* The order in which the providers' types are created is
   * important because it affects the order in which they are
   * returned by goa_provider_get_all.
   */
#ifdef GOA_GOOGLE_ENABLED
  { GOA_GOOGLE_NAME, goa_google_provider_get_type },
#endif
#ifdef GOA_OWNCLOUD_ENABLED
  { GOA_OWNCLOUD_NAME, goa_owncloud_provider_get_type },
#endif
#ifdef GOA_FACEBOOK_ENABLED
  { GOA_FACEBOOK_NAME, goa_facebook_provider_get_type },
#endif
#ifdef GOA_WINDOWS_LIVE_ENABLED
  { GOA_WINDOWS_LIVE_NAME, goa_windows_live_provider_get_type },
#endif
#ifdef GOA_FLICKR_ENABLED
  { GOA_FLICKR_NAME, goa_flickr_provider_get_type },
#endif
#ifdef GOA_POCKET_ENABLED
  { GOA_POCKET_NAME, goa_pocket_provider_get_type },
#endif
#ifdef GOA_FOURSQUARE_ENABLED
  { GOA_FOURSQUARE_NAME, goa_foursquare_provider_get_type },
#endif
#ifdef GOA_EXCHANGE_ENABLED
  { GOA_EXCHANGE_NAME, goa_exchange_provider_get_type },
#endif
#ifdef GOA_LASTFM_ENABLED
  { GOA_LASTFM_NAME, goa_lastfm_provider_get_type },
#endif
#ifdef GOA_TODOIST_ENABLED
  { GOA_TODOIST_NAME, goa_todoist_provider_get_type },
#endif
#ifdef GOA_IMAP_SMTP_ENABLED
  { GOA_IMAP_SMTP_NAME, goa_imap_smtp_provider_get_type },
#endif
#ifdef GOA_KERBEROS_ENABLED
  { GOA_KERBEROS_NAME, goa_kerberos_provider_get_type },
#endif
#ifdef GOA_MEDIA_SERVER_ENABLED
  { GOA_MEDIA_SERVER_NAME, goa_media_server_provider_get_type },
#endif
#ifdef GOA_TELEPATHY_ENABLED
  { GOA_TELEPATHY_NAME, goa_telepathy_factory_get_type },
#endif
  { NULL, NULL }
};

void
goa_provider_ensure_builtins_loaded (void)
{
  static gsize once_init_value = 0;

  goa_provider_ensure_extension_points_registered ();

  if (g_once_init_enter (&once_init_value))
    {
      GSettings *settings;
      gchar **whitelisted_providers;
      guint i;
      guint j;
      gboolean all = FALSE;

      settings = g_settings_new (GOA_SETTINGS_SCHEMA);
      whitelisted_providers = g_settings_get_strv (settings, GOA_SETTINGS_WHITELISTED_PROVIDERS);

      /* Enable everything if there is 'all'. */
      for (i = 0; whitelisted_providers[i] != NULL; i++)
        {
          if (g_strcmp0 (whitelisted_providers[i], "all") == 0)
            {
              g_debug ("Loading all providers: ");
              for (j = 0; ordered_builtins_map[j].name != NULL; j++)
                {
                  g_debug (" - %s", ordered_builtins_map[j].name);
                  g_type_ensure ((*ordered_builtins_map[j].get_type) ());
                }

              all = TRUE;
              break;
            }
        }

      if (all)
        goto cleanup;

      /* Otherwise try them one by one. */
      g_debug ("Loading whitelisted providers: ");
      for (i = 0; ordered_builtins_map[i].name != NULL; i++)
        {
          for (j = 0; whitelisted_providers[j] != NULL; j++)
            {
              if (g_strcmp0 (whitelisted_providers[j], ordered_builtins_map[i].name) == 0)
                {
                  g_debug (" - %s", ordered_builtins_map[j].name);
                  g_type_ensure ((*ordered_builtins_map[i].get_type) ());
                  break;
                }
            }
        }

    cleanup:
      g_strfreev (whitelisted_providers);
      g_object_unref (settings);
      g_once_init_leave (&once_init_value, 1);
    }
}

/* ---------------------------------------------------------------------------------------------------- */

/**
 * goa_provider_get_for_provider_type:
 * @provider_type: A provider type.
 *
 * Returns a #GoaProvider for @provider_type (if available).
 *
 * If @provider_type doesn't contain any "/", a
 * %GOA_PROVIDER_EXTENSION_POINT_NAME extension for @provider_type is looked up
 * and the newly created #GoaProvider, if any, is returned.
 *
 * If @provider_type contains a "/", a
 * %GOA_PROVIDER_FACTORY_EXTENSION_POINT_NAME extension for the first part of
 * @provider_type is looked up. If found, the #GoaProviderFactory is used
 * to create a dynamic #GoaProvider matching the second part of @provider_type.
 *
 * Returns: (transfer full): A #GoaProvider (that must be freed
 * with g_object_unref()) or %NULL if not found.
 */
GoaProvider *
goa_provider_get_for_provider_type (const gchar *provider_type)
{
  GIOExtension *extension;
  GIOExtensionPoint *extension_point;
  gchar **split_provider_type;
  GoaProvider *ret;

  g_return_val_if_fail (provider_type != NULL, NULL);

  goa_provider_ensure_builtins_loaded ();

  ret = NULL;

  split_provider_type = g_strsplit (provider_type, "/", 2);

  if (g_strv_length (split_provider_type) == 1)
    {
      /* Normal provider */
      extension_point = g_io_extension_point_lookup (GOA_PROVIDER_EXTENSION_POINT_NAME);
      extension = g_io_extension_point_get_extension_by_name (extension_point, provider_type);
      if (extension != NULL)
        ret = GOA_PROVIDER (g_object_new (g_io_extension_get_type (extension), NULL));
    }
  else
    {
      /* Dynamic provider created through a factory */
      extension_point = g_io_extension_point_lookup (GOA_PROVIDER_FACTORY_EXTENSION_POINT_NAME);
      extension = g_io_extension_point_get_extension_by_name (extension_point, split_provider_type[0]);
      if (extension != NULL)
        {
          GoaProviderFactory *factory = g_object_new (g_io_extension_get_type (extension), NULL);
          ret = goa_provider_factory_get_provider (factory, split_provider_type[1]);
          g_object_unref (factory);
        }
    }

  g_strfreev (split_provider_type);

  return ret;
}

/* ---------------------------------------------------------------------------------------------------- */

typedef struct
{
  GQueue ret;
  gint pending_calls;
  GTask *task;
} GetAllData;

static void
free_list_and_unref (gpointer data)
{
  g_list_free_full (data, g_object_unref);
}

static gint
compare_providers (GoaProvider *a,
                   GoaProvider *b)
{
  gboolean a_branded;
  gboolean b_branded;

  if (goa_provider_get_provider_features (a) & GOA_PROVIDER_FEATURE_BRANDED)
    a_branded = TRUE;
  else
    a_branded = FALSE;

  if (goa_provider_get_provider_features (b) & GOA_PROVIDER_FEATURE_BRANDED)
    b_branded = TRUE;
  else
    b_branded = FALSE;

  /* g_queue_sort() uses a stable sort, so, if we return 0, the order
   * is not changed. */
  if (a_branded == b_branded)
    return 0;
  else if (a_branded)
    return -1;
  else
    return 1;
}

static void
get_all_check_done (GetAllData *data)
{
  if (data->pending_calls > 0)
    return;

  /* Make sure that branded providers come first, but don't change the
   * order otherwise. */
  g_queue_sort (&data->ret, (GCompareDataFunc) compare_providers, NULL);

  /* Steal the list out of the GQueue. */
  g_task_return_pointer (data->task, data->ret.head, free_list_and_unref);

  g_object_unref (data->task);
  g_slice_free (GetAllData, data);
}

static void
get_providers_cb (GObject      *source,
                  GAsyncResult *res,
                  gpointer      user_data)
{
  GoaProviderFactory *factory = GOA_PROVIDER_FACTORY (source);
  GetAllData *data = user_data;
  GList *providers = NULL;
  GList *l;
  GError *error = NULL;

  if (!goa_provider_factory_get_providers_finish (factory, &providers, res, &error))
    {
      g_critical ("Error getting providers from a factory: %s (%s, %d)",
          error->message,
          g_quark_to_string (error->domain),
          error->code);
      g_clear_error (&error);
      goto out;
    }

  for (l = providers; l != NULL; l = l->next)
    {
      /* Steal the value */
      g_queue_push_tail (&data->ret, l->data);
    }

  g_list_free (providers);

out:
  data->pending_calls--;
  get_all_check_done (data);
}

/**
 * goa_provider_get_all:
 * @callback: The function to call when the request is satisfied.
 * @user_data: Pointer to pass to @callback.
 *
 * Creates a list of all the available #GoaProvider instances.
 *
 * When the result is ready, @callback will be called in the the <link
 * linkend="g-main-context-push-thread-default">thread-default main
 * loop</link> this function was called from. You can then call
 * goa_provider_get_all_finish() to get the result of the operation.
 *
 * See goa_provider_get_for_provider_type() for details on how the providers
 * are found.
 */
void
goa_provider_get_all (GAsyncReadyCallback callback,
                      gpointer            user_data)
{
  GList *extensions;
  GList *l;
  GIOExtensionPoint *extension_point;
  GetAllData *data;
  gint i;

  goa_provider_ensure_builtins_loaded ();

  data = g_slice_new0 (GetAllData);
  data->task = g_task_new (NULL, NULL, callback, user_data);
  g_task_set_source_tag (data->task, goa_provider_get_all);
  g_queue_init (&data->ret);

  /* Load the normal providers. */
  extension_point = g_io_extension_point_lookup (GOA_PROVIDER_EXTENSION_POINT_NAME);
  extensions = g_io_extension_point_get_extensions (extension_point);
  /* TODO: what if there are two extensions with the same name? */
  for (l = extensions, i = 0; l != NULL; l = l->next, i++)
    {
      GIOExtension *extension = l->data;
      /* The extensions are loaded in the reverse order we used in
       * goa_provider_ensure_builtins_loaded, so we need to push
       * extension if front of the already loaded ones. */
      g_queue_push_head (&data->ret, g_object_new (g_io_extension_get_type (extension), NULL));
    }

  /* Load the provider factories and get the dynamic providers out of them. */
  extension_point = g_io_extension_point_lookup (GOA_PROVIDER_FACTORY_EXTENSION_POINT_NAME);
  extensions = g_io_extension_point_get_extensions (extension_point);
  for (l = extensions, i = 0; l != NULL; l = l->next, i++)
    {
      GIOExtension *extension = l->data;
      GoaProviderFactory *factory;

      factory = GOA_PROVIDER_FACTORY (g_object_new (g_io_extension_get_type (extension), NULL));
      goa_provider_factory_get_providers (factory, get_providers_cb, data);
      g_object_unref (factory);
      data->pending_calls++;
    }

  get_all_check_done (data);
}

/**
 * goa_provider_get_all_finish:
 * @out_providers: (out) (transfer full) (element-type GoaProvider):
 * Return location for a list of #GoaProvider instances.
 * @res: A #GAsyncResult obtained from the #GAsyncReadyCallback passed to goa_provider_get_all().
 * @error: Return location for error or %NULL.
 *
 * Finishes an operation started with goa_provider_get_all().
 *
 * Returns: %TRUE if the list was successfully retrieved, %FALSE if @error is set.
 */
gboolean
goa_provider_get_all_finish (GList        **out_providers,
                             GAsyncResult  *result,
                             GError       **error)
{
  GTask *task;
  GList *providers;
  gboolean had_error;

  g_return_val_if_fail (error == NULL || *error == NULL, FALSE);

  g_return_val_if_fail (g_task_is_valid (result, NULL), FALSE);
  task = G_TASK (result);

  g_return_val_if_fail (g_task_get_source_tag (task) == goa_provider_get_all, FALSE);

  /* Workaround for bgo#764163 */
  had_error = g_task_had_error (task);
  providers = g_task_propagate_pointer (task, error);
  if (had_error)
    return FALSE;

  if (out_providers != NULL)
    {
      *out_providers = providers;
      providers = NULL;
    }

  g_list_free_full (providers, g_object_unref);
  return TRUE;
}

/* ---------------------------------------------------------------------------------------------------- */

void
goa_provider_remove_account (GoaProvider          *self,
                             GoaObject            *object,
                             GCancellable         *cancellable,
                             GAsyncReadyCallback   callback,
                             gpointer              user_data)
{
  g_return_if_fail (GOA_IS_PROVIDER (self));
  g_return_if_fail (GOA_IS_OBJECT (object));
  g_return_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable));

  GOA_PROVIDER_GET_CLASS (self)->remove_account (self, object, cancellable, callback, user_data);
}

gboolean
goa_provider_remove_account_finish (GoaProvider   *self,
                                    GAsyncResult  *res,
                                    GError       **error)
{
  g_return_val_if_fail (GOA_IS_PROVIDER (self), FALSE);
  g_return_val_if_fail (G_IS_ASYNC_RESULT (res), FALSE);
  g_return_val_if_fail (error == NULL || *error == NULL, FALSE);

  return GOA_PROVIDER_GET_CLASS (self)->remove_account_finish (self, res, error);
}

/* ---------------------------------------------------------------------------------------------------- */

static void
goa_provider_remove_account_real (GoaProvider          *self,
                                  GoaObject            *object,
                                  GCancellable         *cancellable,
                                  GAsyncReadyCallback   callback,
                                  gpointer              user_data)
{
  GTask *task;

  task = g_task_new (self, cancellable, callback, user_data);
  g_task_set_source_tag (task, goa_provider_remove_account_real);
  g_task_return_boolean (task, TRUE);
  g_object_unref (task);
}

static gboolean
goa_provider_remove_account_finish_real (GoaProvider   *self,
                                         GAsyncResult  *res,
                                         GError       **error)
{
  GTask *task;

  g_return_val_if_fail (g_task_is_valid (res, self), FALSE);
  task = G_TASK (res);

  g_return_val_if_fail (g_task_get_source_tag (task) == goa_provider_remove_account_real, FALSE);

  return g_task_propagate_boolean (task, error);
}

/* ---------------------------------------------------------------------------------------------------- */

/**
 * goa_provider_set_preseed_data:
 * @self: The #GoaProvider
 * @preseed_data: A #GVariant of type a{sv}
 *
 * Sets the #GoaProvider:preseed-data property to feed any information already
 * collected that can be useful when creating a new account.
 *
 * If the @preseed_data #GVariant is floating, it is consumed to allow
 * 'inline' use of the g_variant_new() family of functions.
 *
 * Deprecated: 3.28: This function does nothing.
 */
void
goa_provider_set_preseed_data (GoaProvider *self,
                               GVariant    *preseed_data)
{
}

/**
 * goa_provider_get_preseed_data:
 * @self: The #GoaProvider
 *
 * Gets the #GVariant set through the #GoaProvider:preseed-data property.
 *
 * Returns: (transfer none): A #GVariant that is known to be valid until
 *   the property is overridden or the provider freed.
 *
 * Deprecated: 3.28: This function does nothing.
 */
GVariant *
goa_provider_get_preseed_data (GoaProvider *self)
{
  return NULL;
}

/* ---------------------------------------------------------------------------------------------------- */

GtkWidget *
goa_util_add_row_widget (GtkGrid      *grid,
                         gint          row,
                         const gchar  *label_text,
                         GtkWidget    *widget)
{
  GtkWidget *label;

  g_return_val_if_fail (GTK_IS_GRID (grid), NULL);
  g_return_val_if_fail (GTK_IS_WIDGET (widget), NULL);

  if (label_text != NULL)
    {
      GtkStyleContext *context;

      label = gtk_label_new (label_text);
      context = gtk_widget_get_style_context (label);
      gtk_style_context_add_class (context, GTK_STYLE_CLASS_DIM_LABEL);
      gtk_widget_set_halign (label, GTK_ALIGN_END);
      gtk_widget_set_hexpand (label, TRUE);
      gtk_grid_attach (grid, label, 0, row, 1, 1);
    }

  gtk_grid_attach (grid, widget, 1, row, 3, 1);
  return widget;
}

/* ---------------------------------------------------------------------------------------------------- */

gchar *
goa_util_lookup_keyfile_string (GoaObject    *object,
                                const gchar  *key)
{
  GoaAccount *account;
  GError *error;
  GKeyFile *key_file;
  gchar *path;
  gchar *group;
  gchar *ret;

  ret = NULL;

  account = goa_object_peek_account (object);
  path = g_strdup_printf ("%s/goa-1.0/accounts.conf", g_get_user_config_dir ());
  group = g_strdup_printf ("Account %s", goa_account_get_id (account));

  key_file = g_key_file_new ();
  error = NULL;
  if (!g_key_file_load_from_file (key_file,
                                  path,
                                  G_KEY_FILE_NONE,
                                  &error))
    {
      g_warning ("Error loading keyfile %s: %s (%s, %d)",
                 path,
                 error->message, g_quark_to_string (error->domain), error->code);
      g_error_free (error);
      goto out;
    }
  ret = g_key_file_get_string (key_file,
                               group,
                               key,
                               &error);
  if (ret == NULL)
    {
      /* this is not fatal (think upgrade-path) */
      g_debug ("Error getting value for key %s in group `%s' from keyfile %s: %s (%s, %d)",
               key,
               group,
               path,
               error->message, g_quark_to_string (error->domain), error->code);
      g_error_free (error);
      goto out;
    }

 out:
  g_key_file_unref (key_file);
  g_free (group);
  g_free (path);
  return ret;
}

gboolean
goa_util_lookup_keyfile_boolean (GoaObject    *object,
                                 const gchar  *key)
{
  GoaAccount *account;
  GError *error;
  GKeyFile *key_file;
  gchar *path;
  gchar *group;
  gboolean ret;

  ret = FALSE;

  account = goa_object_peek_account (object);
  path = g_strdup_printf ("%s/goa-1.0/accounts.conf", g_get_user_config_dir ());
  group = g_strdup_printf ("Account %s", goa_account_get_id (account));

  key_file = g_key_file_new ();
  error = NULL;
  if (!g_key_file_load_from_file (key_file,
                                  path,
                                  G_KEY_FILE_NONE,
                                  &error))
    {
      g_warning ("Error loading keyfile %s: %s (%s, %d)",
                 path,
                 error->message, g_quark_to_string (error->domain), error->code);
      g_error_free (error);
      goto out;
    }
  ret = g_key_file_get_boolean (key_file,
                                group,
                                key,
                                &error);
  if (error != NULL)
    {
      /* this is not fatal (think upgrade-path) */
      g_debug ("Error getting boolean value for key %s in group `%s' from keyfile %s: %s (%s, %d)",
               key,
               group,
               path,
               error->message, g_quark_to_string (error->domain), error->code);
      g_error_free (error);
      goto out;
    }

 out:
  g_key_file_unref (key_file);
  g_free (group);
  g_free (path);
  return ret;
}

/* ---------------------------------------------------------------------------------------------------- */

void
goa_util_account_notify_property_cb (GObject *object, GParamSpec *pspec, gpointer user_data)
{
  GoaAccount *account;
  gboolean value;
  const gchar *key;
  const gchar *name;

  g_return_if_fail (GOA_IS_ACCOUNT (object));

  account = GOA_ACCOUNT (object);
  key = user_data;
  name = g_param_spec_get_name (pspec);

  g_object_get (account, name, &value, NULL);
  goa_utils_keyfile_set_boolean (account, key, !value);
}

/* ---------------------------------------------------------------------------------------------------- */

GtkWidget *
goa_util_add_row_switch_from_keyfile_with_blurb (GtkGrid      *grid,
                                                 gint          row,
                                                 GoaObject    *object,
                                                 const gchar  *label_text,
                                                 const gchar  *property,
                                                 const gchar  *blurb)
{
  GoaAccount *account;
  GtkWidget *hbox;
  GtkWidget *switch_;
  gboolean value;

  account = goa_object_peek_account (object);
  g_object_get (account, property, &value, NULL);
  switch_ = gtk_switch_new ();

  if (goa_account_get_attention_needed (account))
    {
      gtk_widget_set_sensitive (switch_, FALSE);
      gtk_switch_set_active (GTK_SWITCH (switch_), FALSE);
    }
  else
    {
      gtk_switch_set_active (GTK_SWITCH (switch_), !value);
      g_object_bind_property (switch_, "active",
                              account, property,
                              G_BINDING_BIDIRECTIONAL | G_BINDING_INVERT_BOOLEAN);
    }

  hbox = gtk_box_new (GTK_ORIENTATION_HORIZONTAL, 6);
  gtk_box_set_homogeneous (GTK_BOX (hbox), FALSE);

  if (blurb != NULL)
    {
      GtkWidget *label;

      label = gtk_label_new_with_mnemonic (blurb);
      gtk_label_set_mnemonic_widget (GTK_LABEL (label), switch_);
      gtk_label_set_width_chars (GTK_LABEL (label), 18);
      gtk_label_set_xalign (GTK_LABEL (label), 0.0);
      gtk_container_add (GTK_CONTAINER (hbox), label);
    }

  gtk_container_add (GTK_CONTAINER (hbox), switch_);
  goa_util_add_row_widget (grid, row, label_text, hbox);
  return switch_;
}