/* -*- mode: C; c-file-style: "gnu"; indent-tabs-mode: nil; -*-
 *
 * Copyright 2024, 2025 GNOME Foundation, Inc.
 *
 * SPDX-License-Identifier: LGPL-2.1-or-later
 *
 * 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.1 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, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 *
 * Authors:
 *  - Philip Withnall <pwithnall@gnome.org>
 */

#include "config.h"

#include <errno.h>
#include <glib.h>
#include <glib-unix.h>
#include <glib/gi18n-lib.h>
#include <gio/gio.h>
#include <libgsystemservice/peer-manager.h>
#include <libmalcontent/manager.h>
#include <libmalcontent-timer/child-timer-service.h>
#include <libmalcontent-timer/time-span.h>
#include <libmalcontent-timer/timer-store.h>
#include <pwd.h>
#include <string.h>
#include <sys/types.h>

#include "child-iface.h"
#include "enums.h"
#include "operation-counter-private.h"


static void mct_child_timer_service_constructed  (GObject      *object);
static void mct_child_timer_service_dispose      (GObject      *object);
static void mct_child_timer_service_get_property (GObject    *object,
                                                  guint       property_id,
                                                  GValue     *value,
                                                  GParamSpec *pspec);
static void mct_child_timer_service_set_property (GObject      *object,
                                                  guint         property_id,
                                                  const GValue *value,
                                                  GParamSpec   *pspec);

static void mct_child_timer_service_method_call (GDBusConnection       *connection,
                                                 const char            *sender,
                                                 const char            *object_path,
                                                 const char            *interface_name,
                                                 const char            *method_name,
                                                 GVariant              *parameters,
                                                 GDBusMethodInvocation *invocation,
                                                 void                  *user_data);
static void mct_child_timer_service_properties_get (MctChildTimerService  *self,
                                                    GDBusConnection       *connection,
                                                    const char            *sender,
                                                    GVariant              *parameters,
                                                    GDBusMethodInvocation *invocation);
static void mct_child_timer_service_properties_set (MctChildTimerService  *self,
                                                    GDBusConnection       *connection,
                                                    const char            *sender,
                                                    GVariant              *parameters,
                                                    GDBusMethodInvocation *invocation);
static void mct_child_timer_service_properties_get_all (MctChildTimerService  *self,
                                                        GDBusConnection       *connection,
                                                        const char            *sender,
                                                        GVariant              *parameters,
                                                        GDBusMethodInvocation *invocation);

static void timer_store_estimated_end_times_changed_cb (MctTimerStore *timer_store,
                                                        const char    *username,
                                                        void          *user_data);
static void policy_manager_session_limits_changed_cb (MctManager *policy_manager,
                                                      uint64_t    uid,
                                                      void       *user_data);
static void mct_child_timer_service_record_usage (MctChildTimerService  *self,
                                                  GDBusConnection       *connection,
                                                  const char            *sender,
                                                  GVariant              *parameters,
                                                  GDBusMethodInvocation *invocation);
static void mct_child_timer_service_get_estimated_times (MctChildTimerService  *self,
                                                         GDBusConnection       *connection,
                                                         const char            *sender,
                                                         GVariant              *parameters,
                                                         GDBusMethodInvocation *invocation);
static void mct_child_timer_service_request_extension (MctChildTimerService  *self,
                                                       GDBusConnection       *connection,
                                                       const char            *sender,
                                                       GVariant              *parameters,
                                                       GDBusMethodInvocation *invocation);

/* These errors do go over the bus, and are registered in mct_child_timer_service_class_init(). */
G_DEFINE_QUARK (MctChildTimerServiceError, mct_child_timer_service_error)

static const gchar *child_timer_service_errors[] =
{
  "org.freedesktop.MalcontentTimer1.Child.Error.InvalidRecord",
  "org.freedesktop.MalcontentTimer1.Child.Error.StorageError",
  "org.freedesktop.MalcontentTimer1.Child.Error.Busy",
  "org.freedesktop.MalcontentTimer1.Child.Error.IdentifyingUser",
  "org.freedesktop.MalcontentTimer1.Child.Error.QueryingPolicy",
  "org.freedesktop.MalcontentTimer1.Child.Error.Disabled",
  "org.freedesktop.MalcontentTimer1.Child.Error.CommunicatingWithAgent",
  "org.freedesktop.MalcontentTimer1.Child.Error.RequestCancelled",
};
static const GDBusErrorEntry child_timer_service_error_map[] =
  {
    { MCT_CHILD_TIMER_SERVICE_ERROR_INVALID_RECORD, "org.freedesktop.MalcontentTimer1.Child.Error.InvalidRecord" },
    { MCT_CHILD_TIMER_SERVICE_ERROR_STORAGE_ERROR, "org.freedesktop.MalcontentTimer1.Child.Error.StorageError" },
    { MCT_CHILD_TIMER_SERVICE_ERROR_BUSY, "org.freedesktop.MalcontentTimer1.Child.Error.Busy" },
    { MCT_CHILD_TIMER_SERVICE_ERROR_IDENTIFYING_USER, "org.freedesktop.MalcontentTimer1.Child.Error.IdentifyingUser" },
    { MCT_CHILD_TIMER_SERVICE_ERROR_QUERYING_POLICY, "org.freedesktop.MalcontentTimer1.Child.Error.QueryingPolicy" },
    { MCT_CHILD_TIMER_SERVICE_ERROR_DISABLED, "org.freedesktop.MalcontentTimer1.Child.Error.Disabled" },
    { MCT_CHILD_TIMER_SERVICE_ERROR_COMMUNICATING_WITH_AGENT, "org.freedesktop.MalcontentTimer1.Child.Error.CommunicatingWithAgent" },
    { MCT_CHILD_TIMER_SERVICE_ERROR_REQUEST_CANCELLED, "org.freedesktop.MalcontentTimer1.Child.Error.RequestCancelled" },

  };
G_STATIC_ASSERT (G_N_ELEMENTS (child_timer_service_error_map) == MCT_CHILD_TIMER_SERVICE_N_ERRORS);
G_STATIC_ASSERT (G_N_ELEMENTS (child_timer_service_error_map) == G_N_ELEMENTS (child_timer_service_errors));

/**
 * MctChildTimerService:
 *
 * An implementation of the `org.freedesktop.MalcontentTimer1.Child` D-Bus
 * interface, allowing a trusted component in a child user account’s session to
 * record screen time and app usage periods for that account.
 *
 * This will expose all the necessary objects on the bus for peers to interact
 * with them, and hooks them up to internal state management using
 * [property@Malcontent.ChildTimerService:timer-store].
 *
 * Since: 0.14.0
 */
struct _MctChildTimerService
{
  GObject parent;

  GDBusConnection *connection;  /* (owned) */
  char *object_path;  /* (owned) */
  unsigned int object_id;

  /* Used to cancel any pending operations when the object is unregistered. */
  GCancellable *cancellable;  /* (owned) */

  MctTimerStore *timer_store;  /* (owned) */
  unsigned long timer_store_estimated_end_times_changed_id;
  GssPeerManager *peer_manager;  /* (owned) */
  MctManager *policy_manager;  /* (owned) */
  unsigned long policy_manager_session_limits_changed_id;
  unsigned int n_pending_operations;

  /* Communication with the extension request agent */
  GPtrArray *pending_extension_agent_requests;  /* (owned) (element-type RequestExtensionData) */
};

typedef enum
{
  PROP_CONNECTION = 1,
  PROP_OBJECT_PATH,
  PROP_TIMER_STORE,
  PROP_PEER_MANAGER,
  PROP_POLICY_MANAGER,
  PROP_BUSY,
} MctChildTimerServiceProperty;

static GParamSpec *props[PROP_BUSY + 1] = { NULL, };

G_DEFINE_TYPE (MctChildTimerService, mct_child_timer_service, G_TYPE_OBJECT)

typedef struct
{
  MctChildTimerService *child_timer_service;  /* (owned) */
  GDBusMethodInvocation *invocation;  /* (owned) */
  MctOperationCounter operation_counter;
  char *record_type;  /* (not nullable) */
  char *identifier;  /* (not nullable) */
  uint64_t duration_secs;
  GVariant *extra_data;  /* (owned) */
  uid_t subject_uid;
  char *cookie_path;  /* (owned) */
  GDBusConnection *connection;  /* (nullable) (owned) */
  unsigned int request_subscribe_id;
  unsigned long cancelled_id;
  unsigned int agent_name_watch_id;
  unsigned int client_name_watch_id;
  char *request_object_path;  /* (owned) (nullable) */
  gboolean response_granted;
  GVariant *response_extra_data;  /* (owned) (nullable) */
} RequestExtensionData;

static void
request_extension_data_free (RequestExtensionData *data)
{
  if (data->request_subscribe_id != 0)
    g_dbus_connection_signal_unsubscribe (data->connection, data->request_subscribe_id);
  data->request_subscribe_id = 0;
  g_clear_object (&data->connection);

  if (data->cancelled_id != 0)
    g_cancellable_disconnect (data->child_timer_service->cancellable, data->cancelled_id);
  data->cancelled_id = 0;

  if (data->agent_name_watch_id != 0)
    g_bus_unwatch_name (data->agent_name_watch_id);
  data->agent_name_watch_id = 0;

  if (data->client_name_watch_id != 0)
    g_bus_unwatch_name (data->client_name_watch_id);
  data->client_name_watch_id = 0;

  g_clear_object (&data->invocation);
  g_clear_object (&data->child_timer_service);
  mct_operation_counter_release_and_clear (&data->operation_counter);
  g_clear_pointer (&data->extra_data, g_variant_unref);
  g_clear_pointer (&data->cookie_path, g_free);
  g_clear_pointer (&data->request_object_path, g_free);
  g_clear_pointer (&data->record_type, g_free);
  g_clear_pointer (&data->identifier, g_free);
  g_clear_pointer (&data->response_extra_data, g_variant_unref);
  g_free (data);
}

G_DEFINE_AUTOPTR_CLEANUP_FUNC (RequestExtensionData, request_extension_data_free)

static void
mct_child_timer_service_class_init (MctChildTimerServiceClass *klass)
{
  GObjectClass *object_class = (GObjectClass *) klass;

  object_class->constructed = mct_child_timer_service_constructed;
  object_class->dispose = mct_child_timer_service_dispose;
  object_class->get_property = mct_child_timer_service_get_property;
  object_class->set_property = mct_child_timer_service_set_property;

  /**
   * MctChildTimerService:connection:
   *
   * D-Bus connection to export objects on.
   *
   * Since: 0.14.0
   */
  props[PROP_CONNECTION] =
      g_param_spec_object ("connection", NULL, NULL,
                           G_TYPE_DBUS_CONNECTION,
                           G_PARAM_READWRITE |
                           G_PARAM_CONSTRUCT_ONLY |
                           G_PARAM_STATIC_STRINGS);

  /**
   * MctChildTimerService:object-path:
   *
   * Object path to root all exported objects at. If this does not end in a
   * slash, one will be added.
   *
   * Since: 0.14.0
   */
  props[PROP_OBJECT_PATH] =
      g_param_spec_string ("object-path", NULL, NULL,
                           "/",
                           G_PARAM_READWRITE |
                           G_PARAM_CONSTRUCT_ONLY |
                           G_PARAM_STATIC_STRINGS);

  /**
   * MctChildTimerService:timer-store:
   *
   * Store for timer data.
   *
   * Since: 0.14.0
   */
  props[PROP_TIMER_STORE] =
      g_param_spec_object ("timer-store", NULL, NULL,
                           MCT_TYPE_TIMER_STORE,
                           G_PARAM_READWRITE |
                           G_PARAM_CONSTRUCT_ONLY |
                           G_PARAM_STATIC_STRINGS);

  /**
   * MctChildTimerService:peer-manager:
   *
   * Peer manager to identify incoming method calls.
   *
   * Since: 0.14.0
   */
  props[PROP_PEER_MANAGER] =
      g_param_spec_object ("peer-manager", NULL, NULL,
                           GSS_TYPE_PEER_MANAGER,
                           G_PARAM_READWRITE |
                           G_PARAM_CONSTRUCT_ONLY |
                           G_PARAM_STATIC_STRINGS);

  /**
   * MctChildTimerService:policy-manager:
   *
   * Policy manager to provide users’ parental controls policies.
   *
   * Since: 0.14.0
   */
  props[PROP_POLICY_MANAGER] =
      g_param_spec_object ("policy-manager", NULL, NULL,
                           MCT_TYPE_MANAGER,
                           G_PARAM_READWRITE |
                           G_PARAM_CONSTRUCT_ONLY |
                           G_PARAM_STATIC_STRINGS);

  /**
   * MctChildTimerService:busy:
   *
   * True if the D-Bus API is busy.
   *
   * For example, if there are any outstanding method calls which haven’t been
   * replied to yet.
   *
   * Since: 0.14.0
   */
  props[PROP_BUSY] =
      g_param_spec_boolean ("busy", NULL, NULL,
                            FALSE,
                            G_PARAM_READABLE |
                            G_PARAM_STATIC_STRINGS);

  g_object_class_install_properties (object_class, G_N_ELEMENTS (props), props);

  /* Error domain registration for D-Bus. We do this here, rather than in a
   * #GOnce section in mct_child_timer_service_error_quark(), to avoid spreading the
   * D-Bus code outside this file. */
  for (size_t i = 0; i < G_N_ELEMENTS (child_timer_service_error_map); i++)
    g_dbus_error_register_error (MCT_CHILD_TIMER_SERVICE_ERROR,
                                 child_timer_service_error_map[i].error_code,
                                 child_timer_service_error_map[i].dbus_error_name);
}

static void
mct_child_timer_service_init (MctChildTimerService *self)
{
  self->cancellable = g_cancellable_new ();
  self->pending_extension_agent_requests = g_ptr_array_new_with_free_func ((GDestroyNotify) request_extension_data_free);
}

static void
mct_child_timer_service_constructed (GObject *object)
{
  MctChildTimerService *self = MCT_CHILD_TIMER_SERVICE (object);

  /* Chain up. */
  G_OBJECT_CLASS (mct_child_timer_service_parent_class)->constructed (object);

  /* Check our construct properties. */
  g_assert (G_IS_DBUS_CONNECTION (self->connection));
  g_assert (g_variant_is_object_path (self->object_path));
  g_assert (MCT_IS_TIMER_STORE (self->timer_store));
  g_assert (GSS_IS_PEER_MANAGER (self->peer_manager));
  g_assert (MCT_IS_MANAGER (self->policy_manager));

  /* Connect to signals on the timer store and policy manager. */
  self->timer_store_estimated_end_times_changed_id =
      g_signal_connect (self->timer_store, "estimated-end-times-changed",
                        G_CALLBACK (timer_store_estimated_end_times_changed_cb),
                        self);
  self->policy_manager_session_limits_changed_id =
      g_signal_connect (self->policy_manager, "session-limits-changed",
                        G_CALLBACK (policy_manager_session_limits_changed_cb),
                        self);
}

static void
mct_child_timer_service_dispose (GObject *object)
{
  MctChildTimerService *self = MCT_CHILD_TIMER_SERVICE (object);

  g_cancellable_cancel (self->cancellable);
  g_clear_object (&self->cancellable);

  g_assert (self->object_id == 0);
  g_assert (self->n_pending_operations == 0);
  g_assert (self->pending_extension_agent_requests->len == 0);

  g_clear_pointer (&self->pending_extension_agent_requests, g_ptr_array_unref);

  g_clear_signal_handler (&self->policy_manager_session_limits_changed_id, self->policy_manager);
  g_clear_object (&self->policy_manager);
  g_clear_object (&self->peer_manager);
  g_clear_signal_handler (&self->timer_store_estimated_end_times_changed_id, self->timer_store);
  g_clear_object (&self->timer_store);

  g_clear_object (&self->connection);
  g_clear_pointer (&self->object_path, g_free);

  /* Chain up to the parent class */
  G_OBJECT_CLASS (mct_child_timer_service_parent_class)->dispose (object);
}

static void
mct_child_timer_service_get_property (GObject    *object,
                                      guint       property_id,
                                      GValue     *value,
                                      GParamSpec *pspec)
{
  MctChildTimerService *self = MCT_CHILD_TIMER_SERVICE (object);

  switch ((MctChildTimerServiceProperty) property_id)
    {
    case PROP_CONNECTION:
      g_value_set_object (value, self->connection);
      break;
    case PROP_OBJECT_PATH:
      g_value_set_string (value, self->object_path);
      break;
    case PROP_TIMER_STORE:
      g_value_set_object (value, self->timer_store);
      break;
    case PROP_PEER_MANAGER:
      g_value_set_object (value, self->peer_manager);
      break;
    case PROP_POLICY_MANAGER:
      g_value_set_object (value, self->policy_manager);
      break;
    case PROP_BUSY:
      g_value_set_boolean (value, mct_child_timer_service_get_busy (self));
      break;
    default:
      g_assert_not_reached ();
    }
}

static void
mct_child_timer_service_set_property (GObject      *object,
                                      guint         property_id,
                                      const GValue *value,
                                      GParamSpec   *pspec)
{
  MctChildTimerService *self = MCT_CHILD_TIMER_SERVICE (object);

  switch ((MctChildTimerServiceProperty) property_id)
    {
    case PROP_CONNECTION:
      /* Construct only. */
      g_assert (self->connection == NULL);
      self->connection = g_value_dup_object (value);
      break;
    case PROP_OBJECT_PATH:
      /* Construct only. */
      g_assert (self->object_path == NULL);
      g_assert (g_variant_is_object_path (g_value_get_string (value)));
      self->object_path = g_value_dup_string (value);
      break;
    case PROP_TIMER_STORE:
      /* Construct only. */
      g_assert (self->timer_store == NULL);
      self->timer_store = g_value_dup_object (value);
      break;
    case PROP_PEER_MANAGER:
      /* Construct only. */
      g_assert (self->peer_manager == NULL);
      self->peer_manager = g_value_dup_object (value);
      break;
    case PROP_POLICY_MANAGER:
      /* Construct only. */
      g_assert (self->policy_manager == NULL);
      self->policy_manager = g_value_dup_object (value);
      break;
    case PROP_BUSY:
      /* Read only. Fall through. */
      G_GNUC_FALLTHROUGH;
    default:
      g_assert_not_reached ();
    }
}

static void
emit_estimated_times_changed (MctChildTimerService *self)
{
  /* Ignore errors from emitting the signal; it can only fail if the parameters
   * are invalid (not possible) or if the connection has been closed. */
  g_dbus_connection_emit_signal (self->connection,
                                 NULL,  /* destination bus name */
                                 self->object_path,
                                 "org.freedesktop.MalcontentTimer1.Child",
                                 "EstimatedTimesChanged",
                                 NULL,
                                 NULL);
}

static void
timer_store_estimated_end_times_changed_cb (MctTimerStore *timer_store,
                                            const char    *username,
                                            void          *user_data)
{
  MctChildTimerService *self = MCT_CHILD_TIMER_SERVICE (user_data);

  emit_estimated_times_changed (self);
}

static void
policy_manager_session_limits_changed_cb (MctManager *policy_manager,
                                          uint64_t    uid,
                                          void       *user_data)
{
  MctChildTimerService *self = MCT_CHILD_TIMER_SERVICE (user_data);

  /* A change in the session limits policy will _probably_ result in the
   * estimated end time changing. It might not, though, so this signal will be
   * over-emitted. Simpler than calculating exactly whether it has changed,
   * though, as that would require retaining state from the last time it was
   * emitted.
   *
   * Note that malcontent-timerd may not even be running when the session limits
   * policy is changed, so this signal is not _guaranteed_ to be emitted.
   * Clients should also listen to MctManager::session-limits-changed
   * themselves. */
  emit_estimated_times_changed (self);
}

/**
 * mct_child_timer_service_register:
 * @self: a child service
 * @error: return location for a [type@GLib.Error]
 *
 * Register the child timer service objects on D-Bus using the connection
 * details given in [property@Malcontent.ChildTimerService.connection] and
 * [property@Malcontent.ChildTimerService.object-path].
 *
 * Use [method@Malcontent.ChildTimerService.unregister] to unregister them.
 * Calls to these two functions must be well paired.
 *
 * Returns: true on success, false otherwise
 * Since: 0.14.0
 */
gboolean
mct_child_timer_service_register (MctChildTimerService  *self,
                                  GError               **error)
{
  g_return_val_if_fail (MCT_IS_CHILD_TIMER_SERVICE (self), FALSE);
  g_return_val_if_fail (error == NULL || *error == NULL, FALSE);

  const GDBusInterfaceVTable interface_vtable =
    {
      mct_child_timer_service_method_call,
      NULL,  /* handled in mct_child_timer_service_method_call() */
      NULL,  /* handled in mct_child_timer_service_method_call() */
      { NULL, },  /* padding */
    };

  guint id = g_dbus_connection_register_object (self->connection,
                                                self->object_path,
                                                (GDBusInterfaceInfo *) &org_freedesktop_malcontent_timer1_child_interface,
                                                &interface_vtable,
                                                g_object_ref (self),
                                                g_object_unref,
                                                error);

  if (id == 0)
    return FALSE;

  self->object_id = id;

  /* This has potentially changed. */
  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_BUSY]);

  return TRUE;
}

/**
 * mct_child_timer_service_unregister:
 * @self: a child service
 *
 * Unregister objects from D-Bus which were previously registered using
 * [method@Malcontent.ChildTimerService.register].
 *
 * Calls to these two functions must be well paired.
 *
 * Since: 0.14.0
 */
void
mct_child_timer_service_unregister (MctChildTimerService *self)
{
  g_return_if_fail (MCT_IS_CHILD_TIMER_SERVICE (self));

  g_cancellable_cancel (self->cancellable);

  g_dbus_connection_unregister_object (self->connection, self->object_id);
  self->object_id = 0;

  /* This has potentially changed. */
  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_BUSY]);
}

static char *
lookup_username (uid_t    uid,
                 GError **error)
{
  char buffer[4096];
  struct passwd pwbuf;
  struct passwd *result;
  int pwuid_errno;
  g_autofree char *username = NULL;
  g_autoptr(GError) local_error = NULL;

  pwuid_errno = getpwuid_r (uid, &pwbuf, buffer, sizeof (buffer), &result);

  if (result != NULL &&
      result->pw_name != NULL && result->pw_name[0] != '\0')
    {
      username = g_locale_to_utf8 (result->pw_name, -1, NULL, NULL, &local_error);
      if (username == NULL)
        {
          g_set_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_DATA,
                       _("Error getting details for UID %d: %s"),
                       (int) uid, local_error->message);
          return NULL;
        }
    }
  else if (result != NULL)
    {
      username = g_strdup_printf ("%d", (int) uid);
    }
  else if (pwuid_errno == 0)
    {
      /* User not found. */
      return NULL;
    }
  else
    {
      /* Error calling getpwuid_r(). */
      g_set_error (error, G_IO_ERROR, g_io_error_from_errno (pwuid_errno),
                   _("Error getting details for UID %d: %s"),
                   (int) uid, g_strerror (pwuid_errno));
      return NULL;
    }

  return g_steal_pointer (&username);
}

static char *
lookup_username_for_peer_invocation (GssPeerManager        *peer_manager,
                                     GDBusMethodInvocation *invocation,
                                     uid_t                 *out_uid)
{
  uid_t uid;
  g_autoptr(GError) local_error = NULL;
  g_autofree char *username = NULL;

  uid = gss_peer_manager_get_peer_uid (peer_manager, g_dbus_method_invocation_get_sender (invocation));
  if (uid != 0 && uid != (uid_t) -1)
    username = lookup_username (uid, &local_error);
  if (local_error != NULL)
    g_debug ("Error looking up username for UID %d: %s", (int) uid, local_error->message);

  if (out_uid != NULL)
    *out_uid = uid;

  return g_steal_pointer (&username);
}

static gboolean
validate_dbus_interface_name (GDBusMethodInvocation *invocation,
                              const gchar           *interface_name)
{
  if (!g_dbus_is_interface_name (interface_name))
    {
      g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR,
                                             G_DBUS_ERROR_UNKNOWN_INTERFACE,
                                             _("Invalid interface name ‘%s’."),
                                             interface_name);
      return FALSE;
    }

  return TRUE;
}

static gboolean
validate_record_type (GDBusMethodInvocation *invocation,
                      const char            *record_type)
{
  g_autoptr(GError) local_error = NULL;

  if (!mct_timer_store_record_type_validate_string (record_type, &local_error))
    {
      g_dbus_method_invocation_return_error (invocation, MCT_CHILD_TIMER_SERVICE_ERROR,
                                             MCT_CHILD_TIMER_SERVICE_ERROR_INVALID_RECORD,
                                             _("Invalid usage parameters: %s"),
                                             local_error->message);
      return FALSE;
    }

  return TRUE;
}

static gboolean
validate_record_type_and_identifier (GDBusMethodInvocation *invocation,
                                     const char            *record_type,
                                     const char            *identifier)
{
  g_autoptr(GError) local_error = NULL;

  if (!mct_timer_store_record_type_validate_string (record_type, &local_error) ||
      !mct_timer_store_record_type_validate_identifier (mct_timer_store_record_type_from_string (record_type), identifier, &local_error))
    {
      g_dbus_method_invocation_return_error (invocation, MCT_CHILD_TIMER_SERVICE_ERROR,
                                             MCT_CHILD_TIMER_SERVICE_ERROR_INVALID_RECORD,
                                             _("Invalid usage parameters: %s"),
                                             local_error->message);
      return FALSE;
    }

  return TRUE;
}

typedef void (*ChildMethodCallFunc) (MctChildTimerService  *self,
                                     GDBusConnection       *connection,
                                     const char            *sender,
                                     GVariant              *parameters,
                                     GDBusMethodInvocation *invocation);

static const struct
  {
    const char *interface_name;
    const char *method_name;
    ChildMethodCallFunc func;
  }
child_methods[] =
  {
    /* Handle properties. */
    { "org.freedesktop.DBus.Properties", "Get",
      mct_child_timer_service_properties_get },
    { "org.freedesktop.DBus.Properties", "Set",
      mct_child_timer_service_properties_set },
    { "org.freedesktop.DBus.Properties", "GetAll",
      mct_child_timer_service_properties_get_all },

    /* Child methods. */
    { "org.freedesktop.MalcontentTimer1.Child", "RecordUsage",
      mct_child_timer_service_record_usage },
    { "org.freedesktop.MalcontentTimer1.Child", "GetEstimatedTimes",
      mct_child_timer_service_get_estimated_times },
    { "org.freedesktop.MalcontentTimer1.Child", "RequestExtension",
      mct_child_timer_service_request_extension },
  };

static void
mct_child_timer_service_method_call (GDBusConnection       *connection,
                                     const char            *sender,
                                     const char            *object_path,
                                     const char            *interface_name,
                                     const char            *method_name,
                                     GVariant              *parameters,
                                     GDBusMethodInvocation *invocation,
                                     void                  *user_data)
{
  MctChildTimerService *self = MCT_CHILD_TIMER_SERVICE (user_data);

  /* Check we’ve implemented all the methods. Unfortunately this can’t be a
   * compile time check because the method array is declared in a separate
   * compilation unit. */
  size_t n_child_interface_methods = 0;
  for (size_t i = 0; org_freedesktop_malcontent_timer1_child_interface.methods[i] != NULL; i++)
    n_child_interface_methods++;

  g_assert (G_N_ELEMENTS (child_methods) ==
            n_child_interface_methods +
            3  /* o.fdo.DBus.Properties */);

  /* Remove the service prefix from the path. */
  g_assert (g_str_equal (object_path, self->object_path));

  /* Work out which method to call. */
  for (gsize i = 0; i < G_N_ELEMENTS (child_methods); i++)
    {
      if (g_str_equal (child_methods[i].interface_name, interface_name) &&
          g_str_equal (child_methods[i].method_name, method_name))
        {
          child_methods[i].func (self, connection, sender, parameters, invocation);
          return;
        }
    }

  /* Make sure we actually called a method implementation. GIO guarantees that
   * this function is only called with methods we’ve declared in the interface
   * info, so this should never fail. */
  g_assert_not_reached ();
}

static void
mct_child_timer_service_properties_get (MctChildTimerService  *self,
                                        GDBusConnection       *connection,
                                        const char            *sender,
                                        GVariant              *parameters,
                                        GDBusMethodInvocation *invocation)
{
  const char *interface_name, *property_name;
  g_variant_get (parameters, "(&s&s)", &interface_name, &property_name);

  /* D-Bus property names can be anything. */
  if (!validate_dbus_interface_name (invocation, interface_name))
    return;

  /* No properties exposed. */
  g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR,
                                         G_DBUS_ERROR_UNKNOWN_PROPERTY,
                                         _("Unknown property ‘%s.%s’."),
                                         interface_name, property_name);
  }

static void
mct_child_timer_service_properties_set (MctChildTimerService  *self,
                                        GDBusConnection       *connection,
                                        const char            *sender,
                                        GVariant              *parameters,
                                        GDBusMethodInvocation *invocation)
{
  const char *interface_name, *property_name;
  g_variant_get (parameters, "(&s&sv)", &interface_name, &property_name, NULL);

  /* D-Bus property names can be anything. */
  if (!validate_dbus_interface_name (invocation, interface_name))
    return;

  /* No properties exposed. */
  g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR,
                                         G_DBUS_ERROR_UNKNOWN_PROPERTY,
                                         _("Unknown property ‘%s.%s’."),
                                         interface_name, property_name);
}

static void
mct_child_timer_service_properties_get_all (MctChildTimerService  *self,
                                            GDBusConnection       *connection,
                                            const char            *sender,
                                            GVariant              *parameters,
                                            GDBusMethodInvocation *invocation)
{
  const char *interface_name;
  g_variant_get (parameters, "(&s)", &interface_name);

  if (!validate_dbus_interface_name (invocation, interface_name))
    return;

  /* Try the interface. */
  if (g_str_equal (interface_name, "org.freedesktop.MalcontentTimer1.Child"))
    g_dbus_method_invocation_return_value (invocation,
                                           g_variant_new_parsed ("(@a{sv} {},)"));
  else
    g_dbus_method_invocation_return_error (invocation, G_DBUS_ERROR,
                                           G_DBUS_ERROR_UNKNOWN_INTERFACE,
                                           _("Unknown interface ‘%s’."),
                                           interface_name);
}

typedef struct
{
  MctChildTimerService *child_timer_service;  /* (owned) */
  GHashTable *time_spans_hash;  /* (owned) (element-type utf8 GPtrArray<MctTimeSpan>) */
  GDBusMethodInvocation *invocation;  /* (owned) */
  uint64_t now_secs;
  MctOperationCounter operation_counter;
} RecordUsageData;

static void
record_usage_data_free (RecordUsageData *data)
{
  g_clear_object (&data->invocation);
  g_clear_pointer (&data->time_spans_hash, g_hash_table_unref);
  g_clear_object (&data->child_timer_service);
  mct_operation_counter_release_and_clear (&data->operation_counter);
  g_free (data);
}

G_DEFINE_AUTOPTR_CLEANUP_FUNC (RecordUsageData, record_usage_data_free)

static void record_usage_ensure_credentials_cb (GObject      *object,
                                                GAsyncResult *result,
                                                void         *user_data);
static void record_usage_open_username_cb (GObject      *object,
                                           GAsyncResult *result,
                                           void         *user_data);
static void record_usage_save_transaction_cb (GObject      *object,
                                              GAsyncResult *result,
                                              void         *user_data);

static void
mct_child_timer_service_record_usage (MctChildTimerService  *self,
                                      GDBusConnection       *connection,
                                      const char            *sender,
                                      GVariant              *parameters,
                                      GDBusMethodInvocation *invocation)
{
  g_autoptr(GError) local_error = NULL;
  g_autoptr(GVariantIter) entries_iter = NULL;
  uint64_t now_secs, start_time, end_time;
  const char *record_type, *identifier;
  g_autoptr(GHashTable) time_spans_hash = NULL;
  GPtrArray *time_spans;  /* (element-type MctTimeSpan) */
  g_autoptr(RecordUsageData) data = NULL;

  /* Validate the parameters and add them to the timer store. */
  g_variant_get (parameters, "(a(ttss))", &entries_iter);
  time_spans_hash = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, (GDestroyNotify) g_ptr_array_unref);
  now_secs = (uint64_t) g_get_real_time () / G_USEC_PER_SEC;

  /* Validate each entry. */
  while (g_variant_iter_loop (entries_iter, "(tt&s&s)", &start_time, &end_time, &record_type, &identifier))
    {
      if (start_time > end_time)
        {
          g_dbus_method_invocation_return_error (invocation, MCT_CHILD_TIMER_SERVICE_ERROR,
                                                 MCT_CHILD_TIMER_SERVICE_ERROR_INVALID_RECORD,
                                                 _("Invalid usage parameters: %s"),
                                                 _("Times out of order"));
          return;
        }
      if (end_time > now_secs)
        {
          g_dbus_method_invocation_return_error (invocation, MCT_CHILD_TIMER_SERVICE_ERROR,
                                                 MCT_CHILD_TIMER_SERVICE_ERROR_INVALID_RECORD,
                                                 _("Invalid usage parameters: %s"),
                                                 _("End time in the future"));
          return;
        }
      if (!validate_record_type_and_identifier (invocation, record_type, identifier))
        return;

      /* Add to the in-progress data. */
      time_spans = g_hash_table_lookup (time_spans_hash, identifier);
      if (time_spans == NULL)
        {
          time_spans = g_ptr_array_new_with_free_func ((GDestroyNotify) mct_time_span_free);
          g_hash_table_insert (time_spans_hash, (void *) identifier, time_spans);
        }
      g_ptr_array_add (time_spans, mct_time_span_new (start_time, end_time));
    }

  if (g_hash_table_size (time_spans_hash) == 0)
    {
      g_dbus_method_invocation_return_error (invocation, MCT_CHILD_TIMER_SERVICE_ERROR,
                                             MCT_CHILD_TIMER_SERVICE_ERROR_INVALID_RECORD,
                                             _("Invalid usage parameters: %s"),
                                             _("No entries"));
      return;
    }

  /* Load the peer’s credentials so we know which user the usage entries come
   * from. */
  data = g_new0 (RecordUsageData, 1);
  data->child_timer_service = g_object_ref (self);
  data->time_spans_hash = g_steal_pointer (&time_spans_hash);
  data->invocation = g_object_ref (invocation);
  data->now_secs = now_secs;
  mct_operation_counter_init_and_hold (&data->operation_counter,
                                       &self->n_pending_operations,
                                       G_OBJECT (self), props[PROP_BUSY]);

  gss_peer_manager_ensure_peer_credentials_async (self->peer_manager,
                                                  sender,
                                                  self->cancellable,
                                                  record_usage_ensure_credentials_cb,
                                                  g_steal_pointer (&data));
}

static void
record_usage_ensure_credentials_cb (GObject      *object,
                                    GAsyncResult *result,
                                    void         *user_data)
{
  GssPeerManager *peer_manager = GSS_PEER_MANAGER (object);
  g_autoptr(RecordUsageData) data = g_steal_pointer (&user_data);
  MctChildTimerService *self = data->child_timer_service;
  GDBusMethodInvocation *invocation = data->invocation;
  g_autofree char *username = NULL;
  g_autoptr(GError) local_error = NULL;

  /* Finish looking up the sender. */
  if (!gss_peer_manager_ensure_peer_credentials_finish (peer_manager,
                                                        result, &local_error))
    {
      g_dbus_method_invocation_return_error (invocation, MCT_CHILD_TIMER_SERVICE_ERROR,
                                             MCT_CHILD_TIMER_SERVICE_ERROR_IDENTIFYING_USER,
                                             _("Error identifying user: %s"),
                                             local_error->message);
      return;
    }

  username = lookup_username_for_peer_invocation (peer_manager, invocation, NULL);
  if (username == NULL)
    {
      g_dbus_method_invocation_return_error (invocation, MCT_CHILD_TIMER_SERVICE_ERROR,
                                             MCT_CHILD_TIMER_SERVICE_ERROR_IDENTIFYING_USER,
                                             _("Error identifying user: %s"),
                                             _("Invalid or unknown user"));
      return;
    }

  mct_timer_store_open_username_async (self->timer_store,
                                       username,
                                       self->cancellable,
                                       record_usage_open_username_cb,
                                       g_steal_pointer (&data));
}

static void
record_usage_open_username_cb (GObject      *object,
                               GAsyncResult *result,
                               void         *user_data)
{
  MctTimerStore *timer_store = MCT_TIMER_STORE (object);
  g_autoptr(RecordUsageData) data = g_steal_pointer (&user_data);
  MctChildTimerService *self = data->child_timer_service;
  GDBusMethodInvocation *invocation = data->invocation;
  GHashTable *time_spans_hash = data->time_spans_hash;
  const MctTimerStoreTransaction *transaction;
  GHashTableIter time_spans_hash_iter;
  const char *identifier;
  GPtrArray *time_spans;  /* (element-type MctTimeSpan) */
  uint64_t expiry_cutoff_secs;
  g_autoptr(GError) local_error = NULL;

  transaction = mct_timer_store_open_username_finish (timer_store, result, &local_error);

  if (g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_BUSY))
    {
      g_dbus_method_invocation_return_error (invocation, MCT_CHILD_TIMER_SERVICE_ERROR,
                                             MCT_CHILD_TIMER_SERVICE_ERROR_BUSY,
                                             _("Error opening user file: %s"),
                                             local_error->message);
      return;
    }
  else if (local_error != NULL)  /* likely a GFileError */
    {
      g_dbus_method_invocation_return_error (invocation, MCT_CHILD_TIMER_SERVICE_ERROR,
                                             MCT_CHILD_TIMER_SERVICE_ERROR_STORAGE_ERROR,
                                             _("Error opening user file: %s"),
                                             local_error->message);
      return;
    }

  /* Now all the time spans have been validated, add them to the timer store. */
  g_hash_table_iter_init (&time_spans_hash_iter, time_spans_hash);
  while (g_hash_table_iter_next (&time_spans_hash_iter, (void **) &identifier, (void **) &time_spans))
    {
      MctTimerStoreRecordType record_type;

      /* For the moment, we can rely on the fact that the identifier spaces for
       * the two current record types are disjoint, so we don’t have to
       * explicitly pass the @record_type via the @time_spans_hash. We may have
       * to in future, though, if more record types are added. */
      if (*identifier == '\0')
        record_type = MCT_TIMER_STORE_RECORD_TYPE_LOGIN_SESSION;
      else
        record_type = MCT_TIMER_STORE_RECORD_TYPE_APP;

      mct_timer_store_add_time_spans (timer_store, transaction, record_type, identifier,
                                      (const MctTimeSpan * const *) time_spans->pdata,
                                      time_spans->len);
    }

  /* Expire old entries when saving.
   * FIXME: Make this configurable */
  expiry_cutoff_secs = data->now_secs - 4 * 7 * 24 * 60 * 60;

  /* Save the changes. */
  mct_timer_store_save_transaction_async (timer_store,
                                          transaction,
                                          expiry_cutoff_secs,
                                          self->cancellable,
                                          record_usage_save_transaction_cb,
                                          g_steal_pointer (&data));
}

static void
record_usage_save_transaction_cb (GObject      *object,
                                  GAsyncResult *result,
                                  void         *user_data)
{
  MctTimerStore *timer_store = MCT_TIMER_STORE (object);
  g_autoptr(RecordUsageData) data = g_steal_pointer (&user_data);
  GDBusMethodInvocation *invocation = data->invocation;
  g_autoptr(GError) local_error = NULL;

  if (!mct_timer_store_save_transaction_finish (timer_store, result, &local_error))
    {
      /* The error is likely a GIOError */
      g_dbus_method_invocation_return_error (invocation, MCT_CHILD_TIMER_SERVICE_ERROR,
                                             MCT_CHILD_TIMER_SERVICE_ERROR_STORAGE_ERROR,
                                             _("Error saving user file: %s"), local_error->message);
    }
  else
    {
      g_dbus_method_invocation_return_value (invocation, NULL);
    }
}

typedef struct
{
  MctChildTimerService *child_timer_service;  /* (owned) */
  GDBusMethodInvocation *invocation;  /* (owned) */
  MctTimerStoreRecordType record_type;
  char *username;  /* (nullable) (owned) */
  MctSessionLimits *session_limits_policy;  /* (nullable) (owned) */
  MctOperationCounter operation_counter;
  unsigned int n_open_retries;
} GetEstimatedTimesData;

static void
get_estimated_times_data_free (GetEstimatedTimesData *data)
{
  g_clear_object (&data->invocation);
  g_clear_object (&data->child_timer_service);
  g_free (data->username);
  g_clear_pointer (&data->session_limits_policy, mct_session_limits_unref);
  mct_operation_counter_release_and_clear (&data->operation_counter);
  g_free (data);
}

G_DEFINE_AUTOPTR_CLEANUP_FUNC (GetEstimatedTimesData, get_estimated_times_data_free)

static void get_estimated_times_ensure_credentials_cb (GObject      *object,
                                                       GAsyncResult *result,
                                                       void         *user_data);
static void get_estimated_times_get_session_limits_cb (GObject      *object,
                                                       GAsyncResult *result,
                                                       void         *user_data);
static void get_estimated_times_open_username_cb (GObject      *object,
                                                  GAsyncResult *result,
                                                  void         *user_data);

static void
mct_child_timer_service_get_estimated_times (MctChildTimerService  *self,
                                             GDBusConnection       *connection,
                                             const char            *sender,
                                             GVariant              *parameters,
                                             GDBusMethodInvocation *invocation)
{
  g_autoptr(GetEstimatedTimesData) data = NULL;
  const char *record_type = NULL;

  g_variant_get (parameters, "(&s)", &record_type);

  if (!validate_record_type (invocation, record_type))
    return;

  /* Load the peer’s credentials so we know which user is querying the usage. */
  data = g_new0 (GetEstimatedTimesData, 1);
  data->child_timer_service = g_object_ref (self);
  data->invocation = g_object_ref (invocation);
  data->record_type = mct_timer_store_record_type_from_string (record_type);
  mct_operation_counter_init_and_hold (&data->operation_counter,
                                       &self->n_pending_operations,
                                       G_OBJECT (self), props[PROP_BUSY]);

  gss_peer_manager_ensure_peer_credentials_async (self->peer_manager,
                                                  sender,
                                                  self->cancellable,
                                                  get_estimated_times_ensure_credentials_cb,
                                                  g_steal_pointer (&data));
}

static void
get_estimated_times_ensure_credentials_cb (GObject      *object,
                                           GAsyncResult *result,
                                           void         *user_data)
{
  GssPeerManager *peer_manager = GSS_PEER_MANAGER (object);
  g_autoptr(GetEstimatedTimesData) data = g_steal_pointer (&user_data);
  MctChildTimerService *self = data->child_timer_service;
  GDBusMethodInvocation *invocation = data->invocation;
  GDBusMessage *message;
  gboolean interactive;
  g_autofree char *username = NULL;
  uid_t uid;
  g_autoptr(GError) local_error = NULL;

  /* Finish looking up the sender. */
  if (!gss_peer_manager_ensure_peer_credentials_finish (peer_manager,
                                                        result, &local_error))
    {
      g_dbus_method_invocation_return_error (invocation, MCT_CHILD_TIMER_SERVICE_ERROR,
                                             MCT_CHILD_TIMER_SERVICE_ERROR_IDENTIFYING_USER,
                                             _("Error identifying user: %s"),
                                             local_error->message);
      return;
    }

  username = lookup_username_for_peer_invocation (peer_manager, invocation, &uid);
  if (username == NULL)
    {
      g_dbus_method_invocation_return_error (invocation, MCT_CHILD_TIMER_SERVICE_ERROR,
                                             MCT_CHILD_TIMER_SERVICE_ERROR_IDENTIFYING_USER,
                                             _("Error identifying user: %s"),
                                             _("Invalid or unknown user"));
      return;
    }

  data->username = g_steal_pointer (&username);

  /* Query the user’s session limits policy. */
  message = g_dbus_method_invocation_get_message (invocation);
  interactive = g_dbus_message_get_flags (message) & G_DBUS_MESSAGE_FLAGS_ALLOW_INTERACTIVE_AUTHORIZATION;

  mct_manager_get_session_limits_async (self->policy_manager,
                                        uid,
                                        interactive ? MCT_MANAGER_GET_VALUE_FLAGS_INTERACTIVE : MCT_MANAGER_GET_VALUE_FLAGS_NONE,
                                        self->cancellable,
                                        get_estimated_times_get_session_limits_cb,
                                        g_steal_pointer (&data));
}

static void
get_estimated_times_get_session_limits_cb (GObject      *object,
                                           GAsyncResult *result,
                                           void         *user_data)
{
  MctManager *policy_manager = MCT_MANAGER (object);
  g_autoptr(GetEstimatedTimesData) data = g_steal_pointer (&user_data);
  MctChildTimerService *self = data->child_timer_service;
  GDBusMethodInvocation *invocation = data->invocation;
  const char *username = data->username;
  g_autoptr(MctSessionLimits) session_limits_policy = NULL;
  g_autoptr(GError) local_error = NULL;

  session_limits_policy = mct_manager_get_session_limits_finish (policy_manager,
                                                                 result, &local_error);

  if (g_error_matches (local_error, MCT_MANAGER_ERROR, MCT_MANAGER_ERROR_INVALID_USER) ||
      g_error_matches (local_error, MCT_MANAGER_ERROR, MCT_MANAGER_ERROR_PERMISSION_DENIED))
    {
      /* MCT_MANAGER_ERROR_INVALID_USER shouldn’t really happen as we’ve
       * already identified the user, but I guess there could be a problem
       * inside AccountsssService.
       *
       * MCT_MANAGER_ERROR_PERMISSION_DENIED definitely shouldn’t happen:
       * the user should always have permissions to query their own session
       * limits, otherwise the install must be corrupt. */
      g_dbus_method_invocation_return_error (invocation, MCT_CHILD_TIMER_SERVICE_ERROR,
                                             MCT_CHILD_TIMER_SERVICE_ERROR_IDENTIFYING_USER,
                                             _("Error identifying user: %s"),
                                             local_error->message);
      return;
    }
  else if (g_error_matches (local_error, MCT_MANAGER_ERROR, MCT_MANAGER_ERROR_DISABLED))
    {
      g_dbus_method_invocation_return_error (invocation, MCT_CHILD_TIMER_SERVICE_ERROR,
                                             MCT_CHILD_TIMER_SERVICE_ERROR_DISABLED,
                                             _("Recording usage is disabled"));
      return;
    }
  else if (local_error != NULL)
    {
      /* Likely a GDBusError or GIOError, or MCT_MANAGER_ERROR_INVALID_DATA */
      g_dbus_method_invocation_return_error (invocation, MCT_CHILD_TIMER_SERVICE_ERROR,
                                             MCT_CHILD_TIMER_SERVICE_ERROR_QUERYING_POLICY,
                                             _("Error querying user parental controls policy: %s"),
                                             local_error->message);
      return;
    }

  data->session_limits_policy = g_steal_pointer (&session_limits_policy);

  /* Now open the user’s file. */
  mct_timer_store_open_username_async (self->timer_store,
                                       username,
                                       self->cancellable,
                                       get_estimated_times_open_username_cb,
                                       g_steal_pointer (&data));
}

static void
get_estimated_times_open_username_cb (GObject      *object,
                                      GAsyncResult *result,
                                      void         *user_data)
{
  MctTimerStore *timer_store = MCT_TIMER_STORE (object);
  g_autoptr(GetEstimatedTimesData) data = g_steal_pointer (&user_data);
  MctChildTimerService *self = data->child_timer_service;
  GDBusMethodInvocation *invocation = data->invocation;
  const char *username = data->username;
  const MctTimerStoreTransaction *transaction;
  g_auto(GVariantBuilder) builder = G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE ("a{s(btttt)}"));
  g_autoptr(GHashTable) total_times_so_far_today = NULL;  /* (element-type utf8 uint64_t) */
  uint64_t start_of_today_secs, now_secs, now_time_of_day_secs, start_of_tomorrow_secs;
  g_autoptr(GDateTime) now_date_time = NULL;
  g_autoptr(GDateTime) start_of_today = NULL;
  g_autoptr(GDateTime) start_of_tomorrow = NULL;
  g_autoptr(GError) local_error = NULL;

  transaction = mct_timer_store_open_username_finish (timer_store, result, &local_error);

  if (g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_BUSY) && data->n_open_retries >= 10)
    {
      g_dbus_method_invocation_return_error (invocation, MCT_CHILD_TIMER_SERVICE_ERROR,
                                             MCT_CHILD_TIMER_SERVICE_ERROR_BUSY,
                                             _("Error opening user file: %s"),
                                             local_error->message);
      return;
    }
  else if (g_error_matches (local_error, G_IO_ERROR, G_IO_ERROR_BUSY))
    {
      /* Try again */
      data->n_open_retries++;
      mct_timer_store_open_username_async (timer_store,
                                           username,
                                           self->cancellable,
                                           get_estimated_times_open_username_cb,
                                           g_steal_pointer (&data));
      return;
    }
  else if (local_error != NULL)  /* likely a GFileError */
    {
      g_dbus_method_invocation_return_error (invocation, MCT_CHILD_TIMER_SERVICE_ERROR,
                                             MCT_CHILD_TIMER_SERVICE_ERROR_STORAGE_ERROR,
                                             _("Error opening user file: %s"),
                                             local_error->message);
      return;
    }

  /* Work out some times. */
  now_secs = g_get_real_time () / G_USEC_PER_SEC;

  now_date_time = g_date_time_new_from_unix_local (now_secs);
  now_time_of_day_secs = ((g_date_time_get_hour (now_date_time) * 60 +
                           g_date_time_get_minute (now_date_time)) * 60 +
                           g_date_time_get_second (now_date_time));
  start_of_today = g_date_time_new_local (g_date_time_get_year (now_date_time),
                                          g_date_time_get_month (now_date_time),
                                          g_date_time_get_day_of_month (now_date_time),
                                          0, 0, 0);
  start_of_tomorrow = g_date_time_add_days (start_of_today, 1);
  g_assert (start_of_today != NULL);
  g_assert (start_of_tomorrow != NULL);
  start_of_today_secs = g_date_time_to_unix (start_of_today);
  start_of_tomorrow_secs = g_date_time_to_unix (start_of_tomorrow);

  /* Calculate the total time the user has spent on each session, app, etc.
   * since the start of today. */
  total_times_so_far_today =
      mct_timer_store_calculate_total_times_between (self->timer_store,
                                                     transaction,
                                                     data->record_type,
                                                     start_of_today_secs,
                                                     now_secs);

  /* Close the file again. */
  mct_timer_store_roll_back_transaction (self->timer_store, transaction);

  /* Get the estimated times. */
  switch (data->record_type)
    {
    case MCT_TIMER_STORE_RECORD_TYPE_LOGIN_SESSION:
      {
        gboolean limit_reached;
        uint64_t time_remaining_secs = 0;
        gboolean time_limit_enabled = FALSE;
        uint64_t *total_session_time_so_far_today_ptr;

        /* Session record is always identified by "" */
        total_session_time_so_far_today_ptr = g_hash_table_lookup (total_times_so_far_today, "");

        limit_reached =
            !mct_session_limits_check_time_remaining (data->session_limits_policy,
                                                      now_date_time,
                                                      (total_session_time_so_far_today_ptr != NULL) ? *total_session_time_so_far_today_ptr : 0,
                                                      &time_remaining_secs,
                                                      &time_limit_enabled,
                                                      NULL);

        if (time_limit_enabled)
          {
            uint64_t current_session_start_time_secs, current_session_estimated_end_time_secs;
            uint64_t next_session_start_time_secs, next_session_estimated_end_time_secs;
            gboolean daily_schedule_set, daily_limit_set;
            unsigned int start_time_secs, end_time_secs, daily_limit_secs;

            daily_schedule_set = mct_session_limits_get_daily_schedule (data->session_limits_policy,
                                                                        &start_time_secs,
                                                                        &end_time_secs);
            daily_limit_set = mct_session_limits_get_daily_limit (data->session_limits_policy,
                                                                  &daily_limit_secs);

            /* Calculate current session times */
            if (!limit_reached)
              {
                /* FIXME: Calculate more of these times */
                current_session_start_time_secs = 0;
                current_session_estimated_end_time_secs = now_secs + time_remaining_secs;
              }
            else
              {
                /* if the limit has been reached, the ‘current session’ is
                 * actually the most recent session */
                current_session_start_time_secs = 0;
                current_session_estimated_end_time_secs = now_secs; // TODO actually need the time the session ended
              }

            /* Calculate next session times */
            if (daily_schedule_set)
              {
                if (now_time_of_day_secs >= start_time_secs)
                  {
                    next_session_start_time_secs = start_of_tomorrow_secs + start_time_secs;
                    next_session_estimated_end_time_secs = start_of_tomorrow_secs + (daily_limit_set ? MIN (end_time_secs, start_time_secs + daily_limit_secs) : end_time_secs);
                  }
                else
                  {
                    next_session_start_time_secs = start_of_today_secs + start_time_secs;
                    next_session_estimated_end_time_secs = start_of_today_secs + (daily_limit_set ? MIN (end_time_secs, start_time_secs + daily_limit_secs) : end_time_secs);
                  }
              }
            else if (daily_limit_set)
              {
                next_session_start_time_secs = start_of_tomorrow_secs;
                next_session_estimated_end_time_secs = start_of_tomorrow_secs + daily_limit_secs;
              }
            else
              {
                /* Unknown limit type */
                next_session_start_time_secs = 0;
                next_session_estimated_end_time_secs = 0;
              }

            g_variant_builder_open (&builder, G_VARIANT_TYPE ("{s(btttt)}"));
            g_variant_builder_add (&builder, "s", "");
            g_variant_builder_add (&builder, "(btttt)",
                                   limit_reached,
                                   current_session_start_time_secs,
                                   current_session_estimated_end_time_secs,
                                   next_session_start_time_secs,
                                   next_session_estimated_end_time_secs);
            g_variant_builder_close (&builder);
          }
        break;
      }
    case MCT_TIMER_STORE_RECORD_TYPE_APP:
      /* FIXME Not supported for now, as no support is in place in MctSessionLimits yet */
      g_dbus_method_invocation_return_error (invocation, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED,
                                             "App time limits are not yet supported");
      return;
    default:
      g_assert_not_reached ();
    }

  g_dbus_method_invocation_return_value (invocation,
                                         g_variant_new ("(t@a{s(btttt)})",
                                                        now_secs,
                                                        g_variant_builder_end (&builder)));
}

static void request_extension_ensure_credentials_cb (GObject      *object,
                                                     GAsyncResult *result,
                                                     void         *user_data);
static void request_extension_agent_cb (GObject      *object,
                                        GAsyncResult *result,
                                        void         *user_data);
static void request_extension_signal_cb (GDBusConnection *connection,
                                         const char      *sender_name,
                                         const char      *object_path,
                                         const char      *interface_name,
                                         const char      *signal_name,
                                         GVariant        *parameters,
                                         void            *user_data);
static void request_extension_cancelled_cb (GCancellable *cancellable,
                                            void         *user_data);
static void request_extension_agent_name_lost_cb (GDBusConnection *connection,
                                                  const char      *name,
                                                  void            *user_data);
static void request_extension_client_name_lost_cb (GDBusConnection *connection,
                                                   const char      *name,
                                                   void            *user_data);

static void
mct_child_timer_service_request_extension (MctChildTimerService  *self,
                                           GDBusConnection       *connection,
                                           const char            *sender,
                                           GVariant              *parameters,
                                           GDBusMethodInvocation *invocation)
{
  g_autoptr(RequestExtensionData) data = NULL;
  g_autofree char *record_type = NULL;
  g_autofree char *identifier = NULL;
  uint64_t duration_secs = 0;
  g_autoptr(GVariant) extra_data = NULL;

  g_variant_get (parameters, "(sst@a{sv})", &record_type, &identifier, &duration_secs, &extra_data);

  if (!validate_record_type_and_identifier (invocation, record_type, identifier))
    return;

  /* Load the peer’s credentials so we know which user is requesting an extension
   * (in particular, we know their session ID for the agent to use). */
  data = g_new0 (RequestExtensionData, 1);
  data->child_timer_service = g_object_ref (self);
  data->invocation = g_object_ref (invocation);
  data->record_type = g_steal_pointer (&record_type);
  data->identifier = g_steal_pointer (&identifier);
  data->duration_secs = duration_secs;
  data->extra_data = g_variant_ref_sink (extra_data);
  mct_operation_counter_init_and_hold (&data->operation_counter,
                                       &self->n_pending_operations,
                                       G_OBJECT (self), props[PROP_BUSY]);

  /* Use a made up object path as a cookie for the caller. It’s opaque to them,
   * but could be used in future to expose a two-way request conversation to the
   * child, if needed. For now, it’s just an opaque and unique cookie. */
  data->cookie_path = g_strdup_printf ("%s/ExtensionRequest%u", self->object_path, g_random_int ());

  gss_peer_manager_ensure_peer_credentials_async (self->peer_manager,
                                                  sender,
                                                  self->cancellable,
                                                  request_extension_ensure_credentials_cb,
                                                  g_steal_pointer (&data));
}

static void
request_extension_ensure_credentials_cb (GObject      *object,
                                         GAsyncResult *result,
                                         void         *user_data)
{
  GssPeerManager *peer_manager = GSS_PEER_MANAGER (object);
  g_autoptr(RequestExtensionData) data = g_steal_pointer (&user_data);
  MctChildTimerService *self = data->child_timer_service;
  GDBusMethodInvocation *invocation = data->invocation;
  const char *sender;
  int subject_pidfd;
  uid_t subject_uid;
  g_autoptr(GUnixFDList) fd_list = NULL;
  int subject_pidfd_idx;
  g_autoptr(GVariant) subject_details = NULL;
  g_autoptr(GVariant) args = NULL;
  g_autoptr(GError) local_error = NULL;
  GDBusCallFlags flags;

  /* Finish looking up the sender. */
  if (!gss_peer_manager_ensure_peer_credentials_finish (peer_manager,
                                                        result, &local_error))
    {
      g_dbus_method_invocation_return_error (invocation, MCT_CHILD_TIMER_SERVICE_ERROR,
                                             MCT_CHILD_TIMER_SERVICE_ERROR_IDENTIFYING_USER,
                                             _("Error identifying user: %s"),
                                             local_error->message);
      return;
    }

  /* Subscribe to signals from the agent before making the request, to avoid a
   * potential race. */
  data->connection = g_object_ref (self->connection);
  data->request_subscribe_id =
      g_dbus_connection_signal_subscribe (self->connection,
                                          "org.freedesktop.MalcontentTimer1.ExtensionAgent",
                                          "org.freedesktop.MalcontentTimer1.ExtensionAgent.Request",
                                          NULL,  /* match all signal names */
                                          NULL,  /* match all object paths */
                                          NULL,  /* match all arg0 values */
                                          G_DBUS_SIGNAL_FLAGS_NONE,
                                          request_extension_signal_cb,
                                          data,
                                          NULL);

  /* Listen for cancellation too. */
  data->cancelled_id = g_cancellable_connect (self->cancellable,
                                              G_CALLBACK (request_extension_cancelled_cb),
                                              data,
                                              NULL);

  /* And watch to see if the agent or client lose their name, in which case
   * they’ll have lost all state to do with our request. */
  sender = g_dbus_method_invocation_get_sender (invocation);

  data->agent_name_watch_id =
      g_bus_watch_name_on_connection (self->connection,
                                      "org.freedesktop.MalcontentTimer1.ExtensionAgent",
                                      G_BUS_NAME_WATCHER_FLAGS_NONE,
                                      NULL,
                                      request_extension_agent_name_lost_cb,
                                      data,
                                      NULL);
  data->client_name_watch_id =
      g_bus_watch_name_on_connection (self->connection,
                                      sender,
                                      G_BUS_NAME_WATCHER_FLAGS_NONE,
                                      NULL,
                                      request_extension_client_name_lost_cb,
                                      data,
                                      NULL);

  subject_pidfd = gss_peer_manager_get_peer_pidfd (peer_manager, sender);
  subject_uid = gss_peer_manager_get_peer_uid (peer_manager, sender);

  if (subject_pidfd < 0 || subject_uid == (uid_t) -1)
    {
      g_dbus_method_invocation_return_error (invocation, MCT_CHILD_TIMER_SERVICE_ERROR,
                                             MCT_CHILD_TIMER_SERVICE_ERROR_IDENTIFYING_USER,
                                             _("Error identifying user: %s"),
                                             _("Invalid or unknown user"));
      return;
    }

  data->subject_uid = subject_uid;

  /* Call the extension agent.
   *
   * See https://www.freedesktop.org/software/polkit/docs/latest/eggdbus-interface-org.freedesktop.PolicyKit1.Authority.html#eggdbus-struct-Subject
   * for documentation on the `subject` argument. */
  fd_list = g_unix_fd_list_new ();
  subject_pidfd_idx = g_unix_fd_list_append (fd_list, subject_pidfd, &local_error);

  if (subject_pidfd_idx < 0)
    {
      g_dbus_method_invocation_return_error (invocation, MCT_CHILD_TIMER_SERVICE_ERROR,
                                             MCT_CHILD_TIMER_SERVICE_ERROR_COMMUNICATING_WITH_AGENT,
                                             _("Error communicating with agent: %s"),
                                             local_error->message);
      return;
    }

  g_auto(GVariantBuilder) subject_details_builder = G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE_VARDICT);
  g_variant_builder_add (&subject_details_builder, "{sv}", "pidfd", g_variant_new_handle (subject_pidfd_idx));
  g_variant_builder_add (&subject_details_builder, "{sv}", "uid", g_variant_new_int32 (subject_uid));
  subject_details = g_variant_builder_end (&subject_details_builder);

  args = g_variant_new ("(sst(s@a{sv})@a{sv})",
                        data->record_type,
                        data->identifier,
                        data->duration_secs,
                        "unix-process",
                        g_steal_pointer (&subject_details),
                        data->extra_data);

  flags = G_DBUS_CALL_FLAGS_NONE;
  if (g_dbus_message_get_flags (g_dbus_method_invocation_get_message (invocation)) & G_DBUS_MESSAGE_FLAGS_ALLOW_INTERACTIVE_AUTHORIZATION)
    flags |= G_DBUS_CALL_FLAGS_ALLOW_INTERACTIVE_AUTHORIZATION;

  g_dbus_connection_call_with_unix_fd_list (self->connection,
                                            "org.freedesktop.MalcontentTimer1.ExtensionAgent",
                                            "/org/freedesktop/MalcontentTimer1/ExtensionAgent",
                                            "org.freedesktop.MalcontentTimer1.ExtensionAgent",
                                            "RequestExtension",
                                            g_steal_pointer (&args),
                                            G_VARIANT_TYPE ("(o)"),
                                            flags,
                                            -1,  /* timeout (ms) */
                                            fd_list,
                                            self->cancellable,
                                            request_extension_agent_cb,
                                            g_steal_pointer (&data));
}

static void
request_extension_agent_cb (GObject      *object,
                            GAsyncResult *result,
                            void         *user_data)
{
  GDBusConnection *connection = G_DBUS_CONNECTION (object);
  g_autoptr(RequestExtensionData) data = g_steal_pointer (&user_data);
  MctChildTimerService *self = data->child_timer_service;
  GDBusMethodInvocation *invocation = data->invocation;
  g_autoptr(GVariant) reply = NULL;
  const char *request_object_path;
  g_autoptr(GError) local_error = NULL;

  reply = g_dbus_connection_call_finish (connection, result, &local_error);
  if (reply == NULL)
    {
      /* We could parse the error code from the agent here, but it wouldn’t
       * actually change what we report to the client. */
      g_dbus_method_invocation_return_error (invocation, MCT_CHILD_TIMER_SERVICE_ERROR,
                                             MCT_CHILD_TIMER_SERVICE_ERROR_COMMUNICATING_WITH_AGENT,
                                             _("Error communicating with agent: %s"),
                                             local_error->message);
      return;
    }

  g_variant_get (reply, "(&o)", &request_object_path);
  g_debug ("%s: got extension request object path %s", G_STRFUNC, request_object_path);
  data->request_object_path = g_strdup (request_object_path);

  /* Note: We explicitly don’t return the `request_object_path` to the caller
   * (a process belonging to the child), because they cannot (and should not)
   * talk directly to the agent. Instead we return an arbitrary cookie they can
   * use to associate this method call with a subsequent `ExtensionResponse`
   * signal. */
  g_dbus_method_invocation_return_value (invocation,
                                         g_variant_new ("(o)", data->cookie_path));

  /* Continue to track the request until we receive the signal, or are cancelled. */
  g_ptr_array_add (self->pending_extension_agent_requests, g_steal_pointer (&data));
}

static const char *
agent_error_name_to_client_error_name (const char *agent_error_name)
{
  if (g_str_equal (agent_error_name, "org.freedesktop.MalcontentTimer1.ExtensionAgent.Error.Cancelled"))
    return "org.freedesktop.MalcontentTimer1.Child.Error.RequestCancelled";
  else
    return "org.freedesktop.MalcontentTimer1.Child.Error.CommunicatingWithAgent";
}

static void
request_extension_send_reply (RequestExtensionData *data,
                              gboolean              granted,
                              GVariant             *extra_data)
{
  MctChildTimerService *self = data->child_timer_service;
  g_autoptr(GVariant) client_extra_data = NULL;
  const char *agent_error_name;

  /* @extra_data must be provided, even if it’s an empty dict */
  g_assert (extra_data != NULL);

  /* If @granted is true, the caller must separately have called
   * request_extension_store_active_extension_async() first. */

  /* Convert the @extra_data from the agent into something suitable for sending
   * to a client. */
  g_auto(GVariantBuilder) client_extra_data_builder = G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE ("a{sv}"));
  if (g_variant_lookup (extra_data, "error-name", "&s", &agent_error_name))
    g_variant_builder_add (&client_extra_data_builder, "{sv}", "error-name",
                           g_variant_new_string (agent_error_name_to_client_error_name (agent_error_name)));
  /* don’t convert `duration-secs` as the client will get that information via
   * GetEstimatedTimes() */
  client_extra_data = g_variant_ref_sink (g_variant_builder_end (&client_extra_data_builder));

  /* Ignore errors from emitting the signal; it can only fail if the
   * parameters are invalid (not possible) or if the connection has been
   * closed. */
  g_dbus_connection_emit_signal (data->connection,
                                 NULL,  /* destination bus name */
                                 self->object_path,
                                 "org.freedesktop.MalcontentTimer1.Child",
                                 "ExtensionResponse",
                                 g_variant_new ("(bo@a{sv})",
                                                granted,
                                                data->cookie_path,
                                                client_extra_data),
                                 NULL);

  /* Mark the extension agent request as finished with. */
  if (data->request_object_path != NULL)
    {
      g_dbus_connection_call (data->connection,
                              "org.freedesktop.MalcontentTimer1.ExtensionAgent",
                              data->request_object_path,
                              "org.freedesktop.MalcontentTimer1.ExtensionAgent.Request",
                              "Close",
                              NULL,
                              NULL,
                              G_DBUS_CALL_FLAGS_NONE,
                              -1,  /* timeout (ms) */
                              NULL,
                              NULL,
                              NULL);
    }
}

static void request_extension_store_active_extension_async (MctChildTimerService *self,
                                                            uid_t                 child_uid,
                                                            uint64_t              requested_duration_secs,
                                                            GVariant             *response_extra_data,
                                                            GCancellable         *cancellable,
                                                            GAsyncReadyCallback   callback,
                                                            void                 *user_data);
static gboolean request_extension_store_active_extension_finish (MctChildTimerService  *self,
                                                                 GAsyncResult          *result,
                                                                 GError               **error);
static void request_extension_signal_store_active_extension_cb (GObject      *object,
                                                                GAsyncResult *result,
                                                                void         *user_data);

static void
request_extension_signal_cb (GDBusConnection *connection,
                             const char      *sender_name,
                             const char      *object_path,
                             const char      *interface_name,
                             const char      *signal_name,
                             GVariant        *parameters,
                             void            *user_data)
{
  RequestExtensionData *data = user_data;
  MctChildTimerService *self = data->child_timer_service;

  /* These two are fixed by our signal subscription. The other arguments could
   * vary. */
  /* this will actually be a unique name, but effectively it is the following:
   * g_assert (g_strcmp0 (sender_name, "org.freedesktop.MalcontentTimer1.ExtensionAgent") == 0); */
  g_assert (g_strcmp0 (interface_name, "org.freedesktop.MalcontentTimer1.ExtensionAgent.Request") == 0);

  if (data->request_object_path == NULL ||
      g_strcmp0 (data->request_object_path, object_path) != 0)
    {
      g_debug ("Ignoring signal with non-matching object path ‘%s’", object_path);
      return;
    }

  if (g_strcmp0 (signal_name, "Response") == 0)
    {
      gboolean granted = FALSE;
      g_autoptr(GVariant) extra_data = NULL;

      if (!g_variant_is_of_type (parameters, G_VARIANT_TYPE ("(ba{sv})")))
        {
          g_debug ("Ignoring signal ‘%s.%s’ with incorrect type ‘%s’",
                   interface_name, signal_name, g_variant_get_type_string (parameters));
          return;
        }

      g_variant_get (parameters, "(b@a{sv})", &granted, &extra_data);
      data->response_granted = granted;
      data->response_extra_data = g_variant_ref (extra_data);

      /* Store the active extension before replying to the child process. */
      if (granted)
        request_extension_store_active_extension_async (self,
                                                        data->subject_uid,
                                                        data->duration_secs,
                                                        extra_data,
                                                        self->cancellable,
                                                        request_extension_signal_store_active_extension_cb,
                                                        data);
      else
        request_extension_signal_store_active_extension_cb (G_OBJECT (self), NULL, data);
    }
  else
    {
      g_debug ("Ignoring unknown signal ‘%s.%s’", interface_name, signal_name);
      return;
    }
}

static void
request_extension_signal_store_active_extension_cb (GObject      *object,
                                                    GAsyncResult *result,
                                                    void         *user_data)
{
  g_autoptr(GError) local_error = NULL;
  RequestExtensionData *data = user_data;
  MctChildTimerService *self = data->child_timer_service;
  gboolean granted = data->response_granted;
  g_autoptr(GVariant) extra_data = g_variant_ref (data->response_extra_data);

  if (result != NULL &&
      !request_extension_store_active_extension_finish (self, result, &local_error))
    {
      /* request_extension_store_active_extension_async() should have been
       * skipped if !granted. So in this case we know it’s safe to discard the
       * `extra_data` (which was generated on the basis of the request being
       * granted) and replace it with something which represents the failure to
       * store the active extension data to the child user’s AccountsService
       * object. */
      g_assert (granted);
      granted = FALSE;
      g_clear_pointer (&extra_data, g_variant_unref);
      extra_data = g_variant_new_parsed ("@a{sv} {'error-name': <'org.freedesktop.MalcontentTimer1.ExtensionAgent.Error.Failed'>}");
    }

  /* Send a reply to the child process. */
  request_extension_send_reply (data, granted, extra_data);

  /* This will free @data, so don’t use it after then. */
  g_ptr_array_remove_fast (self->pending_extension_agent_requests, data);
}

static void request_extension_store_active_extension_get_session_limits_cb (GObject      *object,
                                                                            GAsyncResult *result,
                                                                            void         *user_data);
static void request_extension_store_active_extension_set_session_limits_cb (GObject      *object,
                                                                            GAsyncResult *result,
                                                                            void         *user_data);

typedef struct {
  uint64_t now_secs;
  uid_t child_uid;
  uint64_t requested_duration_secs;
  GVariant *response_extra_data;  /* (owned) */
} StoreActiveExtensionData;

static void
store_active_extension_data_free (StoreActiveExtensionData *data)
{
  g_variant_unref (data->response_extra_data);
  g_free (data);
}

G_DEFINE_AUTOPTR_CLEANUP_FUNC (StoreActiveExtensionData, store_active_extension_data_free);

/* Store the extension in the user’s session limits policy. This will
 * automatically notify them of the change in estimated time via
 * policy_manager_session_limits_changed_cb(). */
static void
request_extension_store_active_extension_async (MctChildTimerService *self,
                                                uid_t                 child_uid,
                                                uint64_t              requested_duration_secs,
                                                GVariant             *response_extra_data,
                                                GCancellable         *cancellable,
                                                GAsyncReadyCallback   callback,
                                                void                 *user_data)
{
  g_autoptr(GTask) task = NULL;
  g_autoptr(StoreActiveExtensionData) data = NULL;
  uint64_t now_secs = (uint64_t) g_get_real_time () / G_USEC_PER_SEC;

  g_return_if_fail (MCT_IS_CHILD_TIMER_SERVICE (self));
  g_return_if_fail (response_extra_data != NULL);
  g_return_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable));

  task = g_task_new (self, cancellable, callback, user_data);
  g_task_set_source_tag (task, request_extension_store_active_extension_async);

  data = g_new0 (StoreActiveExtensionData, 1);
  data->now_secs = now_secs;
  data->child_uid = child_uid;
  data->requested_duration_secs = requested_duration_secs;
  data->response_extra_data = g_variant_ref (response_extra_data);
  g_task_set_task_data (task, g_steal_pointer (&data), (GDestroyNotify) store_active_extension_data_free);

  mct_manager_get_session_limits_async (self->policy_manager,
                                        child_uid,
                                        MCT_MANAGER_GET_VALUE_FLAGS_NONE,
                                        cancellable,
                                        request_extension_store_active_extension_get_session_limits_cb,
                                        g_steal_pointer (&task));
}

static void
request_extension_store_active_extension_get_session_limits_cb (GObject      *object,
                                                                GAsyncResult *result,
                                                                void         *user_data)
{
  MctManager *manager = MCT_MANAGER (object);
  g_autoptr(GTask) task = g_steal_pointer (&user_data);
  StoreActiveExtensionData *data = g_task_get_task_data (task);
  GCancellable *cancellable = g_task_get_cancellable (task);
  g_autoptr(MctSessionLimits) old_session_limits = NULL;
  g_autoptr(MctSessionLimits) new_session_limits = NULL;
  g_autoptr(GError) local_error = NULL;
  g_auto(MctSessionLimitsBuilder) builder = MCT_SESSION_LIMITS_BUILDER_INIT ();
  uint64_t start_time_secs, response_duration_secs, start_of_tomorrow_secs, rest_of_day_duration_secs;
  unsigned int duration_secs;
  g_autoptr(GDateTime) now_date_time = NULL, start_of_today = NULL, start_of_tomorrow = NULL;

  old_session_limits = mct_manager_get_session_limits_finish (manager, result, &local_error);

  if (local_error != NULL)
    {
      g_task_return_error (task, g_steal_pointer (&local_error));
      return;
    }

  /* Create a copy of `old_session_limits`, but with the new extension set on it */
  if (!g_variant_lookup (data->response_extra_data, "duration-secs", "t", &response_duration_secs))
    response_duration_secs = 0;

  now_date_time = g_date_time_new_from_unix_local (data->now_secs);
  start_of_today = g_date_time_new_local (g_date_time_get_year (now_date_time),
                                          g_date_time_get_month (now_date_time),
                                          g_date_time_get_day_of_month (now_date_time),
                                          0, 0, 0);
  start_of_tomorrow = g_date_time_add_days (start_of_today, 1);
  g_assert (start_of_today != NULL);
  g_assert (start_of_tomorrow != NULL);
  start_of_tomorrow_secs = g_date_time_to_unix (start_of_tomorrow);
  g_assert (start_of_tomorrow_secs > data->now_secs);
  rest_of_day_duration_secs = start_of_tomorrow_secs - data->now_secs;
  g_assert (rest_of_day_duration_secs < 2 * 24 * 60 * 60);  /* safety check, should always be less than 2 days */

  start_time_secs = data->now_secs;
  duration_secs = (response_duration_secs != 0) ? response_duration_secs : (data->requested_duration_secs != 0) ? data->requested_duration_secs : rest_of_day_duration_secs;
  g_assert (duration_secs > 0);

  g_debug ("Building new session limits with active extension (%" G_GUINT64_FORMAT ", %u)",
           start_time_secs, duration_secs);

  mct_session_limits_builder_set_from_instance (&builder, old_session_limits);
  mct_session_limits_builder_set_active_extension (&builder, start_time_secs, duration_secs);
  new_session_limits = mct_session_limits_builder_end (&builder);

  mct_manager_set_session_limits_async (manager,
                                        data->child_uid,
                                        new_session_limits,
                                        MCT_MANAGER_SET_VALUE_FLAGS_NONE,
                                        cancellable,
                                        request_extension_store_active_extension_set_session_limits_cb,
                                        g_steal_pointer (&task));
}

static void
request_extension_store_active_extension_set_session_limits_cb (GObject      *object,
                                                                GAsyncResult *result,
                                                                void         *user_data)
{
  MctManager *manager = MCT_MANAGER (object);
  g_autoptr(GTask) task = g_steal_pointer (&user_data);
  g_autoptr(GError) local_error = NULL;

  if (!mct_manager_set_session_limits_finish (manager, result, &local_error))
    g_task_return_error (task, g_steal_pointer (&local_error));
  else
    g_task_return_boolean (task, TRUE);
}

static gboolean
request_extension_store_active_extension_finish (MctChildTimerService  *self,
                                                 GAsyncResult          *result,
                                                 GError               **error)
{
  g_return_val_if_fail (MCT_IS_CHILD_TIMER_SERVICE (self), FALSE);
  g_return_val_if_fail (g_task_is_valid (result, self), FALSE);
  g_return_val_if_fail (error == NULL || *error == NULL, FALSE);

  return g_task_propagate_boolean (G_TASK (result), error);
}

static void
request_extension_cancelled_cb (GCancellable *cancellable,
                                void         *user_data)
{
  RequestExtensionData *data = user_data;
  MctChildTimerService *self = data->child_timer_service;

  /* Send cancellation to the child process. */
  request_extension_send_reply (data, FALSE, g_variant_new_parsed ("@a{sv} {}"));

  /* This will free @data, so don’t use it after then. */
  g_ptr_array_remove_fast (self->pending_extension_agent_requests, data);
}

static void
request_extension_agent_name_lost_cb (GDBusConnection *connection,
                                      const char      *name,
                                      void            *user_data)
{
  RequestExtensionData *data = user_data;
  MctChildTimerService *self = data->child_timer_service;

  /* This is fixed by our name watch handler. */
  g_assert (g_strcmp0 (name, "org.freedesktop.MalcontentTimer1.ExtensionAgent") == 0);

  /* Don’t do anything if the agent hasn’t actually been contacted and returned
   * us a request object path yet. This can happen when we install the name
   * watch handler if the agent is not currently on the bus. */
  if (data->request_object_path == NULL)
    return;

  /* Send cancellation to the child process. */
  request_extension_send_reply (data, FALSE, g_variant_new_parsed ("@a{sv} {}"));

  /* This will free @data, so don’t use it after then. */
  g_ptr_array_remove_fast (self->pending_extension_agent_requests, data);
}

static void
request_extension_client_name_lost_cb (GDBusConnection *connection,
                                       const char      *name,
                                       void            *user_data)
{
  RequestExtensionData *data = user_data;
  MctChildTimerService *self = data->child_timer_service;

  /* Don’t do anything if the agent hasn’t actually been contacted and returned
   * us a request object path yet. If there’s no request object we can’t close
   * it. */
  if (data->request_object_path == NULL)
    return;

  /* Send cancellation; even though the child process isn’t around to hear it,
   * we need to change state consistently. */
  request_extension_send_reply (data, FALSE, g_variant_new_parsed ("@a{sv} {}"));

  /* This will free @data, so don’t use it after then. */
  g_ptr_array_remove_fast (self->pending_extension_agent_requests, data);
}

/**
 * mct_child_timer_service_new:
 * @connection: (transfer none): D-Bus connection to export objects on
 * @object_path: root path to export objects below; must be a valid D-Bus object
 *    path
 * @timer_store: (transfer none): store to use for timer data
 * @peer_manager: (transfer none): peer manager for querying D-Bus peers
 * @policy_manager: (transfer none): policy manager for querying user parental
 *   controls policies
 *
 * Create a new [class@Malcontent.ChildTimerService] instance which is set up to run
 * as a service.
 *
 * Returns: (transfer full): a new [class@Malcontent.ChildTimerService]
 * Since: 0.14.0
 */
MctChildTimerService *
mct_child_timer_service_new (GDBusConnection *connection,
                             const char      *object_path,
                             MctTimerStore   *timer_store,
                             GssPeerManager  *peer_manager,
                             MctManager      *policy_manager)
{
  g_return_val_if_fail (G_IS_DBUS_CONNECTION (connection), NULL);
  g_return_val_if_fail (g_variant_is_object_path (object_path), NULL);
  g_return_val_if_fail (MCT_IS_TIMER_STORE (timer_store), NULL);
  g_return_val_if_fail (GSS_IS_PEER_MANAGER (peer_manager), NULL);
  g_return_val_if_fail (MCT_IS_MANAGER (policy_manager), NULL);

  return g_object_new (MCT_TYPE_CHILD_TIMER_SERVICE,
                       "connection", connection,
                       "object-path", object_path,
                       "timer-store", timer_store,
                       "peer-manager", peer_manager,
                       "policy-manager", policy_manager,
                       NULL);
}

/**
 * mct_child_timer_service_get_busy:
 * @self: a child service
 *
 * Get the value of [property@Malcontent.ChildTimerService.busy].
 *
 * Returns: true if the service is busy, false otherwise
 * Since: 0.14.0
 */
gboolean
mct_child_timer_service_get_busy (MctChildTimerService *self)
{
  g_return_val_if_fail (MCT_IS_CHILD_TIMER_SERVICE (self), FALSE);

  /* Note: We don’t have to include pending_extension_agent_requests in this
   * expression as RequestExtensionData holds an increment on
   * n_pending_operations. */
  return (self->object_id != 0 && self->n_pending_operations > 0);
}

