/*
 * Copyright (C) 2019 Richard Hughes <richard@hughsie.com>
 *
 * SPDX-License-Identifier: GPL-2.0+
 */

#include "config.h"

#include <adwaita.h>
#include <glib/gi18n.h>
#include <glib/gstdio.h>
#include <gtk/gtk.h>
#include <locale.h>
#include <stdlib.h>

#include "gfu-common.h"
#include "gfu-device-row.h"
#include "gfu-release-row.h"

#define GFU_MAIN_REPORT_DEVICE_LIST_INITIAL_DELAY  5  /* s */
#define GFU_MAIN_REPORT_DEVICE_LIST_INTERVAL_DELAY 30 /* days */

/* should this come from FwupdRemote? */
#define GFU_LVFS_PRIVACY_POLICY_URL "https://lvfs.readthedocs.io/en/latest/privacy.html"

typedef void (*GfuMainDialogFunc)(gpointer user_data);

typedef struct {
	AdwApplication *application;
	GSettings *settings;
	GtkBuilder *builder;
	GCancellable *cancellable;
	FwupdClient *client;
	FwupdDevice *device;
	FwupdRelease *release;
	FwupdInstallFlags flags;
	guint refresh_id;
	GPtrArray *dialogs;	   /* of AdwDialog */
	AdwDialog *dialog_current; /* no-ref */
	GfuMainDialogFunc dialog_func;
	GPtrArray *post_requests;
	GHashTable *report_metadata;
	gchar *report_devices_payload;
	GPtrArray *devices;
	FwupdRemote *remote_lvfs;
	gboolean daemon_alive;
} GfuMain;

static void
gfu_main_release_row_button_clicked_cb(GfuReleaseRow *row, GfuMain *self);
static void
gfu_main_refresh_release(GfuMain *self);
static void
gfu_main_refresh_releases(GfuMain *self);
static void
gfu_main_process_dialogs(GfuMain *self);

/* this takes ownership */
static void
gfu_main_add_dialog(GfuMain *self, AdwDialog *dialog, gboolean process_now)
{
	g_ptr_array_add(self->dialogs, dialog);
	if (process_now)
		gfu_main_process_dialogs(self);
}

static void
gfu_main_error_dialog(GfuMain *self, const gchar *title, const gchar *message)
{
	AdwDialog *dialog = adw_alert_dialog_new(title, message);
	/* TRANSLATORS: button text */
	adw_alert_dialog_add_response(ADW_ALERT_DIALOG(dialog), "close", _("Understood"));
	gfu_main_add_dialog(self, dialog, TRUE);
}

static void
gfu_main_error_fatal(GfuMain *self, const gchar *text)
{
	GtkWidget *w = GTK_WIDGET(gtk_builder_get_object(self->builder, "stack_main"));
	GtkWidget *split_view_devices =
	    GTK_WIDGET(gtk_builder_get_object(self->builder, "split_view_devices"));
	gtk_widget_set_visible(GTK_WIDGET(split_view_devices), FALSE);
	gtk_stack_set_visible_child_name(GTK_STACK(w), "error");
	w = GTK_WIDGET(gtk_builder_get_object(self->builder, "label_error"));
	adw_status_page_set_description(ADW_STATUS_PAGE(w), text);
}

static void
gfu_main_set_label(GfuMain *self, const gchar *label_id, const gchar *text)
{
	GtkWidget *w;
	g_autofree gchar *label_id_title = g_strdup_printf("%s_title", label_id);

	/* hide empty box */
	if (text == NULL || text[0] == '\0') {
		w = GTK_WIDGET(gtk_builder_get_object(self->builder, label_id));
		gtk_widget_set_visible(w, FALSE);
		w = GTK_WIDGET(gtk_builder_get_object(self->builder, label_id_title));
		gtk_widget_set_visible(w, FALSE);
		return;
	}

	/* update and display */
	w = GTK_WIDGET(gtk_builder_get_object(self->builder, label_id));
	gtk_label_set_label(GTK_LABEL(w), text);
	gtk_widget_set_visible(w, TRUE);
	w = GTK_WIDGET(gtk_builder_get_object(self->builder, label_id_title));
	gtk_widget_set_visible(w, TRUE);
}

static void
gfu_main_set_label_title(GfuMain *self, const gchar *label_id, const gchar *text)
{
	/* update only the title of a label */
	GtkWidget *w;
	g_autofree gchar *label_id_title = g_strdup_printf("%s_title", label_id);

	/* hide empty box */
	if (text == NULL) {
		w = GTK_WIDGET(gtk_builder_get_object(self->builder, label_id));
		gtk_widget_set_visible(w, FALSE);
		w = GTK_WIDGET(gtk_builder_get_object(self->builder, label_id_title));
		gtk_widget_set_visible(w, FALSE);
		return;
	}

	/* update and display */
	w = GTK_WIDGET(gtk_builder_get_object(self->builder, label_id_title));
	adw_preferences_row_set_title(ADW_PREFERENCES_ROW(w), text);
	gtk_widget_set_visible(w, TRUE);
	w = GTK_WIDGET(gtk_builder_get_object(self->builder, label_id));
	gtk_widget_set_visible(w, TRUE);
}

static GtkWidget *
gfu_main_guid_instance_id_new(const gchar *instance_id, const gchar *guid)
{
	GtkWidget *row = adw_action_row_new();
	g_autofree gchar *guid_monospace = g_strdup_printf("<tt>%s</tt>", guid);

	adw_preferences_row_set_title(ADW_PREFERENCES_ROW(row), guid_monospace);
	adw_preferences_row_set_use_markup(ADW_PREFERENCES_ROW(row), TRUE);
	adw_preferences_row_set_title_selectable(ADW_PREFERENCES_ROW(row), TRUE);

	if (instance_id != NULL) {
		g_autofree gchar *instance_id_safe = g_markup_escape_text(instance_id, -1);
		adw_action_row_set_subtitle(ADW_ACTION_ROW(row), instance_id_safe);
	}

	return row;
}

static GPtrArray *
gfu_main_get_secret_children(GtkWidget *w)
{
	GPtrArray *children = g_object_get_data(G_OBJECT(w), "children");
	if (children != NULL)
		return children;
	children = g_ptr_array_new();
	g_object_set_data_full(G_OBJECT(w),
			       "children",
			       children,
			       (GDestroyNotify)g_ptr_array_unref);
	return children;
}

static void
gfu_main_update_guids(GfuMain *self, GPtrArray *guids, GPtrArray *instance_ids)
{
	GPtrArray *children;
	GtkWidget *w;
	g_autoptr(GHashTable) guid_hash = NULL;

	w = GTK_WIDGET(gtk_builder_get_object(self->builder, "row_guids"));
	gtk_widget_set_visible(w, guids->len > 0);

	/* we have to do this hack as adw_expander_row_remove_all() does not exist */
	children = gfu_main_get_secret_children(w);
	for (guint i = 0; i < children->len; i++) {
		GtkWidget *row = g_ptr_array_index(children, i);
		adw_expander_row_remove(ADW_EXPANDER_ROW(w), row);
	}
	g_ptr_array_set_size(children, 0);

	/* instance IDs */
	guid_hash = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, NULL);
	for (guint i = 0; i < instance_ids->len; i++) {
		const gchar *instance_id = g_ptr_array_index(instance_ids, i);
		g_autofree gchar *guid = fwupd_guid_hash_string(instance_id);
		GtkWidget *row = gfu_main_guid_instance_id_new(instance_id, guid);
		g_hash_table_add(guid_hash, g_steal_pointer(&guid));
		adw_expander_row_add_row(ADW_EXPANDER_ROW(w), row);
		g_ptr_array_add(children, row);
	}

	/* GUID fallback */
	for (guint i = 0; i < guids->len; i++) {
		const gchar *guid = g_ptr_array_index(guids, i);
		GtkWidget *row;
		if (g_hash_table_contains(guid_hash, guid))
			continue;

		row = gfu_main_guid_instance_id_new(NULL, guid);
		adw_expander_row_add_row(ADW_EXPANDER_ROW(w), row);
		g_ptr_array_add(children, row);
	}
}

static void
gfu_main_set_device_flags(GfuMain *self, guint64 flags)
{
	GtkWidget *w =
	    GTK_WIDGET(gtk_builder_get_object(self->builder, "label_device_flags_title"));
	GPtrArray *children = gfu_main_get_secret_children(w);

	gtk_widget_set_visible(GTK_WIDGET(w), FALSE);

	/* we have to do this hack as adw_expander_row_remove_all() does not exist */
	for (guint i = 0; i < children->len; i++) {
		GtkWidget *row = g_ptr_array_index(children, i);
		adw_expander_row_remove(ADW_EXPANDER_ROW(w), row);
	}
	g_ptr_array_set_size(children, 0);

	/* iterate through flags */
	for (guint j = 0; j < 64; j++) {
		GtkWidget *row;
		GtkWidget *icon;
		const gchar *flag_tmp;
		FwupdDeviceFlags flag = (guint64)1 << j;

		/* if flag is not set, skip */
		if ((flags & flag) == 0)
			continue;

		/* some are not interesting */
		flag_tmp = gfu_common_device_flags_to_string(flag);
		if (flag_tmp == NULL)
			continue;

		/* add row with icon */
		row = adw_action_row_new();
		adw_preferences_row_set_title(ADW_PREFERENCES_ROW(row), flag_tmp);
		icon = gtk_image_new_from_icon_name(gfu_common_device_icon_from_flag(flag));
		adw_action_row_add_prefix(ADW_ACTION_ROW(row), icon);
		adw_expander_row_add_row(ADW_EXPANDER_ROW(w), row);

		g_ptr_array_add(children, row);
		gtk_widget_set_visible(GTK_WIDGET(w), TRUE);
	}
}

static void
gfu_main_device_auto_verify_cb(GObject *source_object, GAsyncResult *res, gpointer user_data)
{
	GfuMain *self = (GfuMain *)user_data;
	g_autoptr(GError) error = NULL;

	if (!fwupd_client_verify_finish(self->client, res, &error)) {
		g_warning("failed to verify firmware:  %s", error->message);
		/* TRANSLATORS: the attestation failed */
		gfu_main_set_label(self, "label_device_checksums", _("Not OK"));
		return;
	}

	/* TRANSLATORS: the checksum verified */
	gfu_main_set_label(self, "label_device_checksums", _("OK"));
}

static gchar *
gfu_main_device_problem_to_string(GfuMain *self, FwupdDeviceProblem problem)
{
	if (problem == FWUPD_DEVICE_PROBLEM_SYSTEM_POWER_TOO_LOW) {
		if (fwupd_client_get_battery_level(self->client) == FWUPD_BATTERY_LEVEL_INVALID ||
		    fwupd_client_get_battery_threshold(self->client) ==
			FWUPD_BATTERY_LEVEL_INVALID) {
			/* TRANSLATORS: as in laptop battery power */
			return g_strdup(_("System power is too low to perform the update"));
		}
		return g_strdup_printf(
		    /* TRANSLATORS: as in laptop battery power */
		    _("System power is too low to perform the update (%u%%, requires %u%%)"),
		    fwupd_client_get_battery_level(self->client),
		    fwupd_client_get_battery_threshold(self->client));
	}
	if (problem == FWUPD_DEVICE_PROBLEM_UNREACHABLE) {
		/* TRANSLATORS: for example, a Bluetooth mouse that is in powersave mode */
		return g_strdup(_("Device is unreachable, or out of wireless range"));
	}
	if (problem == FWUPD_DEVICE_PROBLEM_POWER_TOO_LOW) {
		if (fwupd_device_get_battery_level(self->device) == FWUPD_BATTERY_LEVEL_INVALID ||
		    fwupd_device_get_battery_threshold(self->device) ==
			FWUPD_BATTERY_LEVEL_INVALID) {
			/* TRANSLATORS: for example the batteries *inside* the Bluetooth mouse */
			return g_strdup(_("Device battery power is too low"));
		}
		/* TRANSLATORS: for example the batteries *inside* the Bluetooth mouse */
		return g_strdup_printf(_("Device battery power is too low (%u%%, requires %u%%)"),
				       fwupd_device_get_battery_level(self->device),
				       fwupd_device_get_battery_threshold(self->device));
	}
	if (problem == FWUPD_DEVICE_PROBLEM_UPDATE_PENDING) {
		/* TRANSLATORS: usually this is when we're waiting for a reboot */
		return g_strdup(_("Device is waiting for the update to be applied"));
	}
	if (problem == FWUPD_DEVICE_PROBLEM_REQUIRE_AC_POWER) {
		/* TRANSLATORS: as in, wired mains power for a laptop */
		return g_strdup(_("Device requires AC power to be connected"));
	}
	if (problem == FWUPD_DEVICE_PROBLEM_LID_IS_CLOSED) {
		/* TRANSLATORS: lid means "laptop top cover" */
		return g_strdup(_("Device cannot be used while the lid is closed"));
	}
	if (problem == FWUPD_DEVICE_PROBLEM_IN_USE) {
		/* TRANSLATORS: device cannot be interrupted, for instance taking a phone call */
		return g_strdup(_("Device is in use"));
	}
	if (problem == FWUPD_DEVICE_PROBLEM_DISPLAY_REQUIRED) {
		/* TRANSLATORS: device cannot be interrupted, for instance taking a phone call */
		return g_strdup(_("Device cannot be used while there are no displays plugged in"));
	}
	return NULL;
}

static gchar *
gfu_main_device_problems_to_string(GfuMain *self)
{
	g_autoptr(GString) str = g_string_new(NULL);

	/* sanity check */
	if (self->device == NULL)
		return NULL;
	if (fwupd_device_get_problems(self->device) == FWUPD_DEVICE_PROBLEM_NONE)
		return NULL;

	/* translate each problem */
	for (guint i = 0; i < 64; i++) {
		FwupdDeviceProblem problem = 1ull << i;
		g_autofree gchar *tmp = NULL;
		if (!fwupd_device_has_problem(self->device, problem))
			continue;
		tmp = gfu_main_device_problem_to_string(self, problem);
		if (tmp == NULL)
			continue;
		g_string_append_printf(str, "%s\n", tmp);
	}

	/* success */
	if (str->len == 0)
		return NULL;
	g_string_truncate(str, str->len - 1);
	return g_string_free(g_steal_pointer(&str), FALSE);
}

static gboolean
gfu_main_refresh_device_cb(gpointer user_data)
{
	GfuMain *self = (GfuMain *)user_data;
	GtkWidget *w;
	GtkWidget *split_view_devices =
	    GTK_WIDGET(gtk_builder_get_object(self->builder, "split_view_devices"));
	GPtrArray *vendor_ids = NULL;
	g_autofree gchar *install_duration = NULL;
	g_autofree gchar *problems_str = NULL;
	g_autoptr(GString) attr = g_string_new(NULL);
	g_autoptr(GString) version = NULL;

	/* set stack */
	w = GTK_WIDGET(gtk_builder_get_object(self->builder, "stack_main"));
	if (self->device == NULL) {
		gtk_stack_set_visible_child_name(GTK_STACK(w), "loading");
		gtk_widget_set_visible(GTK_WIDGET(split_view_devices), FALSE);
		self->refresh_id = 0;
		return G_SOURCE_REMOVE;
	}

	gtk_stack_set_visible_child_name(GTK_STACK(w), "main");
	gtk_widget_set_visible(GTK_WIDGET(split_view_devices), TRUE);

	/* set device information */
	version = g_string_new(fwupd_device_get_version(self->device));

	if (fwupd_device_get_version_build_date(self->device) != 0) {
		guint64 value = fwupd_device_get_version_build_date(self->device);
		g_autoptr(GDateTime) date = g_date_time_new_from_unix_utc((gint64)value);
		g_autofree gchar *datestr = g_date_time_format(date, "%F");
		g_string_append_printf(version, " [%s]", datestr);
	}

	gfu_main_set_label(self, "label_device_version", version->str);
	gfu_main_set_label(self,
			   "label_device_version_lowest",
			   fwupd_device_get_version_lowest(self->device));
	gfu_main_set_label(self,
			   "label_device_version_bootloader",
			   fwupd_device_get_version_bootloader(self->device));
	problems_str = gfu_main_device_problems_to_string(self);
	gfu_main_set_label(self, "label_device_problems", problems_str);
	if (problems_str != NULL) {
		gfu_main_set_label(self, "label_device_update_error", NULL);
	} else {
		gfu_main_set_label(self,
				   "label_device_update_error",
				   fwupd_device_get_update_error(self->device));
	}
	gfu_main_set_label(self, "label_device_serial", fwupd_device_get_serial(self->device));
	gfu_main_set_label(self, "label_device_vendor", fwupd_device_get_vendor(self->device));
	gfu_main_set_label(self, "label_device_branch", fwupd_device_get_branch(self->device));

	vendor_ids = fwupd_device_get_vendor_ids(self->device);
	if (vendor_ids->len > 0) {
		g_autofree gchar *strv = gfu_common_strjoin_array(", ", vendor_ids);
		gfu_main_set_label(self, "label_device_vendor_ids", strv);
		w = GTK_WIDGET(
		    gtk_builder_get_object(self->builder, "label_device_vendor_ids_title"));
		adw_preferences_row_set_title(ADW_PREFERENCES_ROW(w),
					      /* TRANSLATORS: the hw IDs the device defines,
					       * e.g. USB:0x1234 */
					      ngettext("Vendor ID", "Vendor IDs", vendor_ids->len));
	} else {
		gfu_main_set_label(self, "label_device_vendor_ids", NULL);
	}

	g_string_append_printf(attr, "%u", fwupd_device_get_flashes_left(self->device));
	gfu_main_set_label(self,
			   "label_device_flashes_left",
			   g_strcmp0(attr->str, "0") ? attr->str : NULL);
	install_duration =
	    gfu_common_seconds_to_string(fwupd_device_get_install_duration(self->device));
	gfu_main_set_label(self, "label_device_install_duration", install_duration);

	gfu_main_update_guids(self,
			      fwupd_device_get_guids(self->device),
			      fwupd_device_get_instance_ids(self->device));
	gfu_main_set_device_flags(self, fwupd_device_get_flags(self->device));

	/* device can be verified immediately without hardware access */
	if (fwupd_device_has_flag(self->device, FWUPD_DEVICE_FLAG_CAN_VERIFY) &&
	    !fwupd_device_has_flag(self->device, FWUPD_DEVICE_FLAG_CAN_VERIFY_IMAGE)) {
		fwupd_client_verify_async(self->client,
					  fwupd_device_get_id(self->device),
					  self->cancellable,
					  gfu_main_device_auto_verify_cb,
					  self);
	} else if (fwupd_device_has_flag(self->device, FWUPD_DEVICE_FLAG_CAN_VERIFY_IMAGE)) {
		/* TRANSLATORS: the checksum state was unknown */
		gfu_main_set_label(self, "label_device_checksums", _("Unknown"));
	} else {
		gfu_main_set_label(self, "label_device_checksums", NULL);
	}

	/* unlock section */
	w = GTK_WIDGET(gtk_builder_get_object(self->builder, "label_device_unlock_title"));
	gtk_widget_set_visible(w, fwupd_device_has_flag(self->device, FWUPD_DEVICE_FLAG_LOCKED));

	/* verify button */
	w = GTK_WIDGET(gtk_builder_get_object(self->builder, "button_verify"));
	gtk_widget_set_visible(
	    w,
	    fwupd_device_has_flag(self->device, FWUPD_DEVICE_FLAG_CAN_VERIFY_IMAGE));

	/* verify update button */
	w = GTK_WIDGET(gtk_builder_get_object(self->builder, "button_verify_update"));
	gtk_widget_set_visible(
	    w,
	    fwupd_device_get_checksums(self->device)->len > 0 &&
		fwupd_device_has_flag(self->device, FWUPD_DEVICE_FLAG_CAN_VERIFY));

#if FWUPD_CHECK_VERSION(2, 0, 1)
	/* can tag for emulation */
	w = GTK_WIDGET(gtk_builder_get_object(self->builder, "group_emulation"));
	if (fwupd_device_has_flag(self->device, FWUPD_DEVICE_FLAG_EMULATED) ||
	    fwupd_device_has_flag(self->device, FWUPD_DEVICE_FLAG_CAN_EMULATION_TAG)) {
		gtk_widget_set_visible(w, TRUE);
	} else {
		gtk_widget_set_visible(w, FALSE);
	}

	/* can remove emulated device */
	w = GTK_WIDGET(gtk_builder_get_object(self->builder, "button_emulated_remove"));
	gtk_widget_set_visible(w, fwupd_device_has_flag(self->device, FWUPD_DEVICE_FLAG_EMULATED));

	/* can tag for emulation */
	w = GTK_WIDGET(gtk_builder_get_object(self->builder, "button_emulation_tag"));
	gtk_widget_set_visible(
	    w,
	    !fwupd_device_has_flag(self->device, FWUPD_DEVICE_FLAG_EMULATED) &&
		!fwupd_device_has_flag(self->device, FWUPD_DEVICE_FLAG_EMULATION_TAG));

	/* can untag for emulation */
	w = GTK_WIDGET(gtk_builder_get_object(self->builder, "button_emulation_untag"));
	gtk_widget_set_visible(
	    w,
	    !fwupd_device_has_flag(self->device, FWUPD_DEVICE_FLAG_EMULATED) &&
		fwupd_device_has_flag(self->device, FWUPD_DEVICE_FLAG_EMULATION_TAG));
#else
	w = GTK_WIDGET(gtk_builder_get_object(self->builder, "group_emulation"));
	gtk_widget_set_visible(w, FALSE);
#endif

	/* get new releases */
	gfu_main_refresh_releases(self);
	self->refresh_id = 0;
	return G_SOURCE_REMOVE;
}

static void
gfu_main_refresh_device(GfuMain *self)
{
	if (self->refresh_id != 0)
		g_source_remove(self->refresh_id);
	self->refresh_id = g_timeout_add(100, gfu_main_refresh_device_cb, self);
}

static void
gfu_main_device_added_cb(FwupdClient *client, FwupdDevice *device, GfuMain *self)
{
	GtkWidget *w;
	GtkWidget *l;

	if (fwupd_device_get_version(device) == NULL)
		return;

	/* create and add new row for device */
	l = gfu_device_row_new(device);
	gtk_widget_set_visible(l, TRUE);
	w = GTK_WIDGET(gtk_builder_get_object(self->builder, "listbox_devices"));
	gtk_list_box_insert(GTK_LIST_BOX(w), l, -1);

	/* an emulated device showed up -- we probably care about it right now */
	if (fwupd_device_has_flag(device, FWUPD_DEVICE_FLAG_EMULATED))
		gtk_widget_activate(l);
}

static void
gfu_main_device_removed_cb(FwupdClient *client, FwupdDevice *device, GfuMain *self)
{
	GtkWidget *w;

	w = GTK_WIDGET(gtk_builder_get_object(self->builder, "listbox_devices"));
	for (GtkWidget *c = gtk_widget_get_first_child(w); c != NULL;
	     c = gtk_widget_get_next_sibling(c)) {
		GfuDeviceRow *row = GFU_DEVICE_ROW(c);
		if (fwupd_device_compare(device, gfu_device_row_get_device(row)) == 0) {
			gtk_list_box_remove(GTK_LIST_BOX(w), c);
			break;
		}
	}
}

static void
gfu_main_reboot_shutdown_response_cb(GtkDialog *dialog, const gchar *response, GfuMain *self)
{
	if (g_strcmp0(response, "shutdown") == 0) {
		g_autoptr(GError) error = NULL;
		if (!gfu_common_system_shutdown(&error)) {
			g_debug("Failed to shutdown device: %s\n", error->message);
			gfu_main_error_dialog(self,
					      /* TRANSLATORS: error in shutting down the system */
					      _("Shutdown Failed"),
					      _("A manual shutdown is required."));
		}
		return;
	}
	if (g_strcmp0(response, "reboot") == 0) {
		g_autoptr(GError) error = NULL;
		if (!gfu_common_system_reboot(&error)) {
			g_debug("Failed to reboot device: %s\n", error->message);
			gfu_main_error_dialog(self,
					      /* TRANSLATORS: error in rebooting down the system */
					      _("Reboot Failed"),
					      _("A manual reboot is required."));
		}
		return;
	}
}

static void
gfu_main_build_report_devices_payload(GfuMain *self)
{
#if FWUPD_CHECK_VERSION(1, 9, 20)
	g_autoptr(GError) error_local = NULL;

	g_clear_pointer(&self->report_devices_payload, g_free);

	if (self->remote_lvfs == NULL) {
		g_debug("no LVFS remote");
		return;
	}
	if (self->report_metadata == NULL) {
		g_debug("no report metadata");
		return;
	}
	if (!fwupd_remote_has_flag(self->remote_lvfs, FWUPD_REMOTE_FLAG_ENABLED)) {
		g_debug("%s not enabled, skipping", fwupd_remote_get_id(self->remote_lvfs));
		return;
	}
	if (fwupd_remote_get_report_uri(self->remote_lvfs) == NULL) {
		g_debug("no report URL for %s, skipping", fwupd_remote_get_id(self->remote_lvfs));
		return;
	}

	/* lets give it a go */
	g_free(self->report_devices_payload);
	self->report_devices_payload = fwupd_client_build_report_devices(self->client,
									 self->devices,
									 self->report_metadata,
									 &error_local);
	if (self->report_devices_payload == NULL) {
		g_debug("failed to build report for %s: %s",
			fwupd_remote_get_id(self->remote_lvfs),
			error_local->message);
		return;
	}
#endif
}

static void
gfu_main_update_remotes_cb(GObject *source, GAsyncResult *res, gpointer user_data)
{
	GfuMain *self = (GfuMain *)user_data;
	GtkWidget *w;
	gboolean enabled_any_download_remote = FALSE;
	g_autoptr(GError) error = NULL;
	g_autoptr(GPtrArray) remotes = NULL;

	remotes = fwupd_client_get_remotes_finish(FWUPD_CLIENT(source), res, &error);
	if (remotes == NULL) {
		/* TRANSLATORS: fwupd refused us data */
		gfu_main_error_dialog(self, _("Getting Remotes Failed"), error->message);
		return;
	}
	for (guint i = 0; i < remotes->len; i++) {
		FwupdRemote *remote = g_ptr_array_index(remotes, i);

		/* rather than getting by ID */
		if (g_strcmp0(fwupd_remote_get_id(remote), "lvfs") == 0)
			g_set_object(&self->remote_lvfs, remote);

#if FWUPD_CHECK_VERSION(2, 0, 0)
		/* if another repository is turned on providing metadata */
		g_debug("%s is %s",
			fwupd_remote_get_id(remote),
			fwupd_remote_has_flag(remote, FWUPD_REMOTE_FLAG_ENABLED) ? "enabled"
										 : "disabled");
		if (fwupd_remote_has_flag(remote, FWUPD_REMOTE_FLAG_ENABLED)) {
			if (fwupd_remote_get_kind(remote) == FWUPD_REMOTE_KIND_DOWNLOAD)
				enabled_any_download_remote = TRUE;
			/* lvfs is present and disabled */
		}
#else
		/* if another repository is turned on providing metadata */
		g_debug("%s is %s",
			fwupd_remote_get_id(remote),
			fwupd_remote_get_enabled(remote) ? "enabled" : "disabled");
		if (fwupd_remote_get_enabled(remote)) {
			if (fwupd_remote_get_kind(remote) == FWUPD_REMOTE_KIND_DOWNLOAD)
				enabled_any_download_remote = TRUE;
		}
#endif
	}

	if (self->remote_lvfs != NULL &&
	    !fwupd_remote_has_flag(self->remote_lvfs, FWUPD_REMOTE_FLAG_ENABLED) &&
	    !enabled_any_download_remote) {
		w = GTK_WIDGET(gtk_builder_get_object(self->builder, "banner_enable_lvfs"));
		adw_banner_set_revealed(ADW_BANNER(w), TRUE);
		w = GTK_WIDGET(gtk_builder_get_object(self->builder, "banner_refresh_lvfs"));
		adw_banner_set_revealed(ADW_BANNER(w), FALSE);
	} else if (self->remote_lvfs != NULL && fwupd_remote_needs_refresh(self->remote_lvfs)) {
		w = GTK_WIDGET(gtk_builder_get_object(self->builder, "banner_enable_lvfs"));
		adw_banner_set_revealed(ADW_BANNER(w), FALSE);
		w = GTK_WIDGET(gtk_builder_get_object(self->builder, "banner_refresh_lvfs"));
		adw_banner_set_revealed(ADW_BANNER(w), TRUE);
	} else {
		w = GTK_WIDGET(gtk_builder_get_object(self->builder, "banner_enable_lvfs"));
		adw_banner_set_revealed(ADW_BANNER(w), FALSE);
		w = GTK_WIDGET(gtk_builder_get_object(self->builder, "banner_refresh_lvfs"));
		adw_banner_set_revealed(ADW_BANNER(w), FALSE);
	}

	/* this might have changed */
	gfu_main_build_report_devices_payload(self);
}

static gint
gfu_main_sort_device_list_box_cb(GtkListBoxRow *row1, GtkListBoxRow *row2, gpointer user_data)
{
	FwupdDevice *dev1 = gfu_device_row_get_device(GFU_DEVICE_ROW(row1));
	FwupdDevice *dev2 = gfu_device_row_get_device(GFU_DEVICE_ROW(row2));
	FwupdDevice *roo1 = fwupd_device_get_root(dev1);
	FwupdDevice *roo2 = fwupd_device_get_root(dev2);
	g_autofree gchar *id1 = NULL;
	g_autofree gchar *id2 = NULL;

	if (dev1 == roo1) {
		id1 = g_strdup_printf("%s:", fwupd_device_get_id(dev1));
	} else {
		id1 =
		    g_strdup_printf("%s:%s", fwupd_device_get_id(roo1), fwupd_device_get_id(dev1));
	}
	if (dev2 == roo2) {
		id2 = g_strdup_printf("%s:", fwupd_device_get_id(dev2));
	} else {
		id2 =
		    g_strdup_printf("%s:%s", fwupd_device_get_id(roo2), fwupd_device_get_id(dev2));
	}
	return g_strcmp0(id1, id2);
}

static gboolean
gfu_main_update_devices_activate_child_cb(gpointer user_data)
{
	GfuMain *self = (GfuMain *)user_data;
	GtkWidget *w = GTK_WIDGET(gtk_builder_get_object(self->builder, "listbox_devices"));
	AdwNavigationSplitView *split_view =
	    ADW_NAVIGATION_SPLIT_VIEW(gtk_builder_get_object(self->builder, "split_view"));
	g_autofree gchar *device_id_last = g_settings_get_string(self->settings, "device-id-last");

	/* saved in GSettings */
	if (device_id_last != NULL && device_id_last[0] != '\0') {
		for (GtkWidget *c = gtk_widget_get_first_child(w); c != NULL;
		     c = gtk_widget_get_next_sibling(c)) {
			GfuDeviceRow *row = GFU_DEVICE_ROW(c);
			FwupdDevice *device = gfu_device_row_get_device(row);
			if (g_strcmp0(fwupd_device_get_id(device), device_id_last) == 0) {
				gtk_widget_activate(c);
				return G_SOURCE_REMOVE;
			}
		}
	}

	/* get first device in the list */
	w = gtk_widget_get_first_child(w);
	if (w == NULL)
		return G_SOURCE_REMOVE;

	/* activate the first device */
	if (!adw_navigation_split_view_get_collapsed(split_view))
		gtk_widget_activate(w);

	/* never repeat */
	return G_SOURCE_REMOVE;
}

#if FWUPD_CHECK_VERSION(1, 9, 20)
static void
gfu_main_report_device_list_upload_report_cb(GObject *source, GAsyncResult *res, gpointer user_data)
{
	GfuMain *self = (GfuMain *)user_data;
	g_autofree gchar *uri = NULL;
	g_autoptr(GError) error = NULL;

	/* no point keeping this now */
	g_clear_pointer(&self->report_devices_payload, g_free);

	uri = fwupd_client_upload_report_finish(FWUPD_CLIENT(source), res, &error);
	if (uri == NULL) {
		/* TRANSLATORS: the LVFS remote could not be enabled */
		gfu_main_error_dialog(self, _("Uploading Failed"), error->message);
		return;
	}
}

static void
gfu_main_report_device_list_dialog_response_cb(AdwMessageDialog *dialog,
					       const gchar *response,
					       GfuMain *self)
{
	if (g_strcmp0(response, "upload") != 0)
		return;
	fwupd_client_upload_report_async(self->client,
					 fwupd_remote_get_report_uri(self->remote_lvfs),
					 self->report_devices_payload,
					 NULL, /* signature */
					 FWUPD_CLIENT_UPLOAD_FLAG_ALWAYS_MULTIPART,
					 self->cancellable,
					 gfu_main_report_device_list_upload_report_cb,
					 self);
}

static void
gfu_main_button_copy_text_clicked_cb(GtkButton *button, GtkTextView *text_view)
{
	GdkClipboard *clipboard = gtk_widget_get_clipboard(GTK_WIDGET(text_view));
	GtkTextBuffer *buffer = gtk_text_view_get_buffer(text_view);
	GtkTextIter start, end;
	g_autofree gchar *text = NULL;

	gtk_text_buffer_get_bounds(buffer, &start, &end);
	text = gtk_text_buffer_get_text(buffer, &start, &end, FALSE);
	gdk_clipboard_set_text(clipboard, text);
}

static void
gfu_main_report_device_list_dialog(GfuMain *self)
{
	GtkTextBuffer *text_buffer;
	AdwDialog *dialog;
	GtkWidget *hbox;
	GtkWidget *label_dl;
	GtkWidget *scrolled_window;
	GtkWidget *text_view;
	GtkWidget *vbox;
	GtkWidget *button_copy;
	g_autoptr(GError) error_local = NULL;

	/* TRANSLATORS: dialog title */
	dialog = adw_alert_dialog_new(_("Upload Device List?"), NULL);
	adw_alert_dialog_format_body(
	    ADW_ALERT_DIALOG(dialog),
	    /* TRANSLATORS: dialog body, %1 is typically "Linux Vendor Firmware Service"
	     *  and %2 is a URL to the privacy policy */
	    _("Help to improve the %s by sending the list of current devices. The list is "
	      "anonymous and does not include personally identifiable information  — see the <a "
	      "href=\"%s\">privacy policy</a> for more information."),
	    fwupd_remote_get_title(self->remote_lvfs),
	    GFU_LVFS_PRIVACY_POLICY_URL);
	adw_alert_dialog_add_responses(ADW_ALERT_DIALOG(dialog),
				       "close",
				       /* TRANSLATORS: button text */
				       _("_Cancel"),
				       "upload",
				       /* TRANSLATORS: button text */
				       _("_Upload"),
				       NULL);
	adw_alert_dialog_set_body_use_markup(ADW_ALERT_DIALOG(dialog), TRUE);
	adw_alert_dialog_set_response_appearance(ADW_ALERT_DIALOG(dialog),
						 "upload",
						 ADW_RESPONSE_SUGGESTED);
	adw_alert_dialog_set_default_response(ADW_ALERT_DIALOG(dialog), "upload");

	/* TRANSLATORS: header before the JSON listing */
	label_dl = gtk_label_new(_("Device List"));
	gtk_widget_set_halign(label_dl, GTK_ALIGN_START);
	gtk_widget_add_css_class(label_dl, "title-4");
	gtk_widget_set_hexpand(label_dl, TRUE);
	hbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 9);
	gtk_box_append(GTK_BOX(hbox), label_dl);

	/* json blob */
	text_buffer = gtk_text_buffer_new(NULL);
	gtk_text_buffer_set_text(text_buffer, self->report_devices_payload, -1);
	text_view = gtk_text_view_new_with_buffer(text_buffer);
	gtk_text_view_set_monospace(GTK_TEXT_VIEW(text_view), TRUE);
	gtk_text_view_set_top_margin(GTK_TEXT_VIEW(text_view), 9);
	gtk_text_view_set_bottom_margin(GTK_TEXT_VIEW(text_view), 9);
	gtk_text_view_set_left_margin(GTK_TEXT_VIEW(text_view), 9);
	gtk_text_view_set_right_margin(GTK_TEXT_VIEW(text_view), 9);
	gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(text_view), GTK_WRAP_CHAR);
	gtk_widget_set_margin_end(text_view, 9);
	gtk_widget_add_css_class(text_view, "inline");

	/* make scrollable */
	scrolled_window = gtk_scrolled_window_new();
	gtk_scrolled_window_set_child(GTK_SCROLLED_WINDOW(scrolled_window), text_view);
	gtk_scrolled_window_set_propagate_natural_width(GTK_SCROLLED_WINDOW(scrolled_window), TRUE);
	gtk_scrolled_window_set_propagate_natural_height(GTK_SCROLLED_WINDOW(scrolled_window),
							 TRUE);
	gtk_widget_add_css_class(scrolled_window, "card");

	/* button to copy the contents to the clipboard */
	button_copy = gtk_button_new_from_icon_name("edit-copy-symbolic");
	g_object_set(button_copy, "focus-on-click", FALSE, NULL);
	gtk_widget_set_tooltip_text(button_copy, _("Copy"));
	gtk_widget_set_halign(button_copy, GTK_ALIGN_END);
	gtk_widget_add_css_class(button_copy, "flat");
	gtk_box_append(GTK_BOX(hbox), button_copy);
	g_signal_connect(button_copy,
			 "clicked",
			 G_CALLBACK(gfu_main_button_copy_text_clicked_cb),
			 text_view);

	/* add extra section */
	vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 9);
	gtk_box_append(GTK_BOX(vbox), hbox);
	gtk_box_append(GTK_BOX(vbox), scrolled_window);
	adw_alert_dialog_set_extra_child(ADW_ALERT_DIALOG(dialog), vbox);

	/* show */
	g_signal_connect(dialog,
			 "response",
			 G_CALLBACK(gfu_main_report_device_list_dialog_response_cb),
			 self);
	gfu_main_add_dialog(self, dialog, TRUE);
}

static void
gfu_main_report_check_device_list(GfuMain *self)
{
	GtkWidget *w;

	/* sanity check */
	if (self->remote_lvfs == NULL) {
		g_debug("no LVFS remote");
		return;
	}
	if (fwupd_remote_needs_refresh(self->remote_lvfs)) {
		g_debug("LVFS remote needs refresh");
		return;
	}
	if (self->report_devices_payload == NULL) {
		g_debug("no report metadata");
		return;
	}

	/* show banner */
	w = GTK_WIDGET(gtk_builder_get_object(self->builder, "banner_device_list"));
	adw_banner_set_revealed(ADW_BANNER(w), TRUE);
}

static gboolean
gfu_main_report_device_list_cb(gpointer user_data)
{
	GfuMain *self = (GfuMain *)user_data;
	gint64 interval = GFU_MAIN_REPORT_DEVICE_LIST_INTERVAL_DELAY * 24 * 60 * 60;
	gint64 seconds = g_get_real_time() / G_USEC_PER_SEC;
	gint64 seconds_last = g_settings_get_int64(self->settings, "report-device-list-timestamp");

	if (g_getenv("GNOME_FIRMWARE_FORCE_DEVICE_LIST") != NULL ||
	    seconds > seconds_last + interval) {
		g_settings_set_int64(self->settings, "report-device-list-timestamp", seconds);
		gfu_main_report_check_device_list(self);
	}
	return G_SOURCE_REMOVE;
}
#endif

static void
gfu_main_update_devices_cb(GObject *source, GAsyncResult *res, gpointer user_data)
{
	GtkWidget *w;
	GfuMain *self = (GfuMain *)user_data;
	AdwNavigationSplitView *split_view =
	    ADW_NAVIGATION_SPLIT_VIEW(gtk_builder_get_object(self->builder, "split_view"));
	g_autoptr(GError) error = NULL;

	/* stop the spinner now */
	w = GTK_WIDGET(gtk_builder_get_object(self->builder, "spinner_loading"));
	gtk_spinner_stop(GTK_SPINNER(w));

	if (self->devices != NULL)
		g_ptr_array_unref(self->devices);
	self->devices = fwupd_client_get_devices_finish(FWUPD_CLIENT(source), res, &error);
	if (self->devices == NULL) {
		/* TRANSLATORS: fwupd refused us data */
		gfu_main_error_dialog(self, _("Getting Devices Failed"), error->message);
		return;
	}

	w = GTK_WIDGET(gtk_builder_get_object(self->builder, "device_metadata"));
	gtk_widget_set_visible(GTK_WIDGET(w), self->devices->len > 0);

	/* create a row in the listbox for each updatable device */
	w = GTK_WIDGET(gtk_builder_get_object(self->builder, "listbox_devices"));
	for (guint i = 0; i < self->devices->len; i++) {
		FwupdDevice *device = g_ptr_array_index(self->devices, i);
		GtkWidget *l;

		/* skip devices that don't have a name */
		if (fwupd_device_get_name(device) == NULL)
			continue;
		l = gfu_device_row_new(device);
		gtk_widget_set_visible(l, TRUE);
		gtk_list_box_insert(GTK_LIST_BOX(w), l, -1);
	}

	/* get GfuDeviceRow */
	w = gtk_widget_get_first_child(w);
	if (w == NULL) {
		/* TRANSLATORS: no devices supporting firmware updates were found */
		gfu_main_error_fatal(self, _("No supported devices were found…"));
		return;
	}

	/* show devices */
	w = GTK_WIDGET(gtk_builder_get_object(self->builder, "stack_main"));
	gtk_stack_set_visible_child_name(GTK_STACK(w), "main");
	w = GTK_WIDGET(gtk_builder_get_object(self->builder, "split_view_devices"));
	gtk_widget_set_visible(GTK_WIDGET(w), TRUE);
	adw_navigation_split_view_set_show_content(split_view, true);

	/* activate the first device once the split_view has been collapsed or not */
	g_idle_add(gfu_main_update_devices_activate_child_cb, self);
}

static void
gfu_main_release_row_activated_cb(AdwActionRow *row, GfuMain *self)
{
	FwupdRelease *release = gfu_release_row_get_release(GFU_RELEASE_ROW(row));
	GtkWidget *w;
	GtkWidget *window;
	GtkWidget *split_view = GTK_WIDGET(gtk_builder_get_object(self->builder, "split_view"));

	g_set_object(&self->release, release);
	gfu_main_refresh_release(self);

	window = GTK_WIDGET(gtk_builder_get_object(self->builder, "dialog_main"));
	w = GTK_WIDGET(gtk_builder_get_object(self->builder, "window_release"));
	adw_dialog_set_title(ADW_DIALOG(w), fwupd_release_get_version(release));
	adw_dialog_set_content_width(ADW_DIALOG(w), gtk_widget_get_width(split_view) * 85 / 100);
	adw_dialog_set_content_height(ADW_DIALOG(w), gtk_widget_get_height(split_view) * 60 / 100);
	adw_dialog_present(ADW_DIALOG(w), window);
}

static void
gfu_main_update_releases(GfuMain *self, GPtrArray *releases)
{
	GtkWidget *w;
	GPtrArray *children;
	gboolean updatable = fwupd_device_has_flag(self->device, FWUPD_DEVICE_FLAG_UPDATABLE);

	w = GTK_WIDGET(gtk_builder_get_object(self->builder, "group_releases_none"));
	gtk_widget_set_visible(w, updatable && releases == NULL);
	w = GTK_WIDGET(gtk_builder_get_object(self->builder, "group_releases"));
	gtk_widget_set_visible(w, updatable && releases != NULL);

	/* nothing to do */
	if (releases == NULL)
		return;

	/* we have to do this hack as adw_preferences_group_remove_all() does not exist */
	children = gfu_main_get_secret_children(w);
	for (guint i = 0; i < children->len; i++) {
		GtkWidget *row = g_ptr_array_index(children, i);
		adw_preferences_group_remove(ADW_PREFERENCES_GROUP(w), row);
	}
	g_ptr_array_set_size(children, 0);

	for (guint i = 0; i < releases->len; i++) {
		FwupdRelease *release = g_ptr_array_index(releases, i);
		GtkWidget *row = gfu_release_row_new(self->device, release);
		g_debug("adding release %s", fwupd_release_get_version(release));
		adw_preferences_group_add(ADW_PREFERENCES_GROUP(w), row);
		g_ptr_array_add(children, row);
		g_signal_connect(row,
				 "button-clicked",
				 G_CALLBACK(gfu_main_release_row_button_clicked_cb),
				 self);
		g_signal_connect(row,
				 "activated",
				 G_CALLBACK(gfu_main_release_row_activated_cb),
				 self);
	}
}

static void
gfu_main_get_releases_cb(GObject *source, GAsyncResult *res, gpointer user_data)
{
	GfuMain *self = (GfuMain *)user_data;
	g_autoptr(GError) error = NULL;
	g_autoptr(GPtrArray) releases = NULL;

	releases = fwupd_client_get_releases_finish(FWUPD_CLIENT(source), res, &error);
	if (releases == NULL)
		g_debug("ignoring: %s", error->message);
	gfu_main_update_releases(self, releases);
}

static void
gfu_main_update_progress_bar(GfuMain *self)
{
	GtkWidget *w = GTK_WIDGET(gtk_builder_get_object(self->builder, "progress_bar"));
	GtkRevealer *revealer = GTK_REVEALER(gtk_builder_get_object(self->builder, "revealer"));
	guint percentage = fwupd_client_get_percentage(self->client);
	FwupdStatus status = fwupd_client_get_status(self->client);

	/* nothing to show */
	if (status == FWUPD_STATUS_IDLE || status == FWUPD_STATUS_UNKNOWN ||
	    status == FWUPD_STATUS_SHUTDOWN) {
		gtk_revealer_set_reveal_child(revealer, FALSE);
		return;
	}

	/* update progress */
	if (percentage == 0) {
		gtk_progress_bar_pulse(GTK_PROGRESS_BAR(w));
	} else {
		gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(w), ((gdouble)percentage) / 100.f);
	}

	/* set label */
	g_debug("status: %s [%u%%]", fwupd_status_to_string(status), percentage);
	gtk_progress_bar_set_text(GTK_PROGRESS_BAR(w), gfu_status_to_string(status));
	gtk_revealer_set_reveal_child(revealer, TRUE);
}

static void
gfu_main_client_status_changed_cb(FwupdClient *client, GParamSpec *pspec, GfuMain *self)
{
	FwupdStatus status = fwupd_client_get_status(self->client);

	/* daemon shutdown, maybe due to idle? */
	if (status == FWUPD_STATUS_SHUTDOWN) {
		g_debug("daemon went away");
		self->daemon_alive = FALSE;
	}
	gfu_main_update_progress_bar(self);
}

static void
gfu_main_client_percentage_changed_cb(FwupdClient *client, GParamSpec *pspec, GfuMain *self)
{
	gfu_main_update_progress_bar(self);
}

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

	if (!fwupd_client_update_metadata_bytes_finish(FWUPD_CLIENT(source), res, &error)) {
		/* TRANSLATORS: the list of available firmware failed to be updated */
		gfu_main_error_dialog(self, _("Refreshing Metadata Failed"), error->message);
		return;
	}
}

static void
gfu_main_refresh_remote(GfuMain *self)
{
#if FWUPD_CHECK_VERSION(2, 0, 0)
	fwupd_client_refresh_remote_async(self->client,
					  self->remote_lvfs,
					  FWUPD_CLIENT_DOWNLOAD_FLAG_NONE,
					  self->cancellable,
					  gfu_main_refresh_remote_cb,
					  self);
#else
	fwupd_client_refresh_remote_async(self->client,
					  self->remote_lvfs,
					  self->cancellable,
					  gfu_main_refresh_remote_cb,
					  self);
#endif
}

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

	if (!fwupd_client_modify_remote_finish(FWUPD_CLIENT(source), res, &error)) {
		/* TRANSLATORS: the LVFS remote could not be enabled */
		gfu_main_error_dialog(self, _("Modifying Remote Failed"), error->message);
		return;
	}
	gfu_main_refresh_remote(self);
}

static void
gfu_main_get_remotes_cb(GObject *source, GAsyncResult *res, gpointer user_data)
{
	GfuMain *self = (GfuMain *)user_data;
	g_autoptr(GError) error = NULL;
	g_autoptr(GPtrArray) remotes = NULL;

	remotes = fwupd_client_get_remotes_finish(FWUPD_CLIENT(source), res, &error);
	if (remotes == NULL) {
		/* TRANSLATORS: fwupd refused us data */
		gfu_main_error_dialog(self, _("Getting Remotes Failed"), error->message);
		return;
	}
	for (guint i = 0; i < remotes->len; i++) {
		FwupdRemote *remote = g_ptr_array_index(remotes, i);
		if (!fwupd_remote_has_flag(remote, FWUPD_REMOTE_FLAG_ENABLED))
			continue;
		if (fwupd_remote_get_kind(remote) != FWUPD_REMOTE_KIND_DOWNLOAD)
			continue;
#if FWUPD_CHECK_VERSION(2, 0, 0)
		/* should this be a single async action? */
		fwupd_client_refresh_remote_async(self->client,
						  remote,
						  FWUPD_CLIENT_DOWNLOAD_FLAG_NONE,
						  self->cancellable,
						  gfu_main_refresh_remote_cb,
						  self);
#else
		fwupd_client_refresh_remote_async(self->client,
						  remote,
						  self->cancellable,
						  gfu_main_refresh_remote_cb,
						  self);
#endif
	}
}

#if FWUPD_CHECK_VERSION(1, 9, 20)
static void
gfu_main_activate_upload_report_devices(GSimpleAction *simple,
					GVariant *parameter,
					gpointer user_data)
{
	GfuMain *self = (GfuMain *)user_data;

	/* is this possible */
	if (self->report_devices_payload == NULL) {
		gfu_main_error_dialog(self,
				      /* TRANSLATORS: dialog title */
				      _("Nothing To Do!"),
				      /* TRANSLATORS: dialog title */
				      _("There are no unsupported devices detected."));
		return;
	}

	gfu_main_report_device_list_dialog(self);
}
#endif

#if FWUPD_CHECK_VERSION(2, 0, 1)

typedef struct {
	GfuMain *self;
	gchar *path;
	FwupdInstallFlags install_flags;
} GfuMainInstallHelper;

static void
gfu_main_install_helper_free(GfuMainInstallHelper *helper)
{
	g_free(helper->path);
	g_free(helper);
}

G_DEFINE_AUTOPTR_CLEANUP_FUNC(GfuMainInstallHelper, gfu_main_install_helper_free)

static void
gfu_main_install_file_cb(GObject *source_object, GAsyncResult *res, gpointer user_data)
{
	g_autoptr(GfuMainInstallHelper) helper = (GfuMainInstallHelper *)user_data;
	g_autoptr(GError) error = NULL;
	GfuMain *self = helper->self;

	if (!fwupd_client_install_finish(FWUPD_CLIENT(source_object), res, &error)) {
		gfu_main_error_dialog(self,
				      /* TRANSLATORS: dialog title */
				      _("Installation Failed"),
				      error->message);
		return;
	}
	g_info("file was installed successully");
}

static void
gfu_main_install_details_dialog_response_cb(AdwMessageDialog *dialog,
					    const gchar *response,
					    gpointer user_data)
{
	g_autoptr(GfuMainInstallHelper) helper = (GfuMainInstallHelper *)user_data;
	GfuMain *self = helper->self;
	const gchar *path = helper->path;
	FwupdInstallFlags install_flags = helper->install_flags;

	if (g_strcmp0(response, "install") != 0)
		return;
	fwupd_client_install_async(self->client,
				   FWUPD_DEVICE_ID_ANY,
				   path,
				   install_flags,
				   self->cancellable,
				   gfu_main_install_file_cb,
				   g_steal_pointer(&helper));
}

static void
gfu_main_install_not_compatible(GfuMain *self, FwupdDevice *device)
{
	FwupdRelease *release = fwupd_device_get_release_default(device);
	AdwDialog *dialog;
	g_autofree gchar *body_text = NULL;
	g_autofree gchar *title_text = NULL;
	g_autoptr(GPtrArray) bodies = g_ptr_array_new();

	/* TRANSLATORS: default text when the firmware is not updatable */
	g_ptr_array_add(
	    bodies,
	    g_strdup(_("This firmware is not compatible with devices found on your system.")));

	/* default text */
	if (fwupd_release_get_description(release) != NULL) {
		g_ptr_array_add(
		    bodies,
		    gfu_common_xml_to_text(fwupd_release_get_description(release), NULL));
	}

	/* TRANSLATORS: dialog title, where %1 is the firmware version (e.g. '1.2.3') */
	title_text = g_strdup_printf(_("Cannot Install %s"), fwupd_release_get_version(release));

	/* show UI */
	dialog = adw_alert_dialog_new(title_text, NULL);
	body_text = gfu_common_strjoin_array("\n\n", bodies);
	adw_alert_dialog_set_body(ADW_ALERT_DIALOG(dialog), body_text);
	adw_alert_dialog_add_responses(ADW_ALERT_DIALOG(dialog),
				       "close",
				       /* TRANSLATORS: button text */
				       _("_Close"),
				       NULL);
	adw_alert_dialog_set_body_use_markup(ADW_ALERT_DIALOG(dialog), TRUE);

	/* show */
	gfu_main_add_dialog(self, dialog, TRUE);
}

static void
gfu_main_install_get_details_cb(GObject *source_object, GAsyncResult *res, gpointer user_data)
{
	FwupdRelease *release;
	AdwDialog *dialog;
	const gchar *button_text = NULL;
	g_autofree gchar *body_text = NULL;
	g_autofree gchar *title_text = NULL;
	g_autoptr(FwupdDevice) device = NULL;
	g_autoptr(GError) error = NULL;
	g_autoptr(GfuMainInstallHelper) helper = (GfuMainInstallHelper *)user_data;
	g_autoptr(GPtrArray) bodies = g_ptr_array_new();
	g_autoptr(GPtrArray) devices = NULL;
	GfuMain *self = helper->self;

	devices = fwupd_client_get_details_finish(FWUPD_CLIENT(source_object), res, &error);
	if (devices == NULL) {
		gfu_main_error_dialog(self,
				      /* TRANSLATORS: dialog title */
				      _("Getting File Details Failed"),
				      error->message);
		return;
	}

	/* find the first device in the possibly-composite archive that is present and updatable */
	for (guint i = 0; i < devices->len; i++) {
		FwupdDevice *device_tmp = g_ptr_array_index(devices, i);
		if (fwupd_device_get_id(device_tmp) != NULL &&
		    fwupd_device_has_flag(device_tmp, FWUPD_DEVICE_FLAG_UPDATABLE)) {
			device = g_object_ref(device_tmp);
			break;
		}
	}
	if (device == NULL) {
		device = g_object_ref(g_ptr_array_index(devices, 0));
		gfu_main_install_not_compatible(self, device);
		return;
	}
	release = fwupd_device_get_release_default(device);
	if (fwupd_release_has_flag(release, FWUPD_RELEASE_FLAG_BLOCKED_VERSION)) {
		gfu_main_error_dialog(self,
				      /* TRANSLATORS: dialog title */
				      _("Firmware Version Blocked"),
				      NULL);
		return;
	}
	if (fwupd_release_has_flag(release, FWUPD_RELEASE_FLAG_BLOCKED_APPROVAL)) {
		gfu_main_error_dialog(self,
				      /* TRANSLATORS: dialog title */
				      _("Firmware Not Approved"),
				      NULL);
		return;
	}
	if (fwupd_release_has_flag(release, FWUPD_RELEASE_FLAG_TRUSTED_METADATA)) {
		/* TRANSLATORS: dialog title, where %1 is the device name (e.g. 'ThinkPad P1G3')
		 * and %2 is the firmware version (e.g. '1.2.3') */
		title_text = g_strdup_printf(_("Install %s %s?"),
					     fwupd_release_get_name(release),
					     fwupd_release_get_version(release));
	} else {
		if (fwupd_client_get_only_trusted(self->client)) {
			gfu_main_error_dialog(self,
					      /* TRANSLATORS: dialog title */
					      _("Unsigned Firmware"),
					      _("Only signed firmware can be installed."));
			return;
		}
		/* TRANSLATORS: dialog title, where %1 is the device name (e.g. 'ThinkPad P1G3')
		 * and %2 is the firmware version (e.g. '1.2.3') */
		title_text = g_strdup_printf(_("Install UNTRUSTED %s %s:"),
					     fwupd_release_get_name(release),
					     fwupd_release_get_version(release));
	}
	if (fwupd_release_has_flag(release, FWUPD_RELEASE_FLAG_IS_ALTERNATE_BRANCH)) {
		g_ptr_array_add(
		    bodies,
		    /* TRANSLATORS: this is for an different stream of firmware, e.g.
		     * produced by a different entity than the original vendor.
		     * %1 is something like 'oss' */
		    g_strdup_printf(_("This firmware is from the alternate '%s' branch."),
				    fwupd_release_get_branch(release)));
	}
	if (fwupd_release_has_flag(release, FWUPD_RELEASE_FLAG_IS_COMMUNITY)) {
		g_ptr_array_add(bodies,
				/* TRANSLATORS: this is when the community is now providing updates
				 * rather than the original (dead?) upstream OEM */
				g_strdup(_("This firmware has been built by the community, rather "
					   "than the original OEM vendor.")));
	}
	if (fwupd_release_has_flag(release, FWUPD_RELEASE_FLAG_TRUSTED_REPORT)) {
		g_ptr_array_add(bodies,
				/* TRANSLATORS: this is when an OEM or ODM the user trusts has
				   tested the firmware update */
				g_strdup(_("This firmware has been tested by a trusted vendor.")));
	}

	/* default text */
	if (fwupd_release_get_description(release) != NULL) {
		g_ptr_array_add(
		    bodies,
		    gfu_common_xml_to_text(fwupd_release_get_description(release), NULL));
	}

	/* what is this? */
	if (fwupd_release_has_flag(release, FWUPD_RELEASE_FLAG_IS_UPGRADE)) {
		/* TRANSLATORS: button text */
		button_text = _("Update");
	} else if (fwupd_release_has_flag(release, FWUPD_RELEASE_FLAG_IS_DOWNGRADE)) {
		/* TRANSLATORS: button text */
		button_text = _("Downgrade");
		helper->install_flags = FWUPD_INSTALL_FLAG_ALLOW_OLDER;
	} else {
		/* TRANSLATORS: button text */
		button_text = _("Reinstall");
		helper->install_flags = FWUPD_INSTALL_FLAG_ALLOW_REINSTALL;
	}

	/* show UI */
	dialog = adw_alert_dialog_new(title_text, NULL);
	body_text = gfu_common_strjoin_array("\n\n", bodies);
	adw_alert_dialog_set_body(ADW_ALERT_DIALOG(dialog), body_text);
	adw_alert_dialog_add_responses(ADW_ALERT_DIALOG(dialog),
				       "close",
				       /* TRANSLATORS: button text */
				       _("_Cancel"),
				       "install",
				       button_text,
				       NULL);
	adw_alert_dialog_set_body_use_markup(ADW_ALERT_DIALOG(dialog), TRUE);
	adw_alert_dialog_set_response_appearance(ADW_ALERT_DIALOG(dialog),
						 "install",
						 ADW_RESPONSE_SUGGESTED);
	adw_alert_dialog_set_default_response(ADW_ALERT_DIALOG(dialog), "install");
	adw_alert_dialog_set_response_appearance(ADW_ALERT_DIALOG(dialog),
						 "install",
						 ADW_RESPONSE_SUGGESTED);

	/* show */
	g_signal_connect(dialog,
			 "response",
			 G_CALLBACK(gfu_main_install_details_dialog_response_cb),
			 g_steal_pointer(&helper));
	gfu_main_add_dialog(self, dialog, TRUE);
}

static void
gfu_main_install_file_dialog_cb(GObject *source_object, GAsyncResult *res, gpointer user_data)
{
	g_autofree gchar *path = NULL;
	g_autoptr(GError) error = NULL;
	g_autoptr(GFile) file = NULL;
	g_autoptr(GfuMainInstallHelper) helper = (GfuMainInstallHelper *)user_data;
	GfuMain *self = helper->self;

	file = gtk_file_dialog_open_finish(GTK_FILE_DIALOG(source_object), res, &error);
	if (file == NULL) {
		g_warning("failed to pick file: %s", error->message);
		return;
	}
	path = g_file_get_path(file);
	helper->path = g_strdup(path);
	fwupd_client_get_details_async(self->client,
				       path,
				       self->cancellable,
				       gfu_main_install_get_details_cb,
				       g_steal_pointer(&helper));
}

static void
gfu_main_activate_install_file(GSimpleAction *simple, GVariant *parameter, gpointer user_data)
{
	GfuMain *self = (GfuMain *)user_data;
	GtkWindow *window;
	GtkFileDialog *dialog = gtk_file_dialog_new();
	GtkFileFilter *filter = gtk_file_filter_new();
	g_autoptr(GfuMainInstallHelper) helper = g_new0(GfuMainInstallHelper, 1);

	/* TRANSLATORS: button text */
	gtk_file_dialog_set_accept_label(dialog, _("Install"));
	gtk_file_dialog_set_modal(dialog, TRUE);
	/* TRANSLATORS: window title */
	gtk_file_dialog_set_title(dialog, _("Install Cabinet Archive"));
	gtk_file_filter_add_pattern(filter, "*.cab");
	gtk_file_dialog_set_default_filter(dialog, filter);

	/* build a helper that can be used to install a file */
	helper->self = self;
	window = GTK_WINDOW(gtk_builder_get_object(self->builder, "dialog_main"));
	gtk_file_dialog_open(dialog,
			     window,
			     self->cancellable,
			     gfu_main_install_file_dialog_cb,
			     g_steal_pointer(&helper));
}
#endif

#if FWUPD_CHECK_VERSION(2, 0, 1)
static void
gfu_main_emulation_load_cb(GObject *source_object, GAsyncResult *res, gpointer user_data)
{
	GfuMain *self = (GfuMain *)user_data;
	g_autoptr(GError) error = NULL;

	if (!fwupd_client_emulation_load_finish(FWUPD_CLIENT(source_object), res, &error)) {
		gfu_main_error_dialog(self,
				      /* TRANSLATORS: dialog title */
				      _("Loading Emulation Data Failed"),
				      error->message);
		return;
	}
	g_info("emulation was loaded successully");
}

static void
gfu_main_emulation_load_dialog_cb(GObject *source_object, GAsyncResult *res, gpointer user_data)
{
	GfuMain *self = (GfuMain *)user_data;
	g_autofree gchar *path = NULL;
	g_autoptr(GError) error = NULL;
	g_autoptr(GFile) file = NULL;

	file = gtk_file_dialog_open_finish(GTK_FILE_DIALOG(source_object), res, &error);
	if (file == NULL) {
		g_warning("failed to pick file: %s", error->message);
		return;
	}
	path = g_file_get_path(file);
	fwupd_client_emulation_load_async(self->client,
					  path,
					  self->cancellable,
					  gfu_main_emulation_load_cb,
					  self);
}

static void
gfu_main_activate_emulation_load(GSimpleAction *simple, GVariant *parameter, gpointer user_data)
{
	GfuMain *self = (GfuMain *)user_data;
	GtkWindow *window;
	GtkFileDialog *dialog = gtk_file_dialog_new();
	GtkFileFilter *filter = gtk_file_filter_new();

	/* TRANSLATORS: button text */
	gtk_file_dialog_set_accept_label(dialog, _("Load"));
	gtk_file_dialog_set_modal(dialog, TRUE);
	/* TRANSLATORS: window title */
	gtk_file_dialog_set_title(dialog, _("Load Device Emulation Data"));
	gtk_file_filter_add_pattern(filter, "*.zip");
	gtk_file_dialog_set_default_filter(dialog, filter);

	window = GTK_WINDOW(gtk_builder_get_object(self->builder, "dialog_main"));
	gtk_file_dialog_open(dialog,
			     window,
			     self->cancellable,
			     gfu_main_emulation_load_dialog_cb,
			     self);
}

static void
gfu_main_emulation_save_cb(GObject *source_object, GAsyncResult *res, gpointer user_data)
{
	GfuMain *self = (GfuMain *)user_data;
	g_autoptr(GError) error = NULL;

	if (!fwupd_client_emulation_save_finish(FWUPD_CLIENT(source_object), res, &error)) {
		gfu_main_error_dialog(self,
				      /* TRANSLATORS: dialog title */
				      _("Saving Emulation Data Failed"),
				      error->message);
		return;
	}
	g_info("emulation was saved successully");
}

static void
gfu_main_activate_emulation_save_dialog_cb(GObject *source_object,
					   GAsyncResult *res,
					   gpointer user_data)
{
	GfuMain *self = (GfuMain *)user_data;
	g_autofree gchar *path = NULL;
	g_autoptr(GError) error = NULL;
	g_autoptr(GFile) file = NULL;

	file = gtk_file_dialog_save_finish(GTK_FILE_DIALOG(source_object), res, &error);
	if (file == NULL) {
		g_warning("failed to save file: %s", error->message);
		return;
	}
	path = g_file_get_path(file);
	fwupd_client_emulation_save_async(self->client,
					  path,
					  self->cancellable,
					  gfu_main_emulation_save_cb,
					  self);
}

static void
gfu_main_activate_emulation_save(GSimpleAction *simple, GVariant *parameter, gpointer user_data)
{
	GfuMain *self = (GfuMain *)user_data;
	GtkWindow *window;
	GtkFileDialog *dialog = gtk_file_dialog_new();
	GtkFileFilter *filter = gtk_file_filter_new();

	/* TRANSLATORS: button text */
	gtk_file_dialog_set_accept_label(dialog, _("Save"));
	gtk_file_dialog_set_modal(dialog, TRUE);
	/* TRANSLATORS: window title */
	gtk_file_dialog_set_title(dialog, _("Save Device Emulation Data"));
	gtk_file_dialog_set_initial_name(dialog, "emulation.zip");
	gtk_file_filter_add_pattern(filter, "*.zip");
	gtk_file_dialog_set_default_filter(dialog, filter);

	window = GTK_WINDOW(gtk_builder_get_object(self->builder, "dialog_main"));
	gtk_file_dialog_save(dialog,
			     window,
			     self->cancellable,
			     gfu_main_activate_emulation_save_dialog_cb,
			     self);
}
#endif

static void
gfu_main_activate_refresh_metadata(GSimpleAction *simple, GVariant *parameter, gpointer user_data)
{
	GfuMain *self = (GfuMain *)user_data;
	fwupd_client_get_remotes_async(self->client,
				       self->cancellable,
				       gfu_main_get_remotes_cb,
				       self);
}

#if FWUPD_CHECK_VERSION(1, 9, 17)
static void
gfu_main_request_invalidate_cb(FwupdRequest *request, AdwDialog *dialog)
{
	g_debug("request invalidated");
	adw_dialog_close(dialog);
}
#endif

static void
gfu_main_show_request_real(GfuMain *self, FwupdRequest *request, GdkPixbuf *pixbuf)
{
	AdwDialog *dialog;
	const gchar *title;

	if (fwupd_request_get_kind(request) == FWUPD_REQUEST_KIND_POST) {
		/* TRANSLATORS: the user needs to do something, e.g. remove the device */
		title = _("Further Action Required");
	} else {
		/* TRANSLATORS: the user needs to do something, e.g. remove the device */
		title = _("Action Required");
	}
	dialog = adw_alert_dialog_new(title, fwupd_request_get_message(request));

	/* TRANSLATORS: button text */
	adw_alert_dialog_add_response(ADW_ALERT_DIALOG(dialog), "ok", _("Continue"));

	/* image is optional */
	if (pixbuf != NULL) {
		GtkWidget *image = gtk_image_new_from_pixbuf(pixbuf);
		gtk_widget_set_size_request(image,
					    gdk_pixbuf_get_width(pixbuf),
					    gdk_pixbuf_get_height(pixbuf));
		adw_alert_dialog_set_extra_child(ADW_ALERT_DIALOG(dialog), image);
	}
#if FWUPD_CHECK_VERSION(1, 9, 17)
	g_signal_connect(request, "invalidate", G_CALLBACK(gfu_main_request_invalidate_cb), dialog);
#endif

	gfu_main_add_dialog(self, dialog, TRUE);
}

typedef struct {
	GfuMain *self;
	FwupdRequest *request;
} GfuMainAsyncReleaseHelper;

static void
gfu_main_async_helper_free(GfuMainAsyncReleaseHelper *helper)
{
	g_object_unref(helper->request);
	g_free(helper);
}

G_DEFINE_AUTOPTR_CLEANUP_FUNC(GfuMainAsyncReleaseHelper, gfu_main_async_helper_free);

static void
gfu_main_show_request_download_cb(GObject *source_object, GAsyncResult *res, gpointer user_data)
{
	g_autoptr(GfuMainAsyncReleaseHelper) helper = (GfuMainAsyncReleaseHelper *)user_data;
	g_autoptr(GBytes) blob = NULL;
	g_autoptr(GError) error = NULL;
	g_autoptr(GInputStream) stream = NULL;
	g_autoptr(GdkPixbuf) pixbuf = NULL;

	blob = fwupd_client_download_bytes_finish(FWUPD_CLIENT(source_object), res, &error);
	if (blob == NULL) {
		g_warning("failed to download image: %s", error->message);
	} else {
		stream = g_memory_input_stream_new_from_bytes(blob);
		pixbuf = gdk_pixbuf_new_from_stream(stream, helper->self->cancellable, &error);
		if (pixbuf == NULL) {
			g_warning("failed to load image: %s", error->message);
		} else {
			g_debug("loaded pixbuf %ux%u",
				(guint)gdk_pixbuf_get_width(pixbuf),
				(guint)gdk_pixbuf_get_height(pixbuf));
		}
	}
	gfu_main_show_request_real(helper->self, helper->request, pixbuf);
}

static void
gfu_main_show_request(GfuMain *self, FwupdRequest *request)
{
	GfuMainAsyncReleaseHelper *helper;

	/* no image */
	if (fwupd_request_get_image(request) == NULL) {
		gfu_main_show_request_real(self, request, NULL);
		return;
	}

	/* image required */
	helper = g_new0(GfuMainAsyncReleaseHelper, 1);
	helper->self = self;
	helper->request = g_object_ref(request);
	fwupd_client_download_bytes_async(self->client,
					  fwupd_request_get_image(request),
					  FWUPD_CLIENT_DOWNLOAD_FLAG_NONE,
					  self->cancellable,
					  gfu_main_show_request_download_cb,
					  helper);
}

static void
gfu_main_install_release_cb(GObject *source, GAsyncResult *res, gpointer user_data)
{
	GfuMain *self = (GfuMain *)user_data;
	AdwDialog *dialog;
	g_autoptr(GError) error = NULL;

	if (!fwupd_client_install_bytes_finish(FWUPD_CLIENT(source), res, &error)) {
		/* TRANSLATORS: the firmware install failed for an unspecified reason */
		gfu_main_error_dialog(self, _("Installation Failed"), error->message);
		return;
	}

	/* show any manual action required */
	for (guint i = 0; i < self->post_requests->len; i++) {
		FwupdRequest *request = g_ptr_array_index(self->post_requests, i);
		gfu_main_show_request(self, request);
	}
	g_ptr_array_set_size(self->post_requests, 0);

	/* update the list of releases */
	gfu_main_refresh_device(self);

	/* if successful, prompt for reboot */
	if (fwupd_device_has_flag(self->device, FWUPD_DEVICE_FLAG_NEEDS_SHUTDOWN)) {
		/* TRANSLATORS: prompting a shutdown to the user */
		dialog = adw_alert_dialog_new(
		    _("Shutdown Required"),
		    _("The update requires the system to shutdown to complete."));
		/* TRANSLATORS: button text */
		adw_alert_dialog_add_response(ADW_ALERT_DIALOG(dialog), "shutdown", _("Shutdown"));
		adw_alert_dialog_set_response_appearance(ADW_ALERT_DIALOG(dialog),
							 "shutdown",
							 ADW_RESPONSE_DESTRUCTIVE);
		/* TRANSLATORS: button text */
		adw_alert_dialog_add_response(ADW_ALERT_DIALOG(dialog), "close", _("Later"));
		g_signal_connect(dialog,
				 "response",
				 G_CALLBACK(gfu_main_reboot_shutdown_response_cb),
				 self);
		gfu_main_add_dialog(self, dialog, TRUE);
	} else if (fwupd_device_has_flag(self->device, FWUPD_DEVICE_FLAG_NEEDS_REBOOT)) {
		/* TRANSLATORS: prompting a reboot to the user */
		dialog = adw_alert_dialog_new(_("Reboot Required"),
					      _("The update requires a reboot to complete."));
		/* TRANSLATORS: button text */
		adw_alert_dialog_add_response(ADW_ALERT_DIALOG(dialog), "reboot", _("Reboot"));
		adw_alert_dialog_set_response_appearance(ADW_ALERT_DIALOG(dialog),
							 "reboot",
							 ADW_RESPONSE_DESTRUCTIVE);
		/* TRANSLATORS: button text */
		adw_alert_dialog_add_response(ADW_ALERT_DIALOG(dialog), "close", _("Later"));
		g_signal_connect(dialog,
				 "response",
				 G_CALLBACK(gfu_main_reboot_shutdown_response_cb),
				 self);
		gfu_main_add_dialog(self, dialog, TRUE);
	} else if (fwupd_release_get_update_message(self->release) == NULL) {
		/* TRANSLATORS: the installation was successful onto the device */
		dialog = adw_alert_dialog_new(_("Installation Successful"), NULL);
		adw_alert_dialog_format_body_markup(ADW_ALERT_DIALOG(dialog),
						    /* TRANSLATORS: dialog text, %1 is a
						     * version number, %2 is a device name */
						    _("Installed firmware version %s on %s."),
						    fwupd_release_get_version(self->release),
						    fwupd_device_get_name(self->device));
		/* TRANSLATORS: button text */
		adw_alert_dialog_add_response(ADW_ALERT_DIALOG(dialog), "no", _("Close"));
		gfu_main_add_dialog(self, dialog, TRUE);
	}
}

static void
gfu_main_release_install_cb(gpointer user_data)
{
	GfuMain *self = (GfuMain *)user_data;

	/* begin installing, show loading animation */
#if FWUPD_CHECK_VERSION(2, 0, 0)
	fwupd_client_install_release_async(self->client,
					   self->device,
					   self->release,
					   self->flags | FWUPD_INSTALL_FLAG_ALLOW_BRANCH_SWITCH,
					   FWUPD_CLIENT_DOWNLOAD_FLAG_NONE,
					   self->cancellable,
					   gfu_main_install_release_cb,
					   self);
#else
	fwupd_client_install_release2_async(self->client,
					    self->device,
					    self->release,
					    self->flags | FWUPD_INSTALL_FLAG_ALLOW_BRANCH_SWITCH,
					    FWUPD_CLIENT_DOWNLOAD_FLAG_NONE,
					    self->cancellable,
					    gfu_main_install_release_cb,
					    self);
#endif
}

static AdwDialog *
gfu_main_release_show_fde_warning(GfuMain *self)
{
	AdwDialog *dialog;

	/* TRANSLATORS: e.g. bitlocker */
	dialog = adw_alert_dialog_new(_("Full Disk Encryption"), NULL);
	adw_alert_dialog_add_response(ADW_ALERT_DIALOG(dialog), "ok", _("Continue"));
	adw_alert_dialog_add_response(ADW_ALERT_DIALOG(dialog), "close", _("Cancel"));

	adw_alert_dialog_format_body_markup(
	    ADW_ALERT_DIALOG(dialog),
	    "%s %s",
	    /* TRANSLATORS: the platform secret is stored in the PCRx registers on the TPM */
	    _("Some of the platform secrets may be invalidated when updating this firmware."),
	    /* TRANSLATORS: 'recovery key' here refers to a code, rather than a metal thing */
	    _("Please ensure you have the volume recovery key before continuing."));

	/* success */
	return dialog;
}

static AdwDialog *
gfu_main_release_show_branch_warning(GfuMain *self)
{
	AdwDialog *dialog;
	g_autoptr(GString) body = g_string_new(NULL);
	g_autoptr(GString) head = g_string_new(NULL);

	if (fwupd_device_get_vendor(self->device) != NULL &&
	    fwupd_release_get_vendor(self->release) != NULL) {
		g_string_append_printf(head,
				       /* TRANSLATORS: title, %2 is the device vendor name */
				       _("Firmware Not Supplied By %s"),
				       fwupd_device_get_vendor(self->device));
	} else {
		/* TRANSLATORS: branch switch title */
		g_string_append(head, _("Firmware Not Supplied By Vendor"));
	}
	dialog = adw_alert_dialog_new(head->str, NULL);

	/* TRANSLATORS: buttons */
	adw_alert_dialog_add_response(ADW_ALERT_DIALOG(dialog), "close", _("Cancel"));
	/* TRANSLATORS: button text to switch to another stream of firmware */
	adw_alert_dialog_add_response(ADW_ALERT_DIALOG(dialog), "ok", _("Switch Branch"));

	if (fwupd_device_get_vendor(self->device) != NULL) {
		g_string_append_printf(body,
				       /* TRANSLATORS: %1 is the device vendor name */
				       _("Your hardware may be damaged using this firmware, "
					 "and installing this release may void any warranty "
					 "with %s."),
				       fwupd_device_get_vendor(self->device));
	} else {
		g_string_append(body,
				/* TRANSLATORS: the vendor is unknown */
				_("Your hardware may be damaged using this firmware, "
				  "and installing this release may void any warranty "
				  "with the vendor."));
	}
	g_string_append(body, "\n\n");
	g_string_append(body,
			/* TRANSLATORS: should the branch be changed */
			_("You must understand the consequences of changing the firmware branch."));
	adw_alert_dialog_set_body(ADW_ALERT_DIALOG(dialog), body->str);

	/* success */
	return dialog;
}

static AdwDialog *
gfu_main_release_show_confirmation(GfuMain *self)
{
	AdwDialog *dialog;
	g_autofree gchar *title_string = NULL;
	gboolean upgrade = fwupd_release_has_flag(self->release, FWUPD_RELEASE_FLAG_IS_UPGRADE);
	gboolean downgrade = fwupd_release_has_flag(self->release, FWUPD_RELEASE_FLAG_IS_DOWNGRADE);
	gboolean reinstall = !downgrade && !upgrade;

	/* make sure user wants to install file to device */
	if (reinstall) {
		/* TRANSLATORS: %1 is the version */
		title_string =
		    g_strdup_printf(_("Reinstall %s?"), fwupd_release_get_version(self->release));
	} else if (upgrade) {
		/* TRANSLATORS: %1 is the version */
		title_string =
		    g_strdup_printf(_("Upgrade To %s?"), fwupd_release_get_version(self->release));
	} else if (downgrade) {
		/* TRANSLATORS: %1 is the version */
		title_string = g_strdup_printf(_("Downgrade To %s?"),
					       fwupd_release_get_version(self->release));
	} else {
		/* TRANSLATORS: %1 is the version */
		title_string =
		    g_strdup_printf(_("Install %s?"), fwupd_release_get_version(self->release));
	}
	dialog = adw_alert_dialog_new(title_string, NULL);

	/* TRANSLATORS: button text */
	adw_alert_dialog_add_response(ADW_ALERT_DIALOG(dialog), "close", _("_Cancel"));

	self->flags = FWUPD_INSTALL_FLAG_NONE;
	if (reinstall) {
		/* TRANSLATORS: button text: install the same version again */
		adw_alert_dialog_add_response(ADW_ALERT_DIALOG(dialog), "ok", _("_Reinstall"));
		self->flags |= FWUPD_INSTALL_FLAG_ALLOW_REINSTALL;
		/* upgrade or downgrade */
	} else {
		if (upgrade) {
			/* TRANSLATORS: button text, move from old -> new */
			adw_alert_dialog_add_response(ADW_ALERT_DIALOG(dialog),
						      "ok",
						      _("_Upgrade"));
		} else if (downgrade) {
			self->flags |= FWUPD_INSTALL_FLAG_ALLOW_OLDER;
			/* TRANSLATORS: button text, move from new -> old */
			adw_alert_dialog_add_response(ADW_ALERT_DIALOG(dialog),
						      "ok",
						      _("_Downgrade"));
		}
	}
	if (fwupd_device_has_flag(self->device, FWUPD_DEVICE_FLAG_USABLE_DURING_UPDATE)) {
		adw_alert_dialog_set_body(
		    ADW_ALERT_DIALOG(dialog),
		    /* TRANSLATORS: the device can be used normallly */
		    _("The device will remain usable for the duration of the update."));
	} else {
		adw_alert_dialog_set_body(
		    ADW_ALERT_DIALOG(dialog),
		    /* TRANSLATORS: the device will be non-fuctional for a while */
		    _("The device will be unusable while the update is installing."));
	}

	/* success */
	return ADW_DIALOG(dialog);
}

static void
gfu_main_release_install_response_cb(GtkDialog *dialog, const gchar *response, GfuMain *self)
{
	self->dialog_current = NULL;

	if (g_strcmp0(response, "close") == 0) {
		g_ptr_array_set_size(self->dialogs, 0);
		return;
	}

	g_ptr_array_remove(self->dialogs, dialog);
	gfu_main_process_dialogs(self);
}

static void
gfu_main_process_dialogs(GfuMain *self)
{
	GtkWidget *window = GTK_WIDGET(gtk_builder_get_object(self->builder, "dialog_main"));
	if (self->dialog_current != NULL)
		return;
	if (self->dialogs->len == 0) {
		if (self->dialog_func != NULL) {
			self->dialog_func(self);
			self->dialog_func = NULL;
		}
		return;
	}
	self->dialog_current = ADW_DIALOG(g_ptr_array_index(self->dialogs, 0));
	g_signal_connect(self->dialog_current,
			 "response",
			 G_CALLBACK(gfu_main_release_install_response_cb),
			 self);
	adw_dialog_present(ADW_DIALOG(self->dialog_current), window);
}

static void
gfu_main_release_row_button_clicked_cb(GfuReleaseRow *row, GfuMain *self)
{
	FwupdRelease *release = gfu_release_row_get_release(GFU_RELEASE_ROW(row));

	g_set_object(&self->release, release);

	gfu_main_add_dialog(self, gfu_main_release_show_confirmation(self), FALSE);
	if (g_strcmp0(fwupd_device_get_branch(self->device),
		      fwupd_release_get_branch(self->release)) != 0) {
		gfu_main_add_dialog(self, gfu_main_release_show_branch_warning(self), FALSE);
	}
	if (fwupd_device_has_flag(self->device, FWUPD_DEVICE_FLAG_AFFECTS_FDE))
		gfu_main_add_dialog(self, gfu_main_release_show_fde_warning(self), FALSE);

	/* when all the dialogs have been agreed to, install the release */
	self->dialog_func = gfu_main_release_install_cb;
	gfu_main_process_dialogs(self);
}

static void
gfu_main_device_verify_cb(GObject *source_object, GAsyncResult *res, gpointer user_data)
{
	GfuMain *self = (GfuMain *)user_data;
	AdwDialog *dialog;
	g_autoptr(GError) error = NULL;

	if (!fwupd_client_verify_finish(self->client, res, &error)) {
		/* TRANSLATORS: verify means checking the actual checksum of the firmware */
		gfu_main_error_dialog(self, _("Verification Failed"), error->message);
		return;
	}

	/* TRANSLATORS: inform the user that the firmware verification was successful */
	dialog = adw_alert_dialog_new(_("Verification Succeeded"), NULL);
	/* TRANSLATORS: button text */
	adw_alert_dialog_add_response(ADW_ALERT_DIALOG(dialog), "ok", _("OK"));
	adw_alert_dialog_format_body_markup(
	    ADW_ALERT_DIALOG(dialog),
	    /* TRANSLATORS: firmware is cryptographically identical */
	    _("%s firmware checksums matched."),
	    fwupd_device_get_name(self->device));
	gfu_main_add_dialog(self, dialog, TRUE);
	gfu_main_refresh_device(self);
}

static void
gfu_main_device_verify_response_cb(AdwAlertDialog *dialog, const gchar *response, GfuMain *self)
{
	if (g_strcmp0(response, "yes") == 0) {
		fwupd_client_verify_async(self->client,
					  fwupd_device_get_id(self->device),
					  self->cancellable,
					  gfu_main_device_verify_cb,
					  self);
	}
}

static void
gfu_main_device_verify_clicked_cb(GtkWidget *widget, GfuMain *self)
{
	AdwDialog *dialog;

	/* TRANSLATORS: dialog title */
	dialog = adw_alert_dialog_new(_("Verify Checksums?"),
				      /* TRANSLATORS: device will "go away" and then "come back" */
				      _("The device may be unusable during this action."));
	/* TRANSLATORS: button text */
	adw_alert_dialog_add_response(ADW_ALERT_DIALOG(dialog), "yes", _("Verify"));
	/* TRANSLATORS: button text */
	adw_alert_dialog_add_response(ADW_ALERT_DIALOG(dialog), "no", _("Cancel"));
	g_signal_connect(dialog, "response", G_CALLBACK(gfu_main_device_verify_response_cb), self);
	gfu_main_add_dialog(self, dialog, TRUE);
}

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

	if (!fwupd_client_verify_update_finish(FWUPD_CLIENT(source), res, &error)) {
		/* TRANSLATORS: verify means checking the actual checksum of the firmware */
		gfu_main_error_dialog(self, _("Checksum Update Failed"), error->message);
		return;
	}
	gfu_main_refresh_device(self);
}

static void
gfu_main_device_verify_update_response_cb(GtkDialog *dialog, const gchar *response, GfuMain *self)
{
	if (g_strcmp0(response, "yes") == 0) {
		fwupd_client_verify_update_async(self->client,
						 fwupd_device_get_id(self->device),
						 self->cancellable,
						 gfu_main_verify_update_cb,
						 self);
	}
}

static void
gfu_main_device_verify_update_cb(GtkWidget *widget, GfuMain *self)
{
	AdwDialog *dialog;

	/* TRANSLATORS: dialog title */
	dialog = adw_alert_dialog_new(
	    _("Update Checksum?"),
	    /* TRANSLATORS: save what we have now as "valid" */
	    _("This will record the current device cryptographic hashes as verified."));
	/* TRANSLATORS: button text */
	adw_alert_dialog_add_response(ADW_ALERT_DIALOG(dialog), "yes", _("Record"));
	/* TRANSLATORS: button text */
	adw_alert_dialog_add_response(ADW_ALERT_DIALOG(dialog), "no", _("Cancel"));
	adw_alert_dialog_set_default_response(ADW_ALERT_DIALOG(dialog), "yes");
	adw_alert_dialog_set_response_appearance(ADW_ALERT_DIALOG(dialog),
						 "yes",
						 ADW_RESPONSE_SUGGESTED);
	g_signal_connect(dialog,
			 "response",
			 G_CALLBACK(gfu_main_device_verify_update_response_cb),
			 self);
	gfu_main_add_dialog(self, dialog, TRUE);
}

#if FWUPD_CHECK_VERSION(2, 0, 1)
static void
gfu_main_device_emulated_remove_flag_cb(GObject *source_object,
					GAsyncResult *res,
					gpointer user_data)
{
	GfuMain *self = (GfuMain *)user_data;
	g_autoptr(GError) error = NULL;

	if (!fwupd_client_modify_device_finish(FWUPD_CLIENT(source_object), res, &error)) {
		gfu_main_error_dialog(self,
				      /* TRANSLATORS: dialog title */
				      _("Remove Emulated Device Failed"),
				      error->message);
		return;
	}
	g_info("emulated device deleted successully");
	g_idle_add(gfu_main_update_devices_activate_child_cb, self);
}

static void
gfu_main_device_emulated_remove_cb(GtkWidget *widget, GfuMain *self)
{
	fwupd_client_modify_device_async(self->client,
					 fwupd_device_get_id(self->device),
					 "Flags",
					 "~emulated",
					 self->cancellable,
					 gfu_main_device_emulated_remove_flag_cb,
					 self);
}

static void
gfu_main_modify_device_flags_cb(GObject *source_object, GAsyncResult *res, gpointer user_data)
{
	GfuMain *self = (GfuMain *)user_data;
	g_autoptr(GError) error = NULL;

	if (!fwupd_client_modify_device_finish(FWUPD_CLIENT(source_object), res, &error)) {
		gfu_main_error_dialog(self,
				      /* TRANSLATORS: dialog title */
				      _("Modifying Device Failed"),
				      error->message);
		return;
	}
	g_info("device flags modified successully");
}

static void
gfu_main_device_emulation_tag_cb(GtkWidget *widget, GfuMain *self)
{
	fwupd_client_modify_device_async(self->client,
					 fwupd_device_get_id(self->device),
					 "Flags",
					 "emulation-tag",
					 self->cancellable,
					 gfu_main_modify_device_flags_cb,
					 self);
}

static void
gfu_main_device_emulation_untag_cb(GtkWidget *widget, GfuMain *self)
{
	fwupd_client_modify_device_async(self->client,
					 fwupd_device_get_id(self->device),
					 "Flags",
					 "~emulation-tag",
					 self->cancellable,
					 gfu_main_modify_device_flags_cb,
					 self);
}
#endif

static void
gfu_main_about_activated_cb(GSimpleAction *action, GVariant *parameter, gpointer user_data)
{
	GfuMain *self = (GfuMain *)user_data;
	GList *windows;
	GtkWindow *parent = NULL;
	GtkWidget *about = NULL;
	const gchar *developers[] = {"Richard Hughes", "Andrew Schwenn", NULL};
	const gchar *artists[] = {"Jakub Steiner", NULL};
	const gchar *copyright = "Copyright \xc2\xa9 2019 Richard Hughes";

	windows = gtk_application_get_windows(GTK_APPLICATION(self->application));
	if (windows)
		parent = windows->data;

	about = g_object_new(ADW_TYPE_ABOUT_DIALOG,
			     /* TRANSLATORS: the application name for the about UI */
			     "application-name",
			     _("GNOME Firmware"),
			     "developers",
			     developers,
			     "copyright",
			     copyright,
			     "license-type",
			     GTK_LICENSE_GPL_2_0,
			     "application-icon",
			     "org.gnome.Firmware",
			     "translator-credits",
			     /* TRANSLATORS: you can put your name here :) */
			     _("translator-credits"),
			     "version",
			     VERSION,
			     "issue-url",
			     "https://gitlab.gnome.org/World/gnome-firmware/-/issues",
			     "support-url",
			     "https://gitlab.gnome.org/World/gnome-firmware/-/issues",
			     "artists",
			     artists,
			     NULL);

	adw_dialog_present(ADW_DIALOG(about), GTK_WIDGET(parent));
}

static void
gfu_main_quit_activated_cb(GSimpleAction *action, GVariant *parameter, gpointer user_data)
{
	GfuMain *self = (GfuMain *)user_data;
	g_application_quit(G_APPLICATION(self->application));
}

static GActionEntry actions[] = {
    {"about", gfu_main_about_activated_cb, NULL, NULL, NULL},
    {"refresh", gfu_main_activate_refresh_metadata, NULL, NULL, NULL},
#if FWUPD_CHECK_VERSION(2, 0, 1)
    {"install-file", gfu_main_activate_install_file, NULL, NULL, NULL},
    {"emulation-load", gfu_main_activate_emulation_load, NULL, NULL, NULL},
    {"emulation-save", gfu_main_activate_emulation_save, NULL, NULL, NULL},
#endif
#if FWUPD_CHECK_VERSION(1, 9, 20)
    {"upload-report-devices", gfu_main_activate_upload_report_devices, NULL, NULL, NULL},
#endif
    {"quit", gfu_main_quit_activated_cb, NULL, NULL, NULL}};

static void
gfu_main_refresh_release(GfuMain *self)
{
	GtkWidget *w;
	GPtrArray *cats = NULL;
	GPtrArray *checks = NULL;
	GPtrArray *issues = NULL;
	g_autofree gchar *desc = NULL;
	g_autofree gchar *install_duration = NULL;
	g_autofree gchar *release_flags = NULL;
	g_autoptr(GError) error = NULL;
	g_autoptr(GString) attr = g_string_new(NULL);

	if (self->release == NULL)
		return;

	cats = fwupd_release_get_categories(self->release);
	if (cats->len == 0) {
		gfu_main_set_label(self, "label_release_categories", NULL);
	} else {
		for (guint i = 0; i < cats->len; i++) {
			g_string_append_printf(attr,
					       "%s\n",
					       (const gchar *)g_ptr_array_index(cats, i));
		}
		if (attr->len > 0)
			g_string_truncate(attr, attr->len - 1);
		gfu_main_set_label(self, "label_release_categories", attr->str);
		w = GTK_WIDGET(
		    gtk_builder_get_object(self->builder, "label_release_categories_title"));
		adw_preferences_row_set_title(ADW_PREFERENCES_ROW(w),
					      ngettext("Category", "Categories", cats->len));
	}

	g_string_set_size(attr, 0);
	checks = fwupd_release_get_checksums(self->release);
	if (checks->len == 0) {
		gfu_main_set_label(self, "label_release_checksum", NULL);
	} else {
		for (guint i = 0; i < checks->len; i++) {
			g_autofree gchar *tmp =
			    gfu_common_checksum_format(g_ptr_array_index(checks, i));
			g_string_append_printf(attr, "%s\n", tmp);
		}
		if (attr->len > 0)
			g_string_truncate(attr, attr->len - 1);
		gfu_main_set_label(self, "label_release_checksum", attr->str);
		w = GTK_WIDGET(
		    gtk_builder_get_object(self->builder, "label_release_checksum_title"));
		adw_preferences_row_set_title(ADW_PREFERENCES_ROW(w),
					      ngettext("Checksum", "Checksums", checks->len));
	}

	issues = fwupd_release_get_issues(self->release);
	if (issues->len == 0) {
		gfu_main_set_label(self, "label_release_issues", NULL);
	} else {
		g_autoptr(GString) str = g_string_new(NULL);
		for (guint i = 0; i < issues->len; i++) {
			const gchar *tmp = g_ptr_array_index(issues, i);
			g_string_append_printf(str, "%s\n", tmp);
		}
		if (str->len > 0)
			g_string_truncate(str, str->len - 1);
		gfu_main_set_label(self, "label_release_issues", str->str);
		gfu_main_set_label_title(self,
					 "label_release_issues",
					 /* TRANSLATORS: list of security issues, e.g. CVEs */
					 ngettext("Fixed Issue", "Fixed Issues", checks->len));
	}

	gfu_main_set_label(self,
			   "label_release_filename",
			   fwupd_release_get_filename(self->release));
	gfu_main_set_label(self,
			   "label_release_protocol",
			   fwupd_release_get_protocol(self->release));
	gfu_main_set_label(self,
			   "label_release_appstream_id",
			   fwupd_release_get_appstream_id(self->release));
	gfu_main_set_label(self,
			   "label_release_remote_id",
			   fwupd_release_get_remote_id(self->release));
	gfu_main_set_label(self, "label_release_vendor", fwupd_release_get_vendor(self->release));
	gfu_main_set_label(self, "label_release_summary", fwupd_release_get_summary(self->release));

	g_string_set_size(attr, 0);
	desc = gfu_common_xml_to_text(fwupd_release_get_description(self->release), &error);
	if (desc == NULL) {
		g_debug("failed to get release description for version %s: %s",
			fwupd_release_get_version(self->release),
			error->message);
		gfu_main_set_label(self, "label_release_description", NULL);
	} else {
		g_string_append(attr, desc);
		if (attr->len > 0)
			g_string_truncate(attr, attr->len - 1);
		gfu_main_set_label(self, "label_release_description", attr->str);
	}

	gfu_main_set_label(self,
			   "label_release_size",
			   g_format_size(fwupd_release_get_size(self->release)));

	if (g_strcmp0(fwupd_release_get_license(self->release), "LicenseRef-proprietary") == 0) {
		/* TRANSLATORS: as in non-free */
		gfu_main_set_label(self, "label_release_license", _("Proprietary"));
	} else {
		gfu_main_set_label(self,
				   "label_release_license",
				   fwupd_release_get_license(self->release));
	}
	release_flags = gfu_common_release_flags_to_strings(fwupd_release_get_flags(self->release));
	gfu_main_set_label(self, "label_release_flags", release_flags);

	install_duration =
	    gfu_common_seconds_to_string(fwupd_release_get_install_duration(self->release));
	gfu_main_set_label(self, "label_release_install_duration", install_duration);

	gfu_main_set_label(self,
			   "label_release_update_message",
			   fwupd_release_get_update_message(self->release));
}

static void
gfu_main_refresh_releases(GfuMain *self)
{
	GtkWidget *w;

	w = GTK_WIDGET(gtk_builder_get_object(self->builder, "group_releases"));
	gtk_widget_set_visible(w, FALSE);

	/* async call for releases */
	if (self->device != NULL &&
	    (fwupd_device_has_flag(self->device, FWUPD_DEVICE_FLAG_UPDATABLE) ||
	     fwupd_device_has_flag(self->device, FWUPD_DEVICE_FLAG_UPDATABLE_HIDDEN))) {
		fwupd_client_get_releases_async(self->client,
						fwupd_device_get_id(self->device),
						self->cancellable,
						(GAsyncReadyCallback)gfu_main_get_releases_cb,
						self);
	} else {
		gfu_main_update_releases(self, NULL);
	}
}

static void
gfu_main_device_row_selected_cb(GtkListBox *box, GtkListBoxRow *row, GfuMain *self)
{
	AdwNavigationSplitView *split_view =
	    ADW_NAVIGATION_SPLIT_VIEW(gtk_builder_get_object(self->builder, "split_view"));
	FwupdDevice *device;

	if (row == NULL)
		return;

	adw_navigation_split_view_set_show_content(split_view, true);

	device = gfu_device_row_get_device(GFU_DEVICE_ROW(row));
	g_set_object(&self->device, device);
	gfu_main_refresh_device(self);
	g_settings_set_string(self->settings, "device-id-last", fwupd_device_get_id(device));
}

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

	if (self->report_metadata != NULL)
		g_hash_table_unref(self->report_metadata);
	self->report_metadata =
	    fwupd_client_get_report_metadata_finish(FWUPD_CLIENT(source), res, &error);
	if (self->report_metadata == NULL) {
		g_warning("failed to get report metadata: %s", error->message);
		return;
	}

	/* this will be valid now */
	gfu_main_build_report_devices_payload(self);
}

static void
gfu_main_set_feature_flags_cb(GObject *source, GAsyncResult *res, gpointer user_data)
{
	GfuMain *self = (GfuMain *)user_data;
	g_autoptr(GError) error = NULL;
	if (!fwupd_client_set_feature_flags_finish(FWUPD_CLIENT(source), res, &error)) {
		g_warning("%s", error->message);
		if (g_error_matches(error, FWUPD_ERROR, FWUPD_ERROR_NOT_SUPPORTED)) {
			gfu_main_error_fatal(self,
					     /* TRANSLATORS: maybe try Linux? */
					     _("The fwupd service is not available for your OS."));
			return;
		}
	}
	fwupd_client_get_devices_async(self->client,
				       self->cancellable,
				       gfu_main_update_devices_cb,
				       self);
	fwupd_client_get_remotes_async(self->client,
				       self->cancellable,
				       gfu_main_update_remotes_cb,
				       self);
	fwupd_client_get_report_metadata_async(self->client,
					       self->cancellable,
					       gfu_main_get_report_metadata_cb,
					       self);
}

static void
gfu_main_unlock_cb(GObject *source, GAsyncResult *res, gpointer user_data)
{
	GfuMain *self = (GfuMain *)user_data;
	AdwDialog *dialog;
	g_autoptr(GError) error = NULL;

	if (!fwupd_client_verify_update_finish(FWUPD_CLIENT(source), res, &error)) {
		/* TRANSLATORS: unlock means to make the device functional in another mode */
		gfu_main_error_dialog(self, _("Unlocking Device Failed"), error->message);
		return;
	}

	/* if successful, prompt for reboot */
	if (fwupd_device_has_flag(self->device, FWUPD_DEVICE_FLAG_NEEDS_SHUTDOWN)) {
		/* TRANSLATORS: prompting a shutdown to the user */
		dialog = adw_alert_dialog_new(
		    _("Shutdown Required"),
		    _("Unlocking a device requires the system to shutdown to complete."));
		/* TRANSLATORS: button text */
		adw_alert_dialog_add_response(ADW_ALERT_DIALOG(dialog), "shutdown", _("Shutdown"));
		adw_alert_dialog_set_response_appearance(ADW_ALERT_DIALOG(dialog),
							 "shutdown",
							 ADW_RESPONSE_DESTRUCTIVE);
		/* TRANSLATORS: button text */
		adw_alert_dialog_add_response(ADW_ALERT_DIALOG(dialog), "close", _("Later"));
		g_signal_connect(dialog,
				 "response",
				 G_CALLBACK(gfu_main_reboot_shutdown_response_cb),
				 self);
		gfu_main_add_dialog(self, dialog, TRUE);
	} else if (fwupd_device_has_flag(self->device, FWUPD_DEVICE_FLAG_NEEDS_REBOOT)) {
		/* TRANSLATORS: prompting a reboot to the user */
		dialog =
		    adw_alert_dialog_new(_("Restart Required"),
					 _("Unlocking a device requires a reboot to complete."));
		/* TRANSLATORS: button text */
		adw_alert_dialog_add_response(ADW_ALERT_DIALOG(dialog), "reboot", _("Reboot"));
		adw_alert_dialog_set_response_appearance(ADW_ALERT_DIALOG(dialog),
							 "reboot",
							 ADW_RESPONSE_DESTRUCTIVE);
		/* TRANSLATORS: button text */
		adw_alert_dialog_add_response(ADW_ALERT_DIALOG(dialog), "close", _("Later"));
		g_signal_connect(dialog,
				 "response",
				 G_CALLBACK(gfu_main_reboot_shutdown_response_cb),
				 self);
		gfu_main_add_dialog(self, dialog, TRUE);
	}
}

static void
gfu_main_device_unlock_cb(GtkWidget *widget, gboolean state, GfuMain *self)
{
	if (state)
		return;
	fwupd_client_unlock_async(self->client,
				  fwupd_device_get_id(self->device),
				  self->cancellable,
				  gfu_main_unlock_cb,
				  self);
}

static void
gfu_main_banner_refresh_lvfs_clicked_cb(AdwBanner *banner, gpointer user_data)
{
	GfuMain *self = (GfuMain *)user_data;

	/* hide banner */
	adw_banner_set_revealed(banner, FALSE);

	gfu_main_refresh_remote(self);
}

static void
gfu_main_banner_enable_lvfs_clicked_cb(AdwBanner *banner, gpointer user_data)
{
	GfuMain *self = (GfuMain *)user_data;

	/* hide banner */
	adw_banner_set_revealed(banner, FALSE);

	/* enable remote */
	fwupd_client_modify_remote_async(self->client,
					 "lvfs",
					 "Enabled",
					 "true",
					 self->cancellable,
					 gfu_main_modify_remote_cb,
					 self);
}

static void
gfu_main_banner_device_list_clicked_cb(AdwBanner *banner, gpointer user_data)
{
#if FWUPD_CHECK_VERSION(1, 9, 20)
	GfuMain *self = (GfuMain *)user_data;
	GtkWidget *w;

	gfu_main_report_device_list_dialog(self);

	w = GTK_WIDGET(gtk_builder_get_object(self->builder, "banner_device_list"));
	adw_banner_set_revealed(ADW_BANNER(w), FALSE);
#endif
}

static void
gfu_main_client_connect(GfuMain *self)
{
	GtkWidget *w;

	/* get data */
	fwupd_client_set_feature_flags_async(
	    self->client,
	    FWUPD_FEATURE_FLAG_FDE_WARNING | FWUPD_FEATURE_FLAG_SWITCH_BRANCH |
		FWUPD_FEATURE_FLAG_SHOW_PROBLEMS | FWUPD_FEATURE_FLAG_REQUESTS |
		FWUPD_FEATURE_FLAG_REQUESTS_NON_GENERIC | FWUPD_FEATURE_FLAG_ALLOW_AUTHENTICATION |
		FWUPD_FEATURE_FLAG_UPDATE_ACTION,
	    self->cancellable,
	    gfu_main_set_feature_flags_cb,
	    self);

	/* clear existing devices */
	w = GTK_WIDGET(gtk_builder_get_object(self->builder, "stack_main"));
	gtk_stack_set_visible_child_name(GTK_STACK(w), "loading");
	w = GTK_WIDGET(gtk_builder_get_object(self->builder, "listbox_devices"));
	gtk_list_box_remove_all(GTK_LIST_BOX(w));

#if FWUPD_CHECK_VERSION(1, 9, 20)
	/* wait for the user to settle in */
	g_timeout_add_seconds(GFU_MAIN_REPORT_DEVICE_LIST_INITIAL_DELAY,
			      gfu_main_report_device_list_cb,
			      self);
#endif
}

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

	/* get result */
	if (!fwupd_client_connect_finish(FWUPD_CLIENT(source), res, &error)) {
		g_warning("%s", error->message);
		/* TRANSLATORS: maybe try Linux? */
		gfu_main_error_fatal(self, _("The fwupd service is not available for your OS."));
		return;
	}

	/* set user agent */
	fwupd_client_set_user_agent_for_package(self->client, GETTEXT_PACKAGE, VERSION);

	/* start the query chain */
	gfu_main_client_connect(self);
}

static void
gfu_main_connect_fwupd(GfuMain *self)
{
	/* cleared if shutdown */
	self->daemon_alive = TRUE;
	fwupd_client_connect_async(self->client,
				   self->cancellable,
				   gfu_main_client_connect_cb,
				   self);
}

static void
gfu_main_startup_cb(GApplication *application, GfuMain *self)
{
	/* add application menu items */
	g_action_map_add_action_entries(G_ACTION_MAP(application),
					actions,
					G_N_ELEMENTS(actions),
					self);
}

static void
gfu_main_activate_cb(GApplication *application, GfuMain *self)
{
	GtkWidget *w;
	GtkWidget *main_window;
	g_autoptr(GtkCssProvider) css_provider = gtk_css_provider_new();

	main_window = GTK_WIDGET (gtk_application_get_active_window (GTK_APPLICATION (application)));

	if (main_window != NULL) {
		gtk_window_present (GTK_WINDOW (main_window));
		return;
	}

	/* get UI */
	self->builder = gtk_builder_new_from_resource("/org/gnome/Firmware/gfu-main.ui");

	main_window = GTK_WIDGET(gtk_builder_get_object(self->builder, "dialog_main"));
	gtk_application_add_window(GTK_APPLICATION(self->application), GTK_WINDOW(main_window));
	gtk_window_set_default_size(GTK_WINDOW(main_window), 1024, 600);
	gtk_window_set_icon_name(GTK_WINDOW(main_window), "org.gnome.Firmware");

	/* buttons */
	w = GTK_WIDGET(gtk_builder_get_object(self->builder, "switch_device_unlock"));
	g_signal_connect(w, "state-set", G_CALLBACK(gfu_main_device_unlock_cb), self);
	w = GTK_WIDGET(gtk_builder_get_object(self->builder, "button_verify"));
	g_signal_connect(w, "clicked", G_CALLBACK(gfu_main_device_verify_clicked_cb), self);
	w = GTK_WIDGET(gtk_builder_get_object(self->builder, "button_verify_update"));
	g_signal_connect(w, "clicked", G_CALLBACK(gfu_main_device_verify_update_cb), self);
#if FWUPD_CHECK_VERSION(2, 0, 1)
	w = GTK_WIDGET(gtk_builder_get_object(self->builder, "button_emulated_remove"));
	g_signal_connect(w, "clicked", G_CALLBACK(gfu_main_device_emulated_remove_cb), self);
	w = GTK_WIDGET(gtk_builder_get_object(self->builder, "button_emulation_tag"));
	g_signal_connect(w, "clicked", G_CALLBACK(gfu_main_device_emulation_tag_cb), self);
	w = GTK_WIDGET(gtk_builder_get_object(self->builder, "button_emulation_untag"));
	g_signal_connect(w, "clicked", G_CALLBACK(gfu_main_device_emulation_untag_cb), self);
#endif
	w = GTK_WIDGET(gtk_builder_get_object(self->builder, "listbox_devices"));
	g_signal_connect(w, "row-activated", G_CALLBACK(gfu_main_device_row_selected_cb), self);
	w = GTK_WIDGET(gtk_builder_get_object(self->builder, "banner_enable_lvfs"));
	g_signal_connect(w,
			 "button-clicked",
			 G_CALLBACK(gfu_main_banner_enable_lvfs_clicked_cb),
			 self);
	w = GTK_WIDGET(gtk_builder_get_object(self->builder, "banner_refresh_lvfs"));
	g_signal_connect(w,
			 "button-clicked",
			 G_CALLBACK(gfu_main_banner_refresh_lvfs_clicked_cb),
			 self);
	w = GTK_WIDGET(gtk_builder_get_object(self->builder, "banner_device_list"));
	g_signal_connect(w,
			 "button-clicked",
			 G_CALLBACK(gfu_main_banner_device_list_clicked_cb),
			 self);

	w = GTK_WIDGET(gtk_builder_get_object(self->builder, "window_release"));
	g_signal_connect(ADW_DIALOG(w), "close-attempt", G_CALLBACK(gtk_widget_hide), self);

	/* sort by parent */
	w = GTK_WIDGET(gtk_builder_get_object(self->builder, "listbox_devices"));
	gtk_list_box_set_sort_func(GTK_LIST_BOX(w), gfu_main_sort_device_list_box_cb, self, NULL);

	/* start spinner */
	w = GTK_WIDGET(gtk_builder_get_object(self->builder, "spinner_loading"));
	gtk_spinner_start(GTK_SPINNER(w));

	/* required for https://gitlab.gnome.org/GNOME/libadwaita/-/merge_requests/1118 */
	gtk_css_provider_load_from_string(css_provider,
					  "textview.inline,\n"
					  "textview.inline text {\n"
					  "background: none;\n"
					  "color: inherit;\n"
					  "}\n");
	gtk_style_context_add_provider_for_display(gdk_display_get_default(),
						   GTK_STYLE_PROVIDER(css_provider),
						   GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);

	/* show main UI */
	gtk_widget_show(main_window);
	gfu_main_update_progress_bar(self);

	/* connect to fwupd */
	gfu_main_connect_fwupd(self);
}

static void
gfu_main_client_changed_cb(FwupdClient *client, GfuMain *self)
{
	if (!self->daemon_alive) {
		g_debug("refreshing device list as daemon came back");
		gfu_main_client_connect(self);
	}
}

static void
gfu_main_client_device_changed_cb(FwupdClient *client, FwupdDevice *device, GfuMain *self)
{
	GtkWidget *w;

	/* same as last time, so just refresh */
	if (self->device != NULL && fwupd_device_compare(self->device, device) == 0) {
		g_set_object(&self->device, device);
		gfu_main_refresh_device(self);
	}

	/* update the device list */
	w = GTK_WIDGET(gtk_builder_get_object(self->builder, "listbox_devices"));
	for (GtkWidget *c = gtk_widget_get_first_child(w); c != NULL;
	     c = gtk_widget_get_next_sibling(c)) {
		GfuDeviceRow *row = GFU_DEVICE_ROW(c);
		if (fwupd_device_compare(device, gfu_device_row_get_device(row)) == 0) {
			gfu_device_row_set_device(row, device);
			break;
		}
	}
}

#if FWUPD_CHECK_VERSION(2, 0, 1)
static void
gfu_main_emulation_tag_quit_cb(GObject *source, GAsyncResult *res, gpointer user_data)
{
	GfuMain *self = (GfuMain *)user_data;
	g_autoptr(GError) error = NULL;

	if (!fwupd_client_quit_finish(FWUPD_CLIENT(source), res, &error)) {
		/* TRANSLATORS: the service here is fwupd.service */
		gfu_main_error_dialog(self, _("Reloading Service Failed"), error->message);
		return;
	}
}

static void
gfu_main_emulation_tag_response_cb(AdwAlertDialog *dialog, const gchar *response, GfuMain *self)
{
	if (g_strcmp0(response, "ok") == 0) {
		fwupd_client_quit_async(self->client,
					self->cancellable,
					gfu_main_emulation_tag_quit_cb,
					self);
	}
}

static void
gfu_main_show_request_restart_daemon(GfuMain *self)
{
	AdwDialog *dialog;

	dialog = adw_alert_dialog_new(
	    /* TRANSLATORS: dialog title */
	    _("Reload Required"),
	    /* TRANSLATORS: device will "go away" and then "come back" */
	    _("The fwupd service needs to be restarted so that device enumeration can be "
	      "recorded."));

	/* TRANSLATORS: button text, where reload is in reference to the fwupd.service process */
	adw_alert_dialog_add_response(ADW_ALERT_DIALOG(dialog), "ok", _("Reload"));
	/* TRANSLATORS: button text */
	adw_alert_dialog_add_response(ADW_ALERT_DIALOG(dialog), "close", _("Cancel"));

	g_signal_connect(dialog, "response", G_CALLBACK(gfu_main_emulation_tag_response_cb), self);
	gfu_main_add_dialog(self, dialog, TRUE);
}
#endif

static void
gfu_main_device_request_cb(FwupdClient *client, FwupdRequest *request, GfuMain *self)
{
	/* nothing sensible to show */
	if (fwupd_request_get_message(request) == NULL)
		return;

	/* show this now */
	if (fwupd_request_get_kind(request) == FWUPD_REQUEST_KIND_IMMEDIATE) {
#if FWUPD_CHECK_VERSION(2, 0, 1)
		if (g_strcmp0(fwupd_request_get_id(request), FWUPD_REQUEST_ID_RESTART_DAEMON) ==
		    0) {
			gfu_main_show_request_restart_daemon(self);
			return;
		}
#endif
		gfu_main_show_request(self, request);
		return;
	}

	/* save for later */
	if (fwupd_request_get_kind(request) == FWUPD_REQUEST_KIND_POST)
		g_ptr_array_add(self->post_requests, g_object_ref(request));
}

static void
gfu_main_free(GfuMain *self)
{
	g_free(self->report_devices_payload);
	if (self->refresh_id != 0)
		g_source_remove(self->refresh_id);
	if (self->builder != NULL)
		g_object_unref(self->builder);
	if (self->cancellable != NULL)
		g_object_unref(self->cancellable);
	if (self->device != NULL)
		g_object_unref(self->device);
	if (self->release != NULL)
		g_object_unref(self->release);
	if (self->client != NULL)
		g_object_unref(self->client);
	if (self->application != NULL)
		g_object_unref(self->application);
	if (self->settings != NULL)
		g_object_unref(self->settings);
	if (self->dialogs != NULL)
		g_ptr_array_unref(self->dialogs);
	if (self->post_requests != NULL)
		g_ptr_array_unref(self->post_requests);
	if (self->devices != NULL)
		g_ptr_array_unref(self->devices);
	if (self->report_metadata != NULL)
		g_hash_table_unref(self->report_metadata);
	if (self->remote_lvfs != NULL)
		g_object_unref(self->remote_lvfs);
	g_free(self);
}

G_DEFINE_AUTOPTR_CLEANUP_FUNC(GfuMain, gfu_main_free)

int
main(int argc, char **argv)
{
	gboolean verbose = FALSE;
	g_autoptr(GError) error = NULL;
	g_autoptr(GfuMain) self = g_new0(GfuMain, 1);
	g_autoptr(GOptionContext) context = NULL;
	const GOptionEntry options[] = {{"verbose",
					 'v',
					 0,
					 G_OPTION_ARG_NONE,
					 &verbose,
					 /* TRANSLATORS: command line option */
					 N_("Show extra debugging information"),
					 NULL},
					{NULL}};

	setlocale(LC_ALL, "");

	bindtextdomain(GETTEXT_PACKAGE, LOCALEDIR);
	bind_textdomain_codeset(GETTEXT_PACKAGE, "UTF-8");
	textdomain(GETTEXT_PACKAGE);

	/* TRANSLATORS: command description */
	context = g_option_context_new(_("GNOME Firmware"));
	g_option_context_add_main_entries(context, options, NULL);
	g_option_context_set_ignore_unknown_options (context, TRUE);
	if (!g_option_context_parse(context, &argc, &argv, &error)) {
		/* TRANSLATORS: the user has sausages for fingers */
		g_print("%s: %s\n", _("Failed to parse command line options"), error->message);
		return EXIT_FAILURE;
	}

	/* set verbose? */
	if (verbose)
		g_setenv("G_MESSAGES_DEBUG", "all", FALSE);

	self->cancellable = g_cancellable_new();
	self->client = fwupd_client_new();
	g_signal_connect(self->client,
			 "notify::percentage",
			 G_CALLBACK(gfu_main_client_percentage_changed_cb),
			 self);
	g_signal_connect(self->client,
			 "notify::status",
			 G_CALLBACK(gfu_main_client_status_changed_cb),
			 self);
	g_signal_connect(self->client, "changed", G_CALLBACK(gfu_main_client_changed_cb), self);
	g_signal_connect(self->client,
			 "device-changed",
			 G_CALLBACK(gfu_main_client_device_changed_cb),
			 self);
	g_signal_connect(self->client, "device-added", G_CALLBACK(gfu_main_device_added_cb), self);
	g_signal_connect(self->client,
			 "device-removed",
			 G_CALLBACK(gfu_main_device_removed_cb),
			 self);
	/* required for interactive devices */
	g_signal_connect(FWUPD_CLIENT(self->client),
			 "device-request",
			 G_CALLBACK(gfu_main_device_request_cb),
			 self);

	self->dialogs = g_ptr_array_new_with_free_func((GDestroyNotify)adw_dialog_close);
	self->post_requests = g_ptr_array_new_with_free_func((GDestroyNotify)g_object_unref);

	/* ensure single instance */
	self->settings = g_settings_new("org.gnome.Firmware");
	self->application = adw_application_new("org.gnome.Firmware", G_APPLICATION_DEFAULT_FLAGS);
	g_signal_connect(self->application, "startup", G_CALLBACK(gfu_main_startup_cb), self);
	g_signal_connect(self->application, "activate", G_CALLBACK(gfu_main_activate_cb), self);

	/* wait */
	return g_application_run(G_APPLICATION(self->application), argc, argv);
}
