/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
 * vi:set noexpandtab tabstop=8 shiftwidth=8:
 *
 * Copyright (C) 2013-2017 Richard Hughes <richard@hughsie.com>
 * Copyright (C) 2014-2018 Kalev Lember <klember@redhat.com>
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 */

#include "config.h"

#include <glib/gi18n.h>
#include <gio/gio.h>

#include "gs-shell.h"
#include "gs-updates-page.h"
#include "gs-common.h"
#include "gs-app-row.h"
#include "gs-plugin-private.h"
#include "gs-removal-dialog.h"
#include "gs-update-monitor.h"
#include "gs-updates-section.h"
#include "gs-upgrade-banner.h"
#include "gs-application.h"
#include "gtk/gtk.h"
#include "gtk/gtkshortcut.h"

/* The "updates-changed" is delays by 3 seconds; give it twice time to be delivered
   and the page reload ignored when the signal comes within this time limit. It's
   because the plugins can emit the signal when the they are refreshing metadata. */
#define IGNORE_UPDATES_CHANGED_WITHIN_SECS 6

typedef enum {
	GS_UPDATES_PAGE_FLAG_NONE		= 0,
	GS_UPDATES_PAGE_FLAG_HAS_UPDATES	= 1 << 0,
	GS_UPDATES_PAGE_FLAG_HAS_UPGRADES	= 1 << 1,
	GS_UPDATES_PAGE_FLAG_LAST
} GsUpdatesPageFlags;

typedef enum {
	GS_UPDATES_PAGE_STATE_STARTUP,
	GS_UPDATES_PAGE_STATE_ACTION_REFRESH,
	GS_UPDATES_PAGE_STATE_ACTION_GET_UPDATES,
	GS_UPDATES_PAGE_STATE_MANAGED,
	GS_UPDATES_PAGE_STATE_IDLE,
	GS_UPDATES_PAGE_STATE_FAILED,
	GS_UPDATES_PAGE_STATE_LAST,
} GsUpdatesPageState;

struct _GsUpdatesPage
{
	GsPage			 parent_instance;

	GsPluginLoader		*plugin_loader;
	GCancellable		*cancellable;
	GCancellable		*cancellable_refresh;
	GCancellable		*cancellable_upgrade;
	GSettings		*settings;
	gulong			 settings_changed_id;
	GSettings		*desktop_settings;
	gboolean		 cache_valid;
	guint			 action_cnt;
	GsShell			*shell;
	GsUpdatesPageState	 state;
	GsUpdatesPageFlags	 result_flags;
	GtkWidget		*button_refresh;
	GtkWidget		*button_stop;
	GtkWidget		*header_spinner_start;
	GtkWidget		*header_start_box;
	gboolean		 has_agreed_to_mobile_data;
	gboolean		 ampm_available;
	guint			 updates_counter;
	gboolean		 is_narrow;

	GtkWidget		*updates_box;
	GtkWidget		*button_updates_mobile;
	GtkWidget		*button_updates_offline;
	GtkWidget		*updates_failed_page;
	GtkLabel		*uptodate_description;
	GtkLabel		*label_last_checked;
	GtkWidget		*scrolledwindow_updates;
	GtkWidget		*stack_updates;
	GtkWidget		*upgrade_banner;
	GtkWidget		*banner_end_of_life;
	GtkWidget		*label_end_of_life;
	GtkWidget		*up_to_date_image;

	GtkSizeGroup		*sizegroup_name;
	GtkSizeGroup		*sizegroup_button_label;
	GtkSizeGroup		*sizegroup_button_image;
	GtkSizeGroup		*sizegroup_header;
	GsUpdatesSection	*sections[GS_UPDATES_SECTION_KIND_LAST];

	guint			 refresh_last_checked_id;
	gint64			 last_loaded_time;
};

enum {
	COLUMN_UPDATE_APP,
	COLUMN_UPDATE_NAME,
	COLUMN_UPDATE_VERSION,
	COLUMN_UPDATE_LAST
};

G_DEFINE_TYPE (GsUpdatesPage, gs_updates_page, GS_TYPE_PAGE)

typedef enum {
	PROP_IS_NARROW = 1,
	/* Overrides: */
	PROP_VADJUSTMENT,
	PROP_TITLE,
	PROP_COUNTER,
} GsUpdatesPageProperty;

static GParamSpec *obj_props[PROP_IS_NARROW + 1] = { NULL, };

static void
gs_updates_page_set_flag (GsUpdatesPage *self, GsUpdatesPageFlags flag)
{
	self->result_flags |= flag;
}

static void
gs_updates_page_clear_flag (GsUpdatesPage *self, GsUpdatesPageFlags flag)
{
	self->result_flags &= ~flag;
}

static const gchar *
gs_updates_page_state_to_string (GsUpdatesPageState state)
{
	if (state == GS_UPDATES_PAGE_STATE_STARTUP)
		return "startup";
	if (state == GS_UPDATES_PAGE_STATE_ACTION_REFRESH)
		return "action-refresh";
	if (state == GS_UPDATES_PAGE_STATE_ACTION_GET_UPDATES)
		return "action-get-updates";
	if (state == GS_UPDATES_PAGE_STATE_MANAGED)
		return "managed";
	if (state == GS_UPDATES_PAGE_STATE_IDLE)
		return "idle";
	if (state == GS_UPDATES_PAGE_STATE_FAILED)
		return "failed";
	return NULL;
}

static void
gs_updates_page_invalidate (GsUpdatesPage *self)
{
	self->cache_valid = FALSE;
}

static GsUpdatesSectionKind
_get_app_section (GsApp *app)
{
	if (gs_app_get_kind (app) == AS_COMPONENT_KIND_OPERATING_SYSTEM &&
	    gs_app_has_quirk (app, GS_APP_QUIRK_NEEDS_REBOOT))
		return GS_UPDATES_SECTION_KIND_OFFLINE;

	if (!gs_app_has_quirk (app, GS_APP_QUIRK_NEEDS_REBOOT) &&
	    (gs_app_get_state (app) == GS_APP_STATE_UPDATABLE_LIVE ||
	     gs_app_get_state (app) == GS_APP_STATE_INSTALLING ||
	     gs_app_get_state (app) == GS_APP_STATE_DOWNLOADING)) {
		if (gs_app_get_kind (app) == AS_COMPONENT_KIND_FIRMWARE)
			return GS_UPDATES_SECTION_KIND_ONLINE_FIRMWARE;
		return GS_UPDATES_SECTION_KIND_ONLINE;
	}
	if (gs_app_get_kind (app) == AS_COMPONENT_KIND_FIRMWARE)
		return GS_UPDATES_SECTION_KIND_OFFLINE_FIRMWARE;
	return GS_UPDATES_SECTION_KIND_OFFLINE;
}

static GsAppList *
_get_all_apps (GsUpdatesPage *self)
{
	GsAppList *apps = gs_app_list_new ();
	for (guint i = 0; i < GS_UPDATES_SECTION_KIND_LAST; i++) {
		GsAppList *list = gs_updates_section_get_list (self->sections[i]);
		gs_app_list_add_list (apps, list);
	}
	return apps;
}

static gchar *
gs_updates_page_last_checked_time_string (GsUpdatesPage *self,
					  gint *out_hours_ago,
					  gint *out_days_ago)
{
	gint64 last_checked;
	gchar *res;

	g_settings_get (self->settings, "check-timestamp", "x", &last_checked);
	res = gs_utils_time_to_timestring (last_checked);
	if (res) {
		g_assert (gs_utils_split_time_difference (last_checked, NULL, out_hours_ago, out_days_ago, NULL, NULL, NULL));
	}

	return res;
}

static void
refresh_headerbar_updates_counter (GsUpdatesPage *self)
{
	guint new_updates_counter = 0;

	if (gs_plugin_loader_get_allow_updates (self->plugin_loader) &&
	    self->state != GS_UPDATES_PAGE_STATE_FAILED) {
		for (size_t i = 0; i < GS_UPDATES_SECTION_KIND_LAST; i++) {
			new_updates_counter += gs_updates_section_get_counter (self->sections[i]);
		}
	}

	if (new_updates_counter == self->updates_counter)
		return;

	self->updates_counter = new_updates_counter;
	g_object_notify (G_OBJECT (self), "counter");
}

static void
section_notify_counter_cb (GObject    *obj,
                           GParamSpec *pspec,
                           void       *user_data)
{
	GsUpdatesPage *self = GS_UPDATES_PAGE (user_data);

	refresh_headerbar_updates_counter (self);
}

static void
gs_updates_page_remove_last_checked_timeout (GsUpdatesPage *self)
{
	if (self->refresh_last_checked_id) {
		g_source_remove (self->refresh_last_checked_id);
		self->refresh_last_checked_id = 0;
	}
}

static void
gs_updates_page_refresh_last_checked (GsUpdatesPage *self);

static gboolean
gs_updates_page_refresh_last_checked_cb (gpointer user_data)
{
	GsUpdatesPage *self = user_data;
	gs_updates_page_refresh_last_checked (self);
	return G_SOURCE_REMOVE;
}

static void
gs_updates_page_refresh_last_checked (GsUpdatesPage *self)
{
	g_autofree gchar *checked_str = NULL;
	gint hours_ago, days_ago;
	checked_str = gs_updates_page_last_checked_time_string (self, &hours_ago, &days_ago);
	if (checked_str != NULL) {
		g_autofree gchar *last_checked = NULL;
		guint interval;

		/* TRANSLATORS: This is the time when we last checked for updates */
		last_checked = g_strdup_printf (_("Last checked: %s"), checked_str);

		/* only shown in uptodate view */
		gtk_label_set_label (self->uptodate_description, last_checked);
		gtk_widget_set_visible (GTK_WIDGET (self->uptodate_description), TRUE);

		/* shown when updates are available */
		gtk_label_set_label (self->label_last_checked, last_checked);
		gtk_widget_set_visible (GTK_WIDGET (self->label_last_checked), TRUE);

		if (hours_ago < 1)
			interval = 60;
		else if (days_ago < 7)
			interval = 60 * 60;
		else
			interval = 60 * 60 * 24;

		gs_updates_page_remove_last_checked_timeout (self);

		self->refresh_last_checked_id = g_timeout_add_seconds (interval,
			gs_updates_page_refresh_last_checked_cb, self);
	} else {
		gtk_widget_set_visible (GTK_WIDGET (self->uptodate_description), FALSE);
		gtk_widget_set_visible (GTK_WIDGET (self->label_last_checked), FALSE);
	}
}

static void
settings_changed_check_timestamp_cb (GSettings  *settings,
                                     const char *key,
                                     gpointer    user_data)
{
	GsUpdatesPage *self = GS_UPDATES_PAGE (user_data);

	gs_updates_page_refresh_last_checked (self);
}

static void
gs_updates_page_update_ui_state (GsUpdatesPage *self)
{
	const gchar *visible_child_name;
	gboolean allow_mobile_refresh = TRUE;

	gs_updates_page_remove_last_checked_timeout (self);

	if (gs_shell_get_mode (self->shell) != GS_SHELL_MODE_UPDATES)
		return;

	/* spinners */
	switch (self->state) {
	case GS_UPDATES_PAGE_STATE_STARTUP:
	case GS_UPDATES_PAGE_STATE_ACTION_GET_UPDATES:
	default:
		gtk_spinner_stop (GTK_SPINNER (self->header_spinner_start));
		gtk_widget_set_visible (self->header_spinner_start, FALSE);
		break;
	}

	/* headerbar refresh icon */
	switch (self->state) {
	case GS_UPDATES_PAGE_STATE_ACTION_REFRESH:
	case GS_UPDATES_PAGE_STATE_ACTION_GET_UPDATES:
		gtk_widget_set_visible (self->button_refresh, FALSE);
		gtk_widget_set_visible (self->button_stop, TRUE);
		break;
	case GS_UPDATES_PAGE_STATE_STARTUP:
	case GS_UPDATES_PAGE_STATE_MANAGED:
		gtk_widget_set_visible (self->button_refresh, FALSE);
		gtk_widget_set_visible (self->button_stop, FALSE);
		break;
	case GS_UPDATES_PAGE_STATE_IDLE:
		if (self->result_flags != GS_UPDATES_PAGE_FLAG_NONE) {
			gtk_widget_set_visible (self->button_refresh, TRUE);
		} else {
			if (gs_plugin_loader_get_network_metered (self->plugin_loader) &&
			    !self->has_agreed_to_mobile_data)
				allow_mobile_refresh = FALSE;
			gtk_widget_set_visible (self->button_refresh, allow_mobile_refresh);
		}
		gtk_widget_set_visible (self->button_stop, FALSE);
		break;
	case GS_UPDATES_PAGE_STATE_FAILED:
		gtk_widget_set_visible (self->button_refresh, TRUE);
		gtk_widget_set_visible (self->button_stop, FALSE);
		break;
	default:
		g_assert_not_reached ();
		break;
	}
	gtk_widget_set_sensitive (self->button_refresh,
				  gs_plugin_loader_get_network_available (self->plugin_loader));

	/* stack */
	switch (self->state) {
	case GS_UPDATES_PAGE_STATE_MANAGED:
		gtk_stack_set_visible_child_name (GTK_STACK (self->stack_updates), "managed");
		break;
	case GS_UPDATES_PAGE_STATE_FAILED:
		gtk_stack_set_visible_child_name (GTK_STACK (self->stack_updates), "failed");
		break;
	case GS_UPDATES_PAGE_STATE_ACTION_GET_UPDATES:
	case GS_UPDATES_PAGE_STATE_ACTION_REFRESH:
		gtk_stack_set_visible_child_name (GTK_STACK (self->stack_updates), "spinner");
		break;
	case GS_UPDATES_PAGE_STATE_STARTUP:
	case GS_UPDATES_PAGE_STATE_IDLE:

		/* if have updates, just show the view, otherwise show network */
		if (self->result_flags != GS_UPDATES_PAGE_FLAG_NONE) {
			gtk_stack_set_visible_child_name (GTK_STACK (self->stack_updates), "view");
			break;
		}

		/* check we have a "free" network connection */
		if (gs_plugin_loader_get_network_available (self->plugin_loader) &&
		    !gs_plugin_loader_get_network_metered (self->plugin_loader)) {
			gtk_stack_set_visible_child_name (GTK_STACK (self->stack_updates), "uptodate");
			break;
		}

		/* expensive network connection */
		if (gs_plugin_loader_get_network_metered (self->plugin_loader)) {
			if (self->has_agreed_to_mobile_data) {
				gtk_stack_set_visible_child_name (GTK_STACK (self->stack_updates), "uptodate");
			} else {
				gtk_stack_set_visible_child_name (GTK_STACK (self->stack_updates), "mobile");
			}
			break;
		}

		/* no network connection */
		gtk_stack_set_visible_child_name (GTK_STACK (self->stack_updates), "offline");
		break;
	default:
		g_assert_not_reached ();
		break;
	}

	/* any updates? */
	gtk_widget_set_visible (self->updates_box,
				self->result_flags & GS_UPDATES_PAGE_FLAG_HAS_UPDATES);

	/* last checked label */
	visible_child_name = gtk_stack_get_visible_child_name (GTK_STACK (self->stack_updates));
	if (g_strcmp0 (visible_child_name, "uptodate") == 0 ||
	    g_strcmp0 (visible_child_name, "view") == 0)
		gs_updates_page_refresh_last_checked (self);

	/* update the counter in headerbar */
	refresh_headerbar_updates_counter (self);
}

static void
gs_updates_page_set_state (GsUpdatesPage *self, GsUpdatesPageState state)
{
	g_debug ("setting state from %s to %s (has-update:%i, has-upgrade:%i)",
		 gs_updates_page_state_to_string (self->state),
		 gs_updates_page_state_to_string (state),
		 (self->result_flags & GS_UPDATES_PAGE_FLAG_HAS_UPDATES) > 0,
		 (self->result_flags & GS_UPDATES_PAGE_FLAG_HAS_UPGRADES) > 0);
	self->state = state;
	gs_updates_page_update_ui_state (self);
}

static void
gs_updates_page_decrement_refresh_count (GsUpdatesPage *self)
{
	/* every job increments this */
	if (self->action_cnt == 0) {
		g_warning ("action_cnt already zero!");
		return;
	}
	if (--self->action_cnt > 0)
		return;

	/* all done */
	gs_updates_page_set_state (self, GS_UPDATES_PAGE_STATE_IDLE);
}

static void
gs_updates_page_network_available_or_metered_notify_cb (GsPluginLoader *plugin_loader,
                                                        GParamSpec     *pspec,
                                                        GsUpdatesPage  *self)
{
	gs_updates_page_update_ui_state (self);
}

static void
gs_updates_page_get_updates_cb (GsPluginLoader *plugin_loader,
                                GAsyncResult *res,
                                GsUpdatesPage *self)
{
	g_autoptr(GError) error = NULL;
	g_autoptr(GsPluginJobListApps) list_apps_job = NULL;
	GsAppList *list;

	self->cache_valid = TRUE;

	/* get the results */
	if (!gs_plugin_loader_job_process_finish (plugin_loader, res, (GsPluginJob **) &list_apps_job, &error)) {
		g_autofree gchar *escaped_text = NULL;
		gs_updates_page_clear_flag (self, GS_UPDATES_PAGE_FLAG_HAS_UPDATES);
		if (!g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED) &&
		    !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
			g_warning ("updates-shell: failed to get updates: %s", error->message);
		escaped_text = g_markup_escape_text (error->message, -1);
		adw_status_page_set_description (ADW_STATUS_PAGE (self->updates_failed_page), escaped_text);
		gs_updates_page_set_state (self, GS_UPDATES_PAGE_STATE_FAILED);
		refresh_headerbar_updates_counter (self);
		return;
	}

	list = gs_plugin_job_list_apps_get_result_list (list_apps_job);
	self->last_loaded_time = g_get_real_time ();

	/* add the results */
	for (guint i = 0; i < gs_app_list_length (list); i++) {
		GsApp *app = gs_app_list_index (list, i);
		GsUpdatesSectionKind section = _get_app_section (app);
		gs_updates_section_add_app (self->sections[section], app);
	}

	/* update the counter in headerbar */
	refresh_headerbar_updates_counter (self);

	/* no results */
	if (gs_app_list_length (list) == 0) {
		g_debug ("updates-shell: no updates to show");
		gs_updates_page_clear_flag (self, GS_UPDATES_PAGE_FLAG_HAS_UPDATES);
	} else {
		gs_updates_page_set_flag (self, GS_UPDATES_PAGE_FLAG_HAS_UPDATES);
	}

	/* only when both set */
	gs_updates_page_decrement_refresh_count (self);
}

static void
gs_updates_page_get_upgrades_cb (GObject *source_object,
                                 GAsyncResult *res,
                                 gpointer user_data)
{
	GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source_object);
	GsUpdatesPage *self = GS_UPDATES_PAGE (user_data);
	g_autoptr(GError) error = NULL;
	g_autoptr(GsPluginJobListDistroUpgrades) list_distro_upgrades_job = NULL;
	GsAppList *list;

	/* get the results */
	gs_plugin_loader_job_process_finish (plugin_loader, res, (GsPluginJob **) &list_distro_upgrades_job, &error);
	list = gs_plugin_job_list_distro_upgrades_get_result_list (list_distro_upgrades_job);

	if (error != NULL) {
		gs_updates_page_clear_flag (self, GS_UPDATES_PAGE_FLAG_HAS_UPGRADES);
		if (!g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED) &&
		    !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
			g_warning ("updates-shell: failed to get upgrades: %s",
				   error->message);
		}
	} else if (gs_app_list_length (list) == 0) {
		g_debug ("updates-shell: no upgrades to show");
		gs_updates_page_clear_flag (self, GS_UPDATES_PAGE_FLAG_HAS_UPGRADES);
		gtk_widget_set_visible (self->upgrade_banner, FALSE);
	} else {
		/* rely on the app list already being sorted with the
		 * chronologically newest release last */
		GsApp *app = gs_app_list_index (list, gs_app_list_length (list) - 1);
		g_debug ("got upgrade %s", gs_app_get_id (app));
		gs_upgrade_banner_set_app (GS_UPGRADE_BANNER (self->upgrade_banner), app);
		gs_updates_page_set_flag (self, GS_UPDATES_PAGE_FLAG_HAS_UPGRADES);
		gtk_widget_set_visible (self->upgrade_banner, TRUE);
	}

	/* only when both set */
	gs_updates_page_decrement_refresh_count (self);
}

typedef struct {
	GsApp		*app; /* (owned) */
	GsUpdatesPage	*self; /* (owned) */
	GsPluginJob	*job; /* (owned) */
} GsPageHelper;

static GsPageHelper *
gs_page_helper_new (GsUpdatesPage *self,
		    GsApp	  *app,
		    GsPluginJob   *job)
{
	GsPageHelper *helper;
	helper = g_slice_new0 (GsPageHelper);
	helper->self = g_object_ref (self);
	helper->app = g_object_ref (app);
	helper->job = g_object_ref (job);
	return helper;
}

static void
gs_page_helper_free (GsPageHelper *helper)
{
	g_clear_object (&helper->app);
	g_clear_object (&helper->job);
	g_clear_object (&helper->self);
	g_slice_free (GsPageHelper, helper);
}

G_DEFINE_AUTOPTR_CLEANUP_FUNC(GsPageHelper, gs_page_helper_free);

static void
gs_updates_page_refine_system_finished_cb (GObject *source_object,
					   GAsyncResult *res,
					   gpointer user_data)
{
	GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source_object);
	g_autoptr(GsPageHelper) helper = user_data;
	GsUpdatesPage *self = helper->self;
	GsApp *app = helper->app;
	g_autofree char *str = NULL;
	g_autoptr(GError) error = NULL;

	/* get result */
	if (!gs_plugin_loader_job_process_finish (plugin_loader, res, NULL, &error)) {
		if (!g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED) &&
		    !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
			g_warning ("Failed to refine system: %s", error->message);
		return;
	}

	/* show or hide the end of life notification */
	if (gs_app_get_state (app) != GS_APP_STATE_UNAVAILABLE) {
		adw_banner_set_revealed (ADW_BANNER (self->banner_end_of_life), FALSE);
		return;
	}

	/* construct a sufficiently scary message */
	if (gs_app_get_name (app) != NULL) {
		/* TRANSLATORS:  the first %s is the distro name, e.g. 'Fedora'
		 * and the second %s is the distro version, e.g. '25' */
		str = g_strdup_printf (_("%s %s has stopped receiving critical software updates"),
				       gs_app_get_name (app),
				       gs_app_get_version (app));
	} else {
		/* TRANSLATORS: This message is meant to tell users that they need to upgrade
		* or else their distro will not get important updates. */
		str = _("Your operating system has stopped receiving critical software updates");
	}

	adw_banner_set_title (ADW_BANNER (self->banner_end_of_life), str);
	adw_banner_set_revealed (ADW_BANNER (self->banner_end_of_life), TRUE);
}

static void
gs_updates_page_get_system_finished_cb (GObject *source_object,
					GAsyncResult *res,
					gpointer user_data)
{
	guint64 require_flags;
	GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source_object);
	GsUpdatesPage *self = user_data;
	GsPageHelper *helper;
	g_autoptr(GsApp) app = NULL;
	g_autoptr(GsPluginJob) plugin_job = NULL;
	g_autoptr(GError) error = NULL;

	app = gs_plugin_loader_get_system_app_finish (plugin_loader, res, &error);
	if (app == NULL) {
		if (!g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED) &&
		    !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
			g_warning ("Failed to get system: %s", error->message);
		return;
	}

	g_return_if_fail (GS_IS_UPDATES_PAGE (self));

	require_flags = GS_PLUGIN_REFINE_REQUIRE_FLAGS_ICON |
		        GS_PLUGIN_REFINE_REQUIRE_FLAGS_SIZE |
		        GS_PLUGIN_REFINE_REQUIRE_FLAGS_UPDATE_SEVERITY |
		        GS_PLUGIN_REFINE_REQUIRE_FLAGS_VERSION;

	plugin_job = gs_plugin_job_refine_new_for_app (app, GS_PLUGIN_REFINE_FLAGS_INTERACTIVE, require_flags);
	helper = gs_page_helper_new (self, app, plugin_job);
	gs_plugin_loader_job_process_async (self->plugin_loader, plugin_job,
					    self->cancellable,
					    gs_updates_page_refine_system_finished_cb,
					    helper);
}

static void
gs_updates_page_load (GsUpdatesPage *self)
{
	guint64 require_flags;
	g_autoptr(GsAppQuery) query = NULL;
	g_autoptr(GsPluginJob) plugin_job = NULL;

	if (self->action_cnt > 0)
		return;

	/* remove all existing apps */
	for (guint i = 0; i < GS_UPDATES_SECTION_KIND_LAST; i++)
		gs_updates_section_remove_all (self->sections[i]);

	require_flags = GS_PLUGIN_REFINE_REQUIRE_FLAGS_ICON |
		        GS_PLUGIN_REFINE_REQUIRE_FLAGS_SIZE |
		        GS_PLUGIN_REFINE_REQUIRE_FLAGS_UPDATE_SEVERITY |
		        GS_PLUGIN_REFINE_REQUIRE_FLAGS_VERSION;
	gs_updates_page_set_state (self, GS_UPDATES_PAGE_STATE_ACTION_GET_UPDATES);
	self->action_cnt++;
	query = gs_app_query_new ("is-for-update", GS_APP_QUERY_TRISTATE_TRUE,
				  "refine-require-flags", require_flags,
				  NULL);
	plugin_job = gs_plugin_job_list_apps_new (query, GS_PLUGIN_LIST_APPS_FLAGS_INTERACTIVE);
	gs_plugin_loader_job_process_async (self->plugin_loader, plugin_job,
					    self->cancellable,
					    (GAsyncReadyCallback) gs_updates_page_get_updates_cb,
					    self);

	/* get the system state */
	gs_plugin_loader_get_system_app_async (self->plugin_loader, self->cancellable,
		gs_updates_page_get_system_finished_cb, self);

	/* don't refresh every each time */
	if ((self->result_flags & GS_UPDATES_PAGE_FLAG_HAS_UPGRADES) == 0) {
		require_flags |= GS_PLUGIN_REFINE_REQUIRE_FLAGS_UPGRADE_REMOVED;
		g_object_unref (plugin_job);
		plugin_job = gs_plugin_job_list_distro_upgrades_new (GS_PLUGIN_LIST_DISTRO_UPGRADES_FLAGS_INTERACTIVE,
								     require_flags);
		gs_plugin_loader_job_process_async (self->plugin_loader,
						    plugin_job,
						    self->cancellable,
						    gs_updates_page_get_upgrades_cb,
						    self);
		self->action_cnt++;
	}
}

static void
gs_updates_page_reload (GsPage *page)
{
	GsUpdatesPage *self = GS_UPDATES_PAGE (page);

	if (self->state == GS_UPDATES_PAGE_STATE_ACTION_REFRESH) {
		g_debug ("ignoring reload as refresh is already in progress");
		return;
	}

	gs_updates_page_invalidate (self);
	gs_updates_page_load (self);
}

static void
gs_updates_page_switch_to (GsPage *page)
{
	GsUpdatesPage *self = GS_UPDATES_PAGE (page);

	if (gs_shell_get_mode (self->shell) != GS_SHELL_MODE_UPDATES) {
		g_warning ("Called switch_to(updates) when in mode %s",
			   gs_shell_get_mode_string (self->shell));
		return;
	}

	/* no need to refresh */
	if (self->cache_valid) {
		gs_updates_page_update_ui_state (self);
		return;
	}

	if (self->state == GS_UPDATES_PAGE_STATE_ACTION_GET_UPDATES) {
		gs_updates_page_update_ui_state (self);
		return;
	}
	gs_updates_page_load (self);
}

static void
gs_updates_page_switch_from (GsPage *page)
{
	GsUpdatesPage *self = GS_UPDATES_PAGE (page);
	gs_updates_page_remove_last_checked_timeout (self);
}

static void
gs_updates_page_refresh_cb (GsPluginLoader *plugin_loader,
                            GAsyncResult *res,
                            GsUpdatesPage *self)
{
	gboolean ret;
	g_autoptr(GDateTime) now = NULL;
	g_autoptr(GError) error = NULL;

	/* get the results */
	ret = gs_plugin_loader_job_process_finish (plugin_loader, res, NULL, &error);
	if (!ret) {
		g_autofree gchar *escaped_text = NULL;
		/* user cancel */
		if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED) ||
		    g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
			gs_updates_page_set_state (self, GS_UPDATES_PAGE_STATE_IDLE);
			return;
		}
		g_warning ("failed to refresh: %s", error->message);
		escaped_text = g_markup_escape_text (error->message, -1);
		adw_status_page_set_description (ADW_STATUS_PAGE (self->updates_failed_page), escaped_text);
		gs_updates_page_set_state (self, GS_UPDATES_PAGE_STATE_FAILED);
		return;
	}

	/* update the last checked timestamp */
	now = g_date_time_new_now_local ();
	g_settings_set (self->settings, "check-timestamp", "x",
			g_date_time_to_unix (now));

	/* get the new list */
	gs_updates_page_invalidate (self);
	gs_page_switch_to (GS_PAGE (self));
	gs_page_scroll_up (GS_PAGE (self));
}

static void
gs_updates_page_get_new_updates (GsUpdatesPage *self)
{
	g_autoptr(GsPluginJob) plugin_job = NULL;

	/* force a check for updates and download */
	gs_updates_page_set_state (self, GS_UPDATES_PAGE_STATE_ACTION_REFRESH);

	g_cancellable_cancel (self->cancellable_refresh);
	g_clear_object (&self->cancellable_refresh);
	self->cancellable_refresh = g_cancellable_new ();

	plugin_job = gs_plugin_job_refresh_metadata_new (1,
							 GS_PLUGIN_REFRESH_METADATA_FLAGS_INTERACTIVE);
	gs_plugin_loader_job_process_async (self->plugin_loader, plugin_job,
					    self->cancellable_refresh,
					    (GAsyncReadyCallback) gs_updates_page_refresh_cb,
					    self);
}

static void
gs_updates_page_show_network_settings (GsUpdatesPage *self)
{
	g_autoptr(GError) error = NULL;
	if (!g_spawn_command_line_async ("gnome-control-center wifi", &error))
		g_warning ("Failed to open the control center: %s", error->message);
}

static void
gs_updates_page_refresh_check_cb (AdwAlertDialog *dialog,
                                  const gchar *response,
                                  GsUpdatesPage *self)
{
	if (g_strcmp0 (response, "check") == 0) {
		self->has_agreed_to_mobile_data = TRUE;
		gs_updates_page_get_new_updates (self);
	}
}

static void
gs_updates_page_button_network_settings_cb (GtkWidget *widget,
                                            GsUpdatesPage *self)
{
	gs_updates_page_show_network_settings (self);
}

static void
gs_updates_page_button_mobile_refresh_cb (GtkWidget *widget,
                                          GsUpdatesPage *self)
{
	self->has_agreed_to_mobile_data = TRUE;
	gs_updates_page_get_new_updates (self);
}

static void
gs_updates_page_button_refresh_cb (GtkWidget *widget,
                                   GsUpdatesPage *self)
{
	AdwDialog *dialog;

	/* check we have a "free" network connection */
	if (gs_plugin_loader_get_network_available (self->plugin_loader) &&
	    !gs_plugin_loader_get_network_metered (self->plugin_loader)) {
		gs_updates_page_get_new_updates (self);

	/* expensive network connection */
	} else if (gs_plugin_loader_get_network_available (self->plugin_loader) &&
	           gs_plugin_loader_get_network_metered (self->plugin_loader)) {
		if (self->has_agreed_to_mobile_data) {
			gs_updates_page_get_new_updates (self);
			return;
		}
		/* TRANSLATORS: this is to explain that downloading updates may cost money */
		dialog = adw_alert_dialog_new (_("Charges May Apply"),
					       /* TRANSLATORS: we need network to do the updates check */
					       _("Checking for updates while using mobile broadband could cause you to incur charges."));
		adw_alert_dialog_add_responses (ADW_ALERT_DIALOG (dialog),
						"cancel",  _("_Cancel"),
					  	/* TRANSLATORS: this is a link to the control-center network panel */
					  	"check",  _("Check _Anyway"),
					  	NULL);
		g_signal_connect (dialog, "response",
				  G_CALLBACK (gs_updates_page_refresh_check_cb),
				  self);
		adw_dialog_present (dialog, GTK_WIDGET (self));
	}
}

static void
gs_updates_page_button_stop_cb (GtkWidget     *widget,
                                GsUpdatesPage *self)
{
	/* cancel existing action? */
	g_cancellable_cancel (self->cancellable_refresh);
	g_clear_object (&self->cancellable_refresh);
}

static void
gs_updates_page_pending_apps_changed_cb (GsPluginLoader *plugin_loader,
                                         GsUpdatesPage *self)
{
	gs_updates_page_invalidate (self);
}

static void
upgrade_download_finished_cb (GObject *source,
                              GAsyncResult *res,
                              gpointer user_data)
{
	GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source);
	g_autoptr(GsPageHelper) helper = user_data;
	g_autoptr(GError) error = NULL;

	g_clear_object (&helper->self->cancellable_upgrade);

	if (!gs_plugin_loader_job_process_finish (plugin_loader, res, NULL, &error)) {
		if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED) ||
		    g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
			return;
		gs_plugin_loader_claim_job_error (plugin_loader,
						  helper->job,
						  helper->app,
						  error);
	} else if (!gs_page_is_active_and_focused (GS_PAGE (helper->self))) {
		g_autoptr(GNotification) notif = NULL;

		notif = g_notification_new (_("Software Upgrades Downloaded"));
		g_notification_set_body (notif, _("Upgrades are ready to be installed"));
		g_notification_set_default_action_and_target (notif, "app.set-mode", "s", "updates");
		/* last the notification for an hour */
		gs_application_send_notification (GS_APPLICATION (g_application_get_default ()), "upgrades-downloaded", notif, 60);
	}
}

static void
gs_updates_page_upgrade_download_cb (GsUpgradeBanner *upgrade_banner,
                                     GsUpdatesPage *self)
{
	GsApp *app;
	GsPageHelper *helper;
	g_autoptr(GsPluginJob) plugin_job = NULL;

	g_application_withdraw_notification (g_application_get_default (), "upgrades-downloaded");

	app = gs_upgrade_banner_get_app (upgrade_banner);
	if (app == NULL) {
		g_warning ("no upgrade available to download");
		return;
	}

	g_clear_object (&self->cancellable_upgrade);
	self->cancellable_upgrade = g_cancellable_new ();
	g_debug ("Starting upgrade download with cancellable %p", self->cancellable_upgrade);
	plugin_job = gs_plugin_job_download_upgrade_new (app, GS_PLUGIN_DOWNLOAD_UPGRADE_FLAGS_INTERACTIVE);
	helper = gs_page_helper_new (self, app, plugin_job);
	gs_plugin_loader_job_process_async (self->plugin_loader, plugin_job,
					    self->cancellable_upgrade,
					    upgrade_download_finished_cb,
					    helper);
}

static void
_cancel_trigger_failed_cb (GObject *source, GAsyncResult *res, gpointer user_data)
{
	GsUpdatesPage *self = GS_UPDATES_PAGE (user_data);
	g_autoptr(GError) error = NULL;
	if (!gs_plugin_loader_job_process_finish (self->plugin_loader, res, NULL, &error)) {
		g_warning ("failed to cancel trigger: %s", error->message);
		return;
	}
}

static void
upgrade_reboot_failed_cb (GObject *source,
                          GAsyncResult *res,
                          gpointer user_data)
{
	GsUpdatesPage *self = (GsUpdatesPage *) user_data;
	GsApp *app;
	g_autoptr(GError) error = NULL;
	g_autoptr(GsPluginJob) plugin_job = NULL;

	/* get result */
	if (gs_utils_invoke_reboot_finish (source, res, &error))
		return;

	if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
		g_debug ("Calling reboot had been cancelled");
	else if (error != NULL)
		g_warning ("Calling reboot failed: %s", error->message);

	app = gs_upgrade_banner_get_app (GS_UPGRADE_BANNER (self->upgrade_banner));
	if (app == NULL) {
		g_warning ("no upgrade to cancel");
		return;
	}

	/* cancel trigger */
	plugin_job = gs_plugin_job_cancel_offline_update_new (GS_PLUGIN_CANCEL_OFFLINE_UPDATE_FLAGS_INTERACTIVE);
	gs_plugin_loader_job_process_async (self->plugin_loader, plugin_job,
					    self->cancellable,
					    _cancel_trigger_failed_cb,
					    self);
}

static void
upgrade_trigger_finished_cb (GObject *source,
                             GAsyncResult *res,
                             gpointer user_data)
{
	GsUpdatesPage *self = (GsUpdatesPage *) user_data;
	g_autoptr(GError) error = NULL;

	g_clear_object (&self->cancellable_upgrade);

	/* get the results */
	if (!gs_plugin_loader_job_process_finish (self->plugin_loader, res, NULL, &error)) {
		g_warning ("Failed to trigger offline update: %s", error->message);
		return;
	}

	/* trigger reboot */
	gs_utils_invoke_reboot_async (NULL, upgrade_reboot_failed_cb, self);
}

static void
trigger_upgrade (GsUpdatesPage *self)
{
	GsApp *upgrade;
	g_autoptr(GsPluginJob) plugin_job = NULL;

	upgrade = gs_upgrade_banner_get_app (GS_UPGRADE_BANNER (self->upgrade_banner));
	if (upgrade == NULL) {
		g_warning ("no upgrade available to install");
		return;
	}

	g_clear_object (&self->cancellable_upgrade);
	self->cancellable_upgrade = g_cancellable_new ();

	plugin_job = gs_plugin_job_trigger_upgrade_new (upgrade, GS_PLUGIN_TRIGGER_UPGRADE_FLAGS_INTERACTIVE);
	gs_plugin_loader_job_process_async (self->plugin_loader, plugin_job,
					    self->cancellable_upgrade,
					    upgrade_trigger_finished_cb,
					    self);
}

static void
gs_updates_page_upgrade_confirm_cb (GtkDialog *dialog,
                                    GtkResponseType response_type,
                                    GsUpdatesPage *self)
{
	/* unmap the dialog */
	adw_dialog_force_close (ADW_DIALOG (dialog));

	switch (response_type) {
	case GTK_RESPONSE_ACCEPT:
		g_debug ("agreed to upgrade removing apps");
		trigger_upgrade (self);
		break;
	case GTK_RESPONSE_CANCEL:
		g_debug ("cancelled removal dialog");
		break;
	case GTK_RESPONSE_DELETE_EVENT:
		break;
	default:
		g_assert_not_reached ();
	}
}

static void
gs_updates_page_upgrade_install_cb (GsUpgradeBanner *upgrade_banner,
                                    GsUpdatesPage *self)
{
	GsAppList *removals;
	GsApp *upgrade;
	GtkWidget *dialog;
	guint cnt = 0;
	guint i;

	upgrade = gs_upgrade_banner_get_app (GS_UPGRADE_BANNER (self->upgrade_banner));
	if (upgrade == NULL) {
		g_warning ("no upgrade available to install");
		return;
	}

	/* count the removals */
	removals = gs_app_get_related (upgrade);
	for (i = 0; i < gs_app_list_length (removals); i++) {
		GsApp *app = gs_app_list_index (removals, i);
		if (gs_app_get_state (app) != GS_APP_STATE_UNAVAILABLE)
			continue;
		cnt++;
	}

	if (cnt == 0) {
		/* no need for a removal confirmation dialog */
		trigger_upgrade (self);
		return;
	}

	dialog = gs_removal_dialog_new ();
	g_signal_connect (dialog, "response",
	                  G_CALLBACK (gs_updates_page_upgrade_confirm_cb),
	                  self);
	gs_removal_dialog_show_upgrade_removals (GS_REMOVAL_DIALOG (dialog),
	                                         upgrade);
	adw_dialog_present (ADW_DIALOG (dialog), GTK_WIDGET (self));
}

static void
gs_updates_page_invalidate_downloaded_upgrade (GsUpdatesPage *self)
{
	GsApp *app;
	app = gs_upgrade_banner_get_app (GS_UPGRADE_BANNER (self->upgrade_banner));
	if (app == NULL)
		return;
	if (gs_app_get_state (app) != GS_APP_STATE_UPDATABLE)
		return;
	gs_app_set_state (app, GS_APP_STATE_AVAILABLE);
	g_debug ("resetting %s to AVAILABLE as the updates have changed",
		 gs_app_get_id (app));
}

static gboolean
gs_shell_update_are_updates_in_progress (GsUpdatesPage *self)
{
	g_autoptr(GsAppList) list = _get_all_apps (self);
	for (guint i = 0; i < gs_app_list_length (list); i++) {
		GsApp *app = gs_app_list_index (list, i);
		switch (gs_app_get_state (app)) {
		case GS_APP_STATE_INSTALLING:
		case GS_APP_STATE_REMOVING:
		case GS_APP_STATE_DOWNLOADING:
			return TRUE;
			break;
		default:
			break;
		}
	}
	return FALSE;
}

static void
gs_updates_page_changed_cb (GsPluginLoader *plugin_loader,
                            GsUpdatesPage *self)
{
	gint64 diff_secs;

	/* if we do a live update and the upgrade is waiting to be deployed
	 * then make sure all new packages are downloaded */
	gs_updates_page_invalidate_downloaded_upgrade (self);

	/* check to see if any apps in the app list are in a processing state */
	if (gs_shell_update_are_updates_in_progress (self)) {
		g_debug ("updates-page: ignoring updates-changed as updates in progress");
		return;
	}

	diff_secs = (g_get_real_time () - self->last_loaded_time) / G_USEC_PER_SEC;
	if (diff_secs <= IGNORE_UPDATES_CHANGED_WITHIN_SECS) {
		g_debug ("updates-page: ignoring updates-changed as did load only %" G_GINT64_FORMAT " secs ago", diff_secs);
		return;
	}

	/* refresh updates list */
	gs_updates_page_reload (GS_PAGE (self));
}

static void
gs_updates_page_allow_updates_notify_cb (GsPluginLoader *plugin_loader,
                                         GParamSpec *pspec,
                                         GsUpdatesPage *self)
{
	if (!gs_plugin_loader_get_allow_updates (plugin_loader)) {
		gs_updates_page_set_state (self, GS_UPDATES_PAGE_STATE_MANAGED);
		return;
	}
	gs_updates_page_set_state (self, GS_UPDATES_PAGE_STATE_IDLE);
}

static void
gs_updates_page_upgrade_cancel_cb (GsUpgradeBanner *upgrade_banner,
                                   GsUpdatesPage *self)
{
	g_debug ("Cancelling upgrade with %p", self->cancellable_upgrade);
	g_cancellable_cancel (self->cancellable_upgrade);
}

static gboolean
gs_updates_page_setup (GsPage *page,
                       GsShell *shell,
                       GsPluginLoader *plugin_loader,
                       GCancellable *cancellable,
                       GError **error)
{
	GsUpdatesPage *self = GS_UPDATES_PAGE (page);

	g_return_val_if_fail (GS_IS_UPDATES_PAGE (self), TRUE);

	for (guint i = 0; i < GS_UPDATES_SECTION_KIND_LAST; i++) {
		self->sections[i] = gs_updates_section_new (i, plugin_loader, page);
		gs_updates_section_set_size_groups (self->sections[i],
						    self->sizegroup_name,
						    self->sizegroup_button_label,
						    self->sizegroup_button_image,
						    self->sizegroup_header);
		gtk_widget_set_vexpand (GTK_WIDGET (self->sections[i]), FALSE);
		g_object_bind_property (G_OBJECT (self), "is-narrow",
					self->sections[i], "is-narrow",
					G_BINDING_SYNC_CREATE);
		g_signal_connect (self->sections[i], "notify::counter",
				  G_CALLBACK (section_notify_counter_cb),
				  self);
		gtk_box_append (GTK_BOX (self->updates_box), GTK_WIDGET (self->sections[i]));
	}

	self->shell = shell;

	self->plugin_loader = g_object_ref (plugin_loader);
	g_signal_connect (self->plugin_loader, "pending-apps-changed",
			  G_CALLBACK (gs_updates_page_pending_apps_changed_cb),
			  self);
	g_signal_connect (self->plugin_loader, "updates-changed",
			  G_CALLBACK (gs_updates_page_changed_cb),
			  self);
	g_signal_connect_object (self->plugin_loader, "notify::allow-updates",
				 G_CALLBACK (gs_updates_page_allow_updates_notify_cb),
				 self, 0);
	g_signal_connect_object (self->plugin_loader, "notify::network-available",
				 G_CALLBACK (gs_updates_page_network_available_or_metered_notify_cb),
				 self, 0);
	g_signal_connect_object (self->plugin_loader, "notify::network-metered",
				 G_CALLBACK (gs_updates_page_network_available_or_metered_notify_cb),
				 self, 0);
	self->cancellable = g_object_ref (cancellable);

	/* setup system upgrades */
	g_signal_connect (self->upgrade_banner, "download-clicked",
			  G_CALLBACK (gs_updates_page_upgrade_download_cb), self);
	g_signal_connect (self->upgrade_banner, "install-clicked",
			  G_CALLBACK (gs_updates_page_upgrade_install_cb), self);
	g_signal_connect (self->upgrade_banner, "cancel-clicked",
			  G_CALLBACK (gs_updates_page_upgrade_cancel_cb), self);

	gs_page_set_header_start_widget (GS_PAGE (self), self->header_start_box);

	/* setup update details window */
	g_signal_connect (self->button_updates_mobile, "clicked",
			  G_CALLBACK (gs_updates_page_button_mobile_refresh_cb),
			  self);
	g_signal_connect (self->button_updates_offline, "clicked",
			  G_CALLBACK (gs_updates_page_button_network_settings_cb),
			  self);

	/* set initial state */
	if (!gs_plugin_loader_get_allow_updates (self->plugin_loader))
		self->state = GS_UPDATES_PAGE_STATE_MANAGED;
	return TRUE;
}

static void
gs_updates_page_get_property (GObject    *object,
                              guint       prop_id,
                              GValue     *value,
                              GParamSpec *pspec)
{
	GsUpdatesPage *self = GS_UPDATES_PAGE (object);

	switch ((GsUpdatesPageProperty) prop_id) {
	case PROP_IS_NARROW:
		g_value_set_boolean (value, gs_updates_page_get_is_narrow (self));
		break;
	case PROP_VADJUSTMENT:
		g_value_set_object (value, gtk_scrolled_window_get_vadjustment (GTK_SCROLLED_WINDOW (self->scrolledwindow_updates)));
		break;
	case PROP_TITLE:
		g_value_set_string (value, C_("Apps to be updated", "Updates"));
		break;
	case PROP_COUNTER:
		g_value_set_uint (value, self->updates_counter);
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
		break;
	}
}

static void
gs_updates_page_set_property (GObject      *object,
                              guint         prop_id,
                              const GValue *value,
                              GParamSpec   *pspec)
{
	GsUpdatesPage *self = GS_UPDATES_PAGE (object);

	switch ((GsUpdatesPageProperty) prop_id) {
	case PROP_IS_NARROW:
		gs_updates_page_set_is_narrow (self, g_value_get_boolean (value));
		break;
	case PROP_VADJUSTMENT:
	case PROP_TITLE:
	case PROP_COUNTER:
		/* Read only */
		g_assert_not_reached ();
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
		break;
	}
}

static void
gs_updates_page_dispose (GObject *object)
{
	GsUpdatesPage *self = GS_UPDATES_PAGE (object);

	gs_updates_page_remove_last_checked_timeout (self);

	g_cancellable_cancel (self->cancellable_refresh);
	g_clear_object (&self->cancellable_refresh);
	g_cancellable_cancel (self->cancellable_upgrade);
	g_clear_object (&self->cancellable_upgrade);

	for (guint i = 0; i < GS_UPDATES_SECTION_KIND_LAST; i++) {
		if (self->sections[i] != NULL) {
			g_signal_handlers_disconnect_by_func (self->sections[i],
							      section_notify_counter_cb,
							      self);
			gtk_widget_unparent (GTK_WIDGET (self->sections[i]));
			self->sections[i] = NULL;
		}
	}

	g_clear_object (&self->plugin_loader);
	g_clear_object (&self->cancellable);
	g_clear_signal_handler (&self->settings_changed_id, self->settings);
	g_clear_object (&self->settings);
	g_clear_object (&self->desktop_settings);

	g_clear_object (&self->sizegroup_name);
	g_clear_object (&self->sizegroup_button_label);
	g_clear_object (&self->sizegroup_button_image);
	g_clear_object (&self->sizegroup_header);

	G_OBJECT_CLASS (gs_updates_page_parent_class)->dispose (object);
}

static void
gs_updates_page_unmap (GtkWidget *widget)
{
	GsUpdatesPage *self = GS_UPDATES_PAGE (widget);

	/* Don’t need to update the ‘last checked’ label while the UI isn’t
	 * visible. The timer will be reinstated by update_ui_state() when the
	 * UI is next shown. */
	gs_updates_page_remove_last_checked_timeout (self);

	GTK_WIDGET_CLASS (gs_updates_page_parent_class)->unmap (widget);
}

static void
gs_updates_page_class_init (GsUpdatesPageClass *klass)
{
	GObjectClass *object_class = G_OBJECT_CLASS (klass);
	GsPageClass *page_class = GS_PAGE_CLASS (klass);
	GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);

	object_class->get_property = gs_updates_page_get_property;
	object_class->set_property = gs_updates_page_set_property;
	object_class->dispose = gs_updates_page_dispose;

	widget_class->unmap = gs_updates_page_unmap;

	page_class->switch_to = gs_updates_page_switch_to;
	page_class->switch_from = gs_updates_page_switch_from;
	page_class->reload = gs_updates_page_reload;
	page_class->setup = gs_updates_page_setup;

	/**
	 * GsUpdatesPage:is-narrow:
	 *
	 * Whether the page is in narrow mode.
	 *
	 * In narrow mode, the page will take up less horizontal space, doing so
	 * by e.g. using icons rather than labels in buttons. This is needed to
	 * keep the UI useable on small form-factors like smartphones.
	 *
	 * Since: 41
	 */
	obj_props[PROP_IS_NARROW] =
		g_param_spec_boolean ("is-narrow", NULL, NULL,
				      FALSE,
				      G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);

	g_object_class_install_properties (object_class, G_N_ELEMENTS (obj_props), obj_props);

	g_object_class_override_property (object_class, PROP_VADJUSTMENT, "vadjustment");
	g_object_class_override_property (object_class, PROP_TITLE, "title");
	g_object_class_override_property (object_class, PROP_COUNTER, "counter");

	gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/Software/gs-updates-page.ui");

	gtk_widget_class_bind_template_child (widget_class, GsUpdatesPage, button_refresh);
	gtk_widget_class_bind_template_child (widget_class, GsUpdatesPage, button_stop);
	gtk_widget_class_bind_template_child (widget_class, GsUpdatesPage, header_spinner_start);
	gtk_widget_class_bind_template_child (widget_class, GsUpdatesPage, header_start_box);
	gtk_widget_class_bind_template_child (widget_class, GsUpdatesPage, updates_box);
	gtk_widget_class_bind_template_child (widget_class, GsUpdatesPage, button_updates_mobile);
	gtk_widget_class_bind_template_child (widget_class, GsUpdatesPage, button_updates_offline);
	gtk_widget_class_bind_template_child (widget_class, GsUpdatesPage, updates_failed_page);
	gtk_widget_class_bind_template_child (widget_class, GsUpdatesPage, uptodate_description);
	gtk_widget_class_bind_template_child (widget_class, GsUpdatesPage, label_last_checked);
	gtk_widget_class_bind_template_child (widget_class, GsUpdatesPage, scrolledwindow_updates);
	gtk_widget_class_bind_template_child (widget_class, GsUpdatesPage, stack_updates);
	gtk_widget_class_bind_template_child (widget_class, GsUpdatesPage, upgrade_banner);
	gtk_widget_class_bind_template_child (widget_class, GsUpdatesPage, banner_end_of_life);
	gtk_widget_class_bind_template_child (widget_class, GsUpdatesPage, up_to_date_image);

	gtk_widget_class_bind_template_callback (widget_class, gs_updates_page_button_refresh_cb);
	gtk_widget_class_bind_template_callback (widget_class, gs_updates_page_button_stop_cb);
}

static void
gs_updates_page_init (GsUpdatesPage *self)
{
	g_type_ensure (GS_TYPE_UPGRADE_BANNER);

	gtk_widget_init_template (GTK_WIDGET (self));

	self->state = GS_UPDATES_PAGE_STATE_STARTUP;

	self->settings = g_settings_new ("org.gnome.software");
	self->settings_changed_id = g_signal_connect (self->settings, "changed::check-timestamp",
						      G_CALLBACK (settings_changed_check_timestamp_cb),
						      self);

	self->desktop_settings = g_settings_new ("org.gnome.desktop.interface");

	self->sizegroup_name = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL);
	self->sizegroup_button_label = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL);
	self->sizegroup_button_image = gtk_size_group_new (GTK_SIZE_GROUP_HORIZONTAL);
	self->sizegroup_header = gtk_size_group_new (GTK_SIZE_GROUP_VERTICAL);

}

/**
 * gs_updates_page_get_is_narrow:
 * @self: a #GsUpdatesPage
 *
 * Get the value of #GsUpdatesPage:is-narrow.
 *
 * Returns: %TRUE if the page is in narrow mode, %FALSE otherwise
 *
 * Since: 41
 */
gboolean
gs_updates_page_get_is_narrow (GsUpdatesPage *self)
{
	g_return_val_if_fail (GS_IS_UPDATES_PAGE (self), FALSE);

	return self->is_narrow;
}

/**
 * gs_updates_page_set_is_narrow:
 * @self: a #GsUpdatesPage
 * @is_narrow: %TRUE to set the page in narrow mode, %FALSE otherwise
 *
 * Set the value of #GsUpdatesPage:is-narrow.
 *
 * Since: 41
 */
void
gs_updates_page_set_is_narrow (GsUpdatesPage *self, gboolean is_narrow)
{
	g_return_if_fail (GS_IS_UPDATES_PAGE (self));

	is_narrow = !!is_narrow;

	if (self->is_narrow == is_narrow)
		return;

	self->is_narrow = is_narrow;
	if (self->is_narrow)
		gtk_image_set_pixel_size (GTK_IMAGE (self->up_to_date_image), 280);
	else
		gtk_image_set_pixel_size (GTK_IMAGE (self->up_to_date_image), 300);
	g_object_notify_by_pspec (G_OBJECT (self), obj_props[PROP_IS_NARROW]);
}

GsUpdatesPage *
gs_updates_page_new (void)
{
	return GS_UPDATES_PAGE (g_object_new (GS_TYPE_UPDATES_PAGE, NULL));
}
