summaryrefslogtreecommitdiffstats
path: root/src/gs-update-monitor.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/gs-update-monitor.c')
-rw-r--r--src/gs-update-monitor.c1476
1 files changed, 1476 insertions, 0 deletions
diff --git a/src/gs-update-monitor.c b/src/gs-update-monitor.c
new file mode 100644
index 0000000..d46f6d0
--- /dev/null
+++ b/src/gs-update-monitor.c
@@ -0,0 +1,1476 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2013-2018 Richard Hughes <richard@hughsie.com>
+ * Copyright (C) 2013 Matthias Clasen <mclasen@redhat.com>
+ * Copyright (C) 2014-2018 Kalev Lember <klember@redhat.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+#include "config.h"
+
+#include <string.h>
+#include <glib/gi18n.h>
+#include <gsettings-desktop-schemas/gdesktop-enums.h>
+#include <locale.h>
+
+#include "gs-update-monitor.h"
+#include "gs-common.h"
+
+#define SECONDS_IN_AN_HOUR (60 * 60)
+#define SECONDS_IN_A_DAY (SECONDS_IN_AN_HOUR * 24)
+#define MINUTES_IN_A_DAY (SECONDS_IN_A_DAY / 60)
+
+struct _GsUpdateMonitor {
+ GObject parent;
+
+ GsApplication *application;
+
+ /* We use three cancellables:
+ * - @shutdown_cancellable is cancelled only during shutdown/dispose of
+ * the #GsUpdateMonitor, to avoid long-running operations keeping the
+ * monitor alive.
+ * - @update_cancellable is for update/upgrade operations, and is
+ * cancelled if they should be cancelled, such as if the computer has
+ * to start trying to save power.
+ * - @refresh_cancellable is for refreshes and other inconsequential
+ * operations which can be cancelled more readily than
+ * @update_cancellable with fewer consequences. It’s cancelled if the
+ * computer is going into low power mode, or if network connectivity
+ * changes.
+ */
+ GCancellable *shutdown_cancellable; /* (owned) (not nullable) */
+ GCancellable *update_cancellable; /* (owned) (not nullable) */
+ GCancellable *refresh_cancellable; /* (owned) (not nullable) */
+
+ GSettings *settings;
+ GsPluginLoader *plugin_loader;
+ GDBusProxy *proxy_upower;
+ GError *last_offline_error;
+
+ GNetworkMonitor *network_monitor;
+ guint network_changed_handler;
+
+#if GLIB_CHECK_VERSION(2, 69, 1)
+ GPowerProfileMonitor *power_profile_monitor; /* (owned) (nullable) */
+ gulong power_profile_changed_handler;
+#endif
+
+ guint cleanup_notifications_id; /* at startup */
+ guint check_startup_id; /* 60s after startup */
+ guint check_hourly_id; /* and then every hour */
+ guint check_daily_id; /* every 3rd day */
+
+ gint64 last_notification_time_usec; /* to notify once per day only */
+};
+
+G_DEFINE_TYPE (GsUpdateMonitor, gs_update_monitor, G_TYPE_OBJECT)
+
+typedef struct {
+ GsUpdateMonitor *monitor;
+} DownloadUpdatesData;
+
+static void
+download_updates_data_free (DownloadUpdatesData *data)
+{
+ g_clear_object (&data->monitor);
+ g_slice_free (DownloadUpdatesData, data);
+}
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC(DownloadUpdatesData, download_updates_data_free);
+
+typedef struct {
+ GsUpdateMonitor *monitor;
+ GsApp *app;
+} WithAppData;
+
+static WithAppData *
+with_app_data_new (GsUpdateMonitor *monitor,
+ GsApp *app)
+{
+ WithAppData *data;
+ data = g_slice_new0 (WithAppData);
+ data->monitor = g_object_ref (monitor);
+ data->app = g_object_ref (app);
+ return data;
+}
+
+static void
+with_app_data_free (WithAppData *data)
+{
+ g_clear_object (&data->monitor);
+ g_clear_object (&data->app);
+ g_slice_free (WithAppData, data);
+}
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC(WithAppData, with_app_data_free);
+
+static void
+check_updates_kind (GsAppList *apps,
+ gboolean *out_has_important,
+ gboolean *out_all_downloaded,
+ gboolean *out_any_downloaded)
+{
+ gboolean has_important, all_downloaded, any_downloaded;
+ guint ii, len;
+ GsApp *app;
+
+ len = gs_app_list_length (apps);
+ has_important = FALSE;
+ all_downloaded = len > 0;
+ any_downloaded = FALSE;
+
+ for (ii = 0; ii < len && (!has_important || all_downloaded || !any_downloaded); ii++) {
+ gboolean is_important;
+
+ app = gs_app_list_index (apps, ii);
+
+ is_important = gs_app_get_update_urgency (app) == AS_URGENCY_KIND_CRITICAL;
+ has_important = has_important || is_important;
+
+ if (gs_app_is_downloaded (app))
+ any_downloaded = TRUE;
+ else
+ all_downloaded = FALSE;
+ }
+
+ *out_has_important = has_important;
+ *out_all_downloaded = all_downloaded;
+ *out_any_downloaded = any_downloaded;
+}
+
+static gboolean
+get_timestamp_difference_days (GsUpdateMonitor *monitor, const gchar *timestamp, gint64 *out_days)
+{
+ gint64 tmp;
+ g_autoptr(GDateTime) last_update = NULL;
+ g_autoptr(GDateTime) now = NULL;
+
+ g_return_val_if_fail (out_days != NULL, FALSE);
+
+ g_settings_get (monitor->settings, timestamp, "x", &tmp);
+ if (tmp == 0)
+ return FALSE;
+
+ last_update = g_date_time_new_from_unix_local (tmp);
+ if (last_update == NULL) {
+ g_warning ("failed to set timestamp %" G_GINT64_FORMAT, tmp);
+ return FALSE;
+ }
+
+ now = g_date_time_new_now_local ();
+
+ *out_days = g_date_time_difference (now, last_update) / G_TIME_SPAN_DAY;
+
+ return TRUE;
+}
+
+static gboolean
+check_if_timestamp_more_than_days_ago (GsUpdateMonitor *monitor, const gchar *timestamp, guint days)
+{
+ gint64 timestamp_days;
+
+ if (!get_timestamp_difference_days (monitor, timestamp, &timestamp_days))
+ return TRUE;
+
+ return timestamp_days >= days;
+}
+
+static gboolean
+should_download_updates (GsUpdateMonitor *monitor)
+{
+#ifdef HAVE_MOGWAI
+ return TRUE;
+#else
+ return g_settings_get_boolean (monitor->settings, "download-updates");
+#endif
+}
+
+/* The days below are discussed at https://gitlab.gnome.org/GNOME/gnome-software/-/issues/947
+ and https://wiki.gnome.org/Design/Apps/Software/Updates#Tentative_Design */
+static gboolean
+should_notify_about_pending_updates (GsUpdateMonitor *monitor,
+ GsAppList *apps,
+ const gchar **out_title,
+ const gchar **out_body)
+{
+ gboolean has_important = FALSE, all_downloaded = FALSE, any_downloaded = FALSE;
+ gboolean should_download, res = FALSE;
+ gint64 timestamp_days;
+
+ if (!get_timestamp_difference_days (monitor, "update-notification-timestamp", &timestamp_days)) {
+ /* Large-enough number to succeed for the initial test */
+ timestamp_days = 365;
+ }
+
+ should_download = should_download_updates (monitor);
+ check_updates_kind (apps, &has_important, &all_downloaded, &any_downloaded);
+
+ if (!gs_app_list_length (apps)) {
+ /* Notify only when the download is disabled and it's the 4th day or it's more than 7 days */
+ if (!should_download && (timestamp_days >= 7 || timestamp_days == 4)) {
+ *out_title = _("Software Updates Are Out of Date");
+ *out_body = _("Please check for software updates.");
+ res = TRUE;
+ }
+ } else if (has_important) {
+ if (timestamp_days >= 1) {
+ if (all_downloaded) {
+ *out_title = _("Critical Software Update Ready to Install");
+ *out_body = _("An important software update is ready to be installed.");
+ res = TRUE;
+ } else if (!should_download) {
+ *out_title = _("Critical Software Updates Available to Download");
+ *out_body = _("Important: critical software updates are waiting.");
+ res = TRUE;
+ }
+ }
+ } else if (all_downloaded) {
+ if (timestamp_days >= 3) {
+ *out_title = _("Software Updates Ready to Install");
+ *out_body = _("Software updates are waiting and ready to be installed.");
+ res = TRUE;
+ }
+ /* To not hide downloaded updates for 14 days when new updates were discovered meanwhile.
+ Never show "Available to Download" when it's supposed to download the updates. */
+ } else if (!should_download && timestamp_days >= 14) {
+ *out_title = _("Software Updates Available to Download");
+ *out_body = _("Please download waiting software updates.");
+ res = TRUE;
+ }
+
+ g_debug ("%s: last_test_days:%" G_GINT64_FORMAT " n-apps:%u should_download:%d has_important:%d "
+ "all_downloaded:%d any_downloaded:%d res:%d%s%s%s%s", G_STRFUNC,
+ timestamp_days, gs_app_list_length (apps), should_download, has_important,
+ all_downloaded, any_downloaded, res,
+ res ? " reason:" : "",
+ res ? *out_title : "",
+ res ? "|" : "",
+ res ? *out_body : "");
+
+ return res;
+}
+
+static void
+reset_update_notification_timestamp (GsUpdateMonitor *monitor)
+{
+ g_autoptr(GDateTime) now = NULL;
+
+ now = g_date_time_new_now_local ();
+ g_settings_set (monitor->settings, "update-notification-timestamp", "x",
+ g_date_time_to_unix (now));
+}
+
+static void
+notify_about_pending_updates (GsUpdateMonitor *monitor,
+ GsAppList *apps)
+{
+ const gchar *title = NULL, *body = NULL;
+ gint64 time_diff_sec;
+ g_autoptr(GNotification) nn = NULL;
+
+ time_diff_sec = (g_get_real_time () - monitor->last_notification_time_usec) / G_USEC_PER_SEC;
+ if (time_diff_sec < SECONDS_IN_A_DAY) {
+ g_debug ("Skipping update notification daily check, because made one only %" G_GINT64_FORMAT "s ago",
+ time_diff_sec);
+ return;
+ }
+
+ if (!should_notify_about_pending_updates (monitor, apps, &title, &body)) {
+ g_debug ("No update notification needed");
+ return;
+ }
+
+ /* To force reload of the Updates page, thus it reflects what
+ the update-monitor notifies about */
+ gs_plugin_loader_emit_updates_changed (monitor->plugin_loader);
+
+ monitor->last_notification_time_usec = g_get_real_time ();
+
+ g_debug ("Notify about update: '%s'", title);
+
+ nn = g_notification_new (title);
+ g_notification_set_body (nn, body);
+ g_notification_set_default_action_and_target (nn, "app.set-mode", "s", "updates");
+ gs_application_send_notification (monitor->application, "updates-available", nn, MINUTES_IN_A_DAY);
+
+ /* Keep the old notification time when there are no updates and the update download is disabled,
+ to notify the user every day after 7 days of no update check */
+ if (gs_app_list_length (apps) ||
+ should_download_updates (monitor))
+ reset_update_notification_timestamp (monitor);
+}
+
+static gboolean
+_filter_by_app_kind (GsApp *app, gpointer user_data)
+{
+ AsComponentKind kind = GPOINTER_TO_UINT (user_data);
+ return gs_app_get_kind (app) == kind;
+}
+
+static gboolean
+_sort_by_rating_cb (GsApp *app1, GsApp *app2, gpointer user_data)
+{
+ if (gs_app_get_rating (app1) < gs_app_get_rating (app2))
+ return -1;
+ if (gs_app_get_rating (app1) > gs_app_get_rating (app2))
+ return 1;
+ return 0;
+}
+
+static GNotification *
+_build_autoupdated_notification (GsUpdateMonitor *monitor, GsAppList *list)
+{
+ guint need_restart_cnt = 0;
+ g_autoptr(GsAppList) list_apps = NULL;
+ g_autoptr(GNotification) n = NULL;
+ g_autoptr(GString) body = g_string_new (NULL);
+ g_autofree gchar *title = NULL;
+
+ /* filter out apps */
+ list_apps = gs_app_list_copy (list);
+ gs_app_list_filter (list_apps,
+ _filter_by_app_kind,
+ GUINT_TO_POINTER(AS_COMPONENT_KIND_DESKTOP_APP));
+ gs_app_list_sort (list_apps, _sort_by_rating_cb, NULL);
+ /* FIXME: add the applications that are currently active that use one
+ * of the updated runtimes */
+ if (gs_app_list_length (list_apps) == 0) {
+ g_debug ("no desktop apps in updated list, ignoring");
+ return NULL;
+ }
+
+ /* how many apps needs updating */
+ for (guint i = 0; i < gs_app_list_length (list_apps); i++) {
+ GsApp *app = gs_app_list_index (list_apps, i);
+ if (gs_app_has_quirk (app, GS_APP_QUIRK_NEEDS_REBOOT))
+ need_restart_cnt++;
+ }
+
+ /* >1 app updated */
+ if (gs_app_list_length (list_apps) > 0) {
+ if (need_restart_cnt > 0) {
+ /* TRANSLATORS: apps were auto-updated and restart is required */
+ title = g_strdup_printf (ngettext ("%u Application Updated — Restart Required",
+ "%u Applications Updated — Restart Required",
+ gs_app_list_length (list_apps)),
+ gs_app_list_length (list_apps));
+ } else {
+ /* TRANSLATORS: apps were auto-updated */
+ title = g_strdup_printf (ngettext ("%u Application Updated",
+ "%u Applications Updated",
+ gs_app_list_length (list_apps)),
+ gs_app_list_length (list_apps));
+ }
+ }
+
+ /* 1 app updated */
+ if (gs_app_list_length (list_apps) == 1) {
+ GsApp *app = gs_app_list_index (list_apps, 0);
+ /* TRANSLATORS: %1 is an application name, e.g. Firefox */
+ g_string_append_printf (body, _("%s has been updated."), gs_app_get_name (app));
+ if (need_restart_cnt > 0) {
+ /* TRANSLATORS: the app needs restarting */
+ g_string_append_printf (body, " %s", _("Please restart the application."));
+ }
+
+ /* 2 apps updated */
+ } else if (gs_app_list_length (list_apps) == 2) {
+ GsApp *app1 = gs_app_list_index (list_apps, 0);
+ GsApp *app2 = gs_app_list_index (list_apps, 1);
+ /* TRANSLATORS: %1 and %2 are both application names, e.g. Firefox */
+ g_string_append_printf (body, _("%s and %s have been updated."),
+ gs_app_get_name (app1),
+ gs_app_get_name (app2));
+ if (need_restart_cnt > 0) {
+ g_string_append (body, " ");
+ /* TRANSLATORS: at least one application needs restarting */
+ g_string_append_printf (body, ngettext ("%u application requires a restart.",
+ "%u applications require a restart.",
+ need_restart_cnt),
+ need_restart_cnt);
+ }
+
+ /* 3+ apps */
+ } else if (gs_app_list_length (list_apps) >= 3) {
+ GsApp *app1 = gs_app_list_index (list_apps, 0);
+ GsApp *app2 = gs_app_list_index (list_apps, 1);
+ GsApp *app3 = gs_app_list_index (list_apps, 2);
+ /* TRANSLATORS: %1, %2 and %3 are all application names, e.g. Firefox */
+ g_string_append_printf (body, _("Includes %s, %s and %s."),
+ gs_app_get_name (app1),
+ gs_app_get_name (app2),
+ gs_app_get_name (app3));
+ if (need_restart_cnt > 0) {
+ g_string_append (body, " ");
+ /* TRANSLATORS: at least one application needs restarting */
+ g_string_append_printf (body, ngettext ("%u application requires a restart.",
+ "%u applications require a restart.",
+ need_restart_cnt),
+ need_restart_cnt);
+ }
+ }
+
+ /* create the notification */
+ n = g_notification_new (title);
+ if (body->len > 0)
+ g_notification_set_body (n, body->str);
+ g_notification_set_default_action_and_target (n, "app.set-mode", "s", "updated");
+ return g_steal_pointer (&n);
+}
+
+static void
+update_finished_cb (GObject *object, GAsyncResult *res, gpointer data)
+{
+ GsUpdateMonitor *monitor = GS_UPDATE_MONITOR (data);
+ GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (object);
+ g_autoptr(GError) error = NULL;
+ g_autoptr(GsAppList) list = NULL;
+
+ /* get result */
+ list = gs_plugin_loader_job_process_finish (plugin_loader, res, &error);
+ if (list == NULL) {
+ gs_plugin_loader_claim_error (plugin_loader,
+ NULL,
+ GS_PLUGIN_ACTION_UPDATE,
+ NULL,
+ TRUE,
+ error);
+ return;
+ }
+
+ /* notifications are optional */
+ if (g_settings_get_boolean (monitor->settings, "download-updates-notify")) {
+ g_autoptr(GNotification) n = NULL;
+ gs_application_withdraw_notification (monitor->application, "updates-installed");
+ n = _build_autoupdated_notification (monitor, list);
+ if (n != NULL)
+ gs_application_send_notification (monitor->application, "updates-installed", n, MINUTES_IN_A_DAY);
+ }
+}
+
+static gboolean
+_should_auto_update (GsApp *app)
+{
+ if (gs_app_get_state (app) != GS_APP_STATE_UPDATABLE_LIVE)
+ return FALSE;
+ if (gs_app_has_quirk (app, GS_APP_QUIRK_NEW_PERMISSIONS))
+ return FALSE;
+ if (gs_app_has_quirk (app, GS_APP_QUIRK_DO_NOT_AUTO_UPDATE))
+ return FALSE;
+ return TRUE;
+}
+
+static void
+download_finished_cb (GObject *object, GAsyncResult *res, gpointer data)
+{
+ GsUpdateMonitor *monitor = GS_UPDATE_MONITOR (data);
+ GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (object);
+ g_autoptr(GError) error = NULL;
+ g_autoptr(GsAppList) list = NULL;
+ g_autoptr(GsAppList) update_online = NULL;
+ g_autoptr(GsAppList) update_offline = NULL;
+
+ /* get result */
+ list = gs_plugin_loader_job_process_finish (plugin_loader, res, &error);
+ if (list == NULL) {
+ gs_plugin_loader_claim_error (plugin_loader,
+ NULL,
+ GS_PLUGIN_ACTION_DOWNLOAD,
+ NULL,
+ TRUE,
+ error);
+ return;
+ }
+
+ update_online = gs_app_list_new ();
+ update_offline = gs_app_list_new ();
+ for (guint i = 0; i < gs_app_list_length (list); i++) {
+ GsApp *app = gs_app_list_index (list, i);
+ if (_should_auto_update (app)) {
+ g_debug ("auto-updating %s", gs_app_get_unique_id (app));
+ gs_app_list_add (update_online, app);
+ } else {
+ gs_app_list_add (update_offline, app);
+ }
+ }
+
+ /* install any apps that can be installed LIVE */
+ if (gs_app_list_length (update_online) > 0) {
+ g_autoptr(GsPluginJob) plugin_job = NULL;
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_UPDATE,
+ "list", update_online,
+ "propagate-error", TRUE,
+ NULL);
+ gs_plugin_loader_job_process_async (monitor->plugin_loader,
+ plugin_job,
+ monitor->update_cancellable,
+ update_finished_cb,
+ monitor);
+ }
+
+ /* show a notification for offline updates */
+ if (gs_app_list_length (update_offline) > 0)
+ notify_about_pending_updates (monitor, update_offline);
+}
+
+static void
+get_updates_finished_cb (GObject *object, GAsyncResult *res, gpointer data)
+{
+ g_autoptr(DownloadUpdatesData) download_updates_data = (DownloadUpdatesData *) data;
+ GsUpdateMonitor *monitor = download_updates_data->monitor;
+ guint64 security_timestamp = 0;
+ guint64 security_timestamp_old = 0;
+ g_autoptr(GError) error = NULL;
+ g_autoptr(GsAppList) apps = NULL;
+ gboolean should_download;
+
+ /* get result */
+ apps = gs_plugin_loader_job_process_finish (GS_PLUGIN_LOADER (object), res, &error);
+ if (apps == NULL) {
+ if (!g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED) &&
+ !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+ g_warning ("failed to get updates: %s", error->message);
+ return;
+ }
+
+ /* no updates */
+ if (gs_app_list_length (apps) == 0) {
+ g_debug ("no updates; withdrawing updates-available notification");
+ gs_application_withdraw_notification (monitor->application, "updates-available");
+ return;
+ }
+
+ /* find security updates, or clear timestamp if there are now none */
+ g_settings_get (monitor->settings,
+ "security-timestamp", "x", &security_timestamp_old);
+ for (guint i = 0; i < gs_app_list_length (apps); i++) {
+ GsApp *app = gs_app_list_index (apps, i);
+ guint64 size_download_bytes;
+ GsSizeType size_download_type = gs_app_get_size_download (app, &size_download_bytes);
+
+ if (gs_app_get_update_urgency (app) == AS_URGENCY_KIND_CRITICAL &&
+ size_download_type == GS_SIZE_TYPE_VALID &&
+ size_download_bytes > 0) {
+ security_timestamp = (guint64) g_get_monotonic_time ();
+ break;
+ }
+ }
+ if (security_timestamp_old != security_timestamp) {
+ g_settings_set (monitor->settings,
+ "security-timestamp", "x", security_timestamp);
+ }
+
+ g_debug ("got %u updates", gs_app_list_length (apps));
+
+ should_download = should_download_updates (monitor);
+
+ if (should_download &&
+ (security_timestamp_old != security_timestamp ||
+ check_if_timestamp_more_than_days_ago (monitor, "install-timestamp", 14))) {
+ g_autoptr(GsPluginJob) plugin_job = NULL;
+
+ /* download any updates; individual plugins are responsible for deciding
+ * whether it’s appropriate to unconditionally download the updates, or
+ * to schedule the download in accordance with the user’s metered data
+ * preferences */
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_DOWNLOAD,
+ "list", apps,
+ "propagate-error", TRUE,
+ NULL);
+ g_debug ("Getting updates");
+ gs_plugin_loader_job_process_async (monitor->plugin_loader,
+ plugin_job,
+ monitor->refresh_cancellable,
+ download_finished_cb,
+ monitor);
+ } else {
+ g_autoptr(GsAppList) update_online = NULL;
+ g_autoptr(GsAppList) update_offline = NULL;
+ GsAppList *notify_list;
+
+ update_online = gs_app_list_new ();
+ update_offline = gs_app_list_new ();
+ for (guint i = 0; i < gs_app_list_length (apps); i++) {
+ GsApp *app = gs_app_list_index (apps, i);
+ if (_should_auto_update (app)) {
+ g_debug ("download for auto-update %s", gs_app_get_unique_id (app));
+ gs_app_list_add (update_online, app);
+ } else {
+ gs_app_list_add (update_offline, app);
+ }
+ }
+
+ g_debug ("Received %u apps to update, %u are online and %u offline updates; will%s download online updates",
+ gs_app_list_length (apps),
+ gs_app_list_length (update_online),
+ gs_app_list_length (update_offline),
+ should_download ? "" : " not");
+
+ if (should_download && gs_app_list_length (update_online) > 0) {
+ g_autoptr(GsPluginJob) plugin_job = NULL;
+
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_DOWNLOAD,
+ "list", update_online,
+ "propagate-error", TRUE,
+ NULL);
+ g_debug ("Getting %u online updates", gs_app_list_length (update_online));
+ gs_plugin_loader_job_process_async (monitor->plugin_loader,
+ plugin_job,
+ monitor->refresh_cancellable,
+ download_finished_cb,
+ monitor);
+ }
+
+ if (should_download)
+ notify_list = update_offline;
+ else
+ notify_list = apps;
+
+ notify_about_pending_updates (monitor, notify_list);
+ }
+}
+
+static gboolean
+should_show_upgrade_notification (GsUpdateMonitor *monitor)
+{
+ return check_if_timestamp_more_than_days_ago (monitor, "upgrade-notification-timestamp", 7);
+}
+
+static void
+get_system_finished_cb (GObject *object, GAsyncResult *res, gpointer data)
+{
+ GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (object);
+ GsUpdateMonitor *monitor = data;
+ g_autoptr(GError) error = NULL;
+ g_autoptr(GNotification) n = NULL;
+ g_autoptr(GsApp) app = NULL;
+
+ /* get result */
+ app = gs_plugin_loader_get_system_app_finish (plugin_loader, res, &error);
+ if (app == NULL) {
+ if (!g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED) &&
+ !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+ g_warning ("failed to get system: %s", error->message);
+ return;
+ }
+
+ /* might be already showing, so just withdraw it and re-issue it */
+ gs_application_withdraw_notification (monitor->application, "eol");
+
+ /* do not show when the main window is active */
+ if (gs_application_has_active_window (monitor->application))
+ return;
+
+ /* is not EOL */
+ if (gs_app_get_state (app) != GS_APP_STATE_UNAVAILABLE)
+ return;
+
+ /* TRANSLATORS: this is when the current operating system version goes end-of-life */
+ n = g_notification_new (_("Operating System Updates Unavailable"));
+ /* TRANSLATORS: this is the message dialog for the distro EOL notice */
+ g_notification_set_body (n, _("Upgrade to continue receiving security updates."));
+ g_notification_set_default_action_and_target (n, "app.set-mode", "s", "updates");
+ gs_application_send_notification (monitor->application, "eol", n, MINUTES_IN_A_DAY);
+}
+
+static void
+get_upgrades_finished_cb (GObject *object,
+ GAsyncResult *res,
+ gpointer data)
+{
+ GsUpdateMonitor *monitor = GS_UPDATE_MONITOR (data);
+ GsApp *app;
+ g_autofree gchar *body = NULL;
+ g_autoptr(GDateTime) now = NULL;
+ g_autoptr(GError) error = NULL;
+ g_autoptr(GNotification) n = NULL;
+ g_autoptr(GsAppList) apps = NULL;
+
+ /* get result */
+ apps = gs_plugin_loader_job_process_finish (GS_PLUGIN_LOADER (object), res, &error);
+ if (apps == NULL) {
+ if (!g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED) &&
+ !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
+ g_warning ("failed to get upgrades: %s",
+ error->message);
+ }
+ return;
+ }
+
+ /* no results */
+ if (gs_app_list_length (apps) == 0) {
+ g_debug ("no upgrades; withdrawing upgrades-available notification");
+ gs_application_withdraw_notification (monitor->application, "upgrades-available");
+ return;
+ }
+
+ /* do not show if gnome-software is already open */
+ if (gs_application_has_active_window (monitor->application))
+ return;
+
+ /* only nag about upgrades once per week */
+ if (!should_show_upgrade_notification (monitor))
+ return;
+
+ g_debug ("showing distro upgrade notification");
+ now = g_date_time_new_now_local ();
+ g_settings_set (monitor->settings, "upgrade-notification-timestamp", "x",
+ g_date_time_to_unix (now));
+
+ /* rely on the app list already being sorted with the
+ * chronologically newest release last */
+ app = gs_app_list_index (apps, gs_app_list_length (apps) - 1);
+
+ /* TRANSLATORS: this is a distro upgrade, the replacement would be the
+ * distro name, e.g. 'Fedora' */
+ body = g_strdup_printf (_("A new version of %s is available to install"),
+ gs_app_get_name (app));
+
+ /* TRANSLATORS: this is a distro upgrade */
+ n = g_notification_new (_("Software Upgrade Available"));
+ g_notification_set_body (n, body);
+ g_notification_set_default_action_and_target (n, "app.set-mode", "s", "updates");
+ gs_application_send_notification (monitor->application, "upgrades-available", n, MINUTES_IN_A_DAY);
+}
+
+static void
+get_updates (GsUpdateMonitor *monitor)
+{
+ g_autoptr(GsPluginJob) plugin_job = NULL;
+ g_autoptr(DownloadUpdatesData) download_updates_data = NULL;
+
+ /* disabled in gsettings or from a plugin */
+ if (!gs_plugin_loader_get_allow_updates (monitor->plugin_loader)) {
+ g_debug ("not getting updates as not enabled");
+ return;
+ }
+
+ download_updates_data = g_slice_new0 (DownloadUpdatesData);
+ download_updates_data->monitor = g_object_ref (monitor);
+
+ /* NOTE: this doesn't actually do any network access */
+ g_debug ("Getting updates");
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_UPDATES,
+ "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_UPDATE_DETAILS |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_UPDATE_SEVERITY,
+ NULL);
+ gs_plugin_loader_job_process_async (monitor->plugin_loader,
+ plugin_job,
+ monitor->update_cancellable,
+ get_updates_finished_cb,
+ g_steal_pointer (&download_updates_data));
+}
+
+void
+gs_update_monitor_autoupdate (GsUpdateMonitor *monitor)
+{
+ get_updates (monitor);
+}
+
+static void
+get_upgrades (GsUpdateMonitor *monitor)
+{
+ g_autoptr(GsPluginJob) plugin_job = NULL;
+
+ /* disabled in gsettings or from a plugin */
+ if (!gs_plugin_loader_get_allow_updates (monitor->plugin_loader)) {
+ g_debug ("not getting upgrades as not enabled");
+ return;
+ }
+
+ /* NOTE: this doesn't actually do any network access, it relies on the
+ * AppStream data being up to date, either by the appstream-data
+ * package being up-to-date, or the metadata being auto-downloaded */
+ g_debug ("Getting upgrades");
+ plugin_job = gs_plugin_job_list_distro_upgrades_new (GS_PLUGIN_LIST_DISTRO_UPGRADES_FLAGS_NONE,
+ GS_PLUGIN_REFINE_FLAGS_NONE);
+ gs_plugin_loader_job_process_async (monitor->plugin_loader,
+ plugin_job,
+ monitor->update_cancellable,
+ get_upgrades_finished_cb,
+ monitor);
+}
+
+static void
+get_system (GsUpdateMonitor *monitor)
+{
+ g_autoptr(GsApp) app = NULL;
+
+ g_debug ("Getting system");
+ gs_plugin_loader_get_system_app_async (monitor->plugin_loader, monitor->update_cancellable,
+ get_system_finished_cb, monitor);
+}
+
+static void
+refresh_cache_finished_cb (GObject *object,
+ GAsyncResult *res,
+ gpointer data)
+{
+ GsUpdateMonitor *monitor = data;
+ g_autoptr(GDateTime) now = NULL;
+ g_autoptr(GError) error = NULL;
+
+ if (!gs_plugin_loader_job_action_finish (GS_PLUGIN_LOADER (object), res, &error)) {
+ if (!g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED) &&
+ !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+ g_warning ("failed to refresh the cache: %s", error->message);
+ return;
+ }
+
+ /* update the last checked timestamp */
+ now = g_date_time_new_now_local ();
+ g_settings_set (monitor->settings, "check-timestamp", "x",
+ g_date_time_to_unix (now));
+
+ get_updates (monitor);
+}
+
+typedef enum {
+ UP_DEVICE_LEVEL_UNKNOWN,
+ UP_DEVICE_LEVEL_NONE,
+ UP_DEVICE_LEVEL_DISCHARGING,
+ UP_DEVICE_LEVEL_LOW,
+ UP_DEVICE_LEVEL_CRITICAL,
+ UP_DEVICE_LEVEL_ACTION,
+ UP_DEVICE_LEVEL_LAST
+} UpDeviceLevel;
+
+static void
+install_language_pack_cb (GObject *object, GAsyncResult *res, gpointer data)
+{
+ g_autoptr(GError) error = NULL;
+ g_autoptr(WithAppData) with_app_data = data;
+
+ if (!gs_plugin_loader_job_action_finish (GS_PLUGIN_LOADER (object), res, &error)) {
+ if (!g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED) &&
+ !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+ g_debug ("failed to install language pack: %s", error->message);
+ return;
+ } else {
+ g_debug ("language pack for %s installed",
+ gs_app_get_name (with_app_data->app));
+ }
+}
+
+static void
+get_language_pack_cb (GObject *object, GAsyncResult *res, gpointer data)
+{
+ GsUpdateMonitor *monitor = GS_UPDATE_MONITOR (data);
+ GsApp *app;
+ g_autoptr(GError) error = NULL;
+ g_autoptr(GsAppList) app_list = NULL;
+
+ app_list = gs_plugin_loader_job_process_finish (GS_PLUGIN_LOADER (object), res, &error);
+ if (app_list == NULL) {
+ if (!g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED) &&
+ !g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED))
+ g_debug ("failed to find language pack: %s", error->message);
+ return;
+ }
+
+ /* none found */
+ if (gs_app_list_length (app_list) == 0) {
+ g_debug ("no language pack found");
+ return;
+ }
+
+ /* there should be one langpack for a given locale */
+ app = g_object_ref (gs_app_list_index (app_list, 0));
+ if (!gs_app_is_installed (app)) {
+ WithAppData *with_app_data;
+ g_autoptr(GsPluginJob) plugin_job = NULL;
+
+ with_app_data = with_app_data_new (monitor, app);
+
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_INSTALL,
+ "app", app,
+ NULL);
+ gs_plugin_loader_job_process_async (monitor->plugin_loader,
+ plugin_job,
+ monitor->update_cancellable,
+ install_language_pack_cb,
+ with_app_data);
+ }
+}
+
+/*
+ * determines active locale and looks for langpacks
+ * installs located language pack, if not already
+ */
+static void
+check_language_pack (GsUpdateMonitor *monitor) {
+
+ const gchar *locale;
+ g_autoptr(GsPluginJob) plugin_job = NULL;
+
+ locale = setlocale (LC_MESSAGES, NULL);
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_LANGPACKS,
+ "search", locale,
+ "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON,
+ NULL);
+ gs_plugin_loader_job_process_async (monitor->plugin_loader,
+ plugin_job,
+ monitor->update_cancellable,
+ get_language_pack_cb,
+ monitor);
+}
+
+static void
+check_updates (GsUpdateMonitor *monitor)
+{
+ gint64 tmp;
+ gboolean refresh_on_metered;
+ g_autoptr(GDateTime) last_refreshed = NULL;
+ g_autoptr(GsPluginJob) plugin_job = NULL;
+
+ /* never check for updates when offline */
+ if (!gs_plugin_loader_get_network_available (monitor->plugin_loader))
+ return;
+
+ /* check for language pack */
+ check_language_pack (monitor);
+
+#ifdef HAVE_MOGWAI
+ refresh_on_metered = TRUE;
+#else
+ refresh_on_metered = g_settings_get_boolean (monitor->settings,
+ "refresh-when-metered");
+#endif
+
+ if (!refresh_on_metered &&
+ gs_plugin_loader_get_network_metered (monitor->plugin_loader))
+ return;
+
+ /* never refresh when the battery is low */
+ if (monitor->proxy_upower != NULL) {
+ g_autoptr(GVariant) val = NULL;
+ val = g_dbus_proxy_get_cached_property (monitor->proxy_upower,
+ "WarningLevel");
+ if (val != NULL) {
+ guint32 level = g_variant_get_uint32 (val);
+ if (level >= UP_DEVICE_LEVEL_LOW) {
+ g_debug ("not getting updates on low power");
+ return;
+ }
+ }
+ } else {
+ g_debug ("no UPower support, so not doing power level checks");
+ }
+
+#if GLIB_CHECK_VERSION(2, 69, 1)
+ /* never refresh when in power saver mode */
+ if (monitor->power_profile_monitor != NULL) {
+ if (g_power_profile_monitor_get_power_saver_enabled (monitor->power_profile_monitor)) {
+ g_debug ("Not getting updates with power saver enabled");
+ return;
+ }
+ } else {
+ g_debug ("No power profile monitor support, so not doing power profile checks");
+ }
+#endif
+
+ g_settings_get (monitor->settings, "check-timestamp", "x", &tmp);
+ last_refreshed = g_date_time_new_from_unix_local (tmp);
+ if (last_refreshed != NULL) {
+ gint now_year, now_month, now_day, now_hour;
+ gint year, month, day;
+ g_autoptr(GDateTime) now = NULL;
+
+ now = g_date_time_new_now_local ();
+
+ g_date_time_get_ymd (now, &now_year, &now_month, &now_day);
+ now_hour = g_date_time_get_hour (now);
+
+ g_date_time_get_ymd (last_refreshed, &year, &month, &day);
+
+ /* check that it is the next day */
+ if (!((now_year > year) ||
+ (now_year == year && now_month > month) ||
+ (now_year == year && now_month == month && now_day > day)))
+ return;
+
+ /* ...and past 6am */
+ if (!(now_hour >= 6))
+ return;
+ }
+
+ if (!should_download_updates (monitor)) {
+ get_updates (monitor);
+ return;
+ }
+
+ g_debug ("Daily update check due");
+ plugin_job = gs_plugin_job_refresh_metadata_new (60 * 60 * 24,
+ GS_PLUGIN_REFRESH_METADATA_FLAGS_NONE);
+ gs_plugin_loader_job_process_async (monitor->plugin_loader, plugin_job,
+ monitor->refresh_cancellable,
+ refresh_cache_finished_cb,
+ monitor);
+}
+
+static gboolean
+check_hourly_cb (gpointer data)
+{
+ GsUpdateMonitor *monitor = data;
+
+ g_debug ("Hourly updates check");
+ check_updates (monitor);
+
+ return G_SOURCE_CONTINUE;
+}
+
+static gboolean
+check_thrice_daily_cb (gpointer data)
+{
+ GsUpdateMonitor *monitor = data;
+
+ g_debug ("Daily upgrades check");
+ get_upgrades (monitor);
+ get_system (monitor);
+
+ return G_SOURCE_CONTINUE;
+}
+
+static void
+stop_upgrades_check (GsUpdateMonitor *monitor)
+{
+ if (monitor->check_daily_id == 0)
+ return;
+
+ g_source_remove (monitor->check_daily_id);
+ monitor->check_daily_id = 0;
+}
+
+static void
+restart_upgrades_check (GsUpdateMonitor *monitor)
+{
+ stop_upgrades_check (monitor);
+ get_upgrades (monitor);
+
+ monitor->check_daily_id = g_timeout_add_seconds (SECONDS_IN_A_DAY / 3,
+ check_thrice_daily_cb,
+ monitor);
+}
+
+static void
+stop_updates_check (GsUpdateMonitor *monitor)
+{
+ if (monitor->check_hourly_id == 0)
+ return;
+
+ g_source_remove (monitor->check_hourly_id);
+ monitor->check_hourly_id = 0;
+}
+
+static void
+restart_updates_check (GsUpdateMonitor *monitor)
+{
+ stop_updates_check (monitor);
+ check_updates (monitor);
+
+ monitor->check_hourly_id = g_timeout_add_seconds (SECONDS_IN_AN_HOUR, check_hourly_cb,
+ monitor);
+}
+
+static gboolean
+check_updates_on_startup_cb (gpointer data)
+{
+ GsUpdateMonitor *monitor = data;
+
+ g_debug ("First hourly updates check");
+ restart_updates_check (monitor);
+
+ if (gs_plugin_loader_get_allow_updates (monitor->plugin_loader))
+ restart_upgrades_check (monitor);
+
+ monitor->check_startup_id = 0;
+ return G_SOURCE_REMOVE;
+}
+
+static void
+check_updates_upower_changed_cb (GDBusProxy *proxy,
+ GParamSpec *pspec,
+ GsUpdateMonitor *monitor)
+{
+ g_debug ("upower changed updates check");
+ check_updates (monitor);
+}
+
+static void
+network_available_notify_cb (GsPluginLoader *plugin_loader,
+ GParamSpec *pspec,
+ GsUpdateMonitor *monitor)
+{
+ check_updates (monitor);
+}
+
+static void
+get_updates_historical_cb (GObject *object, GAsyncResult *res, gpointer data)
+{
+ GsUpdateMonitor *monitor = data;
+ GsApp *app;
+ const gchar *message;
+ const gchar *title;
+ guint64 time_last_notified;
+ g_autoptr(GError) error = NULL;
+ g_autoptr(GsAppList) apps = NULL;
+ g_autoptr(GNotification) notification = NULL;
+
+ /* get result */
+ apps = gs_plugin_loader_job_process_finish (GS_PLUGIN_LOADER (object), res, &error);
+ if (apps == NULL) {
+ if (g_error_matches (error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED) ||
+ g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
+ g_debug ("Failed to get historical updates: %s", error->message);
+ g_clear_error (&monitor->last_offline_error);
+ return;
+ }
+
+ /* save this in case the user clicks the
+ * 'Show Details' button from the notification below */
+ g_clear_error (&monitor->last_offline_error);
+ monitor->last_offline_error = g_error_copy (error);
+
+ /* TRANSLATORS: title when we offline updates have failed */
+ notification = g_notification_new (_("Software Updates Failed"));
+ /* TRANSLATORS: message when we offline updates have failed */
+ g_notification_set_body (notification, _("An important operating system update failed to be installed."));
+ g_notification_add_button (notification, _("Show Details"), "app.show-offline-update-error");
+ g_notification_set_default_action (notification, "app.show-offline-update-error");
+ gs_application_send_notification (monitor->application, "offline-updates", notification, MINUTES_IN_A_DAY);
+ return;
+ }
+
+ /* no results */
+ if (gs_app_list_length (apps) == 0) {
+ g_debug ("no historical updates; withdrawing notification");
+ gs_application_withdraw_notification (monitor->application, "updates-available");
+ return;
+ }
+
+ /* have we notified about this before */
+ app = gs_app_list_index (apps, 0);
+ g_settings_get (monitor->settings,
+ "install-timestamp", "x", &time_last_notified);
+ if (time_last_notified >= gs_app_get_install_date (app))
+ return;
+
+ if (gs_app_get_kind (app) == AS_COMPONENT_KIND_OPERATING_SYSTEM) {
+ /* TRANSLATORS: Notification title when we've done a distro upgrade */
+ notification = g_notification_new (_("System Upgrade Complete"));
+
+ /* TRANSLATORS: This is the notification body when we've done a
+ * distro upgrade. First %s is the distro name and the 2nd %s
+ * is the version, e.g. "Welcome to Fedora 28!" */
+ message = g_strdup_printf (_("Welcome to %s %s!"),
+ gs_app_get_name (app),
+ gs_app_get_version (app));
+ g_notification_set_body (notification, message);
+ } else {
+ /* TRANSLATORS: title when we've done offline updates */
+ title = ngettext ("Software Update Installed",
+ "Software Updates Installed",
+ gs_app_list_length (apps));
+ /* TRANSLATORS: message when we've done offline updates */
+ message = ngettext ("An important operating system update has been installed.",
+ "Important operating system updates have been installed.",
+ gs_app_list_length (apps));
+
+ notification = g_notification_new (title);
+ g_notification_set_body (notification, message);
+ /* TRANSLATORS: Button to look at the updates that were installed.
+ * Note that it has nothing to do with the application reviews, the
+ * users can't express their opinions here. In some languages
+ * "Review (evaluate) something" is a different translation than
+ * "Review (browse) something." */
+ g_notification_add_button_with_target (notification, C_("updates", "Review"), "app.set-mode", "s", "updated");
+ g_notification_set_default_action_and_target (notification, "app.set-mode", "s", "updated");
+ }
+ gs_application_send_notification (monitor->application, "offline-updates", notification, MINUTES_IN_A_DAY);
+
+ /* update the timestamp so we don't show again */
+ g_settings_set (monitor->settings,
+ "install-timestamp", "x", gs_app_get_install_date (app));
+
+ reset_update_notification_timestamp (monitor);
+}
+
+static gboolean
+cleanup_notifications_cb (gpointer user_data)
+{
+ GsUpdateMonitor *monitor = user_data;
+ g_autoptr(GsPluginJob) plugin_job = NULL;
+
+ /* this doesn't do any network access, and is only called once just
+ * after startup, so don’t cancel it with refreshes/updates */
+ g_debug ("getting historical updates for fresh session");
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_UPDATES_HISTORICAL,
+ "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_VERSION,
+ NULL);
+ gs_plugin_loader_job_process_async (monitor->plugin_loader,
+ plugin_job,
+ monitor->shutdown_cancellable,
+ get_updates_historical_cb,
+ monitor);
+
+ /* wait until first check to show */
+ gs_application_withdraw_notification (monitor->application, "updates-available");
+
+ monitor->cleanup_notifications_id = 0;
+ return G_SOURCE_REMOVE;
+}
+
+void
+gs_update_monitor_show_error (GsUpdateMonitor *monitor, GtkWindow *window)
+{
+ const gchar *title;
+ const gchar *msg;
+ gboolean show_detailed_error;
+
+ /* can this happen in reality? */
+ if (monitor->last_offline_error == NULL)
+ return;
+
+ /* TRANSLATORS: this is when the offline update failed */
+ title = _("Failed To Update");
+
+ if (g_error_matches (monitor->last_offline_error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_NOT_SUPPORTED)) {
+ /* TRANSLATORS: the user must have updated manually after
+ * the updates were prepared */
+ msg = _("The system was already up to date.");
+ show_detailed_error = TRUE;
+ } else if (g_error_matches (monitor->last_offline_error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED) ||
+ g_error_matches (monitor->last_offline_error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
+ /* TRANSLATORS: the user aborted the update manually */
+ msg = _("The update was cancelled.");
+ show_detailed_error = FALSE;
+ } else if (g_error_matches (monitor->last_offline_error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_NO_NETWORK)) {
+ /* TRANSLATORS: the package manager needed to download
+ * something with no network available */
+ msg = _("Internet access was required but wasn’t available. "
+ "Please make sure that you have internet access and try again.");
+ show_detailed_error = FALSE;
+ } else if (g_error_matches (monitor->last_offline_error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_NO_SECURITY)) {
+ /* TRANSLATORS: if the package is not signed correctly */
+ msg = _("There were security issues with the update. "
+ "Please consult your software provider for more details.");
+ show_detailed_error = TRUE;
+ } else if (g_error_matches (monitor->last_offline_error, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_NO_SPACE)) {
+ /* TRANSLATORS: we ran out of disk space */
+ msg = _("There wasn’t enough disk space. Please free up some space and try again.");
+ show_detailed_error = FALSE;
+ } else {
+ /* TRANSLATORS: We didn't handle the error type */
+ msg = _("We’re sorry: the update failed to install. "
+ "Please wait for another update and try again. "
+ "If the problem persists, contact your software provider.");
+ show_detailed_error = TRUE;
+ }
+
+ gs_utils_show_error_dialog (window,
+ title,
+ msg,
+ show_detailed_error ? monitor->last_offline_error->message : NULL);
+}
+
+static void
+allow_updates_notify_cb (GsPluginLoader *plugin_loader,
+ GParamSpec *pspec,
+ GsUpdateMonitor *monitor)
+{
+ if (gs_plugin_loader_get_allow_updates (plugin_loader)) {
+ /* We restart the updates check here to avoid the user
+ * potentially waiting for the hourly check */
+ restart_updates_check (monitor);
+ restart_upgrades_check (monitor);
+ } else {
+ stop_upgrades_check (monitor);
+ }
+}
+
+static void
+gs_update_monitor_network_changed_cb (GNetworkMonitor *network_monitor,
+ gboolean available,
+ GsUpdateMonitor *monitor)
+{
+ /* cancel an on-going refresh if we're now in a metered connection */
+ if (!g_settings_get_boolean (monitor->settings, "refresh-when-metered") &&
+ g_network_monitor_get_network_metered (network_monitor)) {
+ g_cancellable_cancel (monitor->refresh_cancellable);
+ g_object_unref (monitor->refresh_cancellable);
+ monitor->refresh_cancellable = g_cancellable_new ();
+ } else {
+ /* Else, it might be time to check for updates */
+ check_updates (monitor);
+ }
+}
+
+#if GLIB_CHECK_VERSION(2, 69, 1)
+static void
+gs_update_monitor_power_profile_changed_cb (GObject *object,
+ GParamSpec *pspec,
+ gpointer user_data)
+{
+ GsUpdateMonitor *self = GS_UPDATE_MONITOR (user_data);
+
+ if (g_power_profile_monitor_get_power_saver_enabled (self->power_profile_monitor)) {
+ /* Cancel ongoing jobs, if we’re now in power saving mode. */
+ g_cancellable_cancel (self->refresh_cancellable);
+ g_object_unref (self->refresh_cancellable);
+ self->refresh_cancellable = g_cancellable_new ();
+
+ g_cancellable_cancel (self->update_cancellable);
+ g_object_unref (self->update_cancellable);
+ self->update_cancellable = g_cancellable_new ();
+ } else {
+ /* Else, it might be time to check for updates */
+ check_updates (self);
+ }
+}
+#endif
+
+static void
+gs_update_monitor_init (GsUpdateMonitor *monitor)
+{
+ GNetworkMonitor *network_monitor;
+ g_autoptr(GError) error = NULL;
+ monitor->settings = g_settings_new ("org.gnome.software");
+
+ /* cleanup at startup */
+ monitor->cleanup_notifications_id =
+ g_idle_add (cleanup_notifications_cb, monitor);
+
+ /* do a first check 60 seconds after login, and then every hour */
+ monitor->check_startup_id =
+ g_timeout_add_seconds (60, check_updates_on_startup_cb, monitor);
+
+ /* we use three cancellables because we want to be able to cancel refresh
+ * operations more opportunistically than other operations, since
+ * they’re less important and cancelling them doesn’t result in much
+ * wasted work, and we want to be able to cancel some operations only on
+ * shutdown. */
+ monitor->shutdown_cancellable = g_cancellable_new ();
+ monitor->update_cancellable = g_cancellable_new ();
+ monitor->refresh_cancellable = g_cancellable_new ();
+
+ /* connect to UPower to get the system power state */
+ monitor->proxy_upower = g_dbus_proxy_new_for_bus_sync (G_BUS_TYPE_SYSTEM,
+ G_DBUS_PROXY_FLAGS_NONE,
+ NULL,
+ "org.freedesktop.UPower",
+ "/org/freedesktop/UPower/devices/DisplayDevice",
+ "org.freedesktop.UPower.Device",
+ NULL,
+ &error);
+ if (monitor->proxy_upower != NULL) {
+ g_signal_connect (monitor->proxy_upower, "notify",
+ G_CALLBACK (check_updates_upower_changed_cb),
+ monitor);
+ } else {
+ g_warning ("failed to connect to upower: %s", error->message);
+ }
+
+ network_monitor = g_network_monitor_get_default ();
+ if (network_monitor != NULL) {
+ monitor->network_monitor = g_object_ref (network_monitor);
+ monitor->network_changed_handler = g_signal_connect (monitor->network_monitor,
+ "network-changed",
+ G_CALLBACK (gs_update_monitor_network_changed_cb),
+ monitor);
+ }
+
+#if GLIB_CHECK_VERSION(2, 69, 1)
+ monitor->power_profile_monitor = g_power_profile_monitor_dup_default ();
+ if (monitor->power_profile_monitor != NULL)
+ monitor->power_profile_changed_handler = g_signal_connect (monitor->power_profile_monitor,
+ "notify::power-saver-enabled",
+ G_CALLBACK (gs_update_monitor_power_profile_changed_cb),
+ monitor);
+#endif
+}
+
+static void
+gs_update_monitor_dispose (GObject *object)
+{
+ GsUpdateMonitor *monitor = GS_UPDATE_MONITOR (object);
+
+ if (monitor->network_changed_handler != 0) {
+ g_signal_handler_disconnect (monitor->network_monitor,
+ monitor->network_changed_handler);
+ monitor->network_changed_handler = 0;
+ }
+
+#if GLIB_CHECK_VERSION(2, 69, 1)
+ g_clear_signal_handler (&monitor->power_profile_changed_handler, monitor->power_profile_monitor);
+ g_clear_object (&monitor->power_profile_monitor);
+#endif
+
+ g_cancellable_cancel (monitor->update_cancellable);
+ g_clear_object (&monitor->update_cancellable);
+ g_cancellable_cancel (monitor->refresh_cancellable);
+ g_clear_object (&monitor->refresh_cancellable);
+ g_cancellable_cancel (monitor->shutdown_cancellable);
+ g_clear_object (&monitor->shutdown_cancellable);
+
+ stop_updates_check (monitor);
+ stop_upgrades_check (monitor);
+
+ if (monitor->check_startup_id != 0) {
+ g_source_remove (monitor->check_startup_id);
+ monitor->check_startup_id = 0;
+ }
+ if (monitor->cleanup_notifications_id != 0) {
+ g_source_remove (monitor->cleanup_notifications_id);
+ monitor->cleanup_notifications_id = 0;
+ }
+ if (monitor->plugin_loader != NULL) {
+ g_signal_handlers_disconnect_by_func (monitor->plugin_loader,
+ network_available_notify_cb,
+ monitor);
+ g_clear_object (&monitor->plugin_loader);
+ }
+ g_clear_object (&monitor->settings);
+ g_clear_object (&monitor->proxy_upower);
+
+ G_OBJECT_CLASS (gs_update_monitor_parent_class)->dispose (object);
+}
+
+static void
+gs_update_monitor_finalize (GObject *object)
+{
+ GsUpdateMonitor *monitor = GS_UPDATE_MONITOR (object);
+
+ g_application_release (G_APPLICATION (monitor->application));
+ g_clear_error (&monitor->last_offline_error);
+
+ G_OBJECT_CLASS (gs_update_monitor_parent_class)->finalize (object);
+}
+
+static void
+gs_update_monitor_class_init (GsUpdateMonitorClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ object_class->dispose = gs_update_monitor_dispose;
+ object_class->finalize = gs_update_monitor_finalize;
+}
+
+GsUpdateMonitor *
+gs_update_monitor_new (GsApplication *application,
+ GsPluginLoader *plugin_loader)
+{
+ GsUpdateMonitor *monitor;
+
+ monitor = GS_UPDATE_MONITOR (g_object_new (GS_TYPE_UPDATE_MONITOR, NULL));
+ monitor->application = application;
+ g_application_hold (G_APPLICATION (monitor->application));
+
+ monitor->plugin_loader = g_object_ref (plugin_loader);
+ g_signal_connect (monitor->plugin_loader, "notify::allow-updates",
+ G_CALLBACK (allow_updates_notify_cb), monitor);
+ g_signal_connect (monitor->plugin_loader, "notify::network-available",
+ G_CALLBACK (network_available_notify_cb), monitor);
+
+ return monitor;
+}