summaryrefslogtreecommitdiffstats
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/README.md7
-rw-r--r--lib/gnome-software-private.h23
-rw-r--r--lib/gnome-software.h24
-rw-r--r--lib/gnome-software.pc.in12
-rw-r--r--lib/gs-app-collation.h21
-rw-r--r--lib/gs-app-list-private.h79
-rw-r--r--lib/gs-app-list.c963
-rw-r--r--lib/gs-app-list.h46
-rw-r--r--lib/gs-app-private.h30
-rw-r--r--lib/gs-app.c4680
-rw-r--r--lib/gs-app.h405
-rw-r--r--lib/gs-autocleanups.h52
-rw-r--r--lib/gs-category-private.h21
-rw-r--r--lib/gs-category.c527
-rw-r--r--lib/gs-category.h51
-rw-r--r--lib/gs-cmd.c699
-rw-r--r--lib/gs-debug.c190
-rw-r--r--lib/gs-debug.h21
-rw-r--r--lib/gs-ioprio.c152
-rw-r--r--lib/gs-ioprio.h31
-rw-r--r--lib/gs-metered.c267
-rw-r--r--lib/gs-metered.h29
-rw-r--r--lib/gs-os-release.c335
-rw-r--r--lib/gs-os-release.h33
-rw-r--r--lib/gs-plugin-event.c313
-rw-r--r--lib/gs-plugin-event.h67
-rw-r--r--lib/gs-plugin-job-private.h44
-rw-r--r--lib/gs-plugin-job.c618
-rw-r--r--lib/gs-plugin-job.h58
-rw-r--r--lib/gs-plugin-loader-sync.c208
-rw-r--r--lib/gs-plugin-loader-sync.h34
-rw-r--r--lib/gs-plugin-loader.c3968
-rw-r--r--lib/gs-plugin-loader.h100
-rw-r--r--lib/gs-plugin-private.h55
-rw-r--r--lib/gs-plugin-types.h280
-rw-r--r--lib/gs-plugin-vfuncs.h942
-rw-r--r--lib/gs-plugin.c2069
-rw-r--r--lib/gs-plugin.h132
-rw-r--r--lib/gs-self-test.c835
-rw-r--r--lib/gs-test.c71
-rw-r--r--lib/gs-test.h20
-rw-r--r--lib/gs-utils.c1226
-rw-r--r--lib/gs-utils.h96
-rw-r--r--lib/meson.build142
44 files changed, 19976 insertions, 0 deletions
diff --git a/lib/README.md b/lib/README.md
new file mode 100644
index 0000000..628c58e
--- /dev/null
+++ b/lib/README.md
@@ -0,0 +1,7 @@
+libgnomesoftware
+================
+
+This is a static library, and is not all API stable.
+
+Only the plugin headers installed into /usr/include/gnome-software should be
+considered API.
diff --git a/lib/gnome-software-private.h b/lib/gnome-software-private.h
new file mode 100644
index 0000000..4cff5c7
--- /dev/null
+++ b/lib/gnome-software-private.h
@@ -0,0 +1,23 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2017 Richard Hughes <richard@hughsie.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+#pragma once
+
+#ifndef I_KNOW_THE_GNOME_SOFTWARE_API_IS_SUBJECT_TO_CHANGE
+#define I_KNOW_THE_GNOME_SOFTWARE_API_IS_SUBJECT_TO_CHANGE
+#endif
+
+#include <gnome-software.h>
+
+#include <gs-app-list-private.h>
+#include <gs-app-private.h>
+#include <gs-category-private.h>
+#include <gs-os-release.h>
+#include <gs-plugin-loader.h>
+#include <gs-plugin-loader-sync.h>
+#include <gs-plugin-private.h>
diff --git a/lib/gnome-software.h b/lib/gnome-software.h
new file mode 100644
index 0000000..4ceeda1
--- /dev/null
+++ b/lib/gnome-software.h
@@ -0,0 +1,24 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2016 Richard Hughes <richard@hughsie.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+#pragma once
+
+#ifndef I_KNOW_THE_GNOME_SOFTWARE_API_IS_SUBJECT_TO_CHANGE
+#error You have to define I_KNOW_THE_GNOME_SOFTWARE_API_IS_SUBJECT_TO_CHANGE
+#endif
+
+#include <gs-app.h>
+#include <gs-app-list.h>
+#include <gs-app-collation.h>
+#include <gs-autocleanups.h>
+#include <gs-category.h>
+#include <gs-metered.h>
+#include <gs-os-release.h>
+#include <gs-plugin.h>
+#include <gs-plugin-vfuncs.h>
+#include <gs-utils.h>
diff --git a/lib/gnome-software.pc.in b/lib/gnome-software.pc.in
new file mode 100644
index 0000000..cde7122
--- /dev/null
+++ b/lib/gnome-software.pc.in
@@ -0,0 +1,12 @@
+prefix=@prefix@
+libdir=@libdir@
+includedir=@includedir@
+plugindir=@libdir@/gs-plugins-@GS_PLUGIN_API_VERSION@
+
+Name: gnome-software
+Description: GNOME Software is a software center for GNOME
+Version: @VERSION@
+Requires.private: gthread-2.0 atk
+Requires: gobject-2.0 gdk-3.0 appstream-glib libsoup-2.4 gio-unix-2.0
+Libs: -L${libdir}
+Cflags: -I${includedir}/gnome-software
diff --git a/lib/gs-app-collation.h b/lib/gs-app-collation.h
new file mode 100644
index 0000000..892c755
--- /dev/null
+++ b/lib/gs-app-collation.h
@@ -0,0 +1,21 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2017-2018 Richard Hughes <richard@hughsie.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+#pragma once
+
+#include <glib-object.h>
+
+#include "gs-app-list.h"
+
+G_BEGIN_DECLS
+
+GsAppList *gs_app_get_related (GsApp *app);
+GsAppList *gs_app_get_addons (GsApp *app);
+GsAppList *gs_app_get_history (GsApp *app);
+
+G_END_DECLS
diff --git a/lib/gs-app-list-private.h b/lib/gs-app-list-private.h
new file mode 100644
index 0000000..ea92b13
--- /dev/null
+++ b/lib/gs-app-list-private.h
@@ -0,0 +1,79 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2016 Richard Hughes <richard@hughsie.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+#pragma once
+
+#include "gs-app-list.h"
+
+G_BEGIN_DECLS
+
+/**
+ * GsAppListFlags:
+ * @GS_APP_LIST_FLAG_NONE: No flags set
+ * @GS_APP_LIST_FLAG_IS_RANDOMIZED: List has been randomized
+ * @GS_APP_LIST_FLAG_IS_TRUNCATED: List has been truncated
+ * @GS_APP_LIST_FLAG_WATCH_APPS: Applications will be monitored
+ * @GS_APP_LIST_FLAG_WATCH_APPS_RELATED: Applications related apps will be monitored
+ * @GS_APP_LIST_FLAG_WATCH_APPS_ADDONS: Applications addon apps will be monitored
+ *
+ * Flags used to describe the list.
+ **/
+typedef enum {
+ GS_APP_LIST_FLAG_NONE = 0,
+ GS_APP_LIST_FLAG_IS_RANDOMIZED = 1 << 0,
+ GS_APP_LIST_FLAG_IS_TRUNCATED = 1 << 1,
+ GS_APP_LIST_FLAG_WATCH_APPS = 1 << 2,
+ GS_APP_LIST_FLAG_WATCH_APPS_RELATED = 1 << 3,
+ GS_APP_LIST_FLAG_WATCH_APPS_ADDONS = 1 << 4,
+ /*< private >*/
+ GS_APP_LIST_FLAG_LAST
+} GsAppListFlags;
+
+/**
+ * GsAppListFilterFlags:
+ * @GS_APP_LIST_FILTER_FLAG_NONE: No flags set
+ * @GS_APP_LIST_FILTER_FLAG_KEY_ID: Filter by ID
+ * @GS_APP_LIST_FILTER_FLAG_KEY_SOURCE: Filter by default source
+ * @GS_APP_LIST_FILTER_FLAG_KEY_VERSION: Filter by version
+ * @GS_APP_LIST_FILTER_FLAG_PREFER_INSTALLED: Prefer installed applications
+ * @GS_APP_LIST_FILTER_FLAG_KEY_ID_PROVIDES: Filter using the provides ID
+ *
+ * Flags to use when filtering. The priority of each #GsApp is used to choose
+ * which application object to keep.
+ **/
+typedef enum {
+ GS_APP_LIST_FILTER_FLAG_NONE = 0,
+ GS_APP_LIST_FILTER_FLAG_KEY_ID = 1 << 0,
+ GS_APP_LIST_FILTER_FLAG_KEY_SOURCE = 1 << 1,
+ GS_APP_LIST_FILTER_FLAG_KEY_VERSION = 1 << 2,
+ GS_APP_LIST_FILTER_FLAG_PREFER_INSTALLED= 1 << 3,
+ GS_APP_LIST_FILTER_FLAG_KEY_ID_PROVIDES = 1 << 4,
+ /*< private >*/
+ GS_APP_LIST_FILTER_FLAG_LAST,
+ GS_APP_LIST_FILTER_FLAG_MASK = G_MAXUINT64
+} GsAppListFilterFlags;
+
+/* All the properties which use #GsAppListFilterFlags are guint64s. */
+G_STATIC_ASSERT (sizeof (GsAppListFilterFlags) == sizeof (guint64));
+
+GsAppList *gs_app_list_copy (GsAppList *list);
+guint gs_app_list_get_size_peak (GsAppList *list);
+void gs_app_list_filter_duplicates (GsAppList *list,
+ GsAppListFilterFlags flags);
+void gs_app_list_randomize (GsAppList *list);
+void gs_app_list_remove_all (GsAppList *list);
+void gs_app_list_truncate (GsAppList *list,
+ guint length);
+gboolean gs_app_list_has_flag (GsAppList *list,
+ GsAppListFlags flag);
+void gs_app_list_add_flag (GsAppList *list,
+ GsAppListFlags flag);
+AsAppState gs_app_list_get_state (GsAppList *list);
+guint gs_app_list_get_progress (GsAppList *list);
+
+G_END_DECLS
diff --git a/lib/gs-app-list.c b/lib/gs-app-list.c
new file mode 100644
index 0000000..f2f599b
--- /dev/null
+++ b/lib/gs-app-list.c
@@ -0,0 +1,963 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2013-2016 Richard Hughes <richard@hughsie.com>
+ * Copyright (C) 2017-2018 Kalev Lember <klember@redhat.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+/**
+ * SECTION:gs-app-list
+ * @title: GsAppList
+ * @include: gnome-software.h
+ * @stability: Unstable
+ * @short_description: An application list
+ *
+ * These functions provide a refcounted list of #GsApp objects.
+ */
+
+#include "config.h"
+
+#include <glib.h>
+
+#include "gs-app-private.h"
+#include "gs-app-list-private.h"
+#include "gs-app-collation.h"
+
+struct _GsAppList
+{
+ GObject parent_instance;
+ GPtrArray *array;
+ GMutex mutex;
+ guint size_peak;
+ GsAppListFlags flags;
+ AsAppState state;
+ guint progress; /* 0–100 inclusive, or %GS_APP_PROGRESS_UNKNOWN */
+};
+
+G_DEFINE_TYPE (GsAppList, gs_app_list, G_TYPE_OBJECT)
+
+enum {
+ PROP_STATE = 1,
+ PROP_PROGRESS,
+ PROP_LAST
+};
+
+/**
+ * gs_app_list_get_state:
+ * @list: A #GsAppList
+ *
+ * Gets the state of the list.
+ *
+ * This method will only return a valid result if gs_app_list_add_flag() has
+ * been called with %GS_APP_LIST_FLAG_WATCH_APPS.
+ *
+ * Returns: the #AsAppState, e.g. %AS_APP_STATE_INSTALLED
+ *
+ * Since: 3.30
+ **/
+AsAppState
+gs_app_list_get_state (GsAppList *list)
+{
+ g_return_val_if_fail (GS_IS_APP_LIST (list), AS_APP_STATE_UNKNOWN);
+ return list->state;
+}
+
+/**
+ * gs_app_list_get_progress:
+ * @list: A #GsAppList
+ *
+ * Gets the average percentage completion of all apps in the list. If any of the
+ * apps in the list has progress %GS_APP_PROGRESS_UNKNOWN, or if the app list
+ * is empty, %GS_APP_PROGRESS_UNKNOWN will be returned.
+ *
+ * This method will only return a valid result if gs_app_list_add_flag() has
+ * been called with %GS_APP_LIST_FLAG_WATCH_APPS.
+ *
+ * Returns: the percentage completion (0–100 inclusive), or %GS_APP_PROGRESS_UNKNOWN for unknown
+ *
+ * Since: 3.30
+ **/
+guint
+gs_app_list_get_progress (GsAppList *list)
+{
+ g_return_val_if_fail (GS_IS_APP_LIST (list), GS_APP_PROGRESS_UNKNOWN);
+ return list->progress;
+}
+
+static void
+gs_app_list_add_watched_for_app (GsAppList *list, GPtrArray *apps, GsApp *app)
+{
+ if (list->flags & GS_APP_LIST_FLAG_WATCH_APPS)
+ g_ptr_array_add (apps, app);
+ if (list->flags & GS_APP_LIST_FLAG_WATCH_APPS_ADDONS) {
+ GsAppList *list2 = gs_app_get_addons (app);
+ for (guint i = 0; i < gs_app_list_length (list2); i++) {
+ GsApp *app2 = gs_app_list_index (list2, i);
+ g_ptr_array_add (apps, app2);
+ }
+ }
+ if (list->flags & GS_APP_LIST_FLAG_WATCH_APPS_RELATED) {
+ GsAppList *list2 = gs_app_get_related (app);
+ for (guint i = 0; i < gs_app_list_length (list2); i++) {
+ GsApp *app2 = gs_app_list_index (list2, i);
+ g_ptr_array_add (apps, app2);
+ }
+ }
+}
+
+static GPtrArray *
+gs_app_list_get_watched_for_app (GsAppList *list, GsApp *app)
+{
+ GPtrArray *apps = g_ptr_array_new ();
+ gs_app_list_add_watched_for_app (list, apps, app);
+ return apps;
+}
+
+static GPtrArray *
+gs_app_list_get_watched (GsAppList *list)
+{
+ GPtrArray *apps = g_ptr_array_new ();
+ for (guint i = 0; i < list->array->len; i++) {
+ GsApp *app_tmp = g_ptr_array_index (list->array, i);
+ gs_app_list_add_watched_for_app (list, apps, app_tmp);
+ }
+ return apps;
+}
+
+static void
+gs_app_list_invalidate_progress (GsAppList *self)
+{
+ guint progress = 0;
+ g_autoptr(GPtrArray) apps = gs_app_list_get_watched (self);
+
+ /* find the average percentage complete of the list */
+ if (apps->len > 0) {
+ guint64 pc_cnt = 0;
+ gboolean unknown_seen = FALSE;
+
+ for (guint i = 0; i < apps->len; i++) {
+ GsApp *app_tmp = g_ptr_array_index (apps, i);
+ guint app_progress = gs_app_get_progress (app_tmp);
+
+ if (app_progress == GS_APP_PROGRESS_UNKNOWN) {
+ unknown_seen = TRUE;
+ break;
+ }
+ pc_cnt += gs_app_get_progress (app_tmp);
+ }
+
+ progress = (!unknown_seen) ? pc_cnt / apps->len : GS_APP_PROGRESS_UNKNOWN;
+ } else {
+ progress = GS_APP_PROGRESS_UNKNOWN;
+ }
+
+ if (self->progress != progress) {
+ self->progress = progress;
+ g_object_notify (G_OBJECT (self), "progress");
+ }
+}
+
+static void
+gs_app_list_invalidate_state (GsAppList *self)
+{
+ AsAppState state = AS_APP_STATE_UNKNOWN;
+ g_autoptr(GPtrArray) apps = gs_app_list_get_watched (self);
+
+ /* find any action state of the list */
+ for (guint i = 0; i < apps->len; i++) {
+ GsApp *app_tmp = g_ptr_array_index (apps, i);
+ AsAppState state_tmp = gs_app_get_state (app_tmp);
+ if (state_tmp == AS_APP_STATE_INSTALLING ||
+ state_tmp == AS_APP_STATE_REMOVING) {
+ state = state_tmp;
+ break;
+ }
+ }
+ if (self->state != state) {
+ self->state = state;
+ g_object_notify (G_OBJECT (self), "state");
+ }
+}
+
+static void
+gs_app_list_progress_notify_cb (GsApp *app, GParamSpec *pspec, GsAppList *self)
+{
+ gs_app_list_invalidate_progress (self);
+}
+
+static void
+gs_app_list_state_notify_cb (GsApp *app, GParamSpec *pspec, GsAppList *self)
+{
+ gs_app_list_invalidate_state (self);
+}
+
+static void
+gs_app_list_maybe_watch_app (GsAppList *list, GsApp *app)
+{
+ g_autoptr(GPtrArray) apps = gs_app_list_get_watched_for_app (list, app);
+ for (guint i = 0; i < apps->len; i++) {
+ GsApp *app_tmp = g_ptr_array_index (apps, i);
+ g_signal_connect_object (app_tmp, "notify::progress",
+ G_CALLBACK (gs_app_list_progress_notify_cb),
+ list, 0);
+ g_signal_connect_object (app_tmp, "notify::state",
+ G_CALLBACK (gs_app_list_state_notify_cb),
+ list, 0);
+ }
+}
+
+static void
+gs_app_list_maybe_unwatch_app (GsAppList *list, GsApp *app)
+{
+ g_autoptr(GPtrArray) apps = gs_app_list_get_watched_for_app (list, app);
+ for (guint i = 0; i < apps->len; i++) {
+ GsApp *app_tmp = g_ptr_array_index (apps, i);
+ g_signal_handlers_disconnect_by_data (app_tmp, list);
+ }
+}
+
+/**
+ * gs_app_list_get_size_peak:
+ * @list: A #GsAppList
+ *
+ * Returns the largest size the list has ever been.
+ *
+ * Returns: integer
+ *
+ * Since: 3.24
+ **/
+guint
+gs_app_list_get_size_peak (GsAppList *list)
+{
+ return list->size_peak;
+}
+
+static GsApp *
+gs_app_list_lookup_safe (GsAppList *list, const gchar *unique_id)
+{
+ for (guint i = 0; i < list->array->len; i++) {
+ GsApp *app = g_ptr_array_index (list->array, i);
+ if (as_utils_unique_id_equal (gs_app_get_unique_id (app), unique_id))
+ return app;
+ }
+ return NULL;
+}
+
+/**
+ * gs_app_list_lookup:
+ * @list: A #GsAppList
+ * @unique_id: A unique_id
+ *
+ * Finds the first matching application in the list using the usual wildcard
+ * rules allowed in unique_ids.
+ *
+ * Returns: (transfer none): a #GsApp, or %NULL if not found
+ *
+ * Since: 3.22
+ **/
+GsApp *
+gs_app_list_lookup (GsAppList *list, const gchar *unique_id)
+{
+ g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&list->mutex);
+ return gs_app_list_lookup_safe (list, unique_id);
+}
+
+/**
+ * gs_app_list_has_flag:
+ * @list: A #GsAppList
+ * @flag: A flag to test, e.g. %GS_APP_LIST_FLAG_IS_TRUNCATED
+ *
+ * Gets if a specific flag is set.
+ *
+ * Returns: %TRUE if the flag is set
+ *
+ * Since: 3.24
+ **/
+gboolean
+gs_app_list_has_flag (GsAppList *list, GsAppListFlags flag)
+{
+ return (list->flags & flag) > 0;
+}
+
+/**
+ * gs_app_list_add_flag:
+ * @list: A #GsAppList
+ * @flag: A flag to test, e.g. %GS_APP_LIST_FLAG_IS_TRUNCATED
+ *
+ * Gets if a specific flag is set.
+ *
+ * Returns: %TRUE if the flag is set
+ *
+ * Since: 3.30
+ **/
+void
+gs_app_list_add_flag (GsAppList *list, GsAppListFlags flag)
+{
+ if (list->flags & flag)
+ return;
+ list->flags |= flag;
+
+ /* turn this on for existing apps */
+ for (guint i = 0; i < list->array->len; i++) {
+ GsApp *app = g_ptr_array_index (list->array, i);
+ gs_app_list_maybe_watch_app (list, app);
+ }
+}
+
+static gboolean
+gs_app_list_check_for_duplicate (GsAppList *list, GsApp *app)
+{
+ GsApp *app_old;
+ const gchar *id;
+
+ /* adding a wildcard */
+ if (gs_app_has_quirk (app, GS_APP_QUIRK_IS_WILDCARD)) {
+ for (guint i = 0; i < list->array->len; i++) {
+ GsApp *app_tmp = g_ptr_array_index (list->array, i);
+ if (!gs_app_has_quirk (app_tmp, GS_APP_QUIRK_IS_WILDCARD))
+ continue;
+ /* not adding exactly the same wildcard */
+ if (g_strcmp0 (gs_app_get_unique_id (app_tmp),
+ gs_app_get_unique_id (app)) == 0)
+ return FALSE;
+ }
+ return TRUE;
+ }
+
+ /* does not exist */
+ id = gs_app_get_unique_id (app);
+ if (id == NULL) {
+ for (guint i = 0; i < list->array->len; i++) {
+ GsApp *app_tmp = g_ptr_array_index (list->array, i);
+ if (app_tmp == app)
+ return FALSE;
+ }
+ /* not much else we can do... */
+ return TRUE;
+ }
+
+ /* existing app is a wildcard */
+ app_old = gs_app_list_lookup_safe (list, id);
+ if (app_old == NULL)
+ return TRUE;
+ if (gs_app_has_quirk (app_old, GS_APP_QUIRK_IS_WILDCARD))
+ return TRUE;
+
+ /* already exists */
+ return FALSE;
+}
+
+typedef enum {
+ GS_APP_LIST_ADD_FLAG_NONE = 0,
+ GS_APP_LIST_ADD_FLAG_CHECK_FOR_DUPE = 1 << 0,
+ GS_APP_LIST_ADD_FLAG_LAST
+} GsAppListAddFlag;
+
+static void
+gs_app_list_add_safe (GsAppList *list, GsApp *app, GsAppListAddFlag flag)
+{
+ const gchar *id;
+
+ /* check for duplicate */
+ if ((flag & GS_APP_LIST_ADD_FLAG_CHECK_FOR_DUPE) > 0 &&
+ !gs_app_list_check_for_duplicate (list, app))
+ return;
+
+ /* if we're lazy-loading the ID then we can't use the ID hash */
+ id = gs_app_get_unique_id (app);
+ if (id == NULL) {
+ gs_app_list_maybe_watch_app (list, app);
+ g_ptr_array_add (list->array, g_object_ref (app));
+ return;
+ }
+
+ /* just use the ref */
+ gs_app_list_maybe_watch_app (list, app);
+ g_ptr_array_add (list->array, g_object_ref (app));
+
+ /* update the historical max */
+ if (list->array->len > list->size_peak)
+ list->size_peak = list->array->len;
+}
+
+/**
+ * gs_app_list_add:
+ * @list: A #GsAppList
+ * @app: A #GsApp
+ *
+ * If the application does not already exist in the list then it is added,
+ * incrementing the reference count.
+ * If the application already exists then a warning is printed to the console.
+ *
+ * Applications that have the application ID lazy-loaded will always be added
+ * to the list, and to clean these up the plugin loader will also call the
+ * gs_app_list_filter_duplicates() method when all plugins have run.
+ *
+ * Since: 3.22
+ **/
+void
+gs_app_list_add (GsAppList *list, GsApp *app)
+{
+ g_autoptr(GMutexLocker) locker = NULL;
+ g_return_if_fail (GS_IS_APP_LIST (list));
+ g_return_if_fail (GS_IS_APP (app));
+ locker = g_mutex_locker_new (&list->mutex);
+ gs_app_list_add_safe (list, app, GS_APP_LIST_ADD_FLAG_CHECK_FOR_DUPE);
+
+ /* recalculate global state */
+ gs_app_list_invalidate_state (list);
+ gs_app_list_invalidate_progress (list);
+}
+
+/**
+ * gs_app_list_remove:
+ * @list: A #GsAppList
+ * @app: A #GsApp
+ *
+ * Removes an application from the list. If the application does not exist the
+ * request is ignored.
+ *
+ * Since: 3.22
+ **/
+void
+gs_app_list_remove (GsAppList *list, GsApp *app)
+{
+ g_autoptr(GMutexLocker) locker = NULL;
+
+ g_return_if_fail (GS_IS_APP_LIST (list));
+ g_return_if_fail (GS_IS_APP (app));
+
+ locker = g_mutex_locker_new (&list->mutex);
+ g_ptr_array_remove (list->array, app);
+ gs_app_list_maybe_unwatch_app (list, app);
+
+ /* recalculate global state */
+ gs_app_list_invalidate_state (list);
+ gs_app_list_invalidate_progress (list);
+}
+
+/**
+ * gs_app_list_add_list:
+ * @list: A #GsAppList
+ * @donor: Another #GsAppList
+ *
+ * Adds all the applications in @donor to @list.
+ *
+ * Since: 3.22
+ **/
+void
+gs_app_list_add_list (GsAppList *list, GsAppList *donor)
+{
+ guint i;
+ g_autoptr(GMutexLocker) locker = NULL;
+
+ g_return_if_fail (GS_IS_APP_LIST (list));
+ g_return_if_fail (GS_IS_APP_LIST (donor));
+ g_return_if_fail (list != donor);
+
+ locker = g_mutex_locker_new (&list->mutex);
+
+ /* add each app */
+ for (i = 0; i < donor->array->len; i++) {
+ GsApp *app = gs_app_list_index (donor, i);
+ gs_app_list_add_safe (list, app, GS_APP_LIST_ADD_FLAG_CHECK_FOR_DUPE);
+ }
+
+ /* recalculate global state */
+ gs_app_list_invalidate_state (list);
+ gs_app_list_invalidate_progress (list);
+}
+
+/**
+ * gs_app_list_index:
+ * @list: A #GsAppList
+ * @idx: An index into the list
+ *
+ * Gets an application at a specific position in the list.
+ *
+ * Returns: (transfer none): a #GsApp, or %NULL if invalid
+ *
+ * Since: 3.22
+ **/
+GsApp *
+gs_app_list_index (GsAppList *list, guint idx)
+{
+ return GS_APP (g_ptr_array_index (list->array, idx));
+}
+
+/**
+ * gs_app_list_length:
+ * @list: A #GsAppList
+ *
+ * Gets the length of the application list.
+ *
+ * Returns: Integer
+ *
+ * Since: 3.22
+ **/
+guint
+gs_app_list_length (GsAppList *list)
+{
+ g_return_val_if_fail (GS_IS_APP_LIST (list), 0);
+ return list->array->len;
+}
+
+static void
+gs_app_list_remove_all_safe (GsAppList *list)
+{
+ for (guint i = 0; i < list->array->len; i++) {
+ GsApp *app = g_ptr_array_index (list->array, i);
+ gs_app_list_maybe_unwatch_app (list, app);
+ }
+ g_ptr_array_set_size (list->array, 0);
+ gs_app_list_invalidate_state (list);
+ gs_app_list_invalidate_progress (list);
+}
+
+/**
+ * gs_app_list_remove_all:
+ * @list: A #GsAppList
+ *
+ * Removes all applications from the list.
+ *
+ * Since: 3.22
+ **/
+void
+gs_app_list_remove_all (GsAppList *list)
+{
+ g_autoptr(GMutexLocker) locker = NULL;
+ g_return_if_fail (GS_IS_APP_LIST (list));
+ locker = g_mutex_locker_new (&list->mutex);
+ gs_app_list_remove_all_safe (list);
+}
+
+/**
+ * gs_app_list_filter:
+ * @list: A #GsAppList
+ * @func: A #GsAppListFilterFunc
+ * @user_data: the user pointer to pass to @func
+ *
+ * If func() returns TRUE for the GsApp, then the app is kept.
+ *
+ * Since: 3.22
+ **/
+void
+gs_app_list_filter (GsAppList *list, GsAppListFilterFunc func, gpointer user_data)
+{
+ guint i;
+ GsApp *app;
+ g_autoptr(GsAppList) old = NULL;
+ g_autoptr(GMutexLocker) locker = NULL;
+
+ g_return_if_fail (GS_IS_APP_LIST (list));
+ g_return_if_fail (func != NULL);
+
+ locker = g_mutex_locker_new (&list->mutex);
+
+ /* deep copy to a temp list and clear the current one */
+ old = gs_app_list_copy (list);
+ gs_app_list_remove_all_safe (list);
+
+ /* see if any of the apps need filtering */
+ for (i = 0; i < old->array->len; i++) {
+ app = gs_app_list_index (old, i);
+ if (func (app, user_data))
+ gs_app_list_add_safe (list, app, GS_APP_LIST_ADD_FLAG_NONE);
+ }
+}
+
+typedef struct {
+ GsAppListSortFunc func;
+ gpointer user_data;
+} GsAppListSortHelper;
+
+static gint
+gs_app_list_sort_cb (gconstpointer a, gconstpointer b, gpointer user_data)
+{
+ GsApp *app1 = GS_APP (*(GsApp **) a);
+ GsApp *app2 = GS_APP (*(GsApp **) b);
+ GsAppListSortHelper *helper = (GsAppListSortHelper *) user_data;
+ return helper->func (app1, app2, user_data);
+}
+
+/**
+ * gs_app_list_sort:
+ * @list: A #GsAppList
+ * @func: A #GCompareFunc
+ * @user_data: user data to pass to @func
+ *
+ * Sorts the application list.
+ *
+ * Since: 3.22
+ **/
+void
+gs_app_list_sort (GsAppList *list, GsAppListSortFunc func, gpointer user_data)
+{
+ g_autoptr(GMutexLocker) locker = NULL;
+ GsAppListSortHelper helper;
+ g_return_if_fail (GS_IS_APP_LIST (list));
+ locker = g_mutex_locker_new (&list->mutex);
+ helper.func = func;
+ helper.user_data = user_data;
+ g_ptr_array_sort_with_data (list->array, gs_app_list_sort_cb, &helper);
+}
+
+/**
+ * gs_app_list_truncate:
+ * @list: A #GsAppList
+ * @length: the new length
+ *
+ * Truncates the application list. It is an error if @length is larger than the
+ * size of the list.
+ *
+ * Since: 3.24
+ **/
+void
+gs_app_list_truncate (GsAppList *list, guint length)
+{
+ g_autoptr(GMutexLocker) locker = NULL;
+
+ g_return_if_fail (GS_IS_APP_LIST (list));
+ g_return_if_fail (length <= list->array->len);
+
+ /* mark this list as unworthy */
+ list->flags |= GS_APP_LIST_FLAG_IS_TRUNCATED;
+
+ /* everything */
+ if (length == 0) {
+ gs_app_list_remove_all (list);
+ return;
+ }
+
+ /* remove the apps in the positions larger than the length */
+ locker = g_mutex_locker_new (&list->mutex);
+ g_ptr_array_set_size (list->array, length);
+}
+
+static gint
+gs_app_list_randomize_cb (gconstpointer a, gconstpointer b, gpointer user_data)
+{
+ GsApp *app1 = GS_APP (*(GsApp **) a);
+ GsApp *app2 = GS_APP (*(GsApp **) b);
+ const gchar *k1;
+ const gchar *k2;
+ g_autofree gchar *key = NULL;
+
+ key = g_strdup_printf ("Plugin::sort-key[%p]", user_data);
+ k1 = gs_app_get_metadata_item (app1, key);
+ k2 = gs_app_get_metadata_item (app2, key);
+ return g_strcmp0 (k1, k2);
+}
+
+/**
+ * gs_app_list_randomize:
+ * @list: A #GsAppList
+ *
+ * Randomize the order of the list, but don't change the order until
+ * the next day.
+ *
+ * Since: 3.22
+ **/
+void
+gs_app_list_randomize (GsAppList *list)
+{
+ guint i;
+ GRand *rand;
+ GsApp *app;
+ gchar sort_key[] = { '\0', '\0', '\0', '\0' };
+ g_autoptr(GDateTime) date = NULL;
+ g_autofree gchar *key = NULL;
+ g_autoptr(GMutexLocker) locker = NULL;
+
+ g_return_if_fail (GS_IS_APP_LIST (list));
+
+ locker = g_mutex_locker_new (&list->mutex);
+
+ /* mark this list as random */
+ list->flags |= GS_APP_LIST_FLAG_IS_RANDOMIZED;
+
+ key = g_strdup_printf ("Plugin::sort-key[%p]", list);
+ rand = g_rand_new ();
+ date = g_date_time_new_now_utc ();
+ g_rand_set_seed (rand, (guint32) g_date_time_get_day_of_year (date));
+ for (i = 0; i < gs_app_list_length (list); i++) {
+ app = gs_app_list_index (list, i);
+ sort_key[0] = (gchar) g_rand_int_range (rand, (gint32) 'A', (gint32) 'Z');
+ sort_key[1] = (gchar) g_rand_int_range (rand, (gint32) 'A', (gint32) 'Z');
+ sort_key[2] = (gchar) g_rand_int_range (rand, (gint32) 'A', (gint32) 'Z');
+ gs_app_set_metadata (app, key, sort_key);
+ }
+ g_ptr_array_sort_with_data (list->array, gs_app_list_randomize_cb, list);
+ for (i = 0; i < gs_app_list_length (list); i++) {
+ app = gs_app_list_index (list, i);
+ gs_app_set_metadata (app, key, NULL);
+ }
+ g_rand_free (rand);
+}
+
+static gboolean
+gs_app_list_filter_app_is_better (GsApp *app, GsApp *found, GsAppListFilterFlags flags)
+{
+ /* optional 1st layer sort */
+ if ((flags & GS_APP_LIST_FILTER_FLAG_PREFER_INSTALLED) > 0) {
+ if (gs_app_is_installed (app) && !gs_app_is_installed (found))
+ return TRUE;
+ if (!gs_app_is_installed (app) && gs_app_is_installed (found))
+ return FALSE;
+ }
+
+ /* 2nd layer, priority and bundle kind */
+ if (gs_app_compare_priority (app, found) < 0)
+ return TRUE;
+
+ /* assume is worse */
+ return FALSE;
+}
+
+static GPtrArray *
+gs_app_list_filter_app_get_keys (GsApp *app, GsAppListFilterFlags flags)
+{
+ GPtrArray *keys = g_ptr_array_new_with_free_func (g_free);
+ g_autoptr(GString) key = NULL;
+
+ /* just use the unique ID */
+ if (flags == GS_APP_LIST_FILTER_FLAG_NONE) {
+ if (gs_app_get_unique_id (app) != NULL)
+ g_ptr_array_add (keys, g_strdup (gs_app_get_unique_id (app)));
+ return keys;
+ }
+
+ /* use the ID and any provides */
+ if (flags & GS_APP_LIST_FILTER_FLAG_KEY_ID_PROVIDES) {
+ GPtrArray *provides = gs_app_get_provides (app);
+ g_ptr_array_add (keys, g_strdup (gs_app_get_id (app)));
+ for (guint i = 0; i < provides->len; i++) {
+ AsProvide *prov = g_ptr_array_index (provides, i);
+ if (as_provide_get_kind (prov) != AS_PROVIDE_KIND_ID)
+ continue;
+ g_ptr_array_add (keys, g_strdup (as_provide_get_value (prov)));
+ }
+ return keys;
+ }
+
+ /* specific compound type */
+ key = g_string_new (NULL);
+ if (flags & GS_APP_LIST_FILTER_FLAG_KEY_ID) {
+ const gchar *tmp = gs_app_get_id (app);
+ if (tmp != NULL)
+ g_string_append (key, gs_app_get_id (app));
+ }
+ if (flags & GS_APP_LIST_FILTER_FLAG_KEY_SOURCE) {
+ const gchar *tmp = gs_app_get_source_default (app);
+ if (tmp != NULL)
+ g_string_append_printf (key, ":%s", tmp);
+ }
+ if (flags & GS_APP_LIST_FILTER_FLAG_KEY_VERSION) {
+ const gchar *tmp = gs_app_get_version (app);
+ if (tmp != NULL)
+ g_string_append_printf (key, ":%s", tmp);
+ }
+ if (key->len == 0)
+ return keys;
+ g_ptr_array_add (keys, g_string_free (g_steal_pointer (&key), FALSE));
+ return keys;
+}
+
+/**
+ * gs_app_list_filter_duplicates:
+ * @list: A #GsAppList
+ * @flags: a #GsAppListFilterFlags, e.g. GS_APP_LIST_FILTER_KEY_ID
+ *
+ * Filter any duplicate applications from the list.
+ *
+ * Since: 3.22
+ **/
+void
+gs_app_list_filter_duplicates (GsAppList *list, GsAppListFilterFlags flags)
+{
+ g_autoptr(GHashTable) hash = NULL;
+ g_autoptr(GHashTable) kept_apps = NULL;
+ g_autoptr(GsAppList) old = NULL;
+ g_autoptr(GMutexLocker) locker = NULL;
+
+ g_return_if_fail (GS_IS_APP_LIST (list));
+
+ locker = g_mutex_locker_new (&list->mutex);
+
+ /* a hash table to hold apps with unique app ids */
+ hash = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL);
+ /* a hash table containing apps we want to keep */
+ kept_apps = g_hash_table_new (g_direct_hash, g_direct_equal);
+
+ for (guint i = 0; i < list->array->len; i++) {
+ GsApp *app = gs_app_list_index (list, i);
+ GsApp *found = NULL;
+ g_autoptr(GPtrArray) keys = NULL;
+
+ /* get all the keys used to identify this app */
+ keys = gs_app_list_filter_app_get_keys (app, flags);
+ for (guint j = 0; j < keys->len; j++) {
+ const gchar *key = g_ptr_array_index (keys, j);
+ found = g_hash_table_lookup (hash, key);
+ if (found != NULL)
+ break;
+ }
+
+ /* new app */
+ if (found == NULL) {
+ for (guint j = 0; j < keys->len; j++) {
+ const gchar *key = g_ptr_array_index (keys, j);
+ g_hash_table_insert (hash, g_strdup (key), app);
+ }
+ g_hash_table_add (kept_apps, app);
+ continue;
+ }
+
+ /* better? */
+ if (flags != GS_APP_LIST_FILTER_FLAG_NONE) {
+ if (gs_app_list_filter_app_is_better (app, found, flags)) {
+ for (guint j = 0; j < keys->len; j++) {
+ const gchar *key = g_ptr_array_index (keys, j);
+ g_hash_table_insert (hash, g_strdup (key), app);
+ }
+ g_hash_table_remove (kept_apps, found);
+ g_hash_table_add (kept_apps, app);
+ continue;
+ }
+ continue;
+ }
+ continue;
+ }
+
+ /* deep copy to a temp list and clear the current one */
+ old = gs_app_list_copy (list);
+ gs_app_list_remove_all_safe (list);
+
+ /* add back the apps we want to keep */
+ for (guint i = 0; i < old->array->len; i++) {
+ GsApp *app = gs_app_list_index (old, i);
+ if (g_hash_table_contains (kept_apps, app))
+ gs_app_list_add_safe (list, app, GS_APP_LIST_ADD_FLAG_NONE);
+ }
+}
+
+/**
+ * gs_app_list_copy:
+ * @list: A #GsAppList
+ *
+ * Returns a deep copy of the application list.
+ *
+ * Returns: A newly allocated #GsAppList
+ *
+ * Since: 3.22
+ **/
+GsAppList *
+gs_app_list_copy (GsAppList *list)
+{
+ GsAppList *new;
+ guint i;
+
+ g_return_val_if_fail (GS_IS_APP_LIST (list), NULL);
+
+ new = gs_app_list_new ();
+ for (i = 0; i < gs_app_list_length (list); i++) {
+ GsApp *app = gs_app_list_index (list, i);
+ gs_app_list_add_safe (new, app, GS_APP_LIST_ADD_FLAG_NONE);
+ }
+ return new;
+}
+
+static void
+gs_app_list_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
+{
+ GsAppList *self = GS_APP_LIST (object);
+ switch (prop_id) {
+ case PROP_STATE:
+ g_value_set_uint (value, self->state);
+ break;
+ case PROP_PROGRESS:
+ g_value_set_uint (value, self->progress);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+gs_app_list_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec)
+{
+ switch (prop_id) {
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+gs_app_list_finalize (GObject *object)
+{
+ GsAppList *list = GS_APP_LIST (object);
+ g_ptr_array_unref (list->array);
+ g_mutex_clear (&list->mutex);
+ G_OBJECT_CLASS (gs_app_list_parent_class)->finalize (object);
+}
+
+static void
+gs_app_list_class_init (GsAppListClass *klass)
+{
+ GParamSpec *pspec;
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ object_class->get_property = gs_app_list_get_property;
+ object_class->set_property = gs_app_list_set_property;
+ object_class->finalize = gs_app_list_finalize;
+
+ /**
+ * GsAppList:state:
+ */
+ pspec = g_param_spec_uint ("state", NULL, NULL,
+ AS_APP_STATE_UNKNOWN,
+ AS_APP_STATE_LAST,
+ AS_APP_STATE_UNKNOWN,
+ G_PARAM_READABLE);
+ g_object_class_install_property (object_class, PROP_STATE, pspec);
+
+ /**
+ * GsAppList:progress:
+ *
+ * A percentage (0–100, inclusive) indicating the progress through the
+ * current task on this app list. The value may otherwise be
+ * %GS_APP_PROGRESS_UNKNOWN if the progress is unknown or has a wide
+ * confidence interval on any app, or if the app list is empty.
+ */
+ pspec = g_param_spec_uint ("progress", NULL, NULL,
+ 0, GS_APP_PROGRESS_UNKNOWN, GS_APP_PROGRESS_UNKNOWN,
+ G_PARAM_READABLE);
+ g_object_class_install_property (object_class, PROP_PROGRESS, pspec);
+}
+
+static void
+gs_app_list_init (GsAppList *list)
+{
+ g_mutex_init (&list->mutex);
+ list->array = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref);
+}
+
+/**
+ * gs_app_list_new:
+ *
+ * Creates a new list.
+ *
+ * Returns: A newly allocated #GsAppList
+ *
+ * Since: 3.22
+ **/
+GsAppList *
+gs_app_list_new (void)
+{
+ GsAppList *list;
+ list = g_object_new (GS_TYPE_APP_LIST, NULL);
+ return GS_APP_LIST (list);
+}
diff --git a/lib/gs-app-list.h b/lib/gs-app-list.h
new file mode 100644
index 0000000..abb405b
--- /dev/null
+++ b/lib/gs-app-list.h
@@ -0,0 +1,46 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2012-2016 Richard Hughes <richard@hughsie.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+#pragma once
+
+#include <glib-object.h>
+
+#include "gs-app.h"
+
+G_BEGIN_DECLS
+
+#define GS_TYPE_APP_LIST (gs_app_list_get_type ())
+
+G_DECLARE_FINAL_TYPE (GsAppList, gs_app_list, GS, APP_LIST, GObject)
+
+typedef gboolean (*GsAppListSortFunc) (GsApp *app1,
+ GsApp *app2,
+ gpointer user_data);
+typedef gboolean (*GsAppListFilterFunc) (GsApp *app,
+ gpointer user_data);
+
+GsAppList *gs_app_list_new (void);
+void gs_app_list_add (GsAppList *list,
+ GsApp *app);
+void gs_app_list_add_list (GsAppList *list,
+ GsAppList *donor);
+void gs_app_list_remove (GsAppList *list,
+ GsApp *app);
+GsApp *gs_app_list_index (GsAppList *list,
+ guint idx);
+GsApp *gs_app_list_lookup (GsAppList *list,
+ const gchar *unique_id);
+guint gs_app_list_length (GsAppList *list);
+void gs_app_list_sort (GsAppList *list,
+ GsAppListSortFunc func,
+ gpointer user_data);
+void gs_app_list_filter (GsAppList *list,
+ GsAppListFilterFunc func,
+ gpointer user_data);
+
+G_END_DECLS
diff --git a/lib/gs-app-private.h b/lib/gs-app-private.h
new file mode 100644
index 0000000..1163bf3
--- /dev/null
+++ b/lib/gs-app-private.h
@@ -0,0 +1,30 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2016 Richard Hughes <richard@hughsie.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+#pragma once
+
+#include "gs-app.h"
+#include "gs-plugin-types.h"
+
+G_BEGIN_DECLS
+
+void gs_app_set_priority (GsApp *app,
+ guint priority);
+guint gs_app_get_priority (GsApp *app);
+void gs_app_set_unique_id (GsApp *app,
+ const gchar *unique_id);
+void gs_app_remove_addon (GsApp *app,
+ GsApp *addon);
+GCancellable *gs_app_get_cancellable (GsApp *app);
+GsPluginAction gs_app_get_pending_action (GsApp *app);
+void gs_app_set_pending_action (GsApp *app,
+ GsPluginAction action);
+gint gs_app_compare_priority (GsApp *app1,
+ GsApp *app2);
+
+G_END_DECLS
diff --git a/lib/gs-app.c b/lib/gs-app.c
new file mode 100644
index 0000000..4a9e6d2
--- /dev/null
+++ b/lib/gs-app.c
@@ -0,0 +1,4680 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2013-2016 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+
+ */
+
+/**
+ * SECTION:gs-app
+ * @title: GsApp
+ * @include: gnome-software.h
+ * @stability: Unstable
+ * @short_description: An application that is either installed or that can be installed
+ *
+ * This object represents a 1:1 mapping to a .desktop file. The design is such
+ * so you can't have different GsApp's for different versions or architectures
+ * of a package. This rule really only applies to GsApps of kind %AS_APP_KIND_DESKTOP
+ * and %AS_APP_KIND_GENERIC. We allow GsApps of kind %AS_APP_KIND_OS_UPDATE or
+ * %AS_APP_KIND_GENERIC, which don't correspond to desktop files, but instead
+ * represent a system update and its individual components.
+ *
+ * The #GsPluginLoader de-duplicates the GsApp instances that are produced by
+ * plugins to ensure that there is a single instance of GsApp for each id, making
+ * the id the primary key for this object. This ensures that actions triggered on
+ * a #GsApp in different parts of gnome-software can be observed by connecting to
+ * signals on the #GsApp.
+ *
+ * Information about other #GsApp objects can be stored in this object, for
+ * instance in the gs_app_add_related() method or gs_app_get_history().
+ */
+
+#include "config.h"
+
+#include <string.h>
+#include <gtk/gtk.h>
+#include <glib/gi18n.h>
+
+#include "gs-app-collation.h"
+#include "gs-app-private.h"
+#include "gs-os-release.h"
+#include "gs-plugin.h"
+#include "gs-utils.h"
+
+typedef struct
+{
+ GObject parent_instance;
+
+ GMutex mutex;
+ gchar *id;
+ gchar *unique_id;
+ gboolean unique_id_valid;
+ gchar *branch;
+ gchar *name;
+ GsAppQuality name_quality;
+ GPtrArray *icons;
+ GPtrArray *sources;
+ GPtrArray *source_ids;
+ gchar *project_group;
+ gchar *developer_name;
+ gchar *agreement;
+ gchar *version;
+ gchar *version_ui;
+ gchar *summary;
+ GsAppQuality summary_quality;
+ gchar *summary_missing;
+ gchar *description;
+ GsAppQuality description_quality;
+ GPtrArray *screenshots;
+ GPtrArray *categories;
+ GPtrArray *key_colors;
+ GHashTable *urls;
+ GHashTable *launchables;
+ gchar *license;
+ GsAppQuality license_quality;
+ gchar **menu_path;
+ gchar *origin;
+ gchar *origin_appstream;
+ gchar *origin_hostname;
+ gchar *update_version;
+ gchar *update_version_ui;
+ gchar *update_details;
+ AsUrgencyKind update_urgency;
+ GsAppPermissions update_permissions;
+ gchar *management_plugin;
+ guint match_value;
+ guint priority;
+ gint rating;
+ GArray *review_ratings;
+ GPtrArray *reviews; /* of AsReview */
+ GPtrArray *provides; /* of AsProvide */
+ guint64 size_installed;
+ guint64 size_download;
+ AsAppKind kind;
+ AsAppState state;
+ AsAppState state_recover;
+ AsAppScope scope;
+ AsBundleKind bundle_kind;
+ guint progress; /* integer 0–100 (inclusive), or %GS_APP_PROGRESS_UNKNOWN */
+ gboolean allow_cancel;
+ GHashTable *metadata;
+ GsAppList *addons;
+ GsAppList *related;
+ GsAppList *history;
+ guint64 install_date;
+ guint64 kudos;
+ gboolean to_be_installed;
+ GsAppQuirk quirk;
+ gboolean license_is_free;
+ GsApp *runtime;
+ GFile *local_file;
+ AsContentRating *content_rating;
+ GdkPixbuf *pixbuf; /* (nullable) (owned) */
+ AsScreenshot *action_screenshot; /* (nullable) (owned) */
+ GCancellable *cancellable;
+ GsPluginAction pending_action;
+ GsAppPermissions permissions;
+ gboolean is_update_downloaded;
+} GsAppPrivate;
+
+enum {
+ PROP_0,
+ PROP_ID,
+ PROP_NAME,
+ PROP_VERSION,
+ PROP_SUMMARY,
+ PROP_DESCRIPTION,
+ PROP_RATING,
+ PROP_KIND,
+ PROP_STATE,
+ PROP_PROGRESS,
+ PROP_CAN_CANCEL_INSTALLATION,
+ PROP_INSTALL_DATE,
+ PROP_QUIRK,
+ PROP_PENDING_ACTION,
+ PROP_KEY_COLORS,
+ PROP_IS_UPDATE_DOWNLOADED,
+ PROP_LAST
+};
+
+static GParamSpec *obj_props[PROP_LAST] = { NULL, };
+
+G_DEFINE_TYPE_WITH_PRIVATE (GsApp, gs_app, G_TYPE_OBJECT)
+
+static gboolean
+_g_set_str (gchar **str_ptr, const gchar *new_str)
+{
+ if (*str_ptr == new_str || g_strcmp0 (*str_ptr, new_str) == 0)
+ return FALSE;
+ g_free (*str_ptr);
+ *str_ptr = g_strdup (new_str);
+ return TRUE;
+}
+
+static gboolean
+_g_set_strv (gchar ***strv_ptr, gchar **new_strv)
+{
+ if (*strv_ptr == new_strv)
+ return FALSE;
+ g_strfreev (*strv_ptr);
+ *strv_ptr = g_strdupv (new_strv);
+ return TRUE;
+}
+
+static gboolean
+_g_set_ptr_array (GPtrArray **array_ptr, GPtrArray *new_array)
+{
+ if (*array_ptr == new_array)
+ return FALSE;
+ if (*array_ptr != NULL)
+ g_ptr_array_unref (*array_ptr);
+ *array_ptr = g_ptr_array_ref (new_array);
+ return TRUE;
+}
+
+static gboolean
+_g_set_array (GArray **array_ptr, GArray *new_array)
+{
+ if (*array_ptr == new_array)
+ return FALSE;
+ if (*array_ptr != NULL)
+ g_array_unref (*array_ptr);
+ *array_ptr = g_array_ref (new_array);
+ return TRUE;
+}
+
+static void
+gs_app_kv_lpad (GString *str, const gchar *key, const gchar *value)
+{
+ gs_utils_append_key_value (str, 20, key, value);
+}
+
+static void
+gs_app_kv_size (GString *str, const gchar *key, guint64 value)
+{
+ g_autofree gchar *tmp = NULL;
+ if (value == GS_APP_SIZE_UNKNOWABLE) {
+ gs_app_kv_lpad (str, key, "unknowable");
+ return;
+ }
+ tmp = g_format_size (value);
+ gs_app_kv_lpad (str, key, tmp);
+}
+
+G_GNUC_PRINTF (3, 4)
+static void
+gs_app_kv_printf (GString *str, const gchar *key, const gchar *fmt, ...)
+{
+ va_list args;
+ g_autofree gchar *tmp = NULL;
+ va_start (args, fmt);
+ tmp = g_strdup_vprintf (fmt, args);
+ va_end (args);
+ gs_app_kv_lpad (str, key, tmp);
+}
+
+static const gchar *
+_as_app_quirk_flag_to_string (GsAppQuirk quirk)
+{
+ switch (quirk) {
+ case GS_APP_QUIRK_PROVENANCE:
+ return "provenance";
+ case GS_APP_QUIRK_COMPULSORY:
+ return "compulsory";
+ case GS_APP_QUIRK_HAS_SOURCE:
+ return "has-source";
+ case GS_APP_QUIRK_IS_WILDCARD:
+ return "is-wildcard";
+ case GS_APP_QUIRK_NEEDS_REBOOT:
+ return "needs-reboot";
+ case GS_APP_QUIRK_NOT_REVIEWABLE:
+ return "not-reviewable";
+ case GS_APP_QUIRK_HAS_SHORTCUT:
+ return "has-shortcut";
+ case GS_APP_QUIRK_NOT_LAUNCHABLE:
+ return "not-launchable";
+ case GS_APP_QUIRK_NEEDS_USER_ACTION:
+ return "needs-user-action";
+ case GS_APP_QUIRK_IS_PROXY:
+ return "is-proxy";
+ case GS_APP_QUIRK_REMOVABLE_HARDWARE:
+ return "removable-hardware";
+ case GS_APP_QUIRK_DEVELOPER_VERIFIED:
+ return "developer-verified";
+ case GS_APP_QUIRK_PARENTAL_FILTER:
+ return "parental-filter";
+ case GS_APP_QUIRK_NEW_PERMISSIONS:
+ return "new-permissions";
+ case GS_APP_QUIRK_PARENTAL_NOT_LAUNCHABLE:
+ return "parental-not-launchable";
+ case GS_APP_QUIRK_HIDE_FROM_SEARCH:
+ return "hide-from-search";
+ case GS_APP_QUIRK_HIDE_EVERYWHERE:
+ return "hide-everywhere";
+ case GS_APP_QUIRK_DO_NOT_AUTO_UPDATE:
+ return "do-not-auto-update";
+ default:
+ return NULL;
+ }
+}
+
+/* mutex must be held */
+static const gchar *
+gs_app_get_unique_id_unlocked (GsApp *app)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+
+ /* invalid */
+ if (priv->id == NULL)
+ return NULL;
+
+ /* hmm, do what we can */
+ if (priv->unique_id == NULL || !priv->unique_id_valid) {
+ g_free (priv->unique_id);
+ priv->unique_id = as_utils_unique_id_build (priv->scope,
+ priv->bundle_kind,
+ priv->origin,
+ priv->kind,
+ priv->id,
+ priv->branch);
+ priv->unique_id_valid = TRUE;
+ }
+ return priv->unique_id;
+}
+
+/**
+ * gs_app_compare_priority:
+ * @app1: a #GsApp
+ * @app2: a #GsApp
+ *
+ * Compares two applications using their priority.
+ *
+ * Use `gs_plugin_add_rule(plugin,GS_PLUGIN_RULE_BETTER_THAN,"plugin-name")`
+ * to set the application priority values.
+ *
+ * Returns: a negative value if @app1 is less than @app2, a positive value if
+ * @app1 is greater than @app2, and zero if @app1 is equal to @app2
+ **/
+gint
+gs_app_compare_priority (GsApp *app1, GsApp *app2)
+{
+ GsAppPrivate *priv1 = gs_app_get_instance_private (app1);
+ GsAppPrivate *priv2 = gs_app_get_instance_private (app2);
+
+ /* prefer prio */
+ if (priv1->priority > priv2->priority)
+ return -1;
+ if (priv1->priority < priv2->priority)
+ return 1;
+
+ /* fall back to bundle kind */
+ if (priv1->bundle_kind < priv2->bundle_kind)
+ return -1;
+ if (priv1->bundle_kind > priv2->bundle_kind)
+ return 1;
+ return 0;
+}
+
+/**
+ * gs_app_quirk_to_string:
+ * @quirk: a #GsAppQuirk
+ *
+ * Returns the quirk bitfield as a string.
+ *
+ * Returns: (transfer full): a string
+ **/
+static gchar *
+gs_app_quirk_to_string (GsAppQuirk quirk)
+{
+ GString *str = g_string_new ("");
+ guint64 i;
+
+ /* nothing set */
+ if (quirk == GS_APP_QUIRK_NONE) {
+ g_string_append (str, "none");
+ return g_string_free (str, FALSE);
+ }
+
+ /* get flags */
+ for (i = 1; i < GS_APP_QUIRK_LAST; i *= 2) {
+ if ((quirk & i) == 0)
+ continue;
+ g_string_append_printf (str, "%s,",
+ _as_app_quirk_flag_to_string (i));
+ }
+
+ /* nothing recognised */
+ if (str->len == 0) {
+ g_string_append (str, "unknown");
+ return g_string_free (str, FALSE);
+ }
+
+ /* remove trailing comma */
+ g_string_truncate (str, str->len - 1);
+ return g_string_free (str, FALSE);
+}
+
+static gchar *
+gs_app_kudos_to_string (guint64 kudos)
+{
+ g_autoptr(GPtrArray) array = g_ptr_array_new ();
+ if ((kudos & GS_APP_KUDO_MY_LANGUAGE) > 0)
+ g_ptr_array_add (array, "my-language");
+ if ((kudos & GS_APP_KUDO_RECENT_RELEASE) > 0)
+ g_ptr_array_add (array, "recent-release");
+ if ((kudos & GS_APP_KUDO_FEATURED_RECOMMENDED) > 0)
+ g_ptr_array_add (array, "featured-recommended");
+ if ((kudos & GS_APP_KUDO_MODERN_TOOLKIT) > 0)
+ g_ptr_array_add (array, "modern-toolkit");
+ if ((kudos & GS_APP_KUDO_SEARCH_PROVIDER) > 0)
+ g_ptr_array_add (array, "search-provider");
+ if ((kudos & GS_APP_KUDO_INSTALLS_USER_DOCS) > 0)
+ g_ptr_array_add (array, "installs-user-docs");
+ if ((kudos & GS_APP_KUDO_USES_NOTIFICATIONS) > 0)
+ g_ptr_array_add (array, "uses-notifications");
+ if ((kudos & GS_APP_KUDO_HAS_KEYWORDS) > 0)
+ g_ptr_array_add (array, "has-keywords");
+ if ((kudos & GS_APP_KUDO_HAS_SCREENSHOTS) > 0)
+ g_ptr_array_add (array, "has-screenshots");
+ if ((kudos & GS_APP_KUDO_POPULAR) > 0)
+ g_ptr_array_add (array, "popular");
+ if ((kudos & GS_APP_KUDO_HIGH_CONTRAST) > 0)
+ g_ptr_array_add (array, "high-contrast");
+ if ((kudos & GS_APP_KUDO_HI_DPI_ICON) > 0)
+ g_ptr_array_add (array, "hi-dpi-icon");
+ if ((kudos & GS_APP_KUDO_SANDBOXED) > 0)
+ g_ptr_array_add (array, "sandboxed");
+ if ((kudos & GS_APP_KUDO_SANDBOXED_SECURE) > 0)
+ g_ptr_array_add (array, "sandboxed-secure");
+ g_ptr_array_add (array, NULL);
+ return g_strjoinv ("|", (gchar **) array->pdata);
+}
+
+/**
+ * gs_app_to_string:
+ * @app: a #GsApp
+ *
+ * Converts the application to a string.
+ * This is not designed to serialize the object but to produce a string suitable
+ * for debugging.
+ *
+ * Returns: A multi-line string
+ *
+ * Since: 3.22
+ **/
+gchar *
+gs_app_to_string (GsApp *app)
+{
+ GString *str = g_string_new ("GsApp:");
+ gs_app_to_string_append (app, str);
+ if (str->len > 0)
+ g_string_truncate (str, str->len - 1);
+ return g_string_free (str, FALSE);
+}
+
+/**
+ * gs_app_to_string_append:
+ * @app: a #GsApp
+ * @str: a #GString
+ *
+ * Appends the application to an existing string.
+ *
+ * Since: 3.26
+ **/
+void
+gs_app_to_string_append (GsApp *app, GString *str)
+{
+ GsAppClass *klass = GS_APP_GET_CLASS (app);
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ AsImage *im;
+ GList *keys;
+ const gchar *tmp;
+ guint i;
+
+ g_return_if_fail (GS_IS_APP (app));
+ g_return_if_fail (str != NULL);
+
+ g_string_append_printf (str, " [%p]\n", app);
+ gs_app_kv_lpad (str, "kind", as_app_kind_to_string (priv->kind));
+ gs_app_kv_lpad (str, "state", as_app_state_to_string (priv->state));
+ if (priv->quirk > 0) {
+ g_autofree gchar *qstr = gs_app_quirk_to_string (priv->quirk);
+ gs_app_kv_lpad (str, "quirk", qstr);
+ }
+ if (priv->progress == GS_APP_PROGRESS_UNKNOWN)
+ gs_app_kv_printf (str, "progress", "unknown");
+ else
+ gs_app_kv_printf (str, "progress", "%u%%", priv->progress);
+ if (priv->id != NULL)
+ gs_app_kv_lpad (str, "id", priv->id);
+ if (priv->unique_id != NULL)
+ gs_app_kv_lpad (str, "unique-id", gs_app_get_unique_id (app));
+ if (priv->scope != AS_APP_SCOPE_UNKNOWN)
+ gs_app_kv_lpad (str, "scope", as_app_scope_to_string (priv->scope));
+ if (priv->bundle_kind != AS_BUNDLE_KIND_UNKNOWN) {
+ gs_app_kv_lpad (str, "bundle-kind",
+ as_bundle_kind_to_string (priv->bundle_kind));
+ }
+ if (priv->kudos > 0) {
+ g_autofree gchar *kudo_str = NULL;
+ kudo_str = gs_app_kudos_to_string (priv->kudos);
+ gs_app_kv_lpad (str, "kudos", kudo_str);
+ }
+ gs_app_kv_printf (str, "kudo-percentage", "%u",
+ gs_app_get_kudos_percentage (app));
+ if (priv->name != NULL)
+ gs_app_kv_lpad (str, "name", priv->name);
+ if (priv->pixbuf != NULL)
+ gs_app_kv_printf (str, "pixbuf", "%p", priv->pixbuf);
+ if (priv->action_screenshot != NULL)
+ gs_app_kv_printf (str, "action-screenshot", "%p", priv->action_screenshot);
+ for (i = 0; i < priv->icons->len; i++) {
+ AsIcon *icon = g_ptr_array_index (priv->icons, i);
+ gs_app_kv_lpad (str, "icon-kind",
+ as_icon_kind_to_string (as_icon_get_kind (icon)));
+ if (as_icon_get_pixbuf (icon) != NULL) {
+ gs_app_kv_printf (str, "icon-pixbuf", "%p",
+ as_icon_get_pixbuf (icon));
+ }
+ if (as_icon_get_name (icon) != NULL)
+ gs_app_kv_lpad (str, "icon-name",
+ as_icon_get_name (icon));
+ if (as_icon_get_prefix (icon) != NULL)
+ gs_app_kv_lpad (str, "icon-prefix",
+ as_icon_get_prefix (icon));
+ if (as_icon_get_filename (icon) != NULL)
+ gs_app_kv_lpad (str, "icon-filename",
+ as_icon_get_filename (icon));
+ }
+ if (priv->match_value != 0)
+ gs_app_kv_printf (str, "match-value", "%05x", priv->match_value);
+ if (priv->priority != 0)
+ gs_app_kv_printf (str, "priority", "%u", priv->priority);
+ if (priv->version != NULL)
+ gs_app_kv_lpad (str, "version", priv->version);
+ if (priv->version_ui != NULL)
+ gs_app_kv_lpad (str, "version-ui", priv->version_ui);
+ if (priv->update_version != NULL)
+ gs_app_kv_lpad (str, "update-version", priv->update_version);
+ if (priv->update_version_ui != NULL)
+ gs_app_kv_lpad (str, "update-version-ui", priv->update_version_ui);
+ if (priv->update_details != NULL)
+ gs_app_kv_lpad (str, "update-details", priv->update_details);
+ if (priv->update_urgency != AS_URGENCY_KIND_UNKNOWN) {
+ gs_app_kv_printf (str, "update-urgency", "%u",
+ priv->update_urgency);
+ }
+ if (priv->summary != NULL)
+ gs_app_kv_lpad (str, "summary", priv->summary);
+ if (priv->description != NULL)
+ gs_app_kv_lpad (str, "description", priv->description);
+ for (i = 0; i < priv->screenshots->len; i++) {
+ AsScreenshot *ss = g_ptr_array_index (priv->screenshots, i);
+ g_autofree gchar *key = NULL;
+ tmp = as_screenshot_get_caption (ss, NULL);
+ im = as_screenshot_get_image (ss, 0, 0);
+ if (im == NULL)
+ continue;
+ key = g_strdup_printf ("screenshot-%02u", i);
+ gs_app_kv_printf (str, key, "%s [%s]",
+ as_image_get_url (im),
+ tmp != NULL ? tmp : "<none>");
+ }
+ for (i = 0; i < priv->sources->len; i++) {
+ g_autofree gchar *key = NULL;
+ tmp = g_ptr_array_index (priv->sources, i);
+ key = g_strdup_printf ("source-%02u", i);
+ gs_app_kv_lpad (str, key, tmp);
+ }
+ for (i = 0; i < priv->source_ids->len; i++) {
+ g_autofree gchar *key = NULL;
+ tmp = g_ptr_array_index (priv->source_ids, i);
+ key = g_strdup_printf ("source-id-%02u", i);
+ gs_app_kv_lpad (str, key, tmp);
+ }
+ if (priv->local_file != NULL) {
+ g_autofree gchar *fn = g_file_get_path (priv->local_file);
+ gs_app_kv_lpad (str, "local-filename", fn);
+ }
+ if (priv->content_rating != NULL) {
+ guint age = as_content_rating_get_minimum_age (priv->content_rating);
+ if (age != G_MAXUINT) {
+ g_autofree gchar *value = g_strdup_printf ("%u", age);
+ gs_app_kv_lpad (str, "content-age", value);
+ }
+ gs_app_kv_lpad (str, "content-rating",
+ as_content_rating_get_kind (priv->content_rating));
+ }
+ tmp = g_hash_table_lookup (priv->urls, as_url_kind_to_string (AS_URL_KIND_HOMEPAGE));
+ if (tmp != NULL)
+ gs_app_kv_lpad (str, "url{homepage}", tmp);
+ keys = g_hash_table_get_keys (priv->launchables);
+ for (GList *l = keys; l != NULL; l = l->next) {
+ g_autofree gchar *key = NULL;
+ key = g_strdup_printf ("launchable{%s}", (const gchar *) l->data);
+ tmp = g_hash_table_lookup (priv->launchables, l->data);
+ gs_app_kv_lpad (str, key, tmp);
+ }
+ g_list_free (keys);
+ if (priv->license != NULL) {
+ gs_app_kv_lpad (str, "license", priv->license);
+ gs_app_kv_lpad (str, "license-is-free",
+ gs_app_get_license_is_free (app) ? "yes" : "no");
+ }
+ if (priv->management_plugin != NULL)
+ gs_app_kv_lpad (str, "management-plugin", priv->management_plugin);
+ if (priv->summary_missing != NULL)
+ gs_app_kv_lpad (str, "summary-missing", priv->summary_missing);
+ if (priv->menu_path != NULL &&
+ priv->menu_path[0] != NULL &&
+ priv->menu_path[0][0] != '\0') {
+ g_autofree gchar *path = g_strjoinv (" → ", priv->menu_path);
+ gs_app_kv_lpad (str, "menu-path", path);
+ }
+ if (priv->branch != NULL)
+ gs_app_kv_lpad (str, "branch", priv->branch);
+ if (priv->origin != NULL && priv->origin[0] != '\0')
+ gs_app_kv_lpad (str, "origin", priv->origin);
+ if (priv->origin_appstream != NULL && priv->origin_appstream[0] != '\0')
+ gs_app_kv_lpad (str, "origin-appstream", priv->origin_appstream);
+ if (priv->origin_hostname != NULL && priv->origin_hostname[0] != '\0')
+ gs_app_kv_lpad (str, "origin-hostname", priv->origin_hostname);
+ if (priv->rating != -1)
+ gs_app_kv_printf (str, "rating", "%i", priv->rating);
+ if (priv->review_ratings != NULL) {
+ for (i = 0; i < priv->review_ratings->len; i++) {
+ guint32 rat = g_array_index (priv->review_ratings, guint32, i);
+ gs_app_kv_printf (str, "review-rating", "[%u:%u]",
+ i, rat);
+ }
+ }
+ if (priv->reviews != NULL)
+ gs_app_kv_printf (str, "reviews", "%u", priv->reviews->len);
+ if (priv->provides != NULL)
+ gs_app_kv_printf (str, "provides", "%u", priv->provides->len);
+ if (priv->install_date != 0) {
+ gs_app_kv_printf (str, "install-date", "%"
+ G_GUINT64_FORMAT "",
+ priv->install_date);
+ }
+ if (priv->size_installed != 0)
+ gs_app_kv_size (str, "size-installed", priv->size_installed);
+ if (priv->size_download != 0)
+ gs_app_kv_size (str, "size-download", gs_app_get_size_download (app));
+ for (i = 0; i < gs_app_list_length (priv->related); i++) {
+ GsApp *app_tmp = gs_app_list_index (priv->related, i);
+ const gchar *id = gs_app_get_unique_id (app_tmp);
+ if (id == NULL)
+ id = gs_app_get_source_default (app_tmp);
+ gs_app_kv_lpad (str, "related", id);
+ }
+ for (i = 0; i < gs_app_list_length (priv->history); i++) {
+ GsApp *app_tmp = gs_app_list_index (priv->history, i);
+ gs_app_kv_lpad (str, "history", gs_app_get_unique_id (app_tmp));
+ }
+ for (i = 0; i < priv->categories->len; i++) {
+ tmp = g_ptr_array_index (priv->categories, i);
+ gs_app_kv_lpad (str, "category", tmp);
+ }
+ for (i = 0; i < priv->key_colors->len; i++) {
+ GdkRGBA *color = g_ptr_array_index (priv->key_colors, i);
+ g_autofree gchar *key = NULL;
+ key = g_strdup_printf ("key-color-%02u", i);
+ gs_app_kv_printf (str, key, "%.0f,%.0f,%.0f",
+ color->red * 255.f,
+ color->green * 255.f,
+ color->blue * 255.f);
+ }
+ keys = g_hash_table_get_keys (priv->metadata);
+ for (GList *l = keys; l != NULL; l = l->next) {
+ GVariant *val;
+ const GVariantType *val_type;
+ g_autofree gchar *key = NULL;
+ g_autofree gchar *val_str = NULL;
+
+ key = g_strdup_printf ("{%s}", (const gchar *) l->data);
+ val = g_hash_table_lookup (priv->metadata, l->data);
+ val_type = g_variant_get_type (val);
+ if (g_variant_type_equal (val_type, G_VARIANT_TYPE_STRING)) {
+ val_str = g_variant_dup_string (val, NULL);
+ } else if (g_variant_type_equal (val_type, G_VARIANT_TYPE_BOOLEAN)) {
+ val_str = g_strdup (g_variant_get_boolean (val) ? "True" : "False");
+ } else if (g_variant_type_equal (val_type, G_VARIANT_TYPE_UINT32)) {
+ val_str = g_strdup_printf ("%" G_GUINT32_FORMAT,
+ g_variant_get_uint32 (val));
+ } else {
+ val_str = g_strdup_printf ("unknown type of %s",
+ g_variant_get_type_string (val));
+ }
+ gs_app_kv_lpad (str, key, val_str);
+ }
+ g_list_free (keys);
+
+ /* add subclassed info */
+ if (klass->to_string != NULL)
+ klass->to_string (app, str);
+
+ /* print runtime data too */
+ if (priv->runtime != NULL) {
+ g_string_append (str, "\n\tRuntime:\n\t");
+ gs_app_to_string_append (priv->runtime, str);
+ }
+ g_string_append_printf (str, "\n");
+}
+
+typedef struct {
+ GsApp *app;
+ GParamSpec *pspec;
+} AppNotifyData;
+
+static gboolean
+notify_idle_cb (gpointer data)
+{
+ AppNotifyData *notify_data = data;
+
+ g_object_notify_by_pspec (G_OBJECT (notify_data->app), notify_data->pspec);
+
+ g_object_unref (notify_data->app);
+ g_free (notify_data);
+
+ return G_SOURCE_REMOVE;
+}
+
+static void
+gs_app_queue_notify (GsApp *app, GParamSpec *pspec)
+{
+ AppNotifyData *notify_data;
+
+ notify_data = g_new (AppNotifyData, 1);
+ notify_data->app = g_object_ref (app);
+ notify_data->pspec = pspec;
+
+ g_idle_add (notify_idle_cb, notify_data);
+}
+
+/**
+ * gs_app_get_id:
+ * @app: a #GsApp
+ *
+ * Gets the application ID.
+ *
+ * Returns: The whole ID, e.g. "gimp.desktop"
+ *
+ * Since: 3.22
+ **/
+const gchar *
+gs_app_get_id (GsApp *app)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_return_val_if_fail (GS_IS_APP (app), NULL);
+ return priv->id;
+}
+
+/**
+ * gs_app_set_id:
+ * @app: a #GsApp
+ * @id: a application ID, e.g. "gimp.desktop"
+ *
+ * Sets the application ID.
+ */
+void
+gs_app_set_id (GsApp *app, const gchar *id)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_autoptr(GMutexLocker) locker = NULL;
+ g_return_if_fail (GS_IS_APP (app));
+ locker = g_mutex_locker_new (&priv->mutex);
+ if (_g_set_str (&priv->id, id))
+ priv->unique_id_valid = FALSE;
+}
+
+/**
+ * gs_app_get_scope:
+ * @app: a #GsApp
+ *
+ * Gets the scope of the application.
+ *
+ * Returns: the #AsAppScope, e.g. %AS_APP_SCOPE_USER
+ *
+ * Since: 3.22
+ **/
+AsAppScope
+gs_app_get_scope (GsApp *app)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_return_val_if_fail (GS_IS_APP (app), AS_APP_SCOPE_UNKNOWN);
+ return priv->scope;
+}
+
+/**
+ * gs_app_set_scope:
+ * @app: a #GsApp
+ * @scope: a #AsAppScope, e.g. AS_APP_SCOPE_SYSTEM
+ *
+ * This sets the scope of the application.
+ *
+ * Since: 3.22
+ **/
+void
+gs_app_set_scope (GsApp *app, AsAppScope scope)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+
+ g_return_if_fail (GS_IS_APP (app));
+
+ /* same */
+ if (scope == priv->scope)
+ return;
+
+ priv->scope = scope;
+
+ /* no longer valid */
+ priv->unique_id_valid = FALSE;
+}
+
+/**
+ * gs_app_get_bundle_kind:
+ * @app: a #GsApp
+ *
+ * Gets the bundle kind of the application.
+ *
+ * Returns: the #AsAppScope, e.g. %AS_BUNDLE_KIND_FLATPAK
+ *
+ * Since: 3.22
+ **/
+AsBundleKind
+gs_app_get_bundle_kind (GsApp *app)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_return_val_if_fail (GS_IS_APP (app), AS_BUNDLE_KIND_UNKNOWN);
+ return priv->bundle_kind;
+}
+
+/**
+ * gs_app_set_bundle_kind:
+ * @app: a #GsApp
+ * @bundle_kind: a #AsAppScope, e.g. AS_BUNDLE_KIND_FLATPAK
+ *
+ * This sets the bundle kind of the application.
+ *
+ * Since: 3.22
+ **/
+void
+gs_app_set_bundle_kind (GsApp *app, AsBundleKind bundle_kind)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+
+ g_return_if_fail (GS_IS_APP (app));
+
+ /* same */
+ if (bundle_kind == priv->bundle_kind)
+ return;
+
+ priv->bundle_kind = bundle_kind;
+
+ /* no longer valid */
+ priv->unique_id_valid = FALSE;
+}
+
+/**
+ * gs_app_get_state:
+ * @app: a #GsApp
+ *
+ * Gets the state of the application.
+ *
+ * Returns: the #AsAppState, e.g. %AS_APP_STATE_INSTALLED
+ *
+ * Since: 3.22
+ **/
+AsAppState
+gs_app_get_state (GsApp *app)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_return_val_if_fail (GS_IS_APP (app), AS_APP_STATE_UNKNOWN);
+ return priv->state;
+}
+
+/**
+ * gs_app_get_progress:
+ * @app: a #GsApp
+ *
+ * Gets the percentage completion.
+ *
+ * Returns: the percentage completion (0–100 inclusive), or %GS_APP_PROGRESS_UNKNOWN for unknown
+ *
+ * Since: 3.22
+ **/
+guint
+gs_app_get_progress (GsApp *app)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_return_val_if_fail (GS_IS_APP (app), GS_APP_PROGRESS_UNKNOWN);
+ return priv->progress;
+}
+
+/**
+ * gs_app_get_allow_cancel:
+ * @app: a #GsApp
+ *
+ * Gets whether the app's installation or upgrade can be cancelled.
+ *
+ * Returns: TRUE if cancellation is possible, FALSE otherwise.
+ *
+ * Since: 3.26
+ **/
+gboolean
+gs_app_get_allow_cancel (GsApp *app)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_return_val_if_fail (GS_IS_APP (app), FALSE);
+ return priv->allow_cancel;
+}
+
+/**
+ * gs_app_set_state_recover:
+ * @app: a #GsApp
+ *
+ * Sets the application state to the last status value that was not
+ * transient.
+ *
+ * Since: 3.22
+ **/
+void
+gs_app_set_state_recover (GsApp *app)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ if (priv->state_recover == AS_APP_STATE_UNKNOWN)
+ return;
+ if (priv->state_recover == priv->state)
+ return;
+
+ g_debug ("recovering state on %s from %s to %s",
+ priv->id,
+ as_app_state_to_string (priv->state),
+ as_app_state_to_string (priv->state_recover));
+
+ /* make sure progress gets reset when recovering state, to prevent
+ * confusing initial states when going through more than one attempt */
+ gs_app_set_progress (app, GS_APP_PROGRESS_UNKNOWN);
+
+ priv->state = priv->state_recover;
+ gs_app_queue_notify (app, obj_props[PROP_STATE]);
+}
+
+/* mutex must be held */
+static gboolean
+gs_app_set_state_internal (GsApp *app, AsAppState state)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ gboolean state_change_ok = FALSE;
+
+ /* same */
+ if (priv->state == state)
+ return FALSE;
+
+ /* check the state change is allowed */
+ switch (priv->state) {
+ case AS_APP_STATE_UNKNOWN:
+ /* unknown has to go into one of the stable states */
+ if (state == AS_APP_STATE_INSTALLED ||
+ state == AS_APP_STATE_QUEUED_FOR_INSTALL ||
+ state == AS_APP_STATE_AVAILABLE ||
+ state == AS_APP_STATE_AVAILABLE_LOCAL ||
+ state == AS_APP_STATE_UPDATABLE ||
+ state == AS_APP_STATE_UPDATABLE_LIVE ||
+ state == AS_APP_STATE_UNAVAILABLE)
+ state_change_ok = TRUE;
+ break;
+ case AS_APP_STATE_INSTALLED:
+ /* installed has to go into an action state */
+ if (state == AS_APP_STATE_UNKNOWN ||
+ state == AS_APP_STATE_REMOVING ||
+ state == AS_APP_STATE_UNAVAILABLE ||
+ state == AS_APP_STATE_UPDATABLE ||
+ state == AS_APP_STATE_UPDATABLE_LIVE)
+ state_change_ok = TRUE;
+ break;
+ case AS_APP_STATE_QUEUED_FOR_INSTALL:
+ if (state == AS_APP_STATE_UNKNOWN ||
+ state == AS_APP_STATE_INSTALLING ||
+ state == AS_APP_STATE_AVAILABLE)
+ state_change_ok = TRUE;
+ break;
+ case AS_APP_STATE_AVAILABLE:
+ /* available has to go into an action state */
+ if (state == AS_APP_STATE_UNKNOWN ||
+ state == AS_APP_STATE_QUEUED_FOR_INSTALL ||
+ state == AS_APP_STATE_INSTALLING)
+ state_change_ok = TRUE;
+ break;
+ case AS_APP_STATE_INSTALLING:
+ /* installing has to go into an stable state */
+ if (state == AS_APP_STATE_UNKNOWN ||
+ state == AS_APP_STATE_INSTALLED ||
+ state == AS_APP_STATE_UPDATABLE ||
+ state == AS_APP_STATE_UPDATABLE_LIVE ||
+ state == AS_APP_STATE_AVAILABLE)
+ state_change_ok = TRUE;
+ break;
+ case AS_APP_STATE_REMOVING:
+ /* removing has to go into an stable state */
+ if (state == AS_APP_STATE_UNKNOWN ||
+ state == AS_APP_STATE_AVAILABLE ||
+ state == AS_APP_STATE_INSTALLED)
+ state_change_ok = TRUE;
+ break;
+ case AS_APP_STATE_UPDATABLE:
+ /* updatable has to go into an action state */
+ if (state == AS_APP_STATE_UNKNOWN ||
+ state == AS_APP_STATE_REMOVING ||
+ state == AS_APP_STATE_INSTALLING)
+ state_change_ok = TRUE;
+ break;
+ case AS_APP_STATE_UPDATABLE_LIVE:
+ /* updatable-live has to go into an action state */
+ if (state == AS_APP_STATE_UNKNOWN ||
+ state == AS_APP_STATE_REMOVING ||
+ state == AS_APP_STATE_INSTALLING)
+ state_change_ok = TRUE;
+ break;
+ case AS_APP_STATE_UNAVAILABLE:
+ /* updatable has to go into an action state */
+ if (state == AS_APP_STATE_UNKNOWN ||
+ state == AS_APP_STATE_AVAILABLE)
+ state_change_ok = TRUE;
+ break;
+ case AS_APP_STATE_AVAILABLE_LOCAL:
+ /* local has to go into an action state */
+ if (state == AS_APP_STATE_UNKNOWN ||
+ state == AS_APP_STATE_INSTALLING)
+ state_change_ok = TRUE;
+ break;
+ default:
+ g_warning ("state %s unhandled",
+ as_app_state_to_string (priv->state));
+ g_assert_not_reached ();
+ }
+
+ /* this state change was unexpected */
+ if (!state_change_ok) {
+ g_warning ("State change on %s from %s to %s is not OK",
+ gs_app_get_unique_id_unlocked (app),
+ as_app_state_to_string (priv->state),
+ as_app_state_to_string (state));
+ }
+
+ priv->state = state;
+
+ if (state == AS_APP_STATE_UNKNOWN ||
+ state == AS_APP_STATE_AVAILABLE_LOCAL ||
+ state == AS_APP_STATE_AVAILABLE)
+ priv->install_date = 0;
+
+ /* save this to simplify error handling in the plugins */
+ switch (state) {
+ case AS_APP_STATE_INSTALLING:
+ case AS_APP_STATE_REMOVING:
+ case AS_APP_STATE_QUEUED_FOR_INSTALL:
+ /* transient, so ignore */
+ break;
+ default:
+ if (priv->state_recover != state)
+ priv->state_recover = state;
+ break;
+ }
+
+ return TRUE;
+}
+
+/**
+ * gs_app_set_progress:
+ * @app: a #GsApp
+ * @percentage: a percentage progress (0–100 inclusive), or %GS_APP_PROGRESS_UNKNOWN
+ *
+ * This sets the progress completion of the application. Use
+ * %GS_APP_PROGRESS_UNKNOWN if the progress is unknown or has a wide confidence
+ * interval.
+ *
+ * If called more than once with the same value then subsequent calls
+ * will be ignored.
+ *
+ * Since: 3.22
+ **/
+void
+gs_app_set_progress (GsApp *app, guint percentage)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_autoptr(GMutexLocker) locker = NULL;
+ g_return_if_fail (GS_IS_APP (app));
+ locker = g_mutex_locker_new (&priv->mutex);
+ if (priv->progress == percentage)
+ return;
+ if (percentage != GS_APP_PROGRESS_UNKNOWN && percentage > 100) {
+ g_warning ("cannot set %u%% for %s, setting instead: 100%%",
+ percentage, gs_app_get_unique_id_unlocked (app));
+ percentage = 100;
+ }
+ priv->progress = percentage;
+ gs_app_queue_notify (app, obj_props[PROP_PROGRESS]);
+}
+
+/**
+ * gs_app_set_allow_cancel:
+ * @app: a #GsApp
+ * @allow_cancel: if the installation or upgrade can be cancelled or not
+ *
+ * This sets a flag indicating whether the operation can be cancelled or not.
+ * This is used by the UI to set the "Cancel" button insensitive as
+ * appropriate.
+ *
+ * Since: 3.26
+ **/
+void
+gs_app_set_allow_cancel (GsApp *app, gboolean allow_cancel)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_autoptr(GMutexLocker) locker = NULL;
+ g_return_if_fail (GS_IS_APP (app));
+ locker = g_mutex_locker_new (&priv->mutex);
+ if (priv->allow_cancel == allow_cancel)
+ return;
+ priv->allow_cancel = allow_cancel;
+ gs_app_queue_notify (app, obj_props[PROP_CAN_CANCEL_INSTALLATION]);
+}
+
+static void
+gs_app_set_pending_action_internal (GsApp *app,
+ GsPluginAction action)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ if (priv->pending_action == action)
+ return;
+
+ priv->pending_action = action;
+ gs_app_queue_notify (app, obj_props[PROP_PENDING_ACTION]);
+}
+
+/**
+ * gs_app_set_state:
+ * @app: a #GsApp
+ * @state: a #AsAppState, e.g. AS_APP_STATE_UPDATABLE_LIVE
+ *
+ * This sets the state of the application.
+ * The following state diagram explains the typical states.
+ * All applications start in state %AS_APP_STATE_UNKNOWN,
+ * but the frontend is not supposed to see GsApps with this state.
+ *
+ * Plugins are responsible for changing the state to one of the other
+ * states before the GsApp is passed to the frontend.
+ *
+ * |[
+ * UPDATABLE --> INSTALLING --> INSTALLED
+ * UPDATABLE --> REMOVING --> AVAILABLE
+ * INSTALLED --> REMOVING --> AVAILABLE
+ * AVAILABLE --> INSTALLING --> INSTALLED
+ * AVAILABLE <--> QUEUED --> INSTALLING --> INSTALLED
+ * UNKNOWN --> UNAVAILABLE
+ * ]|
+ *
+ * Since: 3.22
+ **/
+void
+gs_app_set_state (GsApp *app, AsAppState state)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_autoptr(GMutexLocker) locker = NULL;
+ g_return_if_fail (GS_IS_APP (app));
+
+ locker = g_mutex_locker_new (&priv->mutex);
+
+ if (gs_app_set_state_internal (app, state)) {
+ /* since the state changed, and the pending-action refers to
+ * actions that usually change the state, we assign it to the
+ * appropriate action here */
+ GsPluginAction action = GS_PLUGIN_ACTION_UNKNOWN;
+ if (priv->state == AS_APP_STATE_QUEUED_FOR_INSTALL)
+ action = GS_PLUGIN_ACTION_INSTALL;
+ gs_app_set_pending_action_internal (app, action);
+
+ gs_app_queue_notify (app, obj_props[PROP_STATE]);
+ }
+}
+
+/**
+ * gs_app_get_kind:
+ * @app: a #GsApp
+ *
+ * Gets the kind of the application.
+ *
+ * Returns: the #AsAppKind, e.g. %AS_APP_KIND_UNKNOWN
+ *
+ * Since: 3.22
+ **/
+AsAppKind
+gs_app_get_kind (GsApp *app)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_return_val_if_fail (GS_IS_APP (app), AS_APP_KIND_UNKNOWN);
+ return priv->kind;
+}
+
+/**
+ * gs_app_set_kind:
+ * @app: a #GsApp
+ * @kind: a #AsAppKind, e.g. #AS_APP_KIND_DESKTOP
+ *
+ * This sets the kind of the application.
+ * The following state diagram explains the typical states.
+ * All applications start with kind %AS_APP_KIND_UNKNOWN.
+ *
+ * |[
+ * PACKAGE --> NORMAL
+ * PACKAGE --> SYSTEM
+ * NORMAL --> SYSTEM
+ * ]|
+ *
+ * Since: 3.22
+ **/
+void
+gs_app_set_kind (GsApp *app, AsAppKind kind)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ gboolean state_change_ok = FALSE;
+ g_autoptr(GMutexLocker) locker = NULL;
+
+ g_return_if_fail (GS_IS_APP (app));
+
+ locker = g_mutex_locker_new (&priv->mutex);
+
+ /* same */
+ if (priv->kind == kind)
+ return;
+
+ /* trying to change */
+ if (priv->kind != AS_APP_KIND_UNKNOWN &&
+ kind == AS_APP_KIND_UNKNOWN) {
+ g_warning ("automatically prevented from changing "
+ "kind on %s from %s to %s!",
+ gs_app_get_unique_id_unlocked (app),
+ as_app_kind_to_string (priv->kind),
+ as_app_kind_to_string (kind));
+ return;
+ }
+
+ /* check the state change is allowed */
+ switch (priv->kind) {
+ case AS_APP_KIND_UNKNOWN:
+ case AS_APP_KIND_GENERIC:
+ /* all others derive from generic */
+ state_change_ok = TRUE;
+ break;
+ case AS_APP_KIND_DESKTOP:
+ /* desktop has to be reset to override */
+ if (kind == AS_APP_KIND_UNKNOWN)
+ state_change_ok = TRUE;
+ break;
+ default:
+ /* this can never change state */
+ break;
+ }
+
+ /* this state change was unexpected */
+ if (!state_change_ok) {
+ g_warning ("Kind change on %s from %s to %s is not OK",
+ priv->id,
+ as_app_kind_to_string (priv->kind),
+ as_app_kind_to_string (kind));
+ return;
+ }
+
+ priv->kind = kind;
+ gs_app_queue_notify (app, obj_props[PROP_KIND]);
+
+ /* no longer valid */
+ priv->unique_id_valid = FALSE;
+}
+
+/**
+ * gs_app_get_unique_id:
+ * @app: a #GsApp
+ *
+ * Gets the unique application ID used for de-duplication.
+ * If nothing has been set the value from gs_app_get_id() will be used.
+ *
+ * Returns: The unique ID, e.g. `system/package/fedora/desktop/gimp.desktop/i386/master`, or %NULL
+ *
+ * Since: 3.22
+ **/
+const gchar *
+gs_app_get_unique_id (GsApp *app)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_autoptr(GMutexLocker) locker = NULL;
+ g_return_val_if_fail (GS_IS_APP (app), NULL);
+ locker = g_mutex_locker_new (&priv->mutex);
+ return gs_app_get_unique_id_unlocked (app);
+}
+
+/**
+ * gs_app_set_unique_id:
+ * @app: a #GsApp
+ * @unique_id: a unique application ID, e.g. `system/package/fedora/desktop/gimp.desktop/i386/master`
+ *
+ * Sets the unique application ID. Any #GsApp using the same ID will be
+ * deduplicated. This means that applications that can exist from more than
+ * one plugin should use this method.
+ */
+void
+gs_app_set_unique_id (GsApp *app, const gchar *unique_id)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_autoptr(GMutexLocker) locker = NULL;
+ g_return_if_fail (GS_IS_APP (app));
+
+ locker = g_mutex_locker_new (&priv->mutex);
+
+ /* check for sanity */
+ if (!as_utils_unique_id_valid (unique_id))
+ g_warning ("unique_id %s not valid", unique_id);
+
+ g_free (priv->unique_id);
+ priv->unique_id = g_strdup (unique_id);
+ priv->unique_id_valid = TRUE;
+}
+
+/**
+ * gs_app_get_name:
+ * @app: a #GsApp
+ *
+ * Gets the application name.
+ *
+ * Returns: a string, or %NULL for unset
+ *
+ * Since: 3.22
+ **/
+const gchar *
+gs_app_get_name (GsApp *app)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_return_val_if_fail (GS_IS_APP (app), NULL);
+ return priv->name;
+}
+
+/**
+ * gs_app_set_name:
+ * @app: a #GsApp
+ * @quality: A #GsAppQuality, e.g. %GS_APP_QUALITY_LOWEST
+ * @name: The short localized name, e.g. "Calculator"
+ *
+ * Sets the application name.
+ *
+ * Since: 3.22
+ **/
+void
+gs_app_set_name (GsApp *app, GsAppQuality quality, const gchar *name)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_autoptr(GMutexLocker) locker = NULL;
+ g_return_if_fail (GS_IS_APP (app));
+
+ locker = g_mutex_locker_new (&priv->mutex);
+
+ /* only save this if the data is sufficiently high quality */
+ if (quality < priv->name_quality)
+ return;
+ priv->name_quality = quality;
+ if (_g_set_str (&priv->name, name))
+ g_object_notify_by_pspec (G_OBJECT (app), obj_props[PROP_NAME]);
+}
+
+/**
+ * gs_app_get_branch:
+ * @app: a #GsApp
+ *
+ * Gets the application branch.
+ *
+ * Returns: a string, or %NULL for unset
+ *
+ * Since: 3.22
+ **/
+const gchar *
+gs_app_get_branch (GsApp *app)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_return_val_if_fail (GS_IS_APP (app), NULL);
+ return priv->branch;
+}
+
+/**
+ * gs_app_set_branch:
+ * @app: a #GsApp
+ * @branch: The branch, e.g. "master"
+ *
+ * Sets the application branch.
+ *
+ * Since: 3.22
+ **/
+void
+gs_app_set_branch (GsApp *app, const gchar *branch)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_autoptr(GMutexLocker) locker = NULL;
+ g_return_if_fail (GS_IS_APP (app));
+ locker = g_mutex_locker_new (&priv->mutex);
+ if (_g_set_str (&priv->branch, branch))
+ priv->unique_id_valid = FALSE;
+}
+
+/**
+ * gs_app_get_source_default:
+ * @app: a #GsApp
+ *
+ * Gets the default source.
+ *
+ * Returns: a string, or %NULL
+ *
+ * Since: 3.22
+ **/
+const gchar *
+gs_app_get_source_default (GsApp *app)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_return_val_if_fail (GS_IS_APP (app), NULL);
+ if (priv->sources->len == 0)
+ return NULL;
+ return g_ptr_array_index (priv->sources, 0);
+}
+
+/**
+ * gs_app_add_source:
+ * @app: a #GsApp
+ * @source: a source name
+ *
+ * Adds a source name for the application.
+ *
+ * Since: 3.22
+ **/
+void
+gs_app_add_source (GsApp *app, const gchar *source)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ const gchar *tmp;
+ guint i;
+ g_autoptr(GMutexLocker) locker = NULL;
+
+ g_return_if_fail (GS_IS_APP (app));
+ g_return_if_fail (source != NULL);
+
+ locker = g_mutex_locker_new (&priv->mutex);
+
+ /* check source doesn't already exist */
+ for (i = 0; i < priv->sources->len; i++) {
+ tmp = g_ptr_array_index (priv->sources, i);
+ if (g_strcmp0 (tmp, source) == 0)
+ return;
+ }
+ g_ptr_array_add (priv->sources, g_strdup (source));
+}
+
+/**
+ * gs_app_get_sources:
+ * @app: a #GsApp
+ *
+ * Gets the list of sources for the application.
+ *
+ * Returns: (element-type utf8) (transfer none): a list
+ *
+ * Since: 3.22
+ **/
+GPtrArray *
+gs_app_get_sources (GsApp *app)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_return_val_if_fail (GS_IS_APP (app), NULL);
+ return priv->sources;
+}
+
+/**
+ * gs_app_set_sources:
+ * @app: a #GsApp
+ * @sources: The non-localized short names, e.g. ["gnome-calculator"]
+ *
+ * This name is used for the update page if the application is collected into
+ * the 'OS Updates' group.
+ * It is typically the package names, although this should not be relied upon.
+ *
+ * Since: 3.22
+ **/
+void
+gs_app_set_sources (GsApp *app, GPtrArray *sources)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_autoptr(GMutexLocker) locker = NULL;
+ g_return_if_fail (GS_IS_APP (app));
+ locker = g_mutex_locker_new (&priv->mutex);
+ _g_set_ptr_array (&priv->sources, sources);
+}
+
+/**
+ * gs_app_get_source_id_default:
+ * @app: a #GsApp
+ *
+ * Gets the default source ID.
+ *
+ * Returns: a string, or %NULL for unset
+ *
+ * Since: 3.22
+ **/
+const gchar *
+gs_app_get_source_id_default (GsApp *app)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_return_val_if_fail (GS_IS_APP (app), NULL);
+ if (priv->source_ids->len == 0)
+ return NULL;
+ return g_ptr_array_index (priv->source_ids, 0);
+}
+
+/**
+ * gs_app_get_source_ids:
+ * @app: a #GsApp
+ *
+ * Gets the list of source IDs.
+ *
+ * Returns: (element-type utf8) (transfer none): a list
+ *
+ * Since: 3.22
+ **/
+GPtrArray *
+gs_app_get_source_ids (GsApp *app)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_return_val_if_fail (GS_IS_APP (app), NULL);
+ return priv->source_ids;
+}
+
+/**
+ * gs_app_clear_source_ids:
+ * @app: a #GsApp
+ *
+ * Clear the list of source IDs.
+ *
+ * Since: 3.22
+ **/
+void
+gs_app_clear_source_ids (GsApp *app)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_autoptr(GMutexLocker) locker = NULL;
+ g_return_if_fail (GS_IS_APP (app));
+ locker = g_mutex_locker_new (&priv->mutex);
+ g_ptr_array_set_size (priv->source_ids, 0);
+}
+
+/**
+ * gs_app_set_source_ids:
+ * @app: a #GsApp
+ * @source_ids: The source-id, e.g. ["gnome-calculator;0.134;fedora"]
+ * or ["/home/hughsie/.local/share/applications/0ad.desktop"]
+ *
+ * This ID is used internally to the controlling plugin.
+ *
+ * Since: 3.22
+ **/
+void
+gs_app_set_source_ids (GsApp *app, GPtrArray *source_ids)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_autoptr(GMutexLocker) locker = NULL;
+ g_return_if_fail (GS_IS_APP (app));
+ locker = g_mutex_locker_new (&priv->mutex);
+ _g_set_ptr_array (&priv->source_ids, source_ids);
+}
+
+/**
+ * gs_app_add_source_id:
+ * @app: a #GsApp
+ * @source_id: a source ID, e.g. "gnome-calculator;0.134;fedora"
+ *
+ * Adds a source ID to the application.
+ *
+ * Since: 3.22
+ **/
+void
+gs_app_add_source_id (GsApp *app, const gchar *source_id)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ const gchar *tmp;
+ guint i;
+
+ g_return_if_fail (GS_IS_APP (app));
+ g_return_if_fail (source_id != NULL);
+
+ /* only add if not already present */
+ for (i = 0; i < priv->source_ids->len; i++) {
+ tmp = g_ptr_array_index (priv->source_ids, i);
+ if (g_strcmp0 (tmp, source_id) == 0)
+ return;
+ }
+ g_ptr_array_add (priv->source_ids, g_strdup (source_id));
+}
+
+/**
+ * gs_app_get_project_group:
+ * @app: a #GsApp
+ *
+ * Gets a project group for the application.
+ * Applications belonging to other project groups may not be shown in
+ * this software center.
+ *
+ * Returns: a string, or %NULL for unset
+ *
+ * Since: 3.22
+ **/
+const gchar *
+gs_app_get_project_group (GsApp *app)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_return_val_if_fail (GS_IS_APP (app), NULL);
+ return priv->project_group;
+}
+
+/**
+ * gs_app_get_developer_name:
+ * @app: a #GsApp
+ *
+ * Gets the developer name for the application.
+ *
+ * Returns: a string, or %NULL for unset
+ *
+ * Since: 3.22
+ **/
+const gchar *
+gs_app_get_developer_name (GsApp *app)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_return_val_if_fail (GS_IS_APP (app), NULL);
+ return priv->developer_name;
+}
+
+/**
+ * gs_app_set_project_group:
+ * @app: a #GsApp
+ * @project_group: The non-localized project group, e.g. "GNOME" or "KDE"
+ *
+ * Sets a project group for the application.
+ *
+ * Since: 3.22
+ **/
+void
+gs_app_set_project_group (GsApp *app, const gchar *project_group)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_autoptr(GMutexLocker) locker = NULL;
+ g_return_if_fail (GS_IS_APP (app));
+ locker = g_mutex_locker_new (&priv->mutex);
+ _g_set_str (&priv->project_group, project_group);
+}
+
+/**
+ * gs_app_set_developer_name:
+ * @app: a #GsApp
+ * @developer_name: The developer name, e.g. "Richard Hughes"
+ *
+ * Sets a developer name for the application.
+ *
+ * Since: 3.22
+ **/
+void
+gs_app_set_developer_name (GsApp *app, const gchar *developer_name)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_autoptr(GMutexLocker) locker = NULL;
+ g_return_if_fail (GS_IS_APP (app));
+ locker = g_mutex_locker_new (&priv->mutex);
+ _g_set_str (&priv->developer_name, developer_name);
+}
+
+/**
+ * gs_app_get_pixbuf:
+ * @app: a #GsApp
+ *
+ * Gets a pixbuf to represent the application.
+ *
+ * Returns: (transfer none) (nullable): a #GdkPixbuf, or %NULL
+ *
+ * Since: 3.22
+ **/
+GdkPixbuf *
+gs_app_get_pixbuf (GsApp *app)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_return_val_if_fail (GS_IS_APP (app), NULL);
+ return priv->pixbuf;
+}
+
+/**
+ * gs_app_get_action_screenshot:
+ * @app: a #GsApp
+ *
+ * Gets a screenshot for the pending user action.
+ *
+ * Returns: (transfer none) (nullable): a #AsScreenshot, or %NULL
+ *
+ * Since: 3.38
+ **/
+AsScreenshot *
+gs_app_get_action_screenshot (GsApp *app)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_return_val_if_fail (GS_IS_APP (app), NULL);
+ return priv->action_screenshot;
+}
+
+/**
+ * gs_app_get_icons:
+ * @app: a #GsApp
+ *
+ * Gets the icons for the application.
+ *
+ * Returns: (transfer none) (element-type AsIcon): an array of icons
+ *
+ * Since: 3.22
+ **/
+GPtrArray *
+gs_app_get_icons (GsApp *app)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_return_val_if_fail (GS_IS_APP (app), NULL);
+ return priv->icons;
+}
+
+/**
+ * gs_app_add_icon:
+ * @app: a #GsApp
+ * @icon: a #AsIcon, or %NULL to remove all icons
+ *
+ * Adds an icon to use for the application.
+ * If the first icon added cannot be loaded then the next one is tried.
+ *
+ * Since: 3.22
+ **/
+void
+gs_app_add_icon (GsApp *app, AsIcon *icon)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_autoptr(GMutexLocker) locker = NULL;
+ g_return_if_fail (GS_IS_APP (app));
+ locker = g_mutex_locker_new (&priv->mutex);
+ if (icon == NULL) {
+ g_ptr_array_set_size (priv->icons, 0);
+ return;
+ }
+ g_ptr_array_add (priv->icons, g_object_ref (icon));
+}
+
+/**
+ * gs_app_get_use_drop_shadow:
+ * @app: a #GsApp
+ *
+ * Uses a heuristic to work out if the application pixbuf should have a drop
+ * shadow applied.
+ *
+ * Returns: %TRUE if a drop shadow should be applied
+ *
+ * Since: 3.34
+ **/
+gboolean
+gs_app_get_use_drop_shadow (GsApp *app)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ AsIcon *ic;
+
+ /* guess */
+ if (priv->icons->len == 0)
+ return TRUE;
+
+ /* stock, and symbolic */
+ ic = g_ptr_array_index (priv->icons, 0);
+ return as_icon_get_kind (ic) != AS_ICON_KIND_STOCK ||
+ !g_str_has_suffix (as_icon_get_name (ic), "-symbolic");
+}
+
+/**
+ * gs_app_get_agreement:
+ * @app: a #GsApp
+ *
+ * Gets the agreement text for the application.
+ *
+ * Returns: a string in AppStream description format, or %NULL for unset
+ *
+ * Since: 3.28
+ **/
+const gchar *
+gs_app_get_agreement (GsApp *app)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_return_val_if_fail (GS_IS_APP (app), NULL);
+ return priv->agreement;
+}
+
+/**
+ * gs_app_set_agreement:
+ * @app: a #GsApp
+ * @agreement: The agreement text, e.g. "<p>Foobar</p>"
+ *
+ * Sets the application end-user agreement (e.g. a EULA) in AppStream
+ * description format.
+ *
+ * Since: 3.28
+ **/
+void
+gs_app_set_agreement (GsApp *app, const gchar *agreement)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_autoptr(GMutexLocker) locker = NULL;
+ g_return_if_fail (GS_IS_APP (app));
+ locker = g_mutex_locker_new (&priv->mutex);
+ _g_set_str (&priv->agreement, agreement);
+}
+
+/**
+ * gs_app_get_local_file:
+ * @app: a #GsApp
+ *
+ * Gets the file that backs this application, for instance this might
+ * be a local file in ~/Downloads that we are installing.
+ *
+ * Returns: (transfer none): a #GFile, or %NULL
+ *
+ * Since: 3.22
+ **/
+GFile *
+gs_app_get_local_file (GsApp *app)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_return_val_if_fail (GS_IS_APP (app), NULL);
+ return priv->local_file;
+}
+
+/**
+ * gs_app_set_local_file:
+ * @app: a #GsApp
+ * @local_file: a #GFile, or %NULL
+ *
+ * Sets the file that backs this application, for instance this might
+ * be a local file in ~/Downloads that we are installing.
+ *
+ * Since: 3.22
+ **/
+void
+gs_app_set_local_file (GsApp *app, GFile *local_file)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_autoptr(GMutexLocker) locker = NULL;
+ g_return_if_fail (GS_IS_APP (app));
+ locker = g_mutex_locker_new (&priv->mutex);
+ g_set_object (&priv->local_file, local_file);
+}
+
+/**
+ * gs_app_get_content_rating:
+ * @app: a #GsApp
+ *
+ * Gets the content rating for this application.
+ *
+ * Returns: (transfer none): a #AsContentRating, or %NULL
+ *
+ * Since: 3.24
+ **/
+AsContentRating *
+gs_app_get_content_rating (GsApp *app)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_return_val_if_fail (GS_IS_APP (app), NULL);
+ return priv->content_rating;
+}
+
+/**
+ * gs_app_set_content_rating:
+ * @app: a #GsApp
+ * @content_rating: a #AsContentRating, or %NULL
+ *
+ * Sets the content rating for this application.
+ *
+ * Since: 3.24
+ **/
+void
+gs_app_set_content_rating (GsApp *app, AsContentRating *content_rating)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_autoptr(GMutexLocker) locker = NULL;
+ g_return_if_fail (GS_IS_APP (app));
+ locker = g_mutex_locker_new (&priv->mutex);
+ g_set_object (&priv->content_rating, content_rating);
+}
+
+/**
+ * gs_app_get_runtime:
+ * @app: a #GsApp
+ *
+ * Gets the runtime for the installed application.
+ *
+ * Returns: (transfer none): a #GsApp, or %NULL for unset
+ *
+ * Since: 3.22
+ **/
+GsApp *
+gs_app_get_runtime (GsApp *app)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_return_val_if_fail (GS_IS_APP (app), NULL);
+ return priv->runtime;
+}
+
+/**
+ * gs_app_set_runtime:
+ * @app: a #GsApp
+ * @runtime: a #GsApp
+ *
+ * Sets the runtime that the installed application requires.
+ *
+ * Since: 3.22
+ **/
+void
+gs_app_set_runtime (GsApp *app, GsApp *runtime)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_autoptr(GMutexLocker) locker = NULL;
+ g_return_if_fail (GS_IS_APP (app));
+ g_return_if_fail (app != runtime);
+ locker = g_mutex_locker_new (&priv->mutex);
+ g_set_object (&priv->runtime, runtime);
+}
+
+/**
+ * gs_app_set_pixbuf:
+ * @app: a #GsApp
+ * @pixbuf: (transfer none) (nullable): a #GdkPixbuf, or %NULL
+ *
+ * Sets a pixbuf used to represent the application.
+ *
+ * Since: 3.22
+ **/
+void
+gs_app_set_pixbuf (GsApp *app, GdkPixbuf *pixbuf)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_autoptr(GMutexLocker) locker = NULL;
+ g_return_if_fail (GS_IS_APP (app));
+ locker = g_mutex_locker_new (&priv->mutex);
+ g_set_object (&priv->pixbuf, pixbuf);
+}
+
+/**
+ * gs_app_set_action_screenshot:
+ * @app: a #GsApp
+ * @action_screenshot: (transfer none) (nullable): a #AsScreenshot, or %NULL
+ *
+ * Sets a screenshot used to represent the action.
+ *
+ * Since: 3.38
+ **/
+void
+gs_app_set_action_screenshot (GsApp *app, AsScreenshot *action_screenshot)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_autoptr(GMutexLocker) locker = NULL;
+ g_return_if_fail (GS_IS_APP (app));
+ locker = g_mutex_locker_new (&priv->mutex);
+ g_set_object (&priv->action_screenshot, action_screenshot);
+}
+
+typedef enum {
+ GS_APP_VERSION_FIXUP_RELEASE = 1,
+ GS_APP_VERSION_FIXUP_DISTRO_SUFFIX = 2,
+ GS_APP_VERSION_FIXUP_GIT_SUFFIX = 4,
+ GS_APP_VERSION_FIXUP_LAST,
+} GsAppVersionFixup;
+
+/**
+ * gs_app_get_ui_version:
+ *
+ * convert 1:1.6.2-7.fc17 into "Version 1.6.2"
+ **/
+static gchar *
+gs_app_get_ui_version (const gchar *version, guint64 flags)
+{
+ guint i;
+ gchar *new;
+ gchar *f;
+
+ /* nothing set */
+ if (version == NULL)
+ return NULL;
+
+ /* first remove any epoch */
+ for (i = 0; version[i] != '\0'; i++) {
+ if (version[i] == ':') {
+ version = &version[i+1];
+ break;
+ }
+ if (!g_ascii_isdigit (version[i]))
+ break;
+ }
+
+ /* then remove any distro suffix */
+ new = g_strdup (version);
+ if ((flags & GS_APP_VERSION_FIXUP_DISTRO_SUFFIX) > 0) {
+ f = g_strstr_len (new, -1, ".fc");
+ if (f != NULL)
+ *f= '\0';
+ f = g_strstr_len (new, -1, ".el");
+ if (f != NULL)
+ *f= '\0';
+ }
+
+ /* then remove any release */
+ if ((flags & GS_APP_VERSION_FIXUP_RELEASE) > 0) {
+ f = g_strrstr_len (new, -1, "-");
+ if (f != NULL)
+ *f= '\0';
+ }
+
+ /* then remove any git suffix */
+ if ((flags & GS_APP_VERSION_FIXUP_GIT_SUFFIX) > 0) {
+ f = g_strrstr_len (new, -1, ".2012");
+ if (f != NULL)
+ *f= '\0';
+ f = g_strrstr_len (new, -1, ".2013");
+ if (f != NULL)
+ *f= '\0';
+ }
+
+ return new;
+}
+
+static void
+gs_app_ui_versions_invalidate (GsApp *app)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_free (priv->version_ui);
+ g_free (priv->update_version_ui);
+ priv->version_ui = NULL;
+ priv->update_version_ui = NULL;
+}
+
+static void
+gs_app_ui_versions_populate (GsApp *app)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ guint i;
+ guint64 flags[] = { GS_APP_VERSION_FIXUP_RELEASE |
+ GS_APP_VERSION_FIXUP_DISTRO_SUFFIX |
+ GS_APP_VERSION_FIXUP_GIT_SUFFIX,
+ GS_APP_VERSION_FIXUP_DISTRO_SUFFIX |
+ GS_APP_VERSION_FIXUP_GIT_SUFFIX,
+ GS_APP_VERSION_FIXUP_DISTRO_SUFFIX,
+ 0 };
+
+ /* try each set of bitfields in order */
+ for (i = 0; flags[i] != 0; i++) {
+ priv->version_ui = gs_app_get_ui_version (priv->version, flags[i]);
+ priv->update_version_ui = gs_app_get_ui_version (priv->update_version, flags[i]);
+ if (g_strcmp0 (priv->version_ui, priv->update_version_ui) != 0) {
+ gs_app_queue_notify (app, obj_props[PROP_VERSION]);
+ return;
+ }
+ gs_app_ui_versions_invalidate (app);
+ }
+
+ /* we tried, but failed */
+ priv->version_ui = g_strdup (priv->version);
+ priv->update_version_ui = g_strdup (priv->update_version);
+}
+
+/**
+ * gs_app_get_version:
+ * @app: a #GsApp
+ *
+ * Gets the exact version for the application.
+ *
+ * Returns: a string, or %NULL for unset
+ *
+ * Since: 3.22
+ **/
+const gchar *
+gs_app_get_version (GsApp *app)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_return_val_if_fail (GS_IS_APP (app), NULL);
+ return priv->version;
+}
+
+/**
+ * gs_app_get_version_ui:
+ * @app: a #GsApp
+ *
+ * Gets a version string that can be displayed in a UI.
+ *
+ * Returns: a string, or %NULL for unset
+ *
+ * Since: 3.22
+ **/
+const gchar *
+gs_app_get_version_ui (GsApp *app)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_return_val_if_fail (GS_IS_APP (app), NULL);
+
+ /* work out the two version numbers */
+ if (priv->version != NULL &&
+ priv->version_ui == NULL) {
+ gs_app_ui_versions_populate (app);
+ }
+
+ return priv->version_ui;
+}
+
+/**
+ * gs_app_set_version:
+ * @app: a #GsApp
+ * @version: The version, e.g. "2:1.2.3.fc19"
+ *
+ * This saves the version after stripping out any non-friendly parts, such as
+ * distro tags, git revisions and that kind of thing.
+ *
+ * Since: 3.22
+ **/
+void
+gs_app_set_version (GsApp *app, const gchar *version)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_autoptr(GMutexLocker) locker = NULL;
+ g_return_if_fail (GS_IS_APP (app));
+
+ locker = g_mutex_locker_new (&priv->mutex);
+
+ if (_g_set_str (&priv->version, version)) {
+ gs_app_ui_versions_invalidate (app);
+ gs_app_queue_notify (app, obj_props[PROP_VERSION]);
+ }
+}
+
+/**
+ * gs_app_get_summary:
+ * @app: a #GsApp
+ *
+ * Gets the single-line description of the application.
+ *
+ * Returns: a string, or %NULL for unset
+ *
+ * Since: 3.22
+ **/
+const gchar *
+gs_app_get_summary (GsApp *app)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_return_val_if_fail (GS_IS_APP (app), NULL);
+ return priv->summary;
+}
+
+/**
+ * gs_app_set_summary:
+ * @app: a #GsApp
+ * @quality: a #GsAppQuality, e.g. %GS_APP_QUALITY_LOWEST
+ * @summary: a string, e.g. "A graphical calculator for GNOME"
+ *
+ * The medium length one-line localized name.
+ *
+ * Since: 3.22
+ **/
+void
+gs_app_set_summary (GsApp *app, GsAppQuality quality, const gchar *summary)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_autoptr(GMutexLocker) locker = NULL;
+ g_return_if_fail (GS_IS_APP (app));
+
+ locker = g_mutex_locker_new (&priv->mutex);
+
+ /* only save this if the data is sufficiently high quality */
+ if (quality < priv->summary_quality)
+ return;
+ priv->summary_quality = quality;
+ if (_g_set_str (&priv->summary, summary))
+ g_object_notify_by_pspec (G_OBJECT (app), obj_props[PROP_SUMMARY]);
+}
+
+/**
+ * gs_app_get_description:
+ * @app: a #GsApp
+ *
+ * Gets the long multi-line description of the application.
+ *
+ * Returns: a string, or %NULL for unset
+ *
+ * Since: 3.22
+ **/
+const gchar *
+gs_app_get_description (GsApp *app)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_return_val_if_fail (GS_IS_APP (app), NULL);
+ return priv->description;
+}
+
+/**
+ * gs_app_set_description:
+ * @app: a #GsApp
+ * @quality: a #GsAppQuality, e.g. %GS_APP_QUALITY_LOWEST
+ * @description: a string, e.g. "GNOME Calculator is a graphical calculator for GNOME..."
+ *
+ * Sets the long multi-line description of the application.
+ *
+ * Since: 3.22
+ **/
+void
+gs_app_set_description (GsApp *app, GsAppQuality quality, const gchar *description)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_autoptr(GMutexLocker) locker = NULL;
+ g_return_if_fail (GS_IS_APP (app));
+
+ locker = g_mutex_locker_new (&priv->mutex);
+
+ /* only save this if the data is sufficiently high quality */
+ if (quality < priv->description_quality)
+ return;
+ priv->description_quality = quality;
+ _g_set_str (&priv->description, description);
+}
+
+/**
+ * gs_app_get_url:
+ * @app: a #GsApp
+ * @kind: a #AsUrlKind, e.g. %AS_URL_KIND_HOMEPAGE
+ *
+ * Gets a web address of a specific type.
+ *
+ * Returns: a string, or %NULL for unset
+ *
+ * Since: 3.22
+ **/
+const gchar *
+gs_app_get_url (GsApp *app, AsUrlKind kind)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_autoptr(GMutexLocker) locker = NULL;
+ g_return_val_if_fail (GS_IS_APP (app), NULL);
+ locker = g_mutex_locker_new (&priv->mutex);
+ return g_hash_table_lookup (priv->urls, as_url_kind_to_string (kind));
+}
+
+/**
+ * gs_app_set_url:
+ * @app: a #GsApp
+ * @kind: a #AsUrlKind, e.g. %AS_URL_KIND_HOMEPAGE
+ * @url: a web URL, e.g. "http://www.hughsie.com/"
+ *
+ * Sets a web address of a specific type.
+ *
+ * Since: 3.22
+ **/
+void
+gs_app_set_url (GsApp *app, AsUrlKind kind, const gchar *url)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_autoptr(GMutexLocker) locker = NULL;
+ g_return_if_fail (GS_IS_APP (app));
+ locker = g_mutex_locker_new (&priv->mutex);
+ g_hash_table_insert (priv->urls,
+ g_strdup (as_url_kind_to_string (kind)),
+ g_strdup (url));
+}
+
+/**
+ * gs_app_get_launchable:
+ * @app: a #GsApp
+ * @kind: a #AsLaunchableKind, e.g. %AS_LAUNCHABLE_KIND_DESKTOP_ID
+ *
+ * Gets a launchable of a specific type.
+ *
+ * Returns: a string, or %NULL for unset
+ *
+ * Since: 3.28
+ **/
+const gchar *
+gs_app_get_launchable (GsApp *app, AsLaunchableKind kind)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_return_val_if_fail (GS_IS_APP (app), NULL);
+ return g_hash_table_lookup (priv->launchables,
+ as_launchable_kind_to_string (kind));
+}
+
+/**
+ * gs_app_set_launchable:
+ * @app: a #GsApp
+ * @kind: a #AsLaunchableKind, e.g. %AS_LAUNCHABLE_KIND_DESKTOP_ID
+ * @launchable: a way to launch, e.g. "org.gnome.Sysprof2.desktop"
+ *
+ * Sets a launchable of a specific type.
+ *
+ * Since: 3.28
+ **/
+void
+gs_app_set_launchable (GsApp *app, AsLaunchableKind kind, const gchar *launchable)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_autoptr(GMutexLocker) locker = NULL;
+ g_return_if_fail (GS_IS_APP (app));
+ locker = g_mutex_locker_new (&priv->mutex);
+ g_hash_table_insert (priv->launchables,
+ g_strdup (as_launchable_kind_to_string (kind)),
+ g_strdup (launchable));
+}
+
+/**
+ * gs_app_get_license:
+ * @app: a #GsApp
+ *
+ * Gets the project license of the application.
+ *
+ * Returns: a string, or %NULL for unset
+ *
+ * Since: 3.22
+ **/
+const gchar *
+gs_app_get_license (GsApp *app)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_return_val_if_fail (GS_IS_APP (app), NULL);
+ return priv->license;
+}
+
+/**
+ * gs_app_get_license_is_free:
+ * @app: a #GsApp
+ *
+ * Returns if the application is free software.
+ *
+ * Returns: %TRUE if the application is free software
+ *
+ * Since: 3.22
+ **/
+gboolean
+gs_app_get_license_is_free (GsApp *app)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_return_val_if_fail (GS_IS_APP (app), FALSE);
+ return priv->license_is_free;
+}
+
+static gboolean
+gs_app_get_license_token_is_nonfree (const gchar *token)
+{
+ /* grammar */
+ if (g_strcmp0 (token, "(") == 0)
+ return FALSE;
+ if (g_strcmp0 (token, ")") == 0)
+ return FALSE;
+
+ /* a token, but still nonfree */
+ if (g_str_has_prefix (token, "@LicenseRef-proprietary"))
+ return TRUE;
+
+ /* if it has a prefix, assume it is free */
+ return token[0] != '@';
+}
+
+/**
+ * gs_app_set_license:
+ * @app: a #GsApp
+ * @quality: a #GsAppQuality, e.g. %GS_APP_QUALITY_NORMAL
+ * @license: a SPDX license string, e.g. "GPL-3.0 AND LGPL-2.0+"
+ *
+ * Sets the project licenses used in the application.
+ *
+ * Since: 3.22
+ **/
+void
+gs_app_set_license (GsApp *app, GsAppQuality quality, const gchar *license)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_autoptr(GMutexLocker) locker = NULL;
+ guint i;
+ g_auto(GStrv) tokens = NULL;
+
+ g_return_if_fail (GS_IS_APP (app));
+
+ locker = g_mutex_locker_new (&priv->mutex);
+
+ /* only save this if the data is sufficiently high quality */
+ if (quality <= priv->license_quality)
+ return;
+ if (license == NULL)
+ return;
+ priv->license_quality = quality;
+
+ /* assume free software until we find a nonfree SPDX token */
+ priv->license_is_free = TRUE;
+ tokens = as_utils_spdx_license_tokenize (license);
+ for (i = 0; tokens[i] != NULL; i++) {
+ if (g_strcmp0 (tokens[i], "&") == 0 ||
+ g_strcmp0 (tokens[i], "+") == 0 ||
+ g_strcmp0 (tokens[i], "|") == 0)
+ continue;
+ if (gs_app_get_license_token_is_nonfree (tokens[i])) {
+ priv->license_is_free = FALSE;
+ break;
+ }
+ }
+ _g_set_str (&priv->license, license);
+}
+
+/**
+ * gs_app_get_summary_missing:
+ * @app: a #GsApp
+ *
+ * Gets the one-line summary to use when this application is missing.
+ *
+ * Returns: a string, or %NULL for unset
+ *
+ * Since: 3.22
+ **/
+const gchar *
+gs_app_get_summary_missing (GsApp *app)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_return_val_if_fail (GS_IS_APP (app), NULL);
+ return priv->summary_missing;
+}
+
+/**
+ * gs_app_set_summary_missing:
+ * @app: a #GsApp
+ * @summary_missing: a string, or %NULL
+ *
+ * Sets the one-line summary to use when this application is missing.
+ *
+ * Since: 3.22
+ **/
+void
+gs_app_set_summary_missing (GsApp *app, const gchar *summary_missing)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_autoptr(GMutexLocker) locker = NULL;
+ g_return_if_fail (GS_IS_APP (app));
+ locker = g_mutex_locker_new (&priv->mutex);
+ _g_set_str (&priv->summary_missing, summary_missing);
+}
+
+/**
+ * gs_app_get_menu_path:
+ * @app: a #GsApp
+ *
+ * Returns the menu path which is an array of path elements.
+ * The resulting array is an internal structure and must not be
+ * modified or freed.
+ *
+ * Returns: a %NULL-terminated array of strings
+ *
+ * Since: 3.22
+ **/
+gchar **
+gs_app_get_menu_path (GsApp *app)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_return_val_if_fail (GS_IS_APP (app), NULL);
+ return priv->menu_path;
+}
+
+/**
+ * gs_app_set_menu_path:
+ * @app: a #GsApp
+ * @menu_path: a %NULL-terminated array of strings
+ *
+ * Sets the new menu path. The menu path is an array of path elements.
+ * This function creates a deep copy of the path.
+ *
+ * Since: 3.22
+ **/
+void
+gs_app_set_menu_path (GsApp *app, gchar **menu_path)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_autoptr(GMutexLocker) locker = NULL;
+ g_return_if_fail (GS_IS_APP (app));
+ locker = g_mutex_locker_new (&priv->mutex);
+ _g_set_strv (&priv->menu_path, menu_path);
+}
+
+/**
+ * gs_app_get_origin:
+ * @app: a #GsApp
+ *
+ * Gets the origin for the application, e.g. "fedora".
+ *
+ * Returns: a string, or %NULL for unset
+ *
+ * Since: 3.22
+ **/
+const gchar *
+gs_app_get_origin (GsApp *app)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_return_val_if_fail (GS_IS_APP (app), NULL);
+ return priv->origin;
+}
+
+/**
+ * gs_app_set_origin:
+ * @app: a #GsApp
+ * @origin: a string, or %NULL
+ *
+ * The origin is the original source of the application e.g. "fedora-updates"
+ *
+ * Since: 3.22
+ **/
+void
+gs_app_set_origin (GsApp *app, const gchar *origin)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_autoptr(GMutexLocker) locker = NULL;
+ g_return_if_fail (GS_IS_APP (app));
+
+ locker = g_mutex_locker_new (&priv->mutex);
+
+ /* same */
+ if (g_strcmp0 (origin, priv->origin) == 0)
+ return;
+
+ /* trying to change */
+ if (priv->origin != NULL && origin != NULL) {
+ g_warning ("automatically prevented from changing "
+ "origin on %s from %s to %s!",
+ gs_app_get_unique_id_unlocked (app),
+ priv->origin, origin);
+ return;
+ }
+
+ g_free (priv->origin);
+ priv->origin = g_strdup (origin);
+
+ /* no longer valid */
+ priv->unique_id_valid = FALSE;
+}
+
+/**
+ * gs_app_get_origin_appstream:
+ * @app: a #GsApp
+ *
+ * Gets the appstream origin for the application, e.g. "fedora".
+ *
+ * Returns: a string, or %NULL for unset
+ *
+ * Since: 3.28
+ **/
+const gchar *
+gs_app_get_origin_appstream (GsApp *app)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_return_val_if_fail (GS_IS_APP (app), NULL);
+ return priv->origin_appstream;
+}
+
+/**
+ * gs_app_set_origin_appstream:
+ * @app: a #GsApp
+ * @origin_appstream: a string, or %NULL
+ *
+ * The appstream origin is the appstream source of the application e.g. "fedora"
+ *
+ * Since: 3.28
+ **/
+void
+gs_app_set_origin_appstream (GsApp *app, const gchar *origin_appstream)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_autoptr(GMutexLocker) locker = NULL;
+ g_return_if_fail (GS_IS_APP (app));
+
+ locker = g_mutex_locker_new (&priv->mutex);
+
+ /* same */
+ if (g_strcmp0 (origin_appstream, priv->origin_appstream) == 0)
+ return;
+
+ g_free (priv->origin_appstream);
+ priv->origin_appstream = g_strdup (origin_appstream);
+}
+
+/**
+ * gs_app_get_origin_hostname:
+ * @app: a #GsApp
+ *
+ * Gets the hostname of the origin used to install the application, e.g.
+ * "fedoraproject.org" or "sdk.gnome.org".
+ *
+ * Returns: a string, or %NULL for unset
+ *
+ * Since: 3.22
+ **/
+const gchar *
+gs_app_get_origin_hostname (GsApp *app)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_return_val_if_fail (GS_IS_APP (app), NULL);
+ return priv->origin_hostname;
+}
+
+/**
+ * gs_app_set_origin_hostname:
+ * @app: a #GsApp
+ * @origin_hostname: a string, or %NULL
+ *
+ * The origin is the hostname of the source used to install the application
+ * e.g. "fedoraproject.org"
+ *
+ * You can also use a full URL as @origin_hostname and this will be parsed and
+ * the hostname extracted. This process will also remove any unnecessary DNS
+ * prefixes like "download" or "mirrors".
+ *
+ * Since: 3.22
+ **/
+void
+gs_app_set_origin_hostname (GsApp *app, const gchar *origin_hostname)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_autoptr(GMutexLocker) locker = NULL;
+ g_autoptr(SoupURI) uri = NULL;
+ guint i;
+ const gchar *prefixes[] = { "download.", "mirrors.", NULL };
+
+ g_return_if_fail (GS_IS_APP (app));
+
+ locker = g_mutex_locker_new (&priv->mutex);
+
+ /* same */
+ if (g_strcmp0 (origin_hostname, priv->origin_hostname) == 0)
+ return;
+ g_free (priv->origin_hostname);
+
+ /* use libsoup to convert a URL */
+ uri = soup_uri_new (origin_hostname);
+ if (uri != NULL)
+ origin_hostname = soup_uri_get_host (uri);
+
+ /* remove some common prefixes */
+ for (i = 0; prefixes[i] != NULL; i++) {
+ if (g_str_has_prefix (origin_hostname, prefixes[i]))
+ origin_hostname += strlen (prefixes[i]);
+ }
+
+ /* fallback for localhost */
+ if (g_strcmp0 (origin_hostname, "") == 0)
+ origin_hostname = "localhost";
+
+ /* success */
+ priv->origin_hostname = g_strdup (origin_hostname);
+}
+
+/**
+ * gs_app_add_screenshot:
+ * @app: a #GsApp
+ * @screenshot: a #AsScreenshot
+ *
+ * Adds a screenshot to the application.
+ *
+ * Since: 3.22
+ **/
+void
+gs_app_add_screenshot (GsApp *app, AsScreenshot *screenshot)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_autoptr(GMutexLocker) locker = NULL;
+
+ g_return_if_fail (GS_IS_APP (app));
+ g_return_if_fail (AS_IS_SCREENSHOT (screenshot));
+
+ locker = g_mutex_locker_new (&priv->mutex);
+ g_ptr_array_add (priv->screenshots, g_object_ref (screenshot));
+}
+
+/**
+ * gs_app_get_screenshots:
+ * @app: a #GsApp
+ *
+ * Gets the list of screenshots.
+ *
+ * Returns: (element-type AsScreenshot) (transfer none): a list
+ *
+ * Since: 3.22
+ **/
+GPtrArray *
+gs_app_get_screenshots (GsApp *app)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_return_val_if_fail (GS_IS_APP (app), NULL);
+ return priv->screenshots;
+}
+
+/**
+ * gs_app_get_update_version:
+ * @app: a #GsApp
+ *
+ * Gets the newest update version.
+ *
+ * Returns: a string, or %NULL for unset
+ *
+ * Since: 3.22
+ **/
+const gchar *
+gs_app_get_update_version (GsApp *app)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_return_val_if_fail (GS_IS_APP (app), NULL);
+ return priv->update_version;
+}
+
+/**
+ * gs_app_get_update_version_ui:
+ * @app: a #GsApp
+ *
+ * Gets the update version for the UI.
+ *
+ * Returns: a string, or %NULL for unset
+ *
+ * Since: 3.22
+ **/
+const gchar *
+gs_app_get_update_version_ui (GsApp *app)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_return_val_if_fail (GS_IS_APP (app), NULL);
+
+ /* work out the two version numbers */
+ if (priv->update_version != NULL &&
+ priv->update_version_ui == NULL) {
+ gs_app_ui_versions_populate (app);
+ }
+
+ return priv->update_version_ui;
+}
+
+static void
+gs_app_set_update_version_internal (GsApp *app, const gchar *update_version)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ if (_g_set_str (&priv->update_version, update_version))
+ gs_app_ui_versions_invalidate (app);
+}
+
+/**
+ * gs_app_set_update_version:
+ * @app: a #GsApp
+ * @update_version: a string, e.g. "0.1.2.3"
+ *
+ * Sets the new version number of the update.
+ *
+ * Since: 3.22
+ **/
+void
+gs_app_set_update_version (GsApp *app, const gchar *update_version)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_autoptr(GMutexLocker) locker = NULL;
+ g_return_if_fail (GS_IS_APP (app));
+ locker = g_mutex_locker_new (&priv->mutex);
+ gs_app_set_update_version_internal (app, update_version);
+ gs_app_queue_notify (app, obj_props[PROP_VERSION]);
+}
+
+/**
+ * gs_app_get_update_details:
+ * @app: a #GsApp
+ *
+ * Gets the multi-line description for the update.
+ *
+ * Returns: a string, or %NULL for unset
+ *
+ * Since: 3.22
+ **/
+const gchar *
+gs_app_get_update_details (GsApp *app)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_return_val_if_fail (GS_IS_APP (app), NULL);
+ return priv->update_details;
+}
+
+/**
+ * gs_app_set_update_details:
+ * @app: a #GsApp
+ * @update_details: a string
+ *
+ * Sets the multi-line description for the update.
+ *
+ * Since: 3.22
+ **/
+void
+gs_app_set_update_details (GsApp *app, const gchar *update_details)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_autoptr(GMutexLocker) locker = NULL;
+ g_return_if_fail (GS_IS_APP (app));
+ locker = g_mutex_locker_new (&priv->mutex);
+ _g_set_str (&priv->update_details, update_details);
+}
+
+/**
+ * gs_app_get_update_urgency:
+ * @app: a #GsApp
+ *
+ * Gets the update urgency.
+ *
+ * Returns: a #AsUrgencyKind, or %AS_URGENCY_KIND_UNKNOWN for unset
+ *
+ * Since: 3.22
+ **/
+AsUrgencyKind
+gs_app_get_update_urgency (GsApp *app)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_return_val_if_fail (GS_IS_APP (app), AS_URGENCY_KIND_UNKNOWN);
+ return priv->update_urgency;
+}
+
+/**
+ * gs_app_set_update_urgency:
+ * @app: a #GsApp
+ * @update_urgency: a #AsUrgencyKind
+ *
+ * Sets the update urgency.
+ *
+ * Since: 3.22
+ **/
+void
+gs_app_set_update_urgency (GsApp *app, AsUrgencyKind update_urgency)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_return_if_fail (GS_IS_APP (app));
+ if (update_urgency == priv->update_urgency)
+ return;
+ priv->update_urgency = update_urgency;
+}
+
+/**
+ * gs_app_get_management_plugin:
+ * @app: a #GsApp
+ *
+ * Gets the management plugin.
+ * This is some metadata about the application which is used to work out
+ * which plugin should handle the install, remove or upgrade actions.
+ *
+ * Typically plugins will just set this to the plugin name using
+ * gs_plugin_get_name().
+ *
+ * Returns: a string, or %NULL for unset
+ *
+ * Since: 3.22
+ **/
+const gchar *
+gs_app_get_management_plugin (GsApp *app)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_return_val_if_fail (GS_IS_APP (app), NULL);
+ return priv->management_plugin;
+}
+
+/**
+ * gs_app_set_management_plugin:
+ * @app: a #GsApp
+ * @management_plugin: a string, or %NULL, e.g. "fwupd"
+ *
+ * The management plugin is the plugin that can handle doing install and remove
+ * operations on the #GsApp.
+ * Typical values include "packagekit" and "flatpak"
+ *
+ * It is an error to attempt to change the management plugin once it has been
+ * previously set or to try to use this function on a wildcard application.
+ *
+ * Since: 3.22
+ **/
+void
+gs_app_set_management_plugin (GsApp *app, const gchar *management_plugin)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_autoptr(GMutexLocker) locker = NULL;
+ g_return_if_fail (GS_IS_APP (app));
+
+ locker = g_mutex_locker_new (&priv->mutex);
+
+ /* plugins cannot adopt wildcard packages */
+ if (gs_app_has_quirk (app, GS_APP_QUIRK_IS_WILDCARD)) {
+ g_warning ("plugins should not set the management plugin on "
+ "%s to %s -- create a new GsApp in refine()!",
+ gs_app_get_unique_id_unlocked (app),
+ management_plugin);
+ return;
+ }
+
+ /* same */
+ if (g_strcmp0 (priv->management_plugin, management_plugin) == 0)
+ return;
+
+ /* trying to change */
+ if (priv->management_plugin != NULL && management_plugin != NULL) {
+ g_warning ("automatically prevented from changing "
+ "management plugin on %s from %s to %s!",
+ gs_app_get_unique_id_unlocked (app),
+ priv->management_plugin,
+ management_plugin);
+ return;
+ }
+
+ g_free (priv->management_plugin);
+ priv->management_plugin = g_strdup (management_plugin);
+}
+
+/**
+ * gs_app_get_rating:
+ * @app: a #GsApp
+ *
+ * Gets the percentage rating of the application, where 100 is 5 stars.
+ *
+ * Returns: a percentage, or -1 for unset
+ *
+ * Since: 3.22
+ **/
+gint
+gs_app_get_rating (GsApp *app)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_return_val_if_fail (GS_IS_APP (app), -1);
+ return priv->rating;
+}
+
+/**
+ * gs_app_set_rating:
+ * @app: a #GsApp
+ * @rating: a percentage, or -1 for invalid
+ *
+ * Gets the percentage rating of the application.
+ *
+ * Since: 3.22
+ **/
+void
+gs_app_set_rating (GsApp *app, gint rating)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_autoptr(GMutexLocker) locker = NULL;
+ g_return_if_fail (GS_IS_APP (app));
+ locker = g_mutex_locker_new (&priv->mutex);
+ if (rating == priv->rating)
+ return;
+ priv->rating = rating;
+ gs_app_queue_notify (app, obj_props[PROP_RATING]);
+}
+
+/**
+ * gs_app_get_review_ratings:
+ * @app: a #GsApp
+ *
+ * Gets the review ratings.
+ *
+ * Returns: (element-type guint32) (transfer none): a list
+ *
+ * Since: 3.22
+ **/
+GArray *
+gs_app_get_review_ratings (GsApp *app)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_return_val_if_fail (GS_IS_APP (app), NULL);
+ return priv->review_ratings;
+}
+
+/**
+ * gs_app_set_review_ratings:
+ * @app: a #GsApp
+ * @review_ratings: (element-type guint32): a list
+ *
+ * Sets the review ratings.
+ *
+ * Since: 3.22
+ **/
+void
+gs_app_set_review_ratings (GsApp *app, GArray *review_ratings)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_autoptr(GMutexLocker) locker = NULL;
+ g_return_if_fail (GS_IS_APP (app));
+ locker = g_mutex_locker_new (&priv->mutex);
+ _g_set_array (&priv->review_ratings, review_ratings);
+}
+
+/**
+ * gs_app_get_reviews:
+ * @app: a #GsApp
+ *
+ * Gets all the user-submitted reviews for the application.
+ *
+ * Returns: (element-type AsReview) (transfer none): the list of reviews
+ *
+ * Since: 3.22
+ **/
+GPtrArray *
+gs_app_get_reviews (GsApp *app)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_return_val_if_fail (GS_IS_APP (app), NULL);
+ return priv->reviews;
+}
+
+/**
+ * gs_app_add_review:
+ * @app: a #GsApp
+ * @review: a #AsReview
+ *
+ * Adds a user-submitted review to the application.
+ *
+ * Since: 3.22
+ **/
+void
+gs_app_add_review (GsApp *app, AsReview *review)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_autoptr(GMutexLocker) locker = NULL;
+ g_return_if_fail (GS_IS_APP (app));
+ g_return_if_fail (AS_IS_REVIEW (review));
+ locker = g_mutex_locker_new (&priv->mutex);
+ g_ptr_array_add (priv->reviews, g_object_ref (review));
+}
+
+/**
+ * gs_app_remove_review:
+ * @app: a #GsApp
+ * @review: a #AsReview
+ *
+ * Removes a user-submitted review to the application.
+ *
+ * Since: 3.22
+ **/
+void
+gs_app_remove_review (GsApp *app, AsReview *review)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_autoptr(GMutexLocker) locker = NULL;
+ g_return_if_fail (GS_IS_APP (app));
+ locker = g_mutex_locker_new (&priv->mutex);
+ g_ptr_array_remove (priv->reviews, review);
+}
+
+/**
+ * gs_app_get_provides:
+ * @app: a #GsApp
+ *
+ * Gets all the provides for the application.
+ *
+ * Returns: (element-type AsProvide) (transfer none): the list of provides
+ *
+ * Since: 3.22
+ **/
+GPtrArray *
+gs_app_get_provides (GsApp *app)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_return_val_if_fail (GS_IS_APP (app), NULL);
+ return priv->provides;
+}
+
+/**
+ * gs_app_add_provide:
+ * @app: a #GsApp
+ * @provide: a #AsProvide
+ *
+ * Adds a provide to the application.
+ *
+ * Since: 3.22
+ **/
+void
+gs_app_add_provide (GsApp *app, AsProvide *provide)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_autoptr(GMutexLocker) locker = NULL;
+ g_return_if_fail (GS_IS_APP (app));
+ g_return_if_fail (AS_IS_PROVIDE (provide));
+ locker = g_mutex_locker_new (&priv->mutex);
+ g_ptr_array_add (priv->provides, g_object_ref (provide));
+}
+
+/**
+ * gs_app_get_size_download:
+ * @app: A #GsApp
+ *
+ * Gets the size of the total download needed to either install an available
+ * application, or update an already installed one.
+ *
+ * If there is a runtime not yet installed then this is also added.
+ *
+ * Returns: number of bytes, 0 for unknown, or %GS_APP_SIZE_UNKNOWABLE for invalid
+ *
+ * Since: 3.22
+ **/
+guint64
+gs_app_get_size_download (GsApp *app)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ guint64 sz;
+
+ g_return_val_if_fail (GS_IS_APP (app), G_MAXUINT64);
+
+ /* this app */
+ sz = priv->size_download;
+
+ /* add the runtime if this is not installed */
+ if (priv->runtime != NULL) {
+ if (gs_app_get_state (priv->runtime) == AS_APP_STATE_AVAILABLE)
+ sz += gs_app_get_size_installed (priv->runtime);
+ }
+
+ /* add related apps */
+ for (guint i = 0; i < gs_app_list_length (priv->related); i++) {
+ GsApp *app_related = gs_app_list_index (priv->related, i);
+ sz += gs_app_get_size_download (app_related);
+ }
+
+ return sz;
+}
+
+/**
+ * gs_app_set_size_download:
+ * @app: a #GsApp
+ * @size_download: size in bytes, or %GS_APP_SIZE_UNKNOWABLE for invalid
+ *
+ * Sets the download size of the application, not including any
+ * required runtime.
+ *
+ * Since: 3.22
+ **/
+void
+gs_app_set_size_download (GsApp *app, guint64 size_download)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_return_if_fail (GS_IS_APP (app));
+ if (size_download == priv->size_download)
+ return;
+ priv->size_download = size_download;
+}
+
+/**
+ * gs_app_get_size_installed:
+ * @app: a #GsApp
+ *
+ * Gets the size on disk, either for an existing application of one that could
+ * be installed.
+ *
+ * Returns: size in bytes, 0 for unknown, or %GS_APP_SIZE_UNKNOWABLE for invalid.
+ *
+ * Since: 3.22
+ **/
+guint64
+gs_app_get_size_installed (GsApp *app)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ guint64 sz;
+
+ g_return_val_if_fail (GS_IS_APP (app), G_MAXUINT64);
+
+ /* this app */
+ sz = priv->size_installed;
+
+ /* add related apps */
+ for (guint i = 0; i < gs_app_list_length (priv->related); i++) {
+ GsApp *app_related = gs_app_list_index (priv->related, i);
+ sz += gs_app_get_size_installed (app_related);
+ }
+
+ return sz;
+}
+
+/**
+ * gs_app_set_size_installed:
+ * @app: a #GsApp
+ * @size_installed: size in bytes, or %GS_APP_SIZE_UNKNOWABLE for invalid
+ *
+ * Sets the installed size of the application.
+ *
+ * Since: 3.22
+ **/
+void
+gs_app_set_size_installed (GsApp *app, guint64 size_installed)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_return_if_fail (GS_IS_APP (app));
+ if (size_installed == priv->size_installed)
+ return;
+ priv->size_installed = size_installed;
+}
+
+/**
+ * gs_app_get_metadata_item:
+ * @app: a #GsApp
+ * @key: a string, e.g. "fwupd::device-id"
+ *
+ * Gets some metadata for the application.
+ * Is is expected that plugins namespace any plugin-specific metadata,
+ * for example `fwupd::device-id`.
+ *
+ * Returns: a string, or %NULL for unset
+ *
+ * Since: 3.22
+ **/
+const gchar *
+gs_app_get_metadata_item (GsApp *app, const gchar *key)
+{
+ GVariant *tmp;
+ g_return_val_if_fail (GS_IS_APP (app), NULL);
+ g_return_val_if_fail (key != NULL, NULL);
+ tmp = gs_app_get_metadata_variant (app, key);
+ if (tmp == NULL)
+ return NULL;
+ return g_variant_get_string (tmp, NULL);
+}
+
+/**
+ * gs_app_set_metadata:
+ * @app: a #GsApp
+ * @key: a string, e.g. "fwupd::DeviceID"
+ * @value: a string, e.g. "fubar"
+ *
+ * Sets some metadata for the application.
+ * Is is expected that plugins namespace any plugin-specific metadata.
+ *
+ * Since: 3.22
+ **/
+void
+gs_app_set_metadata (GsApp *app, const gchar *key, const gchar *value)
+{
+ g_autoptr(GVariant) tmp = NULL;
+ g_return_if_fail (GS_IS_APP (app));
+ g_return_if_fail (key != NULL);
+ if (value != NULL)
+ tmp = g_variant_new_string (value);
+ gs_app_set_metadata_variant (app, key, tmp);
+}
+
+/**
+ * gs_app_get_metadata_variant:
+ * @app: a #GsApp
+ * @key: a string, e.g. "fwupd::device-id"
+ *
+ * Gets some metadata for the application.
+ * Is is expected that plugins namespace any plugin-specific metadata.
+ *
+ * Returns: a string, or %NULL for unset
+ *
+ * Since: 3.26
+ **/
+GVariant *
+gs_app_get_metadata_variant (GsApp *app, const gchar *key)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_return_val_if_fail (GS_IS_APP (app), NULL);
+ g_return_val_if_fail (key != NULL, NULL);
+ return g_hash_table_lookup (priv->metadata, key);
+}
+
+/**
+ * gs_app_set_metadata_variant:
+ * @app: a #GsApp
+ * @key: a string, e.g. "fwupd::DeviceID"
+ * @value: a #GVariant
+ *
+ * Sets some metadata for the application.
+ * Is is expected that plugins namespace any plugin-specific metadata,
+ * for example `fwupd::device-id`.
+ *
+ * Since: 3.26
+ **/
+void
+gs_app_set_metadata_variant (GsApp *app, const gchar *key, GVariant *value)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_autoptr(GMutexLocker) locker = NULL;
+ GVariant *found;
+
+ g_return_if_fail (GS_IS_APP (app));
+
+ locker = g_mutex_locker_new (&priv->mutex);
+
+ /* if no value, then remove the key */
+ if (value == NULL) {
+ g_hash_table_remove (priv->metadata, key);
+ return;
+ }
+
+ /* check we're not overwriting */
+ found = g_hash_table_lookup (priv->metadata, key);
+ if (found != NULL) {
+ if (g_variant_equal (found, value))
+ return;
+ if (g_variant_type_equal (g_variant_get_type (value), G_VARIANT_TYPE_STRING) &&
+ g_variant_type_equal (g_variant_get_type (found), G_VARIANT_TYPE_STRING)) {
+ g_debug ("tried overwriting %s key %s from %s to %s",
+ priv->id, key,
+ g_variant_get_string (found, NULL),
+ g_variant_get_string (value, NULL));
+ } else {
+ g_debug ("tried overwriting %s key %s (%s->%s)",
+ priv->id, key,
+ g_variant_get_type_string (found),
+ g_variant_get_type_string (value));
+ }
+ return;
+ }
+ g_hash_table_insert (priv->metadata, g_strdup (key), g_variant_ref (value));
+}
+
+/**
+ * gs_app_get_addons:
+ * @app: a #GsApp
+ *
+ * Gets the list of addons for the application.
+ *
+ * Returns: (transfer none): a list of addons
+ *
+ * Since: 3.22
+ **/
+GsAppList *
+gs_app_get_addons (GsApp *app)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_return_val_if_fail (GS_IS_APP (app), NULL);
+ return priv->addons;
+}
+
+/**
+ * gs_app_add_addon:
+ * @app: a #GsApp
+ * @addon: a #GsApp
+ *
+ * Adds an addon to the list of application addons.
+ *
+ * Since: 3.22
+ **/
+void
+gs_app_add_addon (GsApp *app, GsApp *addon)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_autoptr(GMutexLocker) locker = NULL;
+
+ g_return_if_fail (GS_IS_APP (app));
+ g_return_if_fail (GS_IS_APP (addon));
+
+ locker = g_mutex_locker_new (&priv->mutex);
+ gs_app_list_add (priv->addons, addon);
+}
+
+/**
+ * gs_app_remove_addon:
+ * @app: a #GsApp
+ * @addon: a #GsApp
+ *
+ * Removes an addon from the list of application addons.
+ *
+ * Since: 3.22
+ **/
+void
+gs_app_remove_addon (GsApp *app, GsApp *addon)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_autoptr(GMutexLocker) locker = NULL;
+ g_return_if_fail (GS_IS_APP (app));
+ g_return_if_fail (GS_IS_APP (addon));
+ locker = g_mutex_locker_new (&priv->mutex);
+ gs_app_list_remove (priv->addons, addon);
+}
+
+/**
+ * gs_app_get_related:
+ * @app: a #GsApp
+ *
+ * Gets any related applications.
+ *
+ * Returns: (transfer none): a list of applications
+ *
+ * Since: 3.22
+ **/
+GsAppList *
+gs_app_get_related (GsApp *app)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_return_val_if_fail (GS_IS_APP (app), NULL);
+ return priv->related;
+}
+
+/**
+ * gs_app_add_related:
+ * @app: a #GsApp
+ * @app2: a #GsApp
+ *
+ * Adds a related application.
+ *
+ * Since: 3.22
+ **/
+void
+gs_app_add_related (GsApp *app, GsApp *app2)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ GsAppPrivate *priv2 = gs_app_get_instance_private (app2);
+ g_autoptr(GMutexLocker) locker = NULL;
+
+ g_return_if_fail (GS_IS_APP (app));
+ g_return_if_fail (GS_IS_APP (app2));
+
+ locker = g_mutex_locker_new (&priv->mutex);
+
+ /* if the app is updatable-live and any related app is not then
+ * degrade to the offline state */
+ if (priv->state == AS_APP_STATE_UPDATABLE_LIVE &&
+ priv2->state == AS_APP_STATE_UPDATABLE)
+ priv->state = priv2->state;
+
+ gs_app_list_add (priv->related, app2);
+}
+
+/**
+ * gs_app_get_history:
+ * @app: a #GsApp
+ *
+ * Gets the history of this application.
+ *
+ * Returns: (transfer none): a list
+ *
+ * Since: 3.22
+ **/
+GsAppList *
+gs_app_get_history (GsApp *app)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_return_val_if_fail (GS_IS_APP (app), NULL);
+ return priv->history;
+}
+
+/**
+ * gs_app_add_history:
+ * @app: a #GsApp
+ * @app2: a #GsApp
+ *
+ * Adds a history item for this package.
+ *
+ * Since: 3.22
+ **/
+void
+gs_app_add_history (GsApp *app, GsApp *app2)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_autoptr(GMutexLocker) locker = NULL;
+ g_return_if_fail (GS_IS_APP (app));
+ g_return_if_fail (GS_IS_APP (app2));
+ locker = g_mutex_locker_new (&priv->mutex);
+ gs_app_list_add (priv->history, app2);
+}
+
+/**
+ * gs_app_get_install_date:
+ * @app: a #GsApp
+ *
+ * Gets the date that an application was installed.
+ *
+ * Returns: A UNIX epoch, or 0 for unset
+ *
+ * Since: 3.22
+ **/
+guint64
+gs_app_get_install_date (GsApp *app)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_return_val_if_fail (GS_IS_APP (app), 0);
+ return priv->install_date;
+}
+
+/**
+ * gs_app_set_install_date:
+ * @app: a #GsApp
+ * @install_date: an epoch, or %GS_APP_INSTALL_DATE_UNKNOWN
+ *
+ * Sets the date that an application was installed.
+ *
+ * Since: 3.22
+ **/
+void
+gs_app_set_install_date (GsApp *app, guint64 install_date)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_return_if_fail (GS_IS_APP (app));
+ if (install_date == priv->install_date)
+ return;
+ priv->install_date = install_date;
+}
+
+/**
+ * gs_app_is_installed:
+ * @app: a #GsApp
+ *
+ * Gets whether the app is installed or not.
+ *
+ * Returns: %TRUE if the app is installed, %FALSE otherwise.
+ *
+ * Since: 3.22
+ **/
+gboolean
+gs_app_is_installed (GsApp *app)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_return_val_if_fail (GS_IS_APP (app), FALSE);
+ return (priv->state == AS_APP_STATE_INSTALLED) ||
+ (priv->state == AS_APP_STATE_UPDATABLE) ||
+ (priv->state == AS_APP_STATE_UPDATABLE_LIVE) ||
+ (priv->state == AS_APP_STATE_REMOVING);
+}
+
+/**
+ * gs_app_is_updatable:
+ * @app: a #GsApp
+ *
+ * Gets whether the app is updatable or not.
+ *
+ * Returns: %TRUE if the app is updatable, %FALSE otherwise.
+ *
+ * Since: 3.22
+ **/
+gboolean
+gs_app_is_updatable (GsApp *app)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_return_val_if_fail (GS_IS_APP (app), FALSE);
+ if (priv->kind == AS_APP_KIND_OS_UPGRADE)
+ return TRUE;
+ return (priv->state == AS_APP_STATE_UPDATABLE) ||
+ (priv->state == AS_APP_STATE_UPDATABLE_LIVE);
+}
+
+/**
+ * gs_app_get_categories:
+ * @app: a #GsApp
+ *
+ * Gets the list of categories for an application.
+ *
+ * Returns: (element-type utf8) (transfer none): a list
+ *
+ * Since: 3.22
+ **/
+GPtrArray *
+gs_app_get_categories (GsApp *app)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_return_val_if_fail (GS_IS_APP (app), NULL);
+ return priv->categories;
+}
+
+/**
+ * gs_app_has_category:
+ * @app: a #GsApp
+ * @category: a category ID, e.g. "AudioVideo"
+ *
+ * Checks if the application is in a specific category.
+ *
+ * Returns: %TRUE for success
+ *
+ * Since: 3.22
+ **/
+gboolean
+gs_app_has_category (GsApp *app, const gchar *category)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ const gchar *tmp;
+ guint i;
+
+ g_return_val_if_fail (GS_IS_APP (app), FALSE);
+
+ /* find the category */
+ for (i = 0; i < priv->categories->len; i++) {
+ tmp = g_ptr_array_index (priv->categories, i);
+ if (g_strcmp0 (tmp, category) == 0)
+ return TRUE;
+ }
+ return FALSE;
+}
+
+/**
+ * gs_app_set_categories:
+ * @app: a #GsApp
+ * @categories: a set of categories
+ *
+ * Set the list of categories for an application.
+ *
+ * Since: 3.22
+ **/
+void
+gs_app_set_categories (GsApp *app, GPtrArray *categories)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_autoptr(GMutexLocker) locker = NULL;
+ g_return_if_fail (GS_IS_APP (app));
+ g_return_if_fail (categories != NULL);
+ locker = g_mutex_locker_new (&priv->mutex);
+ _g_set_ptr_array (&priv->categories, categories);
+}
+
+/**
+ * gs_app_add_category:
+ * @app: a #GsApp
+ * @category: a category ID, e.g. "AudioVideo"
+ *
+ * Adds a category ID to an application.
+ *
+ * Since: 3.22
+ **/
+void
+gs_app_add_category (GsApp *app, const gchar *category)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_autoptr(GMutexLocker) locker = NULL;
+ g_return_if_fail (GS_IS_APP (app));
+ g_return_if_fail (category != NULL);
+ locker = g_mutex_locker_new (&priv->mutex);
+ if (gs_app_has_category (app, category))
+ return;
+ g_ptr_array_add (priv->categories, g_strdup (category));
+}
+
+/**
+ * gs_app_remove_category:
+ * @app: a #GsApp
+ * @category: a category ID, e.g. "AudioVideo"
+ *
+ * Removes an category ID from an application, it exists.
+ *
+ * Returns: %TRUE for success
+ *
+ * Since: 3.24
+ **/
+gboolean
+gs_app_remove_category (GsApp *app, const gchar *category)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ const gchar *tmp;
+ guint i;
+ g_autoptr(GMutexLocker) locker = NULL;
+
+ g_return_val_if_fail (GS_IS_APP (app), FALSE);
+
+ locker = g_mutex_locker_new (&priv->mutex);
+
+ for (i = 0; i < priv->categories->len; i++) {
+ tmp = g_ptr_array_index (priv->categories, i);
+ if (g_strcmp0 (tmp, category) != 0)
+ continue;
+ g_ptr_array_remove_index_fast (priv->categories, i);
+ return TRUE;
+ }
+ return FALSE;
+}
+
+/**
+ * gs_app_set_is_update_downloaded:
+ * @app: a #GsApp
+ * @is_update_downloaded: Whether a new update is already downloaded locally
+ *
+ * Sets if the new update is already downloaded for the app.
+ *
+ * Since: 3.36
+ **/
+void
+gs_app_set_is_update_downloaded (GsApp *app, gboolean is_update_downloaded)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_return_if_fail (GS_IS_APP (app));
+ priv->is_update_downloaded = is_update_downloaded;
+}
+
+/**
+ * gs_app_get_is_update_downloaded:
+ * @app: a #GsApp
+ *
+ * Gets if the new update is already downloaded for the app and
+ * is locally available.
+ *
+ * Returns: (element-type gboolean): Whether a new update for the #GsApp is already downloaded.
+ *
+ * Since: 3.36
+ **/
+gboolean
+gs_app_get_is_update_downloaded (GsApp *app)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_return_val_if_fail (GS_IS_APP (app), FALSE);
+ return priv->is_update_downloaded;
+}
+
+/**
+ * gs_app_get_key_colors:
+ * @app: a #GsApp
+ *
+ * Gets the key colors used in the application icon.
+ *
+ * Returns: (element-type GdkRGBA) (transfer none): a list
+ *
+ * Since: 3.22
+ **/
+GPtrArray *
+gs_app_get_key_colors (GsApp *app)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_return_val_if_fail (GS_IS_APP (app), NULL);
+ return priv->key_colors;
+}
+
+/**
+ * gs_app_set_key_colors:
+ * @app: a #GsApp
+ * @key_colors: (element-type GdkRGBA): a set of key colors
+ *
+ * Sets the key colors used in the application icon.
+ *
+ * Since: 3.22
+ **/
+void
+gs_app_set_key_colors (GsApp *app, GPtrArray *key_colors)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_autoptr(GMutexLocker) locker = NULL;
+ g_return_if_fail (GS_IS_APP (app));
+ g_return_if_fail (key_colors != NULL);
+ locker = g_mutex_locker_new (&priv->mutex);
+ if (_g_set_ptr_array (&priv->key_colors, key_colors))
+ gs_app_queue_notify (app, obj_props[PROP_KEY_COLORS]);
+}
+
+/**
+ * gs_app_add_key_color:
+ * @app: a #GsApp
+ * @key_color: a #GdkRGBA
+ *
+ * Adds a key colors used in the application icon.
+ *
+ * Since: 3.22
+ **/
+void
+gs_app_add_key_color (GsApp *app, GdkRGBA *key_color)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_return_if_fail (GS_IS_APP (app));
+ g_return_if_fail (key_color != NULL);
+ g_ptr_array_add (priv->key_colors, gdk_rgba_copy (key_color));
+ gs_app_queue_notify (app, obj_props[PROP_KEY_COLORS]);
+}
+
+/**
+ * gs_app_add_kudo:
+ * @app: a #GsApp
+ * @kudo: a #GsAppKudo, e.g. %GS_APP_KUDO_MY_LANGUAGE
+ *
+ * Adds a kudo to the application.
+ *
+ * Since: 3.22
+ **/
+void
+gs_app_add_kudo (GsApp *app, GsAppKudo kudo)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_return_if_fail (GS_IS_APP (app));
+ if (kudo & GS_APP_KUDO_SANDBOXED_SECURE)
+ kudo |= GS_APP_KUDO_SANDBOXED;
+ priv->kudos |= kudo;
+}
+
+/**
+ * gs_app_remove_kudo:
+ * @app: a #GsApp
+ * @kudo: a #GsAppKudo, e.g. %GS_APP_KUDO_MY_LANGUAGE
+ *
+ * Removes a kudo from the application.
+ *
+ * Since: 3.30
+ **/
+void
+gs_app_remove_kudo (GsApp *app, GsAppKudo kudo)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_return_if_fail (GS_IS_APP (app));
+ priv->kudos &= ~kudo;
+}
+
+/**
+ * gs_app_has_kudo:
+ * @app: a #GsApp
+ * @kudo: a #GsAppKudo, e.g. %GS_APP_KUDO_MY_LANGUAGE
+ *
+ * Finds out if a kudo has been awarded by the application.
+ *
+ * Returns: %TRUE if the app has the specified kudo
+ *
+ * Since: 3.22
+ **/
+gboolean
+gs_app_has_kudo (GsApp *app, GsAppKudo kudo)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_return_val_if_fail (GS_IS_APP (app), FALSE);
+ return (priv->kudos & kudo) > 0;
+}
+
+/**
+ * gs_app_get_kudos:
+ * @app: a #GsApp
+ *
+ * Gets all the kudos the application has been awarded.
+ *
+ * Returns: the kudos, as a bitfield
+ *
+ * Since: 3.22
+ **/
+guint64
+gs_app_get_kudos (GsApp *app)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_return_val_if_fail (GS_IS_APP (app), 0);
+ return priv->kudos;
+}
+
+/**
+ * gs_app_get_kudos_percentage:
+ * @app: a #GsApp
+ *
+ * Gets the kudos, as a percentage value.
+ *
+ * Returns: a percentage, with 0 for no kudos and a maximum of 100.
+ *
+ * Since: 3.22
+ **/
+guint
+gs_app_get_kudos_percentage (GsApp *app)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ guint percentage = 0;
+
+ g_return_val_if_fail (GS_IS_APP (app), 0);
+
+ if ((priv->kudos & GS_APP_KUDO_MY_LANGUAGE) > 0)
+ percentage += 20;
+ if ((priv->kudos & GS_APP_KUDO_RECENT_RELEASE) > 0)
+ percentage += 20;
+ if ((priv->kudos & GS_APP_KUDO_FEATURED_RECOMMENDED) > 0)
+ percentage += 20;
+ if ((priv->kudos & GS_APP_KUDO_MODERN_TOOLKIT) > 0)
+ percentage += 20;
+ if ((priv->kudos & GS_APP_KUDO_SEARCH_PROVIDER) > 0)
+ percentage += 10;
+ if ((priv->kudos & GS_APP_KUDO_INSTALLS_USER_DOCS) > 0)
+ percentage += 10;
+ if ((priv->kudos & GS_APP_KUDO_USES_NOTIFICATIONS) > 0)
+ percentage += 20;
+ if ((priv->kudos & GS_APP_KUDO_HAS_KEYWORDS) > 0)
+ percentage += 5;
+ if ((priv->kudos & GS_APP_KUDO_HAS_SCREENSHOTS) > 0)
+ percentage += 20;
+ if ((priv->kudos & GS_APP_KUDO_HIGH_CONTRAST) > 0)
+ percentage += 20;
+ if ((priv->kudos & GS_APP_KUDO_HI_DPI_ICON) > 0)
+ percentage += 20;
+ if ((priv->kudos & GS_APP_KUDO_SANDBOXED) > 0)
+ percentage += 20;
+ if ((priv->kudos & GS_APP_KUDO_SANDBOXED_SECURE) > 0)
+ percentage += 20;
+
+ /* popular apps should be at *least* 50% */
+ if ((priv->kudos & GS_APP_KUDO_POPULAR) > 0)
+ percentage = MAX (percentage, 50);
+
+ return MIN (percentage, 100);
+}
+
+/**
+ * gs_app_get_to_be_installed:
+ * @app: a #GsApp
+ *
+ * Gets if the application is queued for installation.
+ *
+ * This is only set for addons when the user has selected some addons to be
+ * installed before installing the main application.
+ * Plugins should check all the addons for this property when installing
+ * main applications so that the chosen set of addons is also installed at the
+ * same time. This is never set when applications do not have addons.
+ *
+ * Returns: %TRUE for success
+ *
+ * Since: 3.22
+ **/
+gboolean
+gs_app_get_to_be_installed (GsApp *app)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_return_val_if_fail (GS_IS_APP (app), FALSE);
+
+ return priv->to_be_installed;
+}
+
+/**
+ * gs_app_set_to_be_installed:
+ * @app: a #GsApp
+ * @to_be_installed: if the app is due to be installed
+ *
+ * Sets if the application is queued for installation.
+ *
+ * Since: 3.22
+ **/
+void
+gs_app_set_to_be_installed (GsApp *app, gboolean to_be_installed)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_return_if_fail (GS_IS_APP (app));
+
+ priv->to_be_installed = to_be_installed;
+}
+
+/**
+ * gs_app_has_quirk:
+ * @app: a #GsApp
+ * @quirk: a #GsAppQuirk, e.g. %GS_APP_QUIRK_COMPULSORY
+ *
+ * Finds out if an application has a specific quirk.
+ *
+ * Returns: %TRUE for success
+ *
+ * Since: 3.22
+ **/
+gboolean
+gs_app_has_quirk (GsApp *app, GsAppQuirk quirk)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_return_val_if_fail (GS_IS_APP (app), FALSE);
+
+ return (priv->quirk & quirk) > 0;
+}
+
+/**
+ * gs_app_add_quirk:
+ * @app: a #GsApp
+ * @quirk: a #GsAppQuirk, e.g. %GS_APP_QUIRK_COMPULSORY
+ *
+ * Adds a quirk to an application.
+ *
+ * Since: 3.22
+ **/
+void
+gs_app_add_quirk (GsApp *app, GsAppQuirk quirk)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_autoptr(GMutexLocker) locker = NULL;
+ g_return_if_fail (GS_IS_APP (app));
+
+ /* same */
+ if ((priv->quirk & quirk) > 0)
+ return;
+
+ locker = g_mutex_locker_new (&priv->mutex);
+ priv->quirk |= quirk;
+ gs_app_queue_notify (app, obj_props[PROP_QUIRK]);
+}
+
+/**
+ * gs_app_remove_quirk:
+ * @app: a #GsApp
+ * @quirk: a #GsAppQuirk, e.g. %GS_APP_QUIRK_COMPULSORY
+ *
+ * Removes a quirk from an application.
+ *
+ * Since: 3.22
+ **/
+void
+gs_app_remove_quirk (GsApp *app, GsAppQuirk quirk)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_autoptr(GMutexLocker) locker = NULL;
+ g_return_if_fail (GS_IS_APP (app));
+
+ /* same */
+ if ((priv->quirk & quirk) == 0)
+ return;
+
+ locker = g_mutex_locker_new (&priv->mutex);
+ priv->quirk &= ~quirk;
+ gs_app_queue_notify (app, obj_props[PROP_QUIRK]);
+}
+
+/**
+ * gs_app_set_match_value:
+ * @app: a #GsApp
+ * @match_value: a value
+ *
+ * Set a match quality value, where higher values correspond to a
+ * "better" search match, and should be shown above lower results.
+ *
+ * Since: 3.22
+ **/
+void
+gs_app_set_match_value (GsApp *app, guint match_value)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_return_if_fail (GS_IS_APP (app));
+ priv->match_value = match_value;
+}
+
+/**
+ * gs_app_get_match_value:
+ * @app: a #GsApp
+ *
+ * Get a match quality value, where higher values correspond to a
+ * "better" search match, and should be shown above lower results.
+ *
+ * Note: This value is only valid when processing the result set
+ * and may be overwritten on subsequent searches if the plugin is using
+ * a cache.
+ *
+ * Returns: a value, where higher is better
+ *
+ * Since: 3.22
+ **/
+guint
+gs_app_get_match_value (GsApp *app)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_return_val_if_fail (GS_IS_APP (app), 0);
+ return priv->match_value;
+}
+
+/**
+ * gs_app_set_priority:
+ * @app: a #GsApp
+ * @priority: a value
+ *
+ * Set a priority value.
+ *
+ * Since: 3.22
+ **/
+void
+gs_app_set_priority (GsApp *app, guint priority)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_return_if_fail (GS_IS_APP (app));
+ priv->priority = priority;
+}
+
+/**
+ * gs_app_get_priority:
+ * @app: a #GsApp
+ *
+ * Get a priority value, where higher values will be chosen where
+ * multiple #GsApp's match a specific rule.
+ *
+ * Returns: a value, where higher is better
+ *
+ * Since: 3.22
+ **/
+guint
+gs_app_get_priority (GsApp *app)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_return_val_if_fail (GS_IS_APP (app), 0);
+ return priv->priority;
+}
+
+/**
+ * gs_app_get_cancellable:
+ * @app: a #GsApp
+ *
+ * Get a cancellable to be used with operations related to the #GsApp. This is a
+ * way for views to be able to cancel an on-going operation. If the #GCancellable
+ * is canceled, it will be unreferenced and renewed before returning it, i.e. the
+ * cancellable object will always be ready to use for new operations. So be sure
+ * to keep a reference to it if you do more than just passing the cancellable to
+ * a process.
+ *
+ * Returns: a #GCancellable
+ *
+ * Since: 3.28
+ **/
+GCancellable *
+gs_app_get_cancellable (GsApp *app)
+{
+ g_autoptr(GCancellable) cancellable = NULL;
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->mutex);
+
+ if (priv->cancellable == NULL || g_cancellable_is_cancelled (priv->cancellable)) {
+ cancellable = g_cancellable_new ();
+ g_set_object (&priv->cancellable, cancellable);
+ }
+ return priv->cancellable;
+}
+
+/**
+ * gs_app_get_pending_action:
+ * @app: a #GsApp
+ *
+ * Get the pending action for this #GsApp, or %NULL if no action is pending.
+ *
+ * Returns: the #GsAppAction of the @app.
+ **/
+GsPluginAction
+gs_app_get_pending_action (GsApp *app)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->mutex);
+ return priv->pending_action;
+}
+
+/**
+ * gs_app_set_pending_action:
+ * @app: a #GsApp
+ * @action: a #GsPluginAction
+ *
+ * Set an action that is pending on this #GsApp.
+ **/
+void
+gs_app_set_pending_action (GsApp *app,
+ GsPluginAction action)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->mutex);
+ gs_app_set_pending_action_internal (app, action);
+}
+
+static void
+gs_app_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
+{
+ GsApp *app = GS_APP (object);
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+
+ switch (prop_id) {
+ case PROP_ID:
+ g_value_set_string (value, priv->id);
+ break;
+ case PROP_NAME:
+ g_value_set_string (value, priv->name);
+ break;
+ case PROP_VERSION:
+ g_value_set_string (value, priv->version);
+ break;
+ case PROP_SUMMARY:
+ g_value_set_string (value, priv->summary);
+ break;
+ case PROP_DESCRIPTION:
+ g_value_set_string (value, priv->description);
+ break;
+ case PROP_RATING:
+ g_value_set_int (value, priv->rating);
+ break;
+ case PROP_KIND:
+ g_value_set_uint (value, priv->kind);
+ break;
+ case PROP_STATE:
+ g_value_set_uint (value, priv->state);
+ break;
+ case PROP_PROGRESS:
+ g_value_set_uint (value, priv->progress);
+ break;
+ case PROP_CAN_CANCEL_INSTALLATION:
+ g_value_set_boolean (value, priv->allow_cancel);
+ break;
+ case PROP_INSTALL_DATE:
+ g_value_set_uint64 (value, priv->install_date);
+ break;
+ case PROP_QUIRK:
+ g_value_set_uint64 (value, priv->quirk);
+ break;
+ case PROP_KEY_COLORS:
+ g_value_set_boxed (value, priv->key_colors);
+ break;
+ case PROP_IS_UPDATE_DOWNLOADED:
+ g_value_set_boolean (value, priv->is_update_downloaded);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+gs_app_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec)
+{
+ GsApp *app = GS_APP (object);
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+
+ switch (prop_id) {
+ case PROP_ID:
+ gs_app_set_id (app, g_value_get_string (value));
+ break;
+ case PROP_NAME:
+ gs_app_set_name (app,
+ GS_APP_QUALITY_UNKNOWN,
+ g_value_get_string (value));
+ break;
+ case PROP_VERSION:
+ gs_app_set_version (app, g_value_get_string (value));
+ break;
+ case PROP_SUMMARY:
+ gs_app_set_summary (app,
+ GS_APP_QUALITY_UNKNOWN,
+ g_value_get_string (value));
+ break;
+ case PROP_DESCRIPTION:
+ gs_app_set_description (app,
+ GS_APP_QUALITY_UNKNOWN,
+ g_value_get_string (value));
+ break;
+ case PROP_RATING:
+ gs_app_set_rating (app, g_value_get_int (value));
+ break;
+ case PROP_KIND:
+ gs_app_set_kind (app, g_value_get_uint (value));
+ break;
+ case PROP_STATE:
+ gs_app_set_state_internal (app, g_value_get_uint (value));
+ break;
+ case PROP_PROGRESS:
+ gs_app_set_progress (app, g_value_get_uint (value));
+ break;
+ case PROP_CAN_CANCEL_INSTALLATION:
+ priv->allow_cancel = g_value_get_boolean (value);
+ break;
+ case PROP_INSTALL_DATE:
+ gs_app_set_install_date (app, g_value_get_uint64 (value));
+ break;
+ case PROP_QUIRK:
+ priv->quirk = g_value_get_uint64 (value);
+ break;
+ case PROP_KEY_COLORS:
+ gs_app_set_key_colors (app, g_value_get_boxed (value));
+ break;
+ case PROP_IS_UPDATE_DOWNLOADED:
+ gs_app_set_is_update_downloaded (app, g_value_get_boolean (value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+gs_app_dispose (GObject *object)
+{
+ GsApp *app = GS_APP (object);
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+
+ g_clear_object (&priv->runtime);
+
+ g_clear_pointer (&priv->addons, g_object_unref);
+ g_clear_pointer (&priv->history, g_object_unref);
+ g_clear_pointer (&priv->related, g_object_unref);
+ g_clear_pointer (&priv->screenshots, g_ptr_array_unref);
+ g_clear_pointer (&priv->review_ratings, g_array_unref);
+ g_clear_pointer (&priv->reviews, g_ptr_array_unref);
+ g_clear_pointer (&priv->provides, g_ptr_array_unref);
+ g_clear_pointer (&priv->icons, g_ptr_array_unref);
+
+ G_OBJECT_CLASS (gs_app_parent_class)->dispose (object);
+}
+
+static void
+gs_app_finalize (GObject *object)
+{
+ GsApp *app = GS_APP (object);
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+
+ g_mutex_clear (&priv->mutex);
+ g_free (priv->id);
+ g_free (priv->unique_id);
+ g_free (priv->branch);
+ g_free (priv->name);
+ g_hash_table_unref (priv->urls);
+ g_hash_table_unref (priv->launchables);
+ g_free (priv->license);
+ g_strfreev (priv->menu_path);
+ g_free (priv->origin);
+ g_free (priv->origin_appstream);
+ g_free (priv->origin_hostname);
+ g_ptr_array_unref (priv->sources);
+ g_ptr_array_unref (priv->source_ids);
+ g_free (priv->project_group);
+ g_free (priv->developer_name);
+ g_free (priv->agreement);
+ g_free (priv->version);
+ g_free (priv->version_ui);
+ g_free (priv->summary);
+ g_free (priv->summary_missing);
+ g_free (priv->description);
+ g_free (priv->update_version);
+ g_free (priv->update_version_ui);
+ g_free (priv->update_details);
+ g_free (priv->management_plugin);
+ g_hash_table_unref (priv->metadata);
+ g_ptr_array_unref (priv->categories);
+ g_ptr_array_unref (priv->key_colors);
+ g_clear_object (&priv->cancellable);
+ if (priv->local_file != NULL)
+ g_object_unref (priv->local_file);
+ if (priv->content_rating != NULL)
+ g_object_unref (priv->content_rating);
+ if (priv->pixbuf != NULL)
+ g_object_unref (priv->pixbuf);
+ if (priv->action_screenshot != NULL)
+ g_object_unref (priv->action_screenshot);
+
+ G_OBJECT_CLASS (gs_app_parent_class)->finalize (object);
+}
+
+static void
+gs_app_class_init (GsAppClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ object_class->dispose = gs_app_dispose;
+ object_class->finalize = gs_app_finalize;
+ object_class->get_property = gs_app_get_property;
+ object_class->set_property = gs_app_set_property;
+
+ /**
+ * GsApp:id:
+ */
+ obj_props[PROP_ID] = g_param_spec_string ("id", NULL, NULL,
+ NULL,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT);
+
+ /**
+ * GsApp:name:
+ */
+ obj_props[PROP_NAME] = g_param_spec_string ("name", NULL, NULL,
+ NULL,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT);
+
+ /**
+ * GsApp:version:
+ */
+ obj_props[PROP_VERSION] = g_param_spec_string ("version", NULL, NULL,
+ NULL,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT);
+
+ /**
+ * GsApp:summary:
+ */
+ obj_props[PROP_SUMMARY] = g_param_spec_string ("summary", NULL, NULL,
+ NULL,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT);
+
+ /**
+ * GsApp:description:
+ */
+ obj_props[PROP_DESCRIPTION] = g_param_spec_string ("description", NULL, NULL,
+ NULL,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT);
+
+ /**
+ * GsApp:rating:
+ */
+ obj_props[PROP_RATING] = g_param_spec_int ("rating", NULL, NULL,
+ -1, 100, -1,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT);
+
+ /**
+ * GsApp:kind:
+ */
+ obj_props[PROP_KIND] = g_param_spec_uint ("kind", NULL, NULL,
+ AS_APP_KIND_UNKNOWN,
+ AS_APP_KIND_LAST,
+ AS_APP_KIND_UNKNOWN,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT);
+
+ /**
+ * GsApp:state:
+ */
+ obj_props[PROP_STATE] = g_param_spec_uint ("state", NULL, NULL,
+ AS_APP_STATE_UNKNOWN,
+ AS_APP_STATE_LAST,
+ AS_APP_STATE_UNKNOWN,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT);
+
+ /**
+ * GsApp:progress:
+ *
+ * A percentage (0–100, inclusive) indicating the progress through the
+ * current task on this app. The value may otherwise be
+ * %GS_APP_PROGRESS_UNKNOWN if the progress is unknown or has a wide
+ * confidence interval.
+ */
+ obj_props[PROP_PROGRESS] = g_param_spec_uint ("progress", NULL, NULL,
+ 0, GS_APP_PROGRESS_UNKNOWN, GS_APP_PROGRESS_UNKNOWN,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT);
+
+ /**
+ * GsApp:allow-cancel:
+ */
+ obj_props[PROP_CAN_CANCEL_INSTALLATION] =
+ g_param_spec_boolean ("allow-cancel", NULL, NULL, TRUE,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT);
+
+ /**
+ * GsApp:install-date:
+ */
+ obj_props[PROP_INSTALL_DATE] = g_param_spec_uint64 ("install-date", NULL, NULL,
+ 0, G_MAXUINT64, 0,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT);
+
+ /**
+ * GsApp:quirk:
+ */
+ obj_props[PROP_QUIRK] = g_param_spec_uint64 ("quirk", NULL, NULL,
+ 0, G_MAXUINT64, 0,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT);
+
+ /**
+ * GsApp:pending-action:
+ */
+ obj_props[PROP_PENDING_ACTION] = g_param_spec_uint64 ("pending-action", NULL, NULL,
+ 0, G_MAXUINT64, 0,
+ G_PARAM_READABLE);
+
+ /**
+ * GsApp:key-colors:
+ */
+ obj_props[PROP_KEY_COLORS] = g_param_spec_boxed ("key-colors", NULL, NULL,
+ G_TYPE_PTR_ARRAY, G_PARAM_READWRITE);
+
+ /**
+ * GsApp:is-update-downloaded:
+ */
+ obj_props[PROP_IS_UPDATE_DOWNLOADED] = g_param_spec_boolean ("is-update-downloaded", NULL, NULL,
+ FALSE,
+ G_PARAM_READWRITE);
+
+ g_object_class_install_properties (object_class, PROP_LAST, obj_props);
+}
+
+static void
+gs_app_init (GsApp *app)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+ priv->rating = -1;
+ priv->sources = g_ptr_array_new_with_free_func (g_free);
+ priv->source_ids = g_ptr_array_new_with_free_func (g_free);
+ priv->categories = g_ptr_array_new_with_free_func (g_free);
+ priv->key_colors = g_ptr_array_new_with_free_func ((GDestroyNotify) gdk_rgba_free);
+ priv->addons = gs_app_list_new ();
+ priv->related = gs_app_list_new ();
+ priv->history = gs_app_list_new ();
+ priv->screenshots = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref);
+ priv->reviews = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref);
+ priv->provides = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref);
+ priv->icons = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref);
+ priv->metadata = g_hash_table_new_full (g_str_hash,
+ g_str_equal,
+ g_free,
+ (GDestroyNotify) g_variant_unref);
+ priv->urls = g_hash_table_new_full (g_str_hash,
+ g_str_equal,
+ g_free,
+ g_free);
+ priv->launchables = g_hash_table_new_full (g_str_hash,
+ g_str_equal,
+ g_free,
+ g_free);
+ priv->allow_cancel = TRUE;
+ g_mutex_init (&priv->mutex);
+}
+
+/**
+ * gs_app_new:
+ * @id: an application ID, or %NULL, e.g. "org.gnome.Software.desktop"
+ *
+ * Creates a new application object.
+ *
+ * The ID should only be set when the application ID (with optional prefix) is
+ * known; it is perfectly valid to use gs_app_new() with an @id of %NULL, and
+ * then relying on another plugin to set the @id using gs_app_set_id() based on
+ * some other information.
+ *
+ * For instance, a #GsApp is created with no ID when returning results from the
+ * packagekit plugin, but with the default source name set as the package name.
+ * The source name is read by the appstream plugin, and if matched in the
+ * AppStream XML the correct ID is set, along with other higher quality data
+ * like the application icon and long description.
+ *
+ * Returns: a new #GsApp
+ *
+ * Since: 3.22
+ **/
+GsApp *
+gs_app_new (const gchar *id)
+{
+ GsApp *app;
+ app = g_object_new (GS_TYPE_APP,
+ "id", id,
+ NULL);
+ return GS_APP (app);
+}
+
+/**
+ * gs_app_set_from_unique_id:
+ * @app: a #GsApp
+ * @unique_id: an application unique ID, e.g.
+ * `system/flatpak/gnome/desktop/org.gnome.Software.desktop/master`
+ *
+ * Sets details on an application object.
+ *
+ * The unique ID will be parsed to set some information in the application such
+ * as the scope, bundle kind, id, etc.
+ *
+ * Since: 3.26
+ **/
+void
+gs_app_set_from_unique_id (GsApp *app, const gchar *unique_id)
+{
+ g_auto(GStrv) split = NULL;
+
+ g_return_if_fail (GS_IS_APP (app));
+ g_return_if_fail (unique_id != NULL);
+
+ split = g_strsplit (unique_id, "/", -1);
+ if (g_strv_length (split) != 6)
+ return;
+ if (g_strcmp0 (split[0], "*") != 0)
+ gs_app_set_scope (app, as_app_scope_from_string (split[0]));
+ if (g_strcmp0 (split[1], "*") != 0)
+ gs_app_set_bundle_kind (app, as_bundle_kind_from_string (split[1]));
+ if (g_strcmp0 (split[2], "*") != 0)
+ gs_app_set_origin (app, split[2]);
+ if (g_strcmp0 (split[3], "*") != 0)
+ gs_app_set_kind (app, as_app_kind_from_string (split[3]));
+ if (g_strcmp0 (split[4], "*") != 0)
+ gs_app_set_id (app, split[4]);
+ if (g_strcmp0 (split[5], "*") != 0)
+ gs_app_set_branch (app, split[5]);
+}
+
+/**
+ * gs_app_new_from_unique_id:
+ * @unique_id: an application unique ID, e.g.
+ * `system/flatpak/gnome/desktop/org.gnome.Software.desktop/master`
+ *
+ * Creates a new application object.
+ *
+ * The unique ID will be parsed to set some information in the application such
+ * as the scope, bundle kind, id, etc. Unlike gs_app_new(), it cannot take a
+ * %NULL argument.
+ *
+ * Returns: a new #GsApp
+ *
+ * Since: 3.22
+ **/
+GsApp *
+gs_app_new_from_unique_id (const gchar *unique_id)
+{
+ GsApp *app;
+ g_return_val_if_fail (unique_id != NULL, NULL);
+ app = gs_app_new (NULL);
+ gs_app_set_from_unique_id (app, unique_id);
+ return app;
+}
+
+/**
+ * gs_app_get_origin_ui:
+ * @app: a #GsApp
+ *
+ * Gets the package origin that's suitable for UI use.
+ *
+ * Returns: The package origin for UI use
+ *
+ * Since: 3.32
+ **/
+gchar *
+gs_app_get_origin_ui (GsApp *app)
+{
+ /* use the distro name for official packages */
+ if (gs_app_has_quirk (app, GS_APP_QUIRK_PROVENANCE)) {
+ g_autoptr(GsOsRelease) os_release = gs_os_release_new (NULL);
+ if (os_release != NULL)
+ return g_strdup (gs_os_release_get_name (os_release));
+ }
+
+ /* use "Local file" rather than the filename for local files */
+ if (gs_app_get_state (app) == AS_APP_STATE_AVAILABLE_LOCAL) {
+ /* TRANSLATORS: this is a locally downloaded package */
+ return g_strdup (_("Local file"));
+ }
+
+ /* capitalize "Flathub" and "Flathub Beta" */
+ if (g_strcmp0 (gs_app_get_origin (app), "flathub") == 0) {
+ return g_strdup ("Flathub");
+ } else if (g_strcmp0 (gs_app_get_origin (app), "flathub-beta") == 0) {
+ return g_strdup ("Flathub Beta");
+ }
+
+ /* fall back to origin */
+ return g_strdup (gs_app_get_origin (app));
+}
+
+/**
+ * gs_app_get_packaging_format:
+ * @app: a #GsApp
+ *
+ * Gets the packaging format, e.g. 'RPM' or 'Flatpak'.
+ *
+ * Returns: The packaging format
+ *
+ * Since: 3.32
+ **/
+gchar *
+gs_app_get_packaging_format (GsApp *app)
+{
+ AsBundleKind bundle_kind;
+ const gchar *bundle_kind_ui;
+ const gchar *packaging_format;
+
+ /* does the app have packaging format set? */
+ packaging_format = gs_app_get_metadata_item (app, "GnomeSoftware::PackagingFormat");
+ if (packaging_format != NULL)
+ return g_strdup (packaging_format);
+
+ /* fall back to bundle kind */
+ bundle_kind = gs_app_get_bundle_kind (app);
+ switch (bundle_kind) {
+ case AS_BUNDLE_KIND_UNKNOWN:
+ bundle_kind_ui = NULL;
+ break;
+ case AS_BUNDLE_KIND_LIMBA:
+ bundle_kind_ui = "Limba";
+ break;
+ case AS_BUNDLE_KIND_FLATPAK:
+ bundle_kind_ui = "Flatpak";
+ break;
+ case AS_BUNDLE_KIND_SNAP:
+ bundle_kind_ui = "Snap";
+ break;
+ case AS_BUNDLE_KIND_PACKAGE:
+ bundle_kind_ui = _("Package");
+ break;
+ case AS_BUNDLE_KIND_CABINET:
+ bundle_kind_ui = "Cabinet";
+ break;
+ case AS_BUNDLE_KIND_APPIMAGE:
+ bundle_kind_ui = "AppImage";
+ break;
+ default:
+ g_warning ("unhandled bundle kind %s", as_bundle_kind_to_string (bundle_kind));
+ bundle_kind_ui = as_bundle_kind_to_string (bundle_kind);
+ }
+
+ return g_strdup (bundle_kind_ui);
+}
+
+/**
+ * gs_app_subsume_metadata:
+ * @app: a #GsApp
+ * @donor: another #GsApp
+ *
+ * Copies any metadata from @donor to @app.
+ *
+ * Since: 3.32
+ **/
+void
+gs_app_subsume_metadata (GsApp *app, GsApp *donor)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (donor);
+ g_autoptr(GList) keys = g_hash_table_get_keys (priv->metadata);
+ for (GList *l = keys; l != NULL; l = l->next) {
+ const gchar *key = l->data;
+ GVariant *tmp = gs_app_get_metadata_variant (donor, key);
+ if (gs_app_get_metadata_variant (app, key) != NULL)
+ continue;
+ gs_app_set_metadata_variant (app, key, tmp);
+ }
+}
+
+GsAppPermissions
+gs_app_get_permissions (GsApp *app)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+
+ return priv->permissions;
+}
+
+void
+gs_app_set_permissions (GsApp *app, GsAppPermissions permissions)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+
+ priv->permissions = permissions;
+}
+
+GsAppPermissions
+gs_app_get_update_permissions (GsApp *app)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+
+ return priv->update_permissions;
+}
+
+void
+gs_app_set_update_permissions (GsApp *app, GsAppPermissions update_permissions)
+{
+ GsAppPrivate *priv = gs_app_get_instance_private (app);
+
+ priv->update_permissions = update_permissions;
+}
diff --git a/lib/gs-app.h b/lib/gs-app.h
new file mode 100644
index 0000000..4c199dc
--- /dev/null
+++ b/lib/gs-app.h
@@ -0,0 +1,405 @@
+/* -*- 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) 2014-2018 Kalev Lember <klember@redhat.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+#pragma once
+
+#include <glib-object.h>
+#include <gdk/gdk.h>
+#include <gdk-pixbuf/gdk-pixbuf.h>
+#include <appstream-glib.h>
+
+G_BEGIN_DECLS
+
+#define GS_TYPE_APP (gs_app_get_type ())
+
+G_DECLARE_DERIVABLE_TYPE (GsApp, gs_app, GS, APP, GObject)
+
+struct _GsAppClass
+{
+ GObjectClass parent_class;
+ void (*to_string) (GsApp *app,
+ GString *str);
+ gpointer padding[30];
+};
+
+/**
+ * GsAppKudo:
+ * @GS_APP_KUDO_MY_LANGUAGE: Localised in my language
+ * @GS_APP_KUDO_RECENT_RELEASE: Released recently
+ * @GS_APP_KUDO_FEATURED_RECOMMENDED: Chosen for the front page
+ * @GS_APP_KUDO_MODERN_TOOLKIT: Uses a modern toolkit
+ * @GS_APP_KUDO_SEARCH_PROVIDER: Provides a search provider
+ * @GS_APP_KUDO_INSTALLS_USER_DOCS: Installs user docs
+ * @GS_APP_KUDO_USES_NOTIFICATIONS: Registers notifications
+ * @GS_APP_KUDO_HAS_KEYWORDS: Has at least 1 keyword
+ * @GS_APP_KUDO_HAS_SCREENSHOTS: Supplies screenshots
+ * @GS_APP_KUDO_POPULAR: Is popular
+ * @GS_APP_KUDO_HIGH_CONTRAST: Installs a high contrast icon
+ * @GS_APP_KUDO_HI_DPI_ICON: Installs a HiDPI icon
+ * @GS_APP_KUDO_SANDBOXED: Application is sandboxed
+ * @GS_APP_KUDO_SANDBOXED_SECURE: Application is sandboxed securely
+ *
+ * Any awards given to the application.
+ **/
+typedef enum {
+ GS_APP_KUDO_MY_LANGUAGE = 1 << 0,
+ GS_APP_KUDO_RECENT_RELEASE = 1 << 1,
+ GS_APP_KUDO_FEATURED_RECOMMENDED = 1 << 2,
+ GS_APP_KUDO_MODERN_TOOLKIT = 1 << 3,
+ GS_APP_KUDO_SEARCH_PROVIDER = 1 << 4,
+ GS_APP_KUDO_INSTALLS_USER_DOCS = 1 << 5,
+ GS_APP_KUDO_USES_NOTIFICATIONS = 1 << 6,
+ GS_APP_KUDO_HAS_KEYWORDS = 1 << 7,
+ GS_APP_KUDO_HAS_SCREENSHOTS = 1 << 9,
+ GS_APP_KUDO_POPULAR = 1 << 10,
+ GS_APP_KUDO_HIGH_CONTRAST = 1 << 13,
+ GS_APP_KUDO_HI_DPI_ICON = 1 << 14,
+ GS_APP_KUDO_SANDBOXED = 1 << 15,
+ GS_APP_KUDO_SANDBOXED_SECURE = 1 << 16,
+ /*< private >*/
+ GS_APP_KUDO_LAST
+} GsAppKudo;
+
+/**
+ * GsAppQuirk:
+ * @GS_APP_QUIRK_NONE: No special attributes
+ * @GS_APP_QUIRK_PROVENANCE: Installed by OS vendor
+ * @GS_APP_QUIRK_COMPULSORY: Cannot be removed
+ * @GS_APP_QUIRK_HAS_SOURCE: Has a source to allow staying up-to-date
+ * @GS_APP_QUIRK_IS_WILDCARD: Matches applications from any plugin
+ * @GS_APP_QUIRK_NEEDS_REBOOT: A reboot is required after the action
+ * @GS_APP_QUIRK_NOT_REVIEWABLE: The app is not reviewable
+ * @GS_APP_QUIRK_HAS_SHORTCUT: The app has a shortcut in the system
+ * @GS_APP_QUIRK_NOT_LAUNCHABLE: The app is not launchable (run-able)
+ * @GS_APP_QUIRK_NEEDS_USER_ACTION: The component requires some kind of user action
+ * @GS_APP_QUIRK_IS_PROXY: Is a proxy app that operates on other applications
+ * @GS_APP_QUIRK_REMOVABLE_HARDWARE: The device is unusable whilst the action is performed
+ * @GS_APP_QUIRK_DEVELOPER_VERIFIED: The app developer has been verified
+ * @GS_APP_QUIRK_PARENTAL_FILTER: The app has been filtered by parental controls, and should be hidden
+ * @GS_APP_QUIRK_NEW_PERMISSIONS: The update requires new permissions
+ * @GS_APP_QUIRK_PARENTAL_NOT_LAUNCHABLE: The app cannot be run by the current user due to parental controls, and should not be launchable
+ * @GS_APP_QUIRK_HIDE_FROM_SEARCH: The app should not be shown in search results
+ * @GS_APP_QUIRK_HIDE_EVERYWHERE: The app should not be shown anywhere (it’s blocklisted)
+ * @GS_APP_QUIRK_DO_NOT_AUTO_UPDATE: The app should not be automatically updated
+ *
+ * The application attributes.
+ **/
+typedef enum {
+ GS_APP_QUIRK_NONE = 0, /* Since: 3.32 */
+ GS_APP_QUIRK_PROVENANCE = 1 << 0, /* Since: 3.32 */
+ GS_APP_QUIRK_COMPULSORY = 1 << 1, /* Since: 3.32 */
+ GS_APP_QUIRK_HAS_SOURCE = 1 << 2, /* Since: 3.32 */
+ GS_APP_QUIRK_IS_WILDCARD = 1 << 3, /* Since: 3.32 */
+ GS_APP_QUIRK_NEEDS_REBOOT = 1 << 4, /* Since: 3.32 */
+ GS_APP_QUIRK_NOT_REVIEWABLE = 1 << 5, /* Since: 3.32 */
+ GS_APP_QUIRK_HAS_SHORTCUT = 1 << 6, /* Since: 3.32 */
+ GS_APP_QUIRK_NOT_LAUNCHABLE = 1 << 7, /* Since: 3.32 */
+ GS_APP_QUIRK_NEEDS_USER_ACTION = 1 << 8, /* Since: 3.32 */
+ GS_APP_QUIRK_IS_PROXY = 1 << 9, /* Since: 3.32 */
+ GS_APP_QUIRK_REMOVABLE_HARDWARE = 1 << 10, /* Since: 3.32 */
+ GS_APP_QUIRK_DEVELOPER_VERIFIED = 1 << 11, /* Since: 3.32 */
+ GS_APP_QUIRK_PARENTAL_FILTER = 1 << 12, /* Since: 3.32 */
+ GS_APP_QUIRK_NEW_PERMISSIONS = 1 << 13, /* Since: 3.32 */
+ GS_APP_QUIRK_PARENTAL_NOT_LAUNCHABLE = 1 << 14, /* Since: 3.32 */
+ GS_APP_QUIRK_HIDE_FROM_SEARCH = 1 << 15, /* Since: 3.32 */
+ GS_APP_QUIRK_HIDE_EVERYWHERE = 1 << 16, /* Since: 3.36 */
+ GS_APP_QUIRK_DO_NOT_AUTO_UPDATE = 1 << 17, /* Since: 3.36 */
+ /*< private >*/
+ GS_APP_QUIRK_LAST
+} GsAppQuirk;
+
+#define GS_APP_INSTALL_DATE_UNSET 0
+#define GS_APP_INSTALL_DATE_UNKNOWN 1 /* 1s past the epoch */
+#define GS_APP_SIZE_UNKNOWABLE G_MAXUINT64
+
+/**
+ * GsAppQuality:
+ * @GS_APP_QUALITY_UNKNOWN: The quality value is unknown
+ * @GS_APP_QUALITY_LOWEST: Lowest quality
+ * @GS_APP_QUALITY_NORMAL: Normal quality
+ * @GS_APP_QUALITY_HIGHEST: Highest quality
+ *
+ * Any awards given to the application.
+ **/
+typedef enum {
+ GS_APP_QUALITY_UNKNOWN,
+ GS_APP_QUALITY_LOWEST,
+ GS_APP_QUALITY_NORMAL,
+ GS_APP_QUALITY_HIGHEST,
+ /*< private >*/
+ GS_APP_QUALITY_LAST
+} GsAppQuality;
+
+typedef enum {
+ GS_APP_PERMISSIONS_UNKNOWN = 0,
+ GS_APP_PERMISSIONS_NONE = 1 << 0,
+ GS_APP_PERMISSIONS_NETWORK = 1 << 1,
+ GS_APP_PERMISSIONS_SYSTEM_BUS = 1 << 2,
+ GS_APP_PERMISSIONS_SESSION_BUS = 1 << 3,
+ GS_APP_PERMISSIONS_DEVICES = 1 << 4,
+ GS_APP_PERMISSIONS_HOME_FULL = 1 << 5,
+ GS_APP_PERMISSIONS_HOME_READ = 1 << 6,
+ GS_APP_PERMISSIONS_FILESYSTEM_FULL = 1 << 7,
+ GS_APP_PERMISSIONS_FILESYSTEM_READ = 1 << 8,
+ GS_APP_PERMISSIONS_DOWNLOADS_FULL = 1 << 9,
+ GS_APP_PERMISSIONS_DOWNLOADS_READ = 1 << 10,
+ GS_APP_PERMISSIONS_SETTINGS = 1 << 11,
+ GS_APP_PERMISSIONS_X11 = 1 << 12,
+ GS_APP_PERMISSIONS_ESCAPE_SANDBOX = 1 << 13,
+ /*< private >*/
+ GS_APP_PERMISSIONS_LAST
+} GsAppPermissions;
+
+#define LIMITED_PERMISSIONS (GS_APP_PERMISSIONS_SETTINGS | \
+ GS_APP_PERMISSIONS_NETWORK | \
+ GS_APP_PERMISSIONS_DOWNLOADS_READ | \
+ GS_APP_PERMISSIONS_DOWNLOADS_FULL)
+#define MEDIUM_PERMISSIONS (LIMITED_PERMISSIONS | \
+ GS_APP_PERMISSIONS_X11)
+
+/**
+ * GS_APP_PROGRESS_UNKNOWN:
+ *
+ * A value returned by gs_app_get_progress() if the app’s progress is unknown
+ * or has a wide confidence interval. Typically this would be represented in the
+ * UI using a pulsing progress bar or spinner.
+ *
+ * Since: 3.38
+ */
+#define GS_APP_PROGRESS_UNKNOWN G_MAXUINT
+
+GsApp *gs_app_new (const gchar *id);
+G_DEPRECATED_FOR(gs_app_set_from_unique_id)
+GsApp *gs_app_new_from_unique_id (const gchar *unique_id);
+void gs_app_set_from_unique_id (GsApp *app,
+ const gchar *unique_id);
+gchar *gs_app_to_string (GsApp *app);
+void gs_app_to_string_append (GsApp *app,
+ GString *str);
+
+const gchar *gs_app_get_id (GsApp *app);
+void gs_app_set_id (GsApp *app,
+ const gchar *id);
+AsAppKind gs_app_get_kind (GsApp *app);
+void gs_app_set_kind (GsApp *app,
+ AsAppKind kind);
+AsAppState gs_app_get_state (GsApp *app);
+void gs_app_set_state (GsApp *app,
+ AsAppState state);
+AsAppScope gs_app_get_scope (GsApp *app);
+void gs_app_set_scope (GsApp *app,
+ AsAppScope scope);
+AsBundleKind gs_app_get_bundle_kind (GsApp *app);
+void gs_app_set_bundle_kind (GsApp *app,
+ AsBundleKind bundle_kind);
+void gs_app_set_state_recover (GsApp *app);
+guint gs_app_get_progress (GsApp *app);
+void gs_app_set_progress (GsApp *app,
+ guint percentage);
+gboolean gs_app_get_allow_cancel (GsApp *app);
+void gs_app_set_allow_cancel (GsApp *app,
+ gboolean allow_cancel);
+const gchar *gs_app_get_unique_id (GsApp *app);
+const gchar *gs_app_get_branch (GsApp *app);
+void gs_app_set_branch (GsApp *app,
+ const gchar *branch);
+const gchar *gs_app_get_name (GsApp *app);
+void gs_app_set_name (GsApp *app,
+ GsAppQuality quality,
+ const gchar *name);
+const gchar *gs_app_get_source_default (GsApp *app);
+void gs_app_add_source (GsApp *app,
+ const gchar *source);
+GPtrArray *gs_app_get_sources (GsApp *app);
+void gs_app_set_sources (GsApp *app,
+ GPtrArray *sources);
+const gchar *gs_app_get_source_id_default (GsApp *app);
+void gs_app_add_source_id (GsApp *app,
+ const gchar *source_id);
+GPtrArray *gs_app_get_source_ids (GsApp *app);
+void gs_app_set_source_ids (GsApp *app,
+ GPtrArray *source_ids);
+void gs_app_clear_source_ids (GsApp *app);
+const gchar *gs_app_get_project_group (GsApp *app);
+void gs_app_set_project_group (GsApp *app,
+ const gchar *project_group);
+const gchar *gs_app_get_developer_name (GsApp *app);
+void gs_app_set_developer_name (GsApp *app,
+ const gchar *developer_name);
+const gchar *gs_app_get_agreement (GsApp *app);
+void gs_app_set_agreement (GsApp *app,
+ const gchar *agreement);
+const gchar *gs_app_get_version (GsApp *app);
+const gchar *gs_app_get_version_ui (GsApp *app);
+void gs_app_set_version (GsApp *app,
+ const gchar *version);
+const gchar *gs_app_get_summary (GsApp *app);
+void gs_app_set_summary (GsApp *app,
+ GsAppQuality quality,
+ const gchar *summary);
+const gchar *gs_app_get_summary_missing (GsApp *app);
+void gs_app_set_summary_missing (GsApp *app,
+ const gchar *summary_missing);
+const gchar *gs_app_get_description (GsApp *app);
+void gs_app_set_description (GsApp *app,
+ GsAppQuality quality,
+ const gchar *description);
+const gchar *gs_app_get_url (GsApp *app,
+ AsUrlKind kind);
+void gs_app_set_url (GsApp *app,
+ AsUrlKind kind,
+ const gchar *url);
+const gchar *gs_app_get_launchable (GsApp *app,
+ AsLaunchableKind kind);
+void gs_app_set_launchable (GsApp *app,
+ AsLaunchableKind kind,
+ const gchar *launchable);
+const gchar *gs_app_get_license (GsApp *app);
+gboolean gs_app_get_license_is_free (GsApp *app);
+void gs_app_set_license (GsApp *app,
+ GsAppQuality quality,
+ const gchar *license);
+gchar **gs_app_get_menu_path (GsApp *app);
+void gs_app_set_menu_path (GsApp *app,
+ gchar **menu_path);
+const gchar *gs_app_get_origin (GsApp *app);
+void gs_app_set_origin (GsApp *app,
+ const gchar *origin);
+const gchar *gs_app_get_origin_appstream (GsApp *app);
+void gs_app_set_origin_appstream (GsApp *app,
+ const gchar *origin_appstream);
+const gchar *gs_app_get_origin_hostname (GsApp *app);
+void gs_app_set_origin_hostname (GsApp *app,
+ const gchar *origin_hostname);
+GPtrArray *gs_app_get_screenshots (GsApp *app);
+void gs_app_add_screenshot (GsApp *app,
+ AsScreenshot *screenshot);
+AsScreenshot *gs_app_get_action_screenshot (GsApp *app);
+void gs_app_set_action_screenshot (GsApp *app,
+ AsScreenshot *screenshot);
+const gchar *gs_app_get_update_version (GsApp *app);
+const gchar *gs_app_get_update_version_ui (GsApp *app);
+void gs_app_set_update_version (GsApp *app,
+ const gchar *update_version);
+const gchar *gs_app_get_update_details (GsApp *app);
+void gs_app_set_update_details (GsApp *app,
+ const gchar *update_details);
+AsUrgencyKind gs_app_get_update_urgency (GsApp *app);
+void gs_app_set_update_urgency (GsApp *app,
+ AsUrgencyKind update_urgency);
+const gchar *gs_app_get_management_plugin (GsApp *app);
+void gs_app_set_management_plugin (GsApp *app,
+ const gchar *management_plugin);
+GdkPixbuf *gs_app_get_pixbuf (GsApp *app);
+void gs_app_set_pixbuf (GsApp *app,
+ GdkPixbuf *pixbuf);
+GPtrArray *gs_app_get_icons (GsApp *app);
+void gs_app_add_icon (GsApp *app,
+ AsIcon *icon);
+gboolean gs_app_get_use_drop_shadow (GsApp *app);
+GFile *gs_app_get_local_file (GsApp *app);
+void gs_app_set_local_file (GsApp *app,
+ GFile *local_file);
+AsContentRating *gs_app_get_content_rating (GsApp *app);
+void gs_app_set_content_rating (GsApp *app,
+ AsContentRating *content_rating);
+GsApp *gs_app_get_runtime (GsApp *app);
+void gs_app_set_runtime (GsApp *app,
+ GsApp *runtime);
+const gchar *gs_app_get_metadata_item (GsApp *app,
+ const gchar *key);
+GVariant *gs_app_get_metadata_variant (GsApp *app,
+ const gchar *key);
+void gs_app_set_metadata (GsApp *app,
+ const gchar *key,
+ const gchar *value);
+void gs_app_set_metadata_variant (GsApp *app,
+ const gchar *key,
+ GVariant *value);
+gint gs_app_get_rating (GsApp *app);
+void gs_app_set_rating (GsApp *app,
+ gint rating);
+GArray *gs_app_get_review_ratings (GsApp *app);
+void gs_app_set_review_ratings (GsApp *app,
+ GArray *review_ratings);
+GPtrArray *gs_app_get_reviews (GsApp *app);
+void gs_app_add_review (GsApp *app,
+ AsReview *review);
+void gs_app_remove_review (GsApp *app,
+ AsReview *review);
+GPtrArray *gs_app_get_provides (GsApp *app);
+void gs_app_add_provide (GsApp *app,
+ AsProvide *provide);
+guint64 gs_app_get_size_installed (GsApp *app);
+void gs_app_set_size_installed (GsApp *app,
+ guint64 size_installed);
+guint64 gs_app_get_size_download (GsApp *app);
+void gs_app_set_size_download (GsApp *app,
+ guint64 size_download);
+void gs_app_add_related (GsApp *app,
+ GsApp *app2);
+void gs_app_add_addon (GsApp *app,
+ GsApp *addon);
+void gs_app_add_history (GsApp *app,
+ GsApp *app2);
+guint64 gs_app_get_install_date (GsApp *app);
+void gs_app_set_install_date (GsApp *app,
+ guint64 install_date);
+GPtrArray *gs_app_get_categories (GsApp *app);
+void gs_app_set_categories (GsApp *app,
+ GPtrArray *categories);
+GPtrArray *gs_app_get_key_colors (GsApp *app);
+void gs_app_set_key_colors (GsApp *app,
+ GPtrArray *key_colors);
+void gs_app_add_key_color (GsApp *app,
+ GdkRGBA *key_color);
+void gs_app_set_is_update_downloaded (GsApp *app,
+ gboolean is_update_downloaded);
+gboolean gs_app_get_is_update_downloaded (GsApp *app);
+gboolean gs_app_has_category (GsApp *app,
+ const gchar *category);
+void gs_app_add_category (GsApp *app,
+ const gchar *category);
+gboolean gs_app_remove_category (GsApp *app,
+ const gchar *category);
+void gs_app_add_kudo (GsApp *app,
+ GsAppKudo kudo);
+void gs_app_remove_kudo (GsApp *app,
+ GsAppKudo kudo);
+gboolean gs_app_has_kudo (GsApp *app,
+ GsAppKudo kudo);
+guint64 gs_app_get_kudos (GsApp *app);
+guint gs_app_get_kudos_percentage (GsApp *app);
+gboolean gs_app_get_to_be_installed (GsApp *app);
+void gs_app_set_to_be_installed (GsApp *app,
+ gboolean to_be_installed);
+void gs_app_set_match_value (GsApp *app,
+ guint match_value);
+guint gs_app_get_match_value (GsApp *app);
+
+gboolean gs_app_has_quirk (GsApp *app,
+ GsAppQuirk quirk);
+void gs_app_add_quirk (GsApp *app,
+ GsAppQuirk quirk);
+void gs_app_remove_quirk (GsApp *app,
+ GsAppQuirk quirk);
+gboolean gs_app_is_installed (GsApp *app);
+gboolean gs_app_is_updatable (GsApp *app);
+gchar *gs_app_get_origin_ui (GsApp *app);
+gchar *gs_app_get_packaging_format (GsApp *app);
+void gs_app_subsume_metadata (GsApp *app,
+ GsApp *donor);
+GsAppPermissions gs_app_get_permissions (GsApp *app);
+void gs_app_set_permissions (GsApp *app,
+ GsAppPermissions permissions);
+GsAppPermissions gs_app_get_update_permissions (GsApp *app);
+void gs_app_set_update_permissions (GsApp *app,
+ GsAppPermissions update_permissions);
+
+G_END_DECLS
diff --git a/lib/gs-autocleanups.h b/lib/gs-autocleanups.h
new file mode 100644
index 0000000..aaba9e7
--- /dev/null
+++ b/lib/gs-autocleanups.h
@@ -0,0 +1,52 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2019 Kalev Lember <klember@redhat.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+#pragma once
+
+G_BEGIN_DECLS
+
+#if !GLIB_CHECK_VERSION(2, 61, 1)
+
+/* Backported GRWLock autoptr support for older glib versions */
+
+typedef void GRWLockWriterLocker;
+
+static inline GRWLockWriterLocker *
+g_rw_lock_writer_locker_new (GRWLock *rw_lock)
+{
+ g_rw_lock_writer_lock (rw_lock);
+ return (GRWLockWriterLocker *) rw_lock;
+}
+
+static inline void
+g_rw_lock_writer_locker_free (GRWLockWriterLocker *locker)
+{
+ g_rw_lock_writer_unlock ((GRWLock *) locker);
+}
+
+typedef void GRWLockReaderLocker;
+
+static inline GRWLockReaderLocker *
+g_rw_lock_reader_locker_new (GRWLock *rw_lock)
+{
+ g_rw_lock_reader_lock (rw_lock);
+ return (GRWLockReaderLocker *) rw_lock;
+}
+
+static inline void
+g_rw_lock_reader_locker_free (GRWLockReaderLocker *locker)
+{
+ g_rw_lock_reader_unlock ((GRWLock *) locker);
+}
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC(GRWLockWriterLocker, g_rw_lock_writer_locker_free)
+G_DEFINE_AUTOPTR_CLEANUP_FUNC(GRWLockReaderLocker, g_rw_lock_reader_locker_free)
+
+#endif
+
+G_END_DECLS
diff --git a/lib/gs-category-private.h b/lib/gs-category-private.h
new file mode 100644
index 0000000..1c9c292
--- /dev/null
+++ b/lib/gs-category-private.h
@@ -0,0 +1,21 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2013-2016 Richard Hughes <richard@hughsie.com>
+ * Copyright (C) 2013 Matthias Clasen <mclasen@redhat.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+#pragma once
+
+#include "gs-category.h"
+
+G_BEGIN_DECLS
+
+void gs_category_sort_children (GsCategory *category);
+void gs_category_set_size (GsCategory *category,
+ guint size);
+gchar *gs_category_to_string (GsCategory *category);
+
+G_END_DECLS
diff --git a/lib/gs-category.c b/lib/gs-category.c
new file mode 100644
index 0000000..9da2c73
--- /dev/null
+++ b/lib/gs-category.c
@@ -0,0 +1,527 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2013-2016 Richard Hughes <richard@hughsie.com>
+ * Copyright (C) 2013 Matthias Clasen <mclasen@redhat.com>
+ * Copyright (C) 2015 Kalev Lember <klember@redhat.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+/**
+ * SECTION:gs-category
+ * @short_description: An category that contains applications
+ *
+ * This object provides functionality that allows a plugin to create
+ * a tree structure of categories that each contain #GsApp's.
+ */
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+
+#include "gs-category-private.h"
+
+struct _GsCategory
+{
+ GObject parent_instance;
+
+ gchar *id;
+ gchar *name;
+ gchar *icon;
+ gint score;
+ GPtrArray *desktop_groups;
+ GsCategory *parent;
+ guint size;
+ GPtrArray *children;
+};
+
+G_DEFINE_TYPE (GsCategory, gs_category, G_TYPE_OBJECT)
+
+/**
+ * gs_category_to_string:
+ * @category: a #GsCategory
+ *
+ * Returns a string representation of the category
+ *
+ * Returns: a string
+ *
+ * Since: 3.22
+ **/
+gchar *
+gs_category_to_string (GsCategory *category)
+{
+ guint i;
+ GString *str = g_string_new (NULL);
+ g_string_append_printf (str, "GsCategory[%p]:\n", category);
+ g_string_append_printf (str, " id: %s\n",
+ category->id);
+ if (category->name != NULL) {
+ g_string_append_printf (str, " name: %s\n",
+ category->name);
+ }
+ if (category->icon != NULL) {
+ g_string_append_printf (str, " icon: %s\n",
+ category->icon);
+ }
+ g_string_append_printf (str, " size: %u\n",
+ category->size);
+ g_string_append_printf (str, " desktop-groups: %u\n",
+ category->desktop_groups->len);
+ if (category->parent != NULL) {
+ g_string_append_printf (str, " parent: %s\n",
+ gs_category_get_id (category->parent));
+ }
+ g_string_append_printf (str, " score: %i\n", category->score);
+ if (category->children->len == 0) {
+ g_string_append_printf (str, " children: %u\n",
+ category->children->len);
+ } else {
+ g_string_append (str, " children:\n");
+ for (i = 0; i < category->children->len; i++) {
+ GsCategory *child = g_ptr_array_index (category->children, i);
+ g_string_append_printf (str, " - %s\n",
+ gs_category_get_id (child));
+ }
+ }
+ return g_string_free (str, FALSE);
+}
+
+/**
+ * gs_category_get_size:
+ * @category: a #GsCategory
+ *
+ * Returns how many applications the category could contain.
+ *
+ * NOTE: This may over-estimate the number if duplicate applications are
+ * filtered or core applications are not shown.
+ *
+ * Returns: the number of apps in the category
+ *
+ * Since: 3.22
+ **/
+guint
+gs_category_get_size (GsCategory *category)
+{
+ g_return_val_if_fail (GS_IS_CATEGORY (category), 0);
+ return category->size;
+}
+
+/**
+ * gs_category_set_size:
+ * @category: a #GsCategory
+ * @size: the number of applications
+ *
+ * Sets the number of applications in the category.
+ * Most plugins do not need to call this function.
+ *
+ * Since: 3.22
+ **/
+void
+gs_category_set_size (GsCategory *category, guint size)
+{
+ g_return_if_fail (GS_IS_CATEGORY (category));
+ category->size = size;
+}
+
+/**
+ * gs_category_increment_size:
+ * @category: a #GsCategory
+ *
+ * Adds one to the size count if an application is available
+ *
+ * Since: 3.22
+ **/
+void
+gs_category_increment_size (GsCategory *category)
+{
+ g_return_if_fail (GS_IS_CATEGORY (category));
+ category->size++;
+}
+
+/**
+ * gs_category_get_id:
+ * @category: a #GsCategory
+ *
+ * Gets the category ID.
+ *
+ * Returns: the string, e.g. "other"
+ *
+ * Since: 3.22
+ **/
+const gchar *
+gs_category_get_id (GsCategory *category)
+{
+ g_return_val_if_fail (GS_IS_CATEGORY (category), NULL);
+ return category->id;
+}
+
+/**
+ * gs_category_get_name:
+ * @category: a #GsCategory
+ *
+ * Gets the category name.
+ *
+ * Returns: the string, or %NULL
+ *
+ * Since: 3.22
+ **/
+const gchar *
+gs_category_get_name (GsCategory *category)
+{
+ g_return_val_if_fail (GS_IS_CATEGORY (category), NULL);
+
+ /* special case, we don't want translations in the plugins */
+ if (g_strcmp0 (category->id, "other") == 0) {
+ /* TRANSLATORS: this is where all applications that don't
+ * fit in other groups are put */
+ return _("Other");
+ }
+ if (g_strcmp0 (category->id, "all") == 0) {
+ /* TRANSLATORS: this is a subcategory matching all the
+ * different apps in the parent category, e.g. "Games" */
+ return _("All");
+ }
+ if (g_strcmp0 (category->id, "featured") == 0) {
+ /* TRANSLATORS: this is a subcategory of featured apps */
+ return _("Featured");
+ }
+
+ return category->name;
+}
+
+/**
+ * gs_category_set_name:
+ * @category: a #GsCategory
+ * @name: a category name, or %NULL
+ *
+ * Sets the category name.
+ *
+ * Since: 3.22
+ **/
+void
+gs_category_set_name (GsCategory *category, const gchar *name)
+{
+ g_return_if_fail (GS_IS_CATEGORY (category));
+ g_free (category->name);
+ category->name = g_strdup (name);
+}
+
+/**
+ * gs_category_get_icon:
+ * @category: a #GsCategory
+ *
+ * Gets the category icon.
+ *
+ * Returns: the string, or %NULL
+ *
+ * Since: 3.22
+ **/
+const gchar *
+gs_category_get_icon (GsCategory *category)
+{
+ g_return_val_if_fail (GS_IS_CATEGORY (category), NULL);
+
+ /* special case */
+ if (g_strcmp0 (category->id, "other") == 0)
+ return "emblem-system-symbolic";
+ if (g_strcmp0 (category->id, "all") == 0)
+ return "emblem-default-symbolic";
+ if (g_strcmp0 (category->id, "featured") == 0)
+ return "emblem-favorite-symbolic";
+
+ return category->icon;
+}
+
+/**
+ * gs_category_set_icon:
+ * @category: a #GsCategory
+ * @icon: a category icon, or %NULL
+ *
+ * Sets the category icon.
+ *
+ * Since: 3.22
+ **/
+void
+gs_category_set_icon (GsCategory *category, const gchar *icon)
+{
+ g_return_if_fail (GS_IS_CATEGORY (category));
+ g_free (category->icon);
+ category->icon = g_strdup (icon);
+}
+
+/**
+ * gs_category_get_score:
+ * @category: a #GsCategory
+ *
+ * Gets if the category score.
+ * Important categories may be shown before other categories, or tagged in a
+ * different way, for example with color or in a different section.
+ *
+ * Returns: the string, or %NULL
+ *
+ * Since: 3.22
+ **/
+gint
+gs_category_get_score (GsCategory *category)
+{
+ g_return_val_if_fail (GS_IS_CATEGORY (category), FALSE);
+ return category->score;
+}
+
+/**
+ * gs_category_set_score:
+ * @category: a #GsCategory
+ * @score: a category score, or %NULL
+ *
+ * Sets the category score, where larger numbers get sorted before lower
+ * numbers.
+ *
+ * Since: 3.22
+ **/
+void
+gs_category_set_score (GsCategory *category, gint score)
+{
+ g_return_if_fail (GS_IS_CATEGORY (category));
+ category->score = score;
+}
+
+/**
+ * gs_category_get_desktop_groups:
+ * @category: a #GsCategory
+ *
+ * Gets the list of AppStream groups for the category.
+ *
+ * Returns: (element-type utf8) (transfer none): An array
+ *
+ * Since: 3.22
+ **/
+GPtrArray *
+gs_category_get_desktop_groups (GsCategory *category)
+{
+ g_return_val_if_fail (GS_IS_CATEGORY (category), NULL);
+ return category->desktop_groups;
+}
+
+/**
+ * gs_category_has_desktop_group:
+ * @category: a #GsCategory
+ * @desktop_group: a group of categories found in AppStream, e.g. "AudioVisual::Player"
+ *
+ * Finds out if the category has the specific AppStream desktop group.
+ *
+ * Returns: %TRUE if found, %FALSE otherwise
+ *
+ * Since: 3.22
+ **/
+gboolean
+gs_category_has_desktop_group (GsCategory *category, const gchar *desktop_group)
+{
+ guint i;
+
+ g_return_val_if_fail (GS_IS_CATEGORY (category), FALSE);
+ g_return_val_if_fail (desktop_group != NULL, FALSE);
+
+ for (i = 0; i < category->desktop_groups->len; i++) {
+ const gchar *tmp = g_ptr_array_index (category->desktop_groups, i);
+ if (g_strcmp0 (tmp, desktop_group) == 0)
+ return TRUE;
+ }
+ return FALSE;
+}
+
+/**
+ * gs_category_add_desktop_group:
+ * @category: a #GsCategory
+ * @desktop_group: a group of categories found in AppStream, e.g. "AudioVisual::Player"
+ *
+ * Adds a desktop group to the category.
+ * A desktop group is a set of category strings that all must exist.
+ *
+ * Since: 3.22
+ **/
+void
+gs_category_add_desktop_group (GsCategory *category, const gchar *desktop_group)
+{
+ g_return_if_fail (GS_IS_CATEGORY (category));
+ g_return_if_fail (desktop_group != NULL);
+
+ /* add if not already found */
+ if (gs_category_has_desktop_group (category, desktop_group))
+ return;
+ g_ptr_array_add (category->desktop_groups, g_strdup (desktop_group));
+}
+
+/**
+ * gs_category_find_child:
+ * @category: a #GsCategory
+ * @id: a category ID, e.g. "other"
+ *
+ * Find a child category with a specific ID.
+ *
+ * Returns: (transfer none): the #GsCategory, or %NULL
+ *
+ * Since: 3.22
+ **/
+GsCategory *
+gs_category_find_child (GsCategory *category, const gchar *id)
+{
+ GsCategory *tmp;
+ guint i;
+
+ /* find the subcategory */
+ for (i = 0; i < category->children->len; i++) {
+ tmp = GS_CATEGORY (g_ptr_array_index (category->children, i));
+ if (g_strcmp0 (id, gs_category_get_id (tmp)) == 0)
+ return tmp;
+ }
+ return NULL;
+}
+
+/**
+ * gs_category_get_parent:
+ * @category: a #GsCategory
+ *
+ * Gets the parent category.
+ *
+ * Returns: the #GsCategory or %NULL
+ *
+ * Since: 3.22
+ **/
+GsCategory *
+gs_category_get_parent (GsCategory *category)
+{
+ g_return_val_if_fail (GS_IS_CATEGORY (category), NULL);
+ return category->parent;
+}
+
+/**
+ * gs_category_get_children:
+ * @category: a #GsCategory
+ *
+ * Gets the list if children for a category.
+ *
+ * Return value: (element-type GsApp) (transfer none): A list of children
+ *
+ * Since: 3.22
+ **/
+GPtrArray *
+gs_category_get_children (GsCategory *category)
+{
+ g_return_val_if_fail (GS_IS_CATEGORY (category), NULL);
+ return category->children;
+}
+
+/**
+ * gs_category_add_child:
+ * @category: a #GsCategory
+ * @subcategory: a #GsCategory
+ *
+ * Adds a child category to a parent category.
+ *
+ * Since: 3.22
+ **/
+void
+gs_category_add_child (GsCategory *category, GsCategory *subcategory)
+{
+ g_return_if_fail (GS_IS_CATEGORY (category));
+ g_return_if_fail (GS_IS_CATEGORY (subcategory));
+
+ /* FIXME: do we need this? */
+ subcategory->parent = category;
+ g_object_add_weak_pointer (G_OBJECT (subcategory->parent),
+ (gpointer *) &subcategory->parent);
+
+ g_ptr_array_add (category->children,
+ g_object_ref (subcategory));
+}
+
+static gchar *
+gs_category_get_sort_key (GsCategory *category)
+{
+ guint sort_order = 5;
+ if (g_strcmp0 (gs_category_get_id (category), "featured") == 0)
+ sort_order = 0;
+ else if (g_strcmp0 (gs_category_get_id (category), "all") == 0)
+ sort_order = 2;
+ else if (g_strcmp0 (gs_category_get_id (category), "other") == 0)
+ sort_order = 9;
+ return g_strdup_printf ("%u:%s",
+ sort_order,
+ gs_category_get_name (category));
+}
+
+static gint
+gs_category_sort_children_cb (gconstpointer a, gconstpointer b)
+{
+ GsCategory *ca = GS_CATEGORY (*(GsCategory **) a);
+ GsCategory *cb = GS_CATEGORY (*(GsCategory **) b);
+ g_autofree gchar *id_a = gs_category_get_sort_key (ca);
+ g_autofree gchar *id_b = gs_category_get_sort_key (cb);
+ return g_strcmp0 (id_a, id_b);
+}
+
+/**
+ * gs_category_sort_children:
+ * @category: a #GsCategory
+ *
+ * Sorts the list of children.
+ *
+ * Since: 3.22
+ **/
+void
+gs_category_sort_children (GsCategory *category)
+{
+ g_ptr_array_sort (category->children,
+ gs_category_sort_children_cb);
+}
+
+static void
+gs_category_finalize (GObject *object)
+{
+ GsCategory *category = GS_CATEGORY (object);
+
+ if (category->parent != NULL)
+ g_object_remove_weak_pointer (G_OBJECT (category->parent),
+ (gpointer *) &category->parent);
+ g_ptr_array_unref (category->children);
+ g_ptr_array_unref (category->desktop_groups);
+ g_free (category->id);
+ g_free (category->name);
+ g_free (category->icon);
+
+ G_OBJECT_CLASS (gs_category_parent_class)->finalize (object);
+}
+
+static void
+gs_category_class_init (GsCategoryClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ object_class->finalize = gs_category_finalize;
+}
+
+static void
+gs_category_init (GsCategory *category)
+{
+ category->children = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref);
+ category->desktop_groups = g_ptr_array_new_with_free_func (g_free);
+}
+
+/**
+ * gs_category_new:
+ * @id: an ID, e.g. "all"
+ *
+ * Creates a new category object.
+ *
+ * Returns: the new #GsCategory
+ *
+ * Since: 3.22
+ **/
+GsCategory *
+gs_category_new (const gchar *id)
+{
+ GsCategory *category;
+ category = g_object_new (GS_TYPE_CATEGORY, NULL);
+ category->id = g_strdup (id);
+ return GS_CATEGORY (category);
+}
diff --git a/lib/gs-category.h b/lib/gs-category.h
new file mode 100644
index 0000000..e60bef2
--- /dev/null
+++ b/lib/gs-category.h
@@ -0,0 +1,51 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2013-2016 Richard Hughes <richard@hughsie.com>
+ * Copyright (C) 2013 Matthias Clasen <mclasen@redhat.com>
+ * Copyright (C) 2015 Kalev Lember <klember@redhat.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+#pragma once
+
+#include <glib-object.h>
+#include <gdk/gdk.h>
+
+G_BEGIN_DECLS
+
+#define GS_TYPE_CATEGORY (gs_category_get_type ())
+
+G_DECLARE_FINAL_TYPE (GsCategory, gs_category, GS, CATEGORY, GObject)
+
+GsCategory *gs_category_new (const gchar *id);
+const gchar *gs_category_get_id (GsCategory *category);
+GsCategory *gs_category_get_parent (GsCategory *category);
+
+const gchar *gs_category_get_name (GsCategory *category);
+void gs_category_set_name (GsCategory *category,
+ const gchar *name);
+const gchar *gs_category_get_icon (GsCategory *category);
+void gs_category_set_icon (GsCategory *category,
+ const gchar *icon);
+gint gs_category_get_score (GsCategory *category);
+void gs_category_set_score (GsCategory *category,
+ gint score);
+
+GPtrArray *gs_category_get_desktop_groups (GsCategory *category);
+gboolean gs_category_has_desktop_group (GsCategory *category,
+ const gchar *desktop_group);
+void gs_category_add_desktop_group (GsCategory *category,
+ const gchar *desktop_group);
+
+GsCategory *gs_category_find_child (GsCategory *category,
+ const gchar *id);
+GPtrArray *gs_category_get_children (GsCategory *category);
+void gs_category_add_child (GsCategory *category,
+ GsCategory *subcategory);
+
+guint gs_category_get_size (GsCategory *category);
+void gs_category_increment_size (GsCategory *category);
+
+G_END_DECLS
diff --git a/lib/gs-cmd.c b/lib/gs-cmd.c
new file mode 100644
index 0000000..bc01c95
--- /dev/null
+++ b/lib/gs-cmd.c
@@ -0,0 +1,699 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2013-2017 Richard Hughes <richard@hughsie.com>
+ * Copyright (C) 2014-2015 Kalev Lember <klember@redhat.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+#include "config.h"
+
+#include <glib/gi18n.h>
+#include <gtk/gtk.h>
+#include <locale.h>
+
+#include "gnome-software-private.h"
+
+#include "gs-debug.h"
+
+typedef struct {
+ GsPluginLoader *plugin_loader;
+ guint64 refine_flags;
+ guint max_results;
+} GsCmdSelf;
+
+static void
+gs_cmd_show_results_apps (GsAppList *list)
+{
+ for (guint j = 0; j < gs_app_list_length (list); j++) {
+ GsApp *app = gs_app_list_index (list, j);
+ GsAppList *related = gs_app_get_related (app);
+ g_autofree gchar *tmp = gs_app_to_string (app);
+ g_print ("%s\n", tmp);
+ for (guint i = 0; i < gs_app_list_length (related); i++) {
+ g_autofree gchar *tmp_rel = NULL;
+ GsApp *app_rel = GS_APP (gs_app_list_index (related, i));
+ tmp_rel = gs_app_to_string (app_rel);
+ g_print ("\t%s\n", tmp_rel);
+ }
+ }
+}
+
+static gchar *
+gs_cmd_pad_spaces (const gchar *text, guint length)
+{
+ gsize i;
+ GString *str;
+ str = g_string_sized_new (length + 1);
+ g_string_append (str, text);
+ for (i = strlen (text); i < length; i++)
+ g_string_append_c (str, ' ');
+ return g_string_free (str, FALSE);
+}
+
+static void
+gs_cmd_show_results_categories (GPtrArray *list)
+{
+ for (guint i = 0; i < list->len; i++) {
+ GsCategory *cat = GS_CATEGORY (g_ptr_array_index (list, i));
+ GsCategory *parent = gs_category_get_parent (cat);
+ g_autofree gchar *tmp = NULL;
+ if (parent != NULL){
+ g_autofree gchar *id = NULL;
+ id = g_strdup_printf ("%s/%s [%u]",
+ gs_category_get_id (parent),
+ gs_category_get_id (cat),
+ gs_category_get_size (cat));
+ tmp = gs_cmd_pad_spaces (id, 32);
+ g_print ("%s : %s\n",
+ tmp, gs_category_get_name (cat));
+ } else {
+ GPtrArray *subcats = gs_category_get_children (cat);
+ tmp = gs_cmd_pad_spaces (gs_category_get_id (cat), 32);
+ g_print ("%s : %s\n",
+ tmp, gs_category_get_name (cat));
+ gs_cmd_show_results_categories (subcats);
+ }
+ }
+}
+
+static GsPluginRefineFlags
+gs_cmd_refine_flag_from_string (const gchar *flag, GError **error)
+{
+ if (g_strcmp0 (flag, "all") == 0)
+ return G_MAXINT32;
+ if (g_strcmp0 (flag, "license") == 0)
+ return GS_PLUGIN_REFINE_FLAGS_REQUIRE_LICENSE;
+ if (g_strcmp0 (flag, "url") == 0)
+ return GS_PLUGIN_REFINE_FLAGS_REQUIRE_URL;
+ if (g_strcmp0 (flag, "description") == 0)
+ return GS_PLUGIN_REFINE_FLAGS_REQUIRE_DESCRIPTION;
+ if (g_strcmp0 (flag, "size") == 0)
+ return GS_PLUGIN_REFINE_FLAGS_REQUIRE_SIZE;
+ if (g_strcmp0 (flag, "rating") == 0)
+ return GS_PLUGIN_REFINE_FLAGS_REQUIRE_RATING;
+ if (g_strcmp0 (flag, "version") == 0)
+ return GS_PLUGIN_REFINE_FLAGS_REQUIRE_VERSION;
+ if (g_strcmp0 (flag, "history") == 0)
+ return GS_PLUGIN_REFINE_FLAGS_REQUIRE_HISTORY;
+ if (g_strcmp0 (flag, "setup-action") == 0)
+ return GS_PLUGIN_REFINE_FLAGS_REQUIRE_SETUP_ACTION;
+ if (g_strcmp0 (flag, "update-details") == 0)
+ return GS_PLUGIN_REFINE_FLAGS_REQUIRE_UPDATE_DETAILS;
+ if (g_strcmp0 (flag, "origin") == 0)
+ return GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN;
+ if (g_strcmp0 (flag, "related") == 0)
+ return GS_PLUGIN_REFINE_FLAGS_REQUIRE_RELATED;
+ if (g_strcmp0 (flag, "menu-path") == 0)
+ return GS_PLUGIN_REFINE_FLAGS_REQUIRE_MENU_PATH;
+ if (g_strcmp0 (flag, "upgrade-removed") == 0)
+ return GS_PLUGIN_REFINE_FLAGS_REQUIRE_UPGRADE_REMOVED;
+ if (g_strcmp0 (flag, "provenance") == 0)
+ return GS_PLUGIN_REFINE_FLAGS_REQUIRE_PROVENANCE;
+ if (g_strcmp0 (flag, "reviews") == 0)
+ return GS_PLUGIN_REFINE_FLAGS_REQUIRE_REVIEWS;
+ if (g_strcmp0 (flag, "review-ratings") == 0)
+ return GS_PLUGIN_REFINE_FLAGS_REQUIRE_REVIEW_RATINGS;
+ if (g_strcmp0 (flag, "key-colors") == 0)
+ return GS_PLUGIN_REFINE_FLAGS_REQUIRE_KEY_COLORS;
+ if (g_strcmp0 (flag, "icon") == 0)
+ return GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON;
+ if (g_strcmp0 (flag, "permissions") == 0)
+ return GS_PLUGIN_REFINE_FLAGS_REQUIRE_PERMISSIONS;
+ if (g_strcmp0 (flag, "origin-hostname") == 0)
+ return GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN_HOSTNAME;
+ if (g_strcmp0 (flag, "origin-ui") == 0)
+ return GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN_UI;
+ if (g_strcmp0 (flag, "runtime") == 0)
+ return GS_PLUGIN_REFINE_FLAGS_REQUIRE_RUNTIME;
+ if (g_strcmp0 (flag, "categories") == 0)
+ return GS_PLUGIN_REFINE_FLAGS_REQUIRE_CATEGORIES;
+ if (g_strcmp0 (flag, "project-group") == 0)
+ return GS_PLUGIN_REFINE_FLAGS_REQUIRE_PROJECT_GROUP;
+ if (g_strcmp0 (flag, "developer-name") == 0)
+ return GS_PLUGIN_REFINE_FLAGS_REQUIRE_DEVELOPER_NAME;
+ if (g_strcmp0 (flag, "kudos") == 0)
+ return GS_PLUGIN_REFINE_FLAGS_REQUIRE_KUDOS;
+ if (g_strcmp0 (flag, "content-rating") == 0)
+ return GS_PLUGIN_REFINE_FLAGS_REQUIRE_CONTENT_RATING;
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_NOT_SUPPORTED,
+ "GsPluginRefineFlag '%s' not recognised", flag);
+ return 0;
+}
+
+static guint64
+gs_cmd_parse_refine_flags (const gchar *extra, GError **error)
+{
+ GsPluginRefineFlags tmp;
+ guint i;
+ guint64 refine_flags = GS_PLUGIN_REFINE_FLAGS_DEFAULT;
+ g_auto(GStrv) split = NULL;
+
+ if (extra == NULL)
+ return GS_PLUGIN_REFINE_FLAGS_DEFAULT;
+
+ split = g_strsplit (extra, ",", -1);
+ for (i = 0; split[i] != NULL; i++) {
+ tmp = gs_cmd_refine_flag_from_string (split[i], error);
+ if (tmp == 0)
+ return G_MAXUINT64;
+ refine_flags |= tmp;
+ }
+ return refine_flags;
+}
+
+static guint
+gs_cmd_prompt_for_number (guint maxnum)
+{
+ gint retval;
+ guint answer = 0;
+
+ do {
+ char buffer[64];
+
+ /* swallow the \n at end of line too */
+ if (!fgets (buffer, sizeof (buffer), stdin))
+ break;
+ if (strlen (buffer) == sizeof (buffer) - 1)
+ continue;
+
+ /* get a number */
+ retval = sscanf (buffer, "%u", &answer);
+
+ /* positive */
+ if (retval == 1 && answer > 0 && answer <= maxnum)
+ break;
+
+ /* TRANSLATORS: the user isn't reading the question */
+ g_print (_("Please enter a number from 1 to %u: "), maxnum);
+ } while (TRUE);
+ return answer;
+}
+
+static gboolean
+gs_cmd_action_exec (GsCmdSelf *self, GsPluginAction action, const gchar *name, GError **error)
+{
+ g_autoptr(GsApp) app = NULL;
+ g_autoptr(GsAppList) list = NULL;
+ g_autoptr(GsAppList) list_filtered = NULL;
+ g_autoptr(GsPluginJob) plugin_job2 = NULL;
+ g_autoptr(GsPluginJob) plugin_job = NULL;
+ gboolean show_installed = TRUE;
+
+ /* ensure set */
+ self->refine_flags |= GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON;
+ self->refine_flags |= GS_PLUGIN_REFINE_FLAGS_REQUIRE_SETUP_ACTION;
+
+ /* do search */
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_SEARCH,
+ "search", name,
+ "refine-flags", self->refine_flags,
+ "max-results", self->max_results,
+ NULL);
+ list = gs_plugin_loader_job_process (self->plugin_loader, plugin_job, NULL, error);
+ if (list == NULL)
+ return FALSE;
+ if (gs_app_list_length (list) == 0) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ "no components matched '%s'",
+ name);
+ return FALSE;
+ }
+
+ /* filter */
+ if (action == GS_PLUGIN_ACTION_INSTALL)
+ show_installed = FALSE;
+ list_filtered = gs_app_list_new ();
+ for (guint i = 0; i < gs_app_list_length (list); i++) {
+ GsApp *app_tmp = gs_app_list_index (list, i);
+ if (gs_app_is_installed (app_tmp) == show_installed)
+ gs_app_list_add (list_filtered, app_tmp);
+ }
+
+ /* nothing */
+ if (gs_app_list_length (list_filtered) == 0) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ "no components were in the correct state for '%s %s'",
+ gs_plugin_action_to_string (action), name);
+ return FALSE;
+ }
+
+ /* get one GsApp */
+ if (gs_app_list_length (list_filtered) == 1) {
+ app = g_object_ref (gs_app_list_index (list_filtered, 0));
+ } else {
+ guint idx;
+ /* TRANSLATORS: asking the user to choose an app from a list */
+ g_print ("%s\n", _("Choose an application:"));
+ for (guint i = 0; i < gs_app_list_length (list_filtered); i++) {
+ GsApp *app_tmp = gs_app_list_index (list_filtered, i);
+ g_print ("%u.\t%s\n",
+ i + 1,
+ gs_app_get_unique_id (app_tmp));
+ }
+ idx = gs_cmd_prompt_for_number (gs_app_list_length (list_filtered));
+ app = g_object_ref (gs_app_list_index (list_filtered, idx - 1));
+ }
+
+ /* install */
+ plugin_job2 = gs_plugin_job_newv (action, "app", app, NULL);
+ return gs_plugin_loader_job_action (self->plugin_loader, plugin_job2,
+ NULL, error);
+}
+
+static void
+gs_cmd_self_free (GsCmdSelf *self)
+{
+ if (self->plugin_loader != NULL)
+ g_object_unref (self->plugin_loader);
+ g_free (self);
+}
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC(GsCmdSelf, gs_cmd_self_free)
+
+int
+main (int argc, char **argv)
+{
+ g_autoptr(GOptionContext) context = NULL;
+ gboolean prefer_local = FALSE;
+ gboolean ret;
+ gboolean show_results = FALSE;
+ gboolean verbose = FALSE;
+ gint i;
+ guint cache_age = 0;
+ gint repeat = 1;
+ g_auto(GStrv) plugin_blocklist = NULL;
+ g_auto(GStrv) plugin_allowlist = NULL;
+ g_autoptr(GError) error = NULL;
+ g_autoptr(GsAppList) list = NULL;
+ g_autoptr(GPtrArray) categories = NULL;
+ g_autoptr(GsDebug) debug = gs_debug_new ();
+ g_autofree gchar *plugin_blocklist_str = NULL;
+ g_autofree gchar *plugin_allowlist_str = NULL;
+ g_autofree gchar *refine_flags_str = NULL;
+ g_autoptr(GsApp) app = NULL;
+ g_autoptr(GFile) file = NULL;
+ g_autoptr(GsCmdSelf) self = g_new0 (GsCmdSelf, 1);
+ const GOptionEntry options[] = {
+ { "show-results", '\0', 0, G_OPTION_ARG_NONE, &show_results,
+ "Show the results for the action", NULL },
+ { "refine-flags", '\0', 0, G_OPTION_ARG_STRING, &refine_flags_str,
+ "Set any refine flags required for the action", NULL },
+ { "repeat", '\0', 0, G_OPTION_ARG_INT, &repeat,
+ "Repeat the action this number of times", NULL },
+ { "cache-age", '\0', 0, G_OPTION_ARG_INT, &cache_age,
+ "Use this maximum cache age in seconds", NULL },
+ { "max-results", '\0', 0, G_OPTION_ARG_INT, &self->max_results,
+ "Return a maximum number of results", NULL },
+ { "prefer-local", '\0', 0, G_OPTION_ARG_NONE, &prefer_local,
+ "Prefer local file sources to AppStream", NULL },
+ { "plugin-blocklist", '\0', 0, G_OPTION_ARG_STRING, &plugin_blocklist_str,
+ "Do not load specific plugins", NULL },
+ { "plugin-allowlist", '\0', 0, G_OPTION_ARG_STRING, &plugin_allowlist_str,
+ "Only load specific plugins", NULL },
+ { "verbose", '\0', 0, G_OPTION_ARG_NONE, &verbose,
+ "Show verbose debugging information", NULL },
+ { NULL}
+ };
+
+ setlocale (LC_ALL, "");
+ g_setenv ("G_MESSAGES_DEBUG", "all", TRUE);
+
+ bindtextdomain (GETTEXT_PACKAGE, LOCALEDIR);
+ bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8");
+ textdomain (GETTEXT_PACKAGE);
+
+ gtk_init (&argc, &argv);
+
+ context = g_option_context_new (NULL);
+ g_option_context_set_summary (context, "GNOME Software Test Program");
+ g_option_context_add_main_entries (context, options, NULL);
+ g_option_context_add_group (context, gtk_get_option_group (TRUE));
+ ret = g_option_context_parse (context, &argc, &argv, &error);
+ if (!ret) {
+ g_print ("Failed to parse options: %s\n", error->message);
+ return EXIT_FAILURE;
+ }
+ if (verbose)
+ g_setenv ("GS_DEBUG", "1", TRUE);
+
+ /* prefer local sources */
+ if (prefer_local)
+ g_setenv ("GNOME_SOFTWARE_PREFER_LOCAL", "true", TRUE);
+
+ /* parse any refine flags */
+ self->refine_flags = gs_cmd_parse_refine_flags (refine_flags_str, &error);
+ if (self->refine_flags == G_MAXUINT64) {
+ g_print ("Flag unknown: %s\n", error->message);
+ return EXIT_FAILURE;
+ }
+
+ /* load plugins */
+ self->plugin_loader = gs_plugin_loader_new ();
+ if (g_file_test (LOCALPLUGINDIR, G_FILE_TEST_EXISTS))
+ gs_plugin_loader_add_location (self->plugin_loader, LOCALPLUGINDIR);
+ if (plugin_allowlist_str != NULL)
+ plugin_allowlist = g_strsplit (plugin_allowlist_str, ",", -1);
+ if (plugin_blocklist_str != NULL)
+ plugin_blocklist = g_strsplit (plugin_blocklist_str, ",", -1);
+ ret = gs_plugin_loader_setup (self->plugin_loader,
+ plugin_allowlist,
+ plugin_blocklist,
+ NULL,
+ &error);
+ if (!ret) {
+ g_print ("Failed to setup plugins: %s\n", error->message);
+ return EXIT_FAILURE;
+ }
+ gs_plugin_loader_dump_state (self->plugin_loader);
+
+ /* ensure that at least some metadata of any age is present, and also
+ * spin up the plugins enough as to prime caches */
+ if (g_getenv ("GS_CMD_NO_INITIAL_REFRESH") == NULL) {
+ g_autoptr(GsPluginJob) plugin_job = NULL;
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REFRESH,
+ "age", (guint64) G_MAXUINT,
+ NULL);
+ ret = gs_plugin_loader_job_action (self->plugin_loader, plugin_job,
+ NULL, &error);
+ if (!ret) {
+ g_print ("Failed to refresh plugins: %s\n", error->message);
+ return EXIT_FAILURE;
+ }
+ }
+
+ /* do action */
+ if (argc == 2 && g_strcmp0 (argv[1], "installed") == 0) {
+ for (i = 0; i < repeat; i++) {
+ g_autoptr(GsPluginJob) plugin_job = NULL;
+ if (list != NULL)
+ g_object_unref (list);
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_INSTALLED,
+ "refine-flags", self->refine_flags,
+ "max-results", self->max_results,
+ NULL);
+ list = gs_plugin_loader_job_process (self->plugin_loader, plugin_job,
+ NULL, &error);
+ if (list == NULL) {
+ ret = FALSE;
+ break;
+ }
+ }
+ } else if (argc == 3 && g_strcmp0 (argv[1], "search") == 0) {
+ for (i = 0; i < repeat; i++) {
+ g_autoptr(GsPluginJob) plugin_job = NULL;
+ if (list != NULL)
+ g_object_unref (list);
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_SEARCH,
+ "search", argv[2],
+ "refine-flags", self->refine_flags,
+ "max-results", self->max_results,
+ NULL);
+ list = gs_plugin_loader_job_process (self->plugin_loader, plugin_job, NULL, &error);
+ if (list == NULL) {
+ ret = FALSE;
+ break;
+ }
+ }
+ } else if (argc == 3 && g_strcmp0 (argv[1], "get-alternates") == 0) {
+ app = gs_app_new (argv[2]);
+ gs_app_add_quirk (app, GS_APP_QUIRK_IS_WILDCARD);
+ for (i = 0; i < repeat; i++) {
+ g_autoptr(GsPluginJob) plugin_job = NULL;
+ if (list != NULL)
+ g_object_unref (list);
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_ALTERNATES,
+ "app", app,
+ "refine-flags", self->refine_flags,
+ "max-results", self->max_results,
+ NULL);
+ list = gs_plugin_loader_job_process (self->plugin_loader, plugin_job, NULL, &error);
+ if (list == NULL) {
+ ret = FALSE;
+ break;
+ }
+ }
+ } else if (argc == 4 && g_strcmp0 (argv[1], "action") == 0) {
+ GsPluginAction action = gs_plugin_action_from_string (argv[2]);
+ if (action == GS_PLUGIN_ACTION_UNKNOWN) {
+ ret = FALSE;
+ g_set_error (&error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ "Did not recognise action '%s'", argv[2]);
+ } else {
+ ret = gs_cmd_action_exec (self, action, argv[3], &error);
+ }
+ } else if (argc == 3 && g_strcmp0 (argv[1], "action-upgrade-download") == 0) {
+ g_autoptr(GsPluginJob) plugin_job = NULL;
+ app = gs_app_new (argv[2]);
+ gs_app_set_kind (app, AS_APP_KIND_OS_UPGRADE);
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_UPGRADE_DOWNLOAD,
+ "app", app,
+ NULL);
+ ret = gs_plugin_loader_job_action (self->plugin_loader, plugin_job,
+ NULL, &error);
+ if (ret)
+ gs_app_list_add (list, app);
+ } else if (argc == 3 && g_strcmp0 (argv[1], "refine") == 0) {
+ app = gs_app_new (argv[2]);
+ for (i = 0; i < repeat; i++) {
+ g_autoptr(GsPluginJob) plugin_job = NULL;
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REFINE,
+ "app", app,
+ "refine-flags", self->refine_flags,
+ NULL);
+ ret = gs_plugin_loader_job_action (self->plugin_loader, plugin_job,
+ NULL, &error);
+ if (!ret)
+ break;
+ }
+ list = gs_app_list_new ();
+ gs_app_list_add (list, app);
+ } else if (argc == 3 && g_strcmp0 (argv[1], "launch") == 0) {
+ app = gs_app_new (argv[2]);
+ for (i = 0; i < repeat; i++) {
+ g_autoptr(GsPluginJob) plugin_job = NULL;
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_LAUNCH,
+ "app", app,
+ NULL);
+ ret = gs_plugin_loader_job_action (self->plugin_loader, plugin_job,
+ NULL, &error);
+ if (!ret)
+ break;
+ }
+ } else if (argc == 3 && g_strcmp0 (argv[1], "filename-to-app") == 0) {
+ g_autoptr(GsPluginJob) plugin_job = NULL;
+ file = g_file_new_for_path (argv[2]);
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_FILE_TO_APP,
+ "file", file,
+ "refine-flags", self->refine_flags,
+ "max-results", self->max_results,
+ NULL);
+ app = gs_plugin_loader_job_process_app (self->plugin_loader, plugin_job, NULL, &error);
+ if (app == NULL) {
+ ret = FALSE;
+ } else {
+ list = gs_app_list_new ();
+ gs_app_list_add (list, app);
+ }
+ } else if (argc == 3 && g_strcmp0 (argv[1], "url-to-app") == 0) {
+ g_autoptr(GsPluginJob) plugin_job = NULL;
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_URL_TO_APP,
+ "search", argv[2],
+ "refine-flags", self->refine_flags,
+ "max-results", self->max_results,
+ NULL);
+ app = gs_plugin_loader_job_process_app (self->plugin_loader, plugin_job,
+ NULL, &error);
+ if (app == NULL) {
+ ret = FALSE;
+ } else {
+ list = gs_app_list_new ();
+ gs_app_list_add (list, app);
+ }
+ } else if (argc == 2 && g_strcmp0 (argv[1], "updates") == 0) {
+ for (i = 0; i < repeat; i++) {
+ g_autoptr(GsPluginJob) plugin_job = NULL;
+ if (list != NULL)
+ g_object_unref (list);
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_UPDATES,
+ "refine-flags", self->refine_flags,
+ "max-results", self->max_results,
+ NULL);
+ list = gs_plugin_loader_job_process (self->plugin_loader, plugin_job,
+ NULL, &error);
+ if (list == NULL) {
+ ret = FALSE;
+ break;
+ }
+ }
+ } else if (argc == 2 && g_strcmp0 (argv[1], "upgrades") == 0) {
+ for (i = 0; i < repeat; i++) {
+ g_autoptr(GsPluginJob) plugin_job = NULL;
+ if (list != NULL)
+ g_object_unref (list);
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_DISTRO_UPDATES,
+ "refine-flags", self->refine_flags,
+ "max-results", self->max_results,
+ NULL);
+ list = gs_plugin_loader_job_process (self->plugin_loader, plugin_job,
+ NULL, &error);
+ if (list == NULL) {
+ ret = FALSE;
+ break;
+ }
+ }
+ } else if (argc == 2 && g_strcmp0 (argv[1], "sources") == 0) {
+ g_autoptr(GsPluginJob) plugin_job = NULL;
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_SOURCES,
+ "refine-flags", self->refine_flags,
+ "max-results", self->max_results,
+ NULL);
+ list = gs_plugin_loader_job_process (self->plugin_loader,
+ plugin_job,
+ NULL,
+ &error);
+ if (list == NULL)
+ ret = FALSE;
+ } else if (argc == 2 && g_strcmp0 (argv[1], "popular") == 0) {
+ for (i = 0; i < repeat; i++) {
+ g_autoptr(GsPluginJob) plugin_job = NULL;
+ if (list != NULL)
+ g_object_unref (list);
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_POPULAR,
+ "refine-flags", self->refine_flags,
+ "max-results", self->max_results,
+ NULL);
+ list = gs_plugin_loader_job_process (self->plugin_loader, plugin_job,
+ NULL, &error);
+ if (list == NULL) {
+ ret = FALSE;
+ break;
+ }
+ }
+ } else if (argc == 2 && g_strcmp0 (argv[1], "featured") == 0) {
+ for (i = 0; i < repeat; i++) {
+ g_autoptr(GsPluginJob) plugin_job = NULL;
+ if (list != NULL)
+ g_object_unref (list);
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_FEATURED,
+ "refine-flags", self->refine_flags,
+ "max-results", self->max_results,
+ NULL);
+ list = gs_plugin_loader_job_process (self->plugin_loader, plugin_job,
+ NULL, &error);
+ if (list == NULL) {
+ ret = FALSE;
+ break;
+ }
+ }
+ } else if (argc == 2 && g_strcmp0 (argv[1], "recent") == 0) {
+ if (cache_age == 0)
+ cache_age = 60 * 60 * 24 * 60;
+ for (i = 0; i < repeat; i++) {
+ g_autoptr(GsPluginJob) plugin_job = NULL;
+ if (list != NULL)
+ g_object_unref (list);
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_RECENT,
+ "age", (guint64) cache_age,
+ "refine-flags", self->refine_flags,
+ "max-results", self->max_results,
+ NULL);
+ list = gs_plugin_loader_job_process (self->plugin_loader, plugin_job,
+ NULL, &error);
+ if (list == NULL) {
+ ret = FALSE;
+ break;
+ }
+ }
+ } else if (argc == 2 && g_strcmp0 (argv[1], "get-categories") == 0) {
+ for (i = 0; i < repeat; i++) {
+ g_autoptr(GsPluginJob) plugin_job = NULL;
+ if (categories != NULL)
+ g_ptr_array_unref (categories);
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_CATEGORIES,
+ "refine-flags", self->refine_flags,
+ "max-results", self->max_results,
+ NULL);
+ categories = gs_plugin_loader_job_get_categories (self->plugin_loader,
+ plugin_job,
+ NULL, &error);
+ if (categories == NULL) {
+ ret = FALSE;
+ break;
+ }
+ }
+ } else if (argc == 3 && g_strcmp0 (argv[1], "get-category-apps") == 0) {
+ g_autoptr(GsCategory) category = NULL;
+ g_autoptr(GsCategory) parent = NULL;
+ g_auto(GStrv) split = NULL;
+ split = g_strsplit (argv[2], "/", 2);
+ if (g_strv_length (split) == 1) {
+ category = gs_category_new (split[0]);
+ } else {
+ parent = gs_category_new (split[0]);
+ category = gs_category_new (split[1]);
+ gs_category_add_child (parent, category);
+ }
+ for (i = 0; i < repeat; i++) {
+ g_autoptr(GsPluginJob) plugin_job = NULL;
+ if (list != NULL)
+ g_object_unref (list);
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_GET_CATEGORY_APPS,
+ "category", category,
+ "refine-flags", self->refine_flags,
+ "max-results", self->max_results,
+ NULL);
+ list = gs_plugin_loader_job_process (self->plugin_loader, plugin_job, NULL, &error);
+ if (list == NULL) {
+ ret = FALSE;
+ break;
+ }
+ }
+ } else if (argc >= 2 && g_strcmp0 (argv[1], "refresh") == 0) {
+ g_autoptr(GsPluginJob) plugin_job = NULL;
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REFRESH,
+ "age", (guint64) cache_age,
+ NULL);
+ ret = gs_plugin_loader_job_action (self->plugin_loader, plugin_job,
+ NULL, &error);
+ } else if (argc >= 1 && g_strcmp0 (argv[1], "user-hash") == 0) {
+ g_autofree gchar *user_hash = gs_utils_get_user_hash (&error);
+ if (user_hash == NULL) {
+ ret = FALSE;
+ } else {
+ g_print ("%s\n", user_hash);
+ ret = TRUE;
+ }
+ } else {
+ ret = FALSE;
+ g_set_error_literal (&error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ "Did not recognise option, use 'installed', "
+ "'updates', 'popular', 'get-categories', "
+ "'get-category-apps', 'get-alternates', 'filename-to-app', "
+ "'action install', 'action remove', "
+ "'sources', 'refresh', 'launch' or 'search'");
+ }
+ if (!ret) {
+ g_print ("Failed: %s\n", error->message);
+ return EXIT_FAILURE;
+ }
+
+ if (show_results) {
+ if (list != NULL)
+ gs_cmd_show_results_apps (list);
+ if (categories != NULL)
+ gs_cmd_show_results_categories (categories);
+ }
+ return EXIT_SUCCESS;
+}
diff --git a/lib/gs-debug.c b/lib/gs-debug.c
new file mode 100644
index 0000000..ad429a4
--- /dev/null
+++ b/lib/gs-debug.c
@@ -0,0 +1,190 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2016 Richard Hughes <richard@hughsie.com>
+ * Copyright (C) 2017 Kalev Lember <klember@redhat.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+#include "config.h"
+
+#include <stdio.h>
+#include <unistd.h>
+
+#include "gs-debug.h"
+
+struct _GsDebug
+{
+ GObject parent_instance;
+ GMutex mutex;
+ gboolean use_time;
+};
+
+G_DEFINE_TYPE (GsDebug, gs_debug, G_TYPE_OBJECT)
+
+static GLogWriterOutput
+gs_log_writer_console (GLogLevelFlags log_level,
+ const GLogField *fields,
+ gsize n_fields,
+ gpointer user_data)
+{
+ GsDebug *debug = GS_DEBUG (user_data);
+ const gchar *log_domain = NULL;
+ const gchar *log_message = NULL;
+ g_autofree gchar *tmp = NULL;
+ g_autoptr(GMutexLocker) locker = NULL;
+ g_autoptr(GString) domain = NULL;
+
+ /* enabled */
+ if (g_getenv ("GS_DEBUG") == NULL &&
+ log_level == G_LOG_LEVEL_DEBUG)
+ return G_LOG_WRITER_HANDLED;
+
+ /* get data from arguments */
+ for (gsize i = 0; i < n_fields; i++) {
+ if (g_strcmp0 (fields[i].key, "MESSAGE") == 0) {
+ log_message = fields[i].value;
+ continue;
+ }
+ if (g_strcmp0 (fields[i].key, "GLIB_DOMAIN") == 0) {
+ log_domain = fields[i].value;
+ continue;
+ }
+ }
+
+ /* this is really verbose */
+ if (g_strcmp0 (log_domain, "dconf") == 0 &&
+ log_level == G_LOG_LEVEL_DEBUG)
+ return G_LOG_WRITER_HANDLED;
+
+ /* make threadsafe */
+ locker = g_mutex_locker_new (&debug->mutex);
+ g_assert (locker != NULL);
+
+ /* time header */
+ if (debug->use_time) {
+ g_autoptr(GDateTime) dt = g_date_time_new_now_utc ();
+ tmp = g_strdup_printf ("%02i:%02i:%02i:%04i",
+ g_date_time_get_hour (dt),
+ g_date_time_get_minute (dt),
+ g_date_time_get_second (dt),
+ g_date_time_get_microsecond (dt) / 1000);
+ }
+
+ /* make these shorter */
+ if (g_strcmp0 (log_domain, "PackageKit") == 0) {
+ log_domain = "PK";
+ } else if (g_strcmp0 (log_domain, "GsPlugin") == 0) {
+ log_domain = "Gs";
+ }
+
+ /* pad out domain */
+ domain = g_string_new (log_domain);
+ for (guint i = domain->len; i < 3; i++)
+ g_string_append (domain, " ");
+
+ switch (log_level) {
+ case G_LOG_LEVEL_ERROR:
+ case G_LOG_LEVEL_CRITICAL:
+ case G_LOG_LEVEL_WARNING:
+ /* to screen */
+ if (isatty (fileno (stderr)) == 1) {
+ /* critical in red */
+ if (tmp != NULL)
+ g_printerr ("%c[%dm%s ", 0x1B, 32, tmp);
+ g_printerr ("%s ", domain->str);
+ g_printerr ("%c[%dm%s\n%c[%dm", 0x1B, 31, log_message, 0x1B, 0);
+ } else { /* to file */
+ if (tmp != NULL)
+ g_print ("%s ", tmp);
+ g_print ("%s ", domain->str);
+ g_print ("%s\n", log_message);
+ }
+ break;
+ default:
+ /* to screen */
+ if (isatty (fileno (stdout)) == 1) {
+ /* debug in blue */
+ if (tmp != NULL)
+ g_print ("%c[%dm%s ", 0x1B, 32, tmp);
+ g_print ("%s ", domain->str);
+ g_print ("%c[%dm%s\n%c[%dm", 0x1B, 34, log_message, 0x1B, 0);
+ break;
+ } else { /* to file */
+ if (tmp != NULL)
+ g_print ("%s ", tmp);
+ g_print ("%s ", domain->str);
+ g_print ("%s\n", log_message);
+ }
+ }
+
+ /* success */
+ return G_LOG_WRITER_HANDLED;
+}
+
+static GLogWriterOutput
+gs_log_writer_journald (GLogLevelFlags log_level,
+ const GLogField *fields,
+ gsize n_fields,
+ gpointer user_data)
+{
+ /* important enough to force to the journal */
+ switch (log_level) {
+ case G_LOG_LEVEL_ERROR:
+ case G_LOG_LEVEL_CRITICAL:
+ case G_LOG_LEVEL_WARNING:
+ case G_LOG_LEVEL_INFO:
+ return g_log_writer_journald (log_level, fields, n_fields, user_data);
+ break;
+ default:
+ break;
+ }
+
+ return G_LOG_WRITER_UNHANDLED;
+}
+
+static GLogWriterOutput
+gs_debug_log_writer (GLogLevelFlags log_level,
+ const GLogField *fields,
+ gsize n_fields,
+ gpointer user_data)
+{
+ if (g_log_writer_is_journald (fileno (stderr)))
+ return gs_log_writer_journald (log_level, fields, n_fields, user_data);
+ else
+ return gs_log_writer_console (log_level, fields, n_fields, user_data);
+}
+
+static void
+gs_debug_finalize (GObject *object)
+{
+ GsDebug *debug = GS_DEBUG (object);
+
+ g_mutex_clear (&debug->mutex);
+
+ G_OBJECT_CLASS (gs_debug_parent_class)->finalize (object);
+}
+
+static void
+gs_debug_class_init (GsDebugClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ object_class->finalize = gs_debug_finalize;
+}
+
+static void
+gs_debug_init (GsDebug *debug)
+{
+ g_mutex_init (&debug->mutex);
+ debug->use_time = g_getenv ("GS_DEBUG_NO_TIME") == NULL;
+ g_log_set_writer_func (gs_debug_log_writer,
+ g_object_ref (debug),
+ (GDestroyNotify) g_object_unref);
+}
+
+GsDebug *
+gs_debug_new (void)
+{
+ return GS_DEBUG (g_object_new (GS_TYPE_DEBUG, NULL));
+}
diff --git a/lib/gs-debug.h b/lib/gs-debug.h
new file mode 100644
index 0000000..6dd529d
--- /dev/null
+++ b/lib/gs-debug.h
@@ -0,0 +1,21 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2016 Richard Hughes <richard@hughsie.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+#pragma once
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+#define GS_TYPE_DEBUG (gs_debug_get_type ())
+
+G_DECLARE_FINAL_TYPE (GsDebug, gs_debug, GS, DEBUG, GObject)
+
+GsDebug *gs_debug_new (void);
+
+G_END_DECLS
diff --git a/lib/gs-ioprio.c b/lib/gs-ioprio.c
new file mode 100644
index 0000000..11b9fa6
--- /dev/null
+++ b/lib/gs-ioprio.c
@@ -0,0 +1,152 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2005, Novell, Inc.
+ * Copyright (C) 2006, Jamie McCracken <jamiemcc@gnome.org>
+ * Copyright (C) 2006, Anders Aagaard
+ *
+ * Based mostly on code by Robert Love <rml@novell.com>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a
+ * copy of this software and associated documentation files (the "Software"),
+ * to deal in the Software without restriction, including without limitation
+ * the rights to use, copy, modify, merge, publish, distribute, sublicense,
+ * and/or sell copies of the Software, and to permit persons to whom the
+ * Software is furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+ * DEALINGS IN THE SOFTWARE.
+ */
+
+#include "config.h"
+
+#ifdef __linux__
+
+#include <stdio.h>
+#include <errno.h>
+
+#ifdef HAVE_LINUX_UNISTD_H
+#include <linux/unistd.h>
+#endif
+
+#include <sys/syscall.h>
+#include <unistd.h>
+
+#include <glib/gstdio.h>
+
+#endif /* __linux__ */
+
+#include "gs-ioprio.h"
+
+/* We assume ALL linux architectures have the syscalls defined here */
+#ifdef __linux__
+
+/* Make sure the system call is supported */
+#ifndef __NR_ioprio_set
+
+#if defined(__i386__)
+#define __NR_ioprio_set 289
+#define __NR_ioprio_get 290
+#elif defined(__powerpc__) || defined(__powerpc64__)
+#define __NR_ioprio_set 273
+#define __NR_ioprio_get 274
+#elif defined(__x86_64__)
+#define __NR_ioprio_set 251
+#define __NR_ioprio_get 252
+#elif defined(__ia64__)
+#define __NR_ioprio_set 1274
+#define __NR_ioprio_get 1275
+#elif defined(__alpha__)
+#define __NR_ioprio_set 442
+#define __NR_ioprio_get 443
+#elif defined(__s390x__) || defined(__s390__)
+#define __NR_ioprio_set 282
+#define __NR_ioprio_get 283
+#elif defined(__SH4__)
+#define __NR_ioprio_set 288
+#define __NR_ioprio_get 289
+#elif defined(__SH5__)
+#define __NR_ioprio_set 316
+#define __NR_ioprio_get 317
+#elif defined(__sparc__) || defined(__sparc64__)
+#define __NR_ioprio_set 196
+#define __NR_ioprio_get 218
+#elif defined(__arm__)
+#define __NR_ioprio_set 314
+#define __NR_ioprio_get 315
+#else
+#error "Unsupported architecture!"
+#endif
+
+#endif /* __NR_ioprio_set */
+
+enum {
+ IOPRIO_CLASS_NONE,
+ IOPRIO_CLASS_RT,
+ IOPRIO_CLASS_BE,
+ IOPRIO_CLASS_IDLE,
+};
+
+enum {
+ IOPRIO_WHO_PROCESS = 1,
+ IOPRIO_WHO_PGRP,
+ IOPRIO_WHO_USER,
+};
+
+#define IOPRIO_CLASS_SHIFT 13
+
+static inline int
+ioprio_set (int which, int who, int ioprio_val)
+{
+ return syscall (__NR_ioprio_set, which, who, ioprio_val);
+}
+
+static int
+set_io_priority_idle (void)
+{
+ int ioprio, ioclass;
+
+ ioprio = 7; /* priority is ignored with idle class */
+ ioclass = IOPRIO_CLASS_IDLE << IOPRIO_CLASS_SHIFT;
+
+ return ioprio_set (IOPRIO_WHO_PROCESS, 0, ioprio | ioclass);
+}
+
+static int
+set_io_priority_best_effort (int ioprio_val)
+{
+ int ioclass;
+
+ ioclass = IOPRIO_CLASS_BE << IOPRIO_CLASS_SHIFT;
+
+ return ioprio_set (IOPRIO_WHO_PROCESS, 0, ioprio_val | ioclass);
+}
+
+void
+gs_ioprio_init (void)
+{
+ if (set_io_priority_idle () == -1) {
+ g_message ("Could not set idle IO priority, attempting best effort of 7");
+
+ if (set_io_priority_best_effort (7) == -1) {
+ g_message ("Could not set best effort IO priority either, giving up");
+ }
+ }
+}
+
+#else /* __linux__ */
+
+void
+gs_ioprio_init (void)
+{
+}
+
+#endif /* __linux__ */
diff --git a/lib/gs-ioprio.h b/lib/gs-ioprio.h
new file mode 100644
index 0000000..b876afe
--- /dev/null
+++ b/lib/gs-ioprio.h
@@ -0,0 +1,31 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2006, Anders Aagaard
+ * Copyright (C) 2008, Nokia <ivan.frade@nokia.com>
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library; if not, write to the
+ * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
+ * Boston, MA 02110-1301, USA.
+ */
+
+#pragma once
+
+#include <glib.h>
+
+G_BEGIN_DECLS
+
+void gs_ioprio_init (void);
+
+G_END_DECLS
diff --git a/lib/gs-metered.c b/lib/gs-metered.c
new file mode 100644
index 0000000..d4afec4
--- /dev/null
+++ b/lib/gs-metered.c
@@ -0,0 +1,267 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2019 Endless Mobile, Inc.
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+/**
+ * SECTION:gs-metered
+ * @title: Metered Data Utilities
+ * @include: gnome-software.h
+ * @stability: Unstable
+ * @short_description: Utility functions to help with metered data handling
+ *
+ * Metered data handling is provided by Mogwai, which implements a download
+ * scheduler to control when, and in which order, large downloads happen on
+ * the system.
+ *
+ * All large downloads from gs_plugin_download() or gs_plugin_download_app()
+ * calls should be scheduled using Mogwai, which will notify gnome-software
+ * when those downloads can start and stop, according to system policy.
+ *
+ * The functions in this file make interacting with the scheduling daemon a
+ * little simpler. Since all #GsPlugin method calls happen in worker threads,
+ * typically without a #GMainContext, all interaction with the scheduler should
+ * be blocking. libmogwai-schedule-client was designed to be asynchronous; so
+ * these helpers make it synchronous.
+ *
+ * Since: 2.34
+ */
+
+#include "config.h"
+
+#include <glib.h>
+
+#ifdef HAVE_MOGWAI
+#include <libmogwai-schedule-client/scheduler.h>
+#endif
+
+#include "gs-metered.h"
+
+
+#ifdef HAVE_MOGWAI
+
+/* FIXME: Backported from https://gitlab.gnome.org/GNOME/glib/merge_requests/983.
+ * Drop once we can depend on a version of GLib which includes it .*/
+typedef void MainContextPusher;
+
+static inline MainContextPusher *
+main_context_pusher_new (GMainContext *main_context)
+{
+ g_main_context_push_thread_default (main_context);
+ return (MainContextPusher *) main_context;
+}
+
+static inline void
+main_context_pusher_free (MainContextPusher *pusher)
+{
+ g_main_context_pop_thread_default ((GMainContext *) pusher);
+}
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (MainContextPusher, main_context_pusher_free)
+
+
+typedef struct
+{
+ gboolean *out_download_now; /* (unowned) */
+ GMainContext *context; /* (unowned) */
+} DownloadNowData;
+
+static void
+download_now_cb (GObject *obj,
+ GParamSpec *pspec,
+ gpointer user_data)
+{
+ DownloadNowData *data = user_data;
+ *data->out_download_now = mwsc_schedule_entry_get_download_now (MWSC_SCHEDULE_ENTRY (obj));
+ g_main_context_wakeup (data->context);
+}
+
+typedef struct
+{
+ GError **out_error; /* (unowned) */
+ GMainContext *context; /* (unowned) */
+} InvalidatedData;
+
+static void
+invalidated_cb (MwscScheduleEntry *entry,
+ const GError *error,
+ gpointer user_data)
+{
+ InvalidatedData *data = user_data;
+ *data->out_error = g_error_copy (error);
+ g_main_context_wakeup (data->context);
+}
+
+#endif /* HAVE_MOGWAI */
+
+/**
+ * gs_metered_block_on_download_scheduler:
+ * @parameters: (nullable): a #GVariant of type `a{sv}` specifying parameters
+ * for the schedule entry, or %NULL to pass no parameters
+ * @cancellable: (nullable): a #GCancellable, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Create a schedule entry with the given @parameters, and block until
+ * permission is given to download.
+ *
+ * FIXME: This will currently ignore later revocations of that download
+ * permission, and does not support creating a schedule entry per app.
+ *
+ * If a schedule entry cannot be created, or if @cancellable is cancelled,
+ * an error will be set and %FALSE returned.
+ *
+ * The keys understood by @parameters are listed in the documentation for
+ * mwsc_scheduler_schedule_async().
+ *
+ * This function will likely be called from a #GsPluginLoader worker thread.
+ *
+ * Returns: %TRUE on success, %FALSE otherwise
+ * Since: 2.34
+ */
+gboolean
+gs_metered_block_on_download_scheduler (GVariant *parameters,
+ GCancellable *cancellable,
+ GError **error)
+{
+#ifdef HAVE_MOGWAI
+ g_autoptr(MwscScheduler) scheduler = NULL;
+ g_autoptr(MwscScheduleEntry) schedule_entry = NULL;
+ g_autofree gchar *parameters_str = NULL;
+ g_autoptr(GMainContext) context = NULL;
+ g_autoptr(MainContextPusher) pusher = NULL;
+
+ parameters_str = (parameters != NULL) ? g_variant_print (parameters, TRUE) : g_strdup ("(none)");
+ g_debug ("%s: Waiting with parameters: %s", G_STRFUNC, parameters_str);
+
+ /* Push the context early so that the #MwscScheduler is created to run within it. */
+ context = g_main_context_new ();
+ pusher = main_context_pusher_new (context);
+
+ /* Wait until the download can be scheduled.
+ * FIXME: In future, downloads could be split up by app, so they can all
+ * be scheduled separately and, for example, higher priority ones could
+ * be scheduled with a higher priority. This would have to be aware of
+ * dependencies. */
+ scheduler = mwsc_scheduler_new (cancellable, error);
+ if (scheduler == NULL)
+ return FALSE;
+
+ /* Create a schedule entry for the group of downloads.
+ * FIXME: The underlying OSTree code supports resuming downloads
+ * (at a granularity of individual objects), so it should be
+ * possible to plumb through here. */
+ schedule_entry = mwsc_scheduler_schedule (scheduler, parameters, cancellable,
+ error);
+ if (schedule_entry == NULL)
+ return FALSE;
+
+ /* Wait until the download is allowed to proceed. */
+ if (!mwsc_schedule_entry_get_download_now (schedule_entry)) {
+ gboolean download_now = FALSE;
+ g_autoptr(GError) invalidated_error = NULL;
+ gulong notify_id, invalidated_id;
+ DownloadNowData download_now_data = { &download_now, context };
+ InvalidatedData invalidated_data = { &invalidated_error, context };
+
+ notify_id = g_signal_connect (schedule_entry, "notify::download-now",
+ (GCallback) download_now_cb, &download_now_data);
+ invalidated_id = g_signal_connect (schedule_entry, "invalidated",
+ (GCallback) invalidated_cb, &invalidated_data);
+
+ while (!download_now && invalidated_error == NULL &&
+ !g_cancellable_is_cancelled (cancellable))
+ g_main_context_iteration (context, TRUE);
+
+ g_signal_handler_disconnect (schedule_entry, invalidated_id);
+ g_signal_handler_disconnect (schedule_entry, notify_id);
+
+ if (!download_now && invalidated_error != NULL) {
+ g_propagate_error (error, g_steal_pointer (&invalidated_error));
+ return FALSE;
+ } else if (!download_now && g_cancellable_set_error_if_cancelled (cancellable, error)) {
+ return FALSE;
+ }
+
+ g_assert (download_now);
+ }
+
+ g_debug ("%s: Allowed to download", G_STRFUNC);
+#else /* if !HAVE_MOGWAI */
+ g_debug ("%s: Allowed to download (Mogwai support compiled out)", G_STRFUNC);
+#endif /* !HAVE_MOGWAI */
+
+ return TRUE;
+}
+
+/**
+ * gs_metered_block_app_on_download_scheduler:
+ * @app: a #GsApp to get the scheduler parameters from
+ * @cancellable: a #GCancellable, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Version of gs_metered_block_on_download_scheduler() which extracts the
+ * download parameters from the given @app.
+ *
+ * Returns: %TRUE on success, %FALSE otherwise
+ * Since: 2.34
+ */
+gboolean
+gs_metered_block_app_on_download_scheduler (GsApp *app,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_auto(GVariantDict) parameters_dict = G_VARIANT_DICT_INIT (NULL);
+ g_autoptr(GVariant) parameters = NULL;
+ guint64 download_size = gs_app_get_size_download (app);
+
+ /* Currently no plugins support resumable downloads. This may change in
+ * future, in which case this parameter should be refactored. */
+ g_variant_dict_insert (&parameters_dict, "resumable", "b", FALSE);
+
+ if (download_size != 0 && download_size != GS_APP_SIZE_UNKNOWABLE) {
+ g_variant_dict_insert (&parameters_dict, "size-minimum", "t", download_size);
+ g_variant_dict_insert (&parameters_dict, "size-maximum", "t", download_size);
+ }
+
+ parameters = g_variant_ref_sink (g_variant_dict_end (&parameters_dict));
+
+ return gs_metered_block_on_download_scheduler (parameters, cancellable, error);
+}
+
+/**
+ * gs_metered_block_app_list_on_download_scheduler:
+ * @app_list: a #GsAppList to get the scheduler parameters from
+ * @cancellable: a #GCancellable, or %NULL
+ * @error: return location for a #GError, or %NULL
+ *
+ * Version of gs_metered_block_on_download_scheduler() which extracts the
+ * download parameters from the apps in the given @app_list.
+ *
+ * Returns: %TRUE on success, %FALSE otherwise
+ * Since: 2.34
+ */
+gboolean
+gs_metered_block_app_list_on_download_scheduler (GsAppList *app_list,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_auto(GVariantDict) parameters_dict = G_VARIANT_DICT_INIT (NULL);
+ g_autoptr(GVariant) parameters = NULL;
+
+ /* Currently no plugins support resumable downloads. This may change in
+ * future, in which case this parameter should be refactored. */
+ g_variant_dict_insert (&parameters_dict, "resumable", "b", FALSE);
+
+ /* FIXME: Currently this creates a single Mogwai schedule entry for the
+ * entire app list. Eventually, we probably want one schedule entry per
+ * app being downloaded, so that they can be individually prioritised.
+ * However, that requires much deeper integration into the download
+ * code, and Mogwai does not currently support that level of
+ * prioritisation, so go with this simple implementation for now. */
+ parameters = g_variant_ref_sink (g_variant_dict_end (&parameters_dict));
+
+ return gs_metered_block_on_download_scheduler (parameters, cancellable, error);
+}
diff --git a/lib/gs-metered.h b/lib/gs-metered.h
new file mode 100644
index 0000000..f8cdc1f
--- /dev/null
+++ b/lib/gs-metered.h
@@ -0,0 +1,29 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2019 Endless Mobile, Inc.
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+#pragma once
+
+#include <glib-object.h>
+#include <gio/gio.h>
+
+#include "gs-app.h"
+#include "gs-app-list.h"
+
+G_BEGIN_DECLS
+
+gboolean gs_metered_block_on_download_scheduler (GVariant *parameters,
+ GCancellable *cancellable,
+ GError **error);
+gboolean gs_metered_block_app_on_download_scheduler (GsApp *app,
+ GCancellable *cancellable,
+ GError **error);
+gboolean gs_metered_block_app_list_on_download_scheduler (GsAppList *app_list,
+ GCancellable *cancellable,
+ GError **error);
+
+G_END_DECLS
diff --git a/lib/gs-os-release.c b/lib/gs-os-release.c
new file mode 100644
index 0000000..305c5bb
--- /dev/null
+++ b/lib/gs-os-release.c
@@ -0,0 +1,335 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2016 Kalev Lember <klember@redhat.com>
+ * Copyright (C) 2016 Richard Hughes <richard@hughsie.com>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+/**
+ * SECTION:gs-os-release
+ * @title: GsOsRelease
+ * @include: gnome-software.h
+ * @stability: Unstable
+ * @short_description: Data from os-release
+ *
+ * This object allows plugins to parse /etc/os-release for distribution
+ * metadata information.
+ */
+
+#include "config.h"
+
+#include <glib.h>
+
+#include "gs-os-release.h"
+
+struct _GsOsRelease
+{
+ GObject parent_instance;
+ gchar *name;
+ gchar *version;
+ gchar *id;
+ gchar **id_like;
+ gchar *version_id;
+ gchar *pretty_name;
+ gchar *cpe_name;
+ gchar *distro_codename;
+ gchar *home_url;
+};
+
+static void gs_os_release_initable_iface_init (GInitableIface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (GsOsRelease, gs_os_release, G_TYPE_OBJECT,
+ G_IMPLEMENT_INTERFACE(G_TYPE_INITABLE, gs_os_release_initable_iface_init))
+
+static gboolean
+gs_os_release_initable_init (GInitable *initable,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GsOsRelease *os_release = GS_OS_RELEASE (initable);
+ const gchar *filename;
+ g_autofree gchar *data = NULL;
+ g_auto(GStrv) lines = NULL;
+ guint i;
+
+ g_return_val_if_fail (GS_IS_OS_RELEASE (os_release), FALSE);
+ g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
+
+ /* get contents */
+ filename = g_getenv ("GS_SELF_TEST_OS_RELEASE_FILENAME");
+ if (filename == NULL) {
+ filename = "/etc/os-release";
+ if (!g_file_test (filename, G_FILE_TEST_EXISTS))
+ filename = "/usr/lib/os-release";
+ }
+ if (!g_file_get_contents (filename, &data, NULL, error))
+ return FALSE;
+
+ /* parse */
+ lines = g_strsplit (data, "\n", -1);
+ for (i = 0; lines[i] != NULL; i++) {
+ gchar *tmp;
+
+ /* split the line up into two halves */
+ tmp = g_strstr_len (lines[i], -1, "=");
+ if (tmp == NULL)
+ continue;
+ *tmp = '\0';
+ tmp++;
+
+ /* ignore trailing quote */
+ if (tmp[0] == '\"')
+ tmp++;
+
+ /* ignore trailing quote */
+ g_strdelimit (tmp, "\"", '\0');
+
+ /* match fields we're interested in */
+ if (g_strcmp0 (lines[i], "NAME") == 0) {
+ os_release->name = g_strdup (tmp);
+ continue;
+ }
+ if (g_strcmp0 (lines[i], "VERSION") == 0) {
+ os_release->version = g_strdup (tmp);
+ continue;
+ }
+ if (g_strcmp0 (lines[i], "ID") == 0) {
+ os_release->id = g_strdup (tmp);
+ continue;
+ }
+ if (g_strcmp0 (lines[i], "ID_LIKE") == 0) {
+ os_release->id_like = g_strsplit (tmp, " ", 0);
+ continue;
+ }
+ if (g_strcmp0 (lines[i], "VERSION_ID") == 0) {
+ os_release->version_id = g_strdup (tmp);
+ continue;
+ }
+ if (g_strcmp0 (lines[i], "PRETTY_NAME") == 0) {
+ os_release->pretty_name = g_strdup (tmp);
+ continue;
+ }
+ if (g_strcmp0 (lines[i], "CPE_NAME") == 0) {
+ os_release->cpe_name = g_strdup (tmp);
+ continue;
+ }
+ if (g_strcmp0 (lines[i], "UBUNTU_CODENAME") == 0) {
+ os_release->distro_codename = g_strdup (tmp);
+ continue;
+ }
+ if (g_strcmp0 (lines[i], "HOME_URL") == 0) {
+ os_release->home_url = g_strdup (tmp);
+ continue;
+ }
+ }
+ return TRUE;
+}
+
+/**
+ * gs_os_release_get_name:
+ * @os_release: A #GsOsRelease
+ *
+ * Gets the name from the os-release parser.
+ *
+ * Returns: a string, or %NULL
+ *
+ * Since: 3.22
+ **/
+const gchar *
+gs_os_release_get_name (GsOsRelease *os_release)
+{
+ g_return_val_if_fail (GS_IS_OS_RELEASE (os_release), NULL);
+ return os_release->name;
+}
+
+/**
+ * gs_os_release_get_version:
+ * @os_release: A #GsOsRelease
+ *
+ * Gets the version from the os-release parser.
+ *
+ * Returns: a string, or %NULL
+ *
+ * Since: 3.22
+ **/
+const gchar *
+gs_os_release_get_version (GsOsRelease *os_release)
+{
+ g_return_val_if_fail (GS_IS_OS_RELEASE (os_release), NULL);
+ return os_release->version;
+}
+
+/**
+ * gs_os_release_get_id:
+ * @os_release: A #GsOsRelease
+ *
+ * Gets the ID from the os-release parser.
+ *
+ * Returns: a string, or %NULL
+ *
+ * Since: 3.22
+ **/
+const gchar *
+gs_os_release_get_id (GsOsRelease *os_release)
+{
+ g_return_val_if_fail (GS_IS_OS_RELEASE (os_release), NULL);
+ return os_release->id;
+}
+
+/**
+ * gs_os_release_get_id_like:
+ * @os_release: A #GsOsRelease
+ *
+ * Gets the ID_LIKE from the os-release parser. This is a list of operating
+ * systems that are "closely related" to the local operating system, possibly
+ * by being a derivative distribution.
+ *
+ * Returns: a %NULL terminated list
+ *
+ * Since: 3.26.2
+ **/
+const gchar * const *
+gs_os_release_get_id_like (GsOsRelease *os_release)
+{
+ g_return_val_if_fail (GS_IS_OS_RELEASE (os_release), NULL);
+ return (const gchar * const *) os_release->id_like;
+}
+
+/**
+ * gs_os_release_get_version_id:
+ * @os_release: A #GsOsRelease
+ *
+ * Gets the version ID from the os-release parser.
+ *
+ * Returns: a string, or %NULL
+ *
+ * Since: 3.22
+ **/
+const gchar *
+gs_os_release_get_version_id (GsOsRelease *os_release)
+{
+ g_return_val_if_fail (GS_IS_OS_RELEASE (os_release), NULL);
+ return os_release->version_id;
+}
+
+/**
+ * gs_os_release_get_pretty_name:
+ * @os_release: A #GsOsRelease
+ *
+ * Gets the pretty name from the os-release parser.
+ *
+ * Returns: a string, or %NULL
+ *
+ * Since: 3.22
+ **/
+const gchar *
+gs_os_release_get_pretty_name (GsOsRelease *os_release)
+{
+ g_return_val_if_fail (GS_IS_OS_RELEASE (os_release), NULL);
+ return os_release->pretty_name;
+}
+
+/**
+ * gs_os_release_get_cpe_name:
+ * @os_release: A #GsOsRelease
+ *
+ * Gets the pretty name from the os-release parser.
+ *
+ * Returns: a string, or %NULL
+ *
+ * Since: 3.22
+ **/
+const gchar *
+gs_os_release_get_cpe_name (GsOsRelease *os_release)
+{
+ g_return_val_if_fail (GS_IS_OS_RELEASE (os_release), NULL);
+ return os_release->cpe_name;
+}
+
+/**
+ * gs_os_release_get_distro_codename:
+ * @os_release: A #GsOsRelease
+ *
+ * Gets the distro codename from the os-release parser.
+ *
+ * Returns: a string, or %NULL
+ *
+ * Since: 3.22
+ **/
+const gchar *
+gs_os_release_get_distro_codename (GsOsRelease *os_release)
+{
+ g_return_val_if_fail (GS_IS_OS_RELEASE (os_release), NULL);
+ return os_release->distro_codename;
+}
+
+/**
+ * gs_os_release_get_home_url:
+ * @os_release: A #GsOsRelease
+ *
+ * Gets the home URL from the os-release parser.
+ *
+ * Returns: a string, or %NULL
+ *
+ * Since: 3.22
+ **/
+const gchar *
+gs_os_release_get_home_url (GsOsRelease *os_release)
+{
+ g_return_val_if_fail (GS_IS_OS_RELEASE (os_release), NULL);
+ return os_release->home_url;
+}
+
+static void
+gs_os_release_finalize (GObject *object)
+{
+ GsOsRelease *os_release = GS_OS_RELEASE (object);
+ g_free (os_release->name);
+ g_free (os_release->version);
+ g_free (os_release->id);
+ g_strfreev (os_release->id_like);
+ g_free (os_release->version_id);
+ g_free (os_release->pretty_name);
+ g_free (os_release->cpe_name);
+ g_free (os_release->distro_codename);
+ g_free (os_release->home_url);
+ G_OBJECT_CLASS (gs_os_release_parent_class)->finalize (object);
+}
+
+static void
+gs_os_release_class_init (GsOsReleaseClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ object_class->finalize = gs_os_release_finalize;
+}
+
+static void
+gs_os_release_initable_iface_init (GInitableIface *iface)
+{
+ iface->init = gs_os_release_initable_init;
+}
+
+static void
+gs_os_release_init (GsOsRelease *os_release)
+{
+}
+
+/**
+ * gs_os_release_new:
+ * @error: a #GError, or %NULL
+ *
+ * Creates a new os_release.
+ *
+ * Returns: (transfer full): A newly allocated #GsOsRelease, or %NULL for error
+ *
+ * Since: 3.22
+ **/
+GsOsRelease *
+gs_os_release_new (GError **error)
+{
+ GsOsRelease *os_release;
+ os_release = g_initable_new (GS_TYPE_OS_RELEASE, NULL, error, NULL);
+ return GS_OS_RELEASE (os_release);
+}
diff --git a/lib/gs-os-release.h b/lib/gs-os-release.h
new file mode 100644
index 0000000..336f17c
--- /dev/null
+++ b/lib/gs-os-release.h
@@ -0,0 +1,33 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2016 Kalev Lember <klember@redhat.com>
+ * Copyright (C) 2016 Richard Hughes <richard@hughsie.com>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#include <glib-object.h>
+
+#include "gs-app.h"
+
+G_BEGIN_DECLS
+
+#define GS_TYPE_OS_RELEASE (gs_os_release_get_type ())
+
+G_DECLARE_FINAL_TYPE (GsOsRelease, gs_os_release, GS, OS_RELEASE, GObject)
+
+GsOsRelease *gs_os_release_new (GError **error);
+const gchar *gs_os_release_get_name (GsOsRelease *os_release);
+const gchar *gs_os_release_get_version (GsOsRelease *os_release);
+const gchar *gs_os_release_get_id (GsOsRelease *os_release);
+const gchar * const *gs_os_release_get_id_like (GsOsRelease *os_release);
+const gchar *gs_os_release_get_version_id (GsOsRelease *os_release);
+const gchar *gs_os_release_get_pretty_name (GsOsRelease *os_release);
+const gchar *gs_os_release_get_cpe_name (GsOsRelease *os_release);
+const gchar *gs_os_release_get_distro_codename (GsOsRelease *os_release);
+const gchar *gs_os_release_get_home_url (GsOsRelease *os_release);
+
+G_END_DECLS
diff --git a/lib/gs-plugin-event.c b/lib/gs-plugin-event.c
new file mode 100644
index 0000000..8d1fc83
--- /dev/null
+++ b/lib/gs-plugin-event.c
@@ -0,0 +1,313 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2016 Richard Hughes <richard@hughsie.com>
+ * Copyright (C) 2016 Kalev Lember <klember@redhat.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+/**
+ * SECTION:gs-plugin-event
+ * @title: GsPluginEvent
+ * @include: gnome-software.h
+ * @stability: Unstable
+ * @short_description: Information about a plugin event
+ *
+ * These functions provide a way for plugins to tell the UI layer about events
+ * that may require displaying to the user. Plugins should not assume that a
+ * specific event is actually shown to the user as it may be ignored
+ * automatically.
+ */
+
+#include "config.h"
+
+#include <glib.h>
+
+#include "gs-plugin-private.h"
+#include "gs-plugin-event.h"
+
+struct _GsPluginEvent
+{
+ GObject parent_instance;
+ GsApp *app;
+ GsApp *origin;
+ GsPluginAction action;
+ GError *error;
+ GsPluginEventFlag flags;
+ gchar *unique_id;
+};
+
+G_DEFINE_TYPE (GsPluginEvent, gs_plugin_event, G_TYPE_OBJECT)
+
+/**
+ * gs_plugin_event_set_app:
+ * @event: A #GsPluginEvent
+ * @app: A #GsApp
+ *
+ * Set the application (or source, or whatever component) that caused the event
+ * to be created.
+ *
+ * Since: 3.22
+ **/
+void
+gs_plugin_event_set_app (GsPluginEvent *event, GsApp *app)
+{
+ g_return_if_fail (GS_IS_PLUGIN_EVENT (event));
+ g_return_if_fail (GS_IS_APP (app));
+ g_set_object (&event->app, app);
+}
+
+/**
+ * gs_plugin_event_get_app:
+ * @event: A #GsPluginEvent
+ *
+ * Gets an application that created the event.
+ *
+ * Returns: (transfer none): a #GsApp, or %NULL if unset
+ *
+ * Since: 3.22
+ **/
+GsApp *
+gs_plugin_event_get_app (GsPluginEvent *event)
+{
+ g_return_val_if_fail (GS_IS_PLUGIN_EVENT (event), NULL);
+ return event->app;
+}
+
+/**
+ * gs_plugin_event_set_origin:
+ * @event: A #GsPluginEvent
+ * @origin: A #GsApp
+ *
+ * Set the origin that caused the event to be created.
+ *
+ * Since: 3.22
+ **/
+void
+gs_plugin_event_set_origin (GsPluginEvent *event, GsApp *origin)
+{
+ g_return_if_fail (GS_IS_PLUGIN_EVENT (event));
+ g_return_if_fail (GS_IS_APP (origin));
+ g_set_object (&event->origin, origin);
+}
+
+/**
+ * gs_plugin_event_get_origin:
+ * @event: A #GsPluginEvent
+ *
+ * Gets an origin that created the event.
+ *
+ * Returns: (transfer none): a #GsApp, or %NULL if unset
+ *
+ * Since: 3.22
+ **/
+GsApp *
+gs_plugin_event_get_origin (GsPluginEvent *event)
+{
+ g_return_val_if_fail (GS_IS_PLUGIN_EVENT (event), NULL);
+ return event->origin;
+}
+
+/**
+ * gs_plugin_event_set_action:
+ * @event: A #GsPluginEvent
+ * @action: A #GsPluginAction, e.g. %GS_PLUGIN_ACTION_UPDATE
+ *
+ * Set the action that caused the event to be created.
+ *
+ * Since: 3.22
+ **/
+void
+gs_plugin_event_set_action (GsPluginEvent *event, GsPluginAction action)
+{
+ g_return_if_fail (GS_IS_PLUGIN_EVENT (event));
+ event->action = action;
+}
+
+/**
+ * gs_plugin_event_get_action:
+ * @event: A #GsPluginEvent
+ *
+ * Gets an action that created the event.
+ *
+ * Returns: (transfer none): a #GsPluginAction, e.g. %GS_PLUGIN_ACTION_UPDATE
+ *
+ * Since: 3.22
+ **/
+GsPluginAction
+gs_plugin_event_get_action (GsPluginEvent *event)
+{
+ g_return_val_if_fail (GS_IS_PLUGIN_EVENT (event), 0);
+ return event->action;
+}
+
+/**
+ * gs_plugin_event_get_unique_id:
+ * @event: A #GsPluginEvent
+ *
+ * Gets the unique ID for the event. In most cases (if an app has been set)
+ * this will just be the actual #GsApp unique-id. In the cases where only error
+ * has been set a virtual (but plausible) ID will be generated.
+ *
+ * Returns: a string, or %NULL for invalid
+ *
+ * Since: 3.22
+ **/
+const gchar *
+gs_plugin_event_get_unique_id (GsPluginEvent *event)
+{
+ /* just proxy */
+ if (event->origin != NULL &&
+ gs_app_get_unique_id (event->origin) != NULL) {
+ return gs_app_get_unique_id (event->origin);
+ }
+ if (event->app != NULL &&
+ gs_app_get_unique_id (event->app) != NULL) {
+ return gs_app_get_unique_id (event->app);
+ }
+
+ /* generate from error */
+ if (event->error != NULL) {
+ if (event->unique_id == NULL) {
+ g_autofree gchar *id = NULL;
+ id = g_strdup_printf ("%s.error",
+ gs_plugin_error_to_string (event->error->code));
+ event->unique_id = as_utils_unique_id_build (AS_APP_SCOPE_UNKNOWN,
+ AS_BUNDLE_KIND_UNKNOWN,
+ NULL,
+ AS_APP_KIND_UNKNOWN,
+ id,
+ NULL);
+ }
+ return event->unique_id;
+ }
+
+ /* failed */
+ return NULL;
+}
+
+/**
+ * gs_plugin_event_get_kind:
+ * @event: A #GsPluginEvent
+ * @flag: A #GsPluginEventFlag, e.g. %GS_PLUGIN_EVENT_FLAG_INVALID
+ *
+ * Adds a flag to the event.
+ *
+ * Since: 3.22
+ **/
+void
+gs_plugin_event_add_flag (GsPluginEvent *event, GsPluginEventFlag flag)
+{
+ g_return_if_fail (GS_IS_PLUGIN_EVENT (event));
+ event->flags |= flag;
+}
+
+/**
+ * gs_plugin_event_set_kind:
+ * @event: A #GsPluginEvent
+ * @flag: A #GsPluginEventFlag, e.g. %GS_PLUGIN_EVENT_FLAG_INVALID
+ *
+ * Removes a flag from the event.
+ *
+ * Since: 3.22
+ **/
+void
+gs_plugin_event_remove_flag (GsPluginEvent *event, GsPluginEventFlag flag)
+{
+ g_return_if_fail (GS_IS_PLUGIN_EVENT (event));
+ event->flags &= ~flag;
+}
+
+/**
+ * gs_plugin_event_has_flag:
+ * @event: A #GsPluginEvent
+ * @flag: A #GsPluginEventFlag, e.g. %GS_PLUGIN_EVENT_FLAG_INVALID
+ *
+ * Finds out if the event has a specific flag.
+ *
+ * Returns: %TRUE if the flag is set
+ *
+ * Since: 3.22
+ **/
+gboolean
+gs_plugin_event_has_flag (GsPluginEvent *event, GsPluginEventFlag flag)
+{
+ g_return_val_if_fail (GS_IS_PLUGIN_EVENT (event), FALSE);
+ return ((event->flags & flag) > 0);
+}
+
+/**
+ * gs_plugin_event_set_error:
+ * @event: A #GsPluginEvent
+ * @error: A #GError
+ *
+ * Sets the event error.
+ *
+ * Since: 3.22
+ **/
+void
+gs_plugin_event_set_error (GsPluginEvent *event, const GError *error)
+{
+ g_clear_error (&event->error);
+ event->error = g_error_copy (error);
+}
+
+/**
+ * gs_plugin_event_get_error:
+ * @event: A #GsPluginEvent
+ *
+ * Gets the event error.
+ *
+ * Returns: a #GError, or %NULL for unset
+ *
+ * Since: 3.22
+ **/
+const GError *
+gs_plugin_event_get_error (GsPluginEvent *event)
+{
+ return event->error;
+}
+
+static void
+gs_plugin_event_finalize (GObject *object)
+{
+ GsPluginEvent *event = GS_PLUGIN_EVENT (object);
+ if (event->error != NULL)
+ g_error_free (event->error);
+ if (event->app != NULL)
+ g_object_unref (event->app);
+ if (event->origin != NULL)
+ g_object_unref (event->origin);
+ g_free (event->unique_id);
+ G_OBJECT_CLASS (gs_plugin_event_parent_class)->finalize (object);
+}
+
+static void
+gs_plugin_event_class_init (GsPluginEventClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ object_class->finalize = gs_plugin_event_finalize;
+}
+
+static void
+gs_plugin_event_init (GsPluginEvent *event)
+{
+}
+
+/**
+ * gs_plugin_event_new:
+ *
+ * Creates a new event.
+ *
+ * Returns: A newly allocated #GsPluginEvent
+ *
+ * Since: 3.22
+ **/
+GsPluginEvent *
+gs_plugin_event_new (void)
+{
+ GsPluginEvent *event;
+ event = g_object_new (GS_TYPE_PLUGIN_EVENT, NULL);
+ return GS_PLUGIN_EVENT (event);
+}
diff --git a/lib/gs-plugin-event.h b/lib/gs-plugin-event.h
new file mode 100644
index 0000000..dc74486
--- /dev/null
+++ b/lib/gs-plugin-event.h
@@ -0,0 +1,67 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2016 Richard Hughes <richard@hughsie.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+#pragma once
+
+#include <glib-object.h>
+
+#include "gs-app.h"
+#include "gs-plugin-types.h"
+
+G_BEGIN_DECLS
+
+#define GS_TYPE_PLUGIN_EVENT (gs_plugin_event_get_type ())
+
+G_DECLARE_FINAL_TYPE (GsPluginEvent, gs_plugin_event, GS, PLUGIN_EVENT, GObject)
+
+/**
+ * GsPluginEventFlag:
+ * @GS_PLUGIN_EVENT_FLAG_NONE: No special flags set
+ * @GS_PLUGIN_EVENT_FLAG_INVALID: Event is no longer valid, e.g. was dismissed
+ * @GS_PLUGIN_EVENT_FLAG_VISIBLE: Event is is visible on the screen
+ * @GS_PLUGIN_EVENT_FLAG_WARNING: Event should be shown with more urgency
+ * @GS_PLUGIN_EVENT_FLAG_INTERACTIVE: The plugin job was created with interactive=True
+ *
+ * Any flags an event can have.
+ **/
+typedef enum {
+ GS_PLUGIN_EVENT_FLAG_NONE = 0, /* Since: 3.22 */
+ GS_PLUGIN_EVENT_FLAG_INVALID = 1 << 0, /* Since: 3.22 */
+ GS_PLUGIN_EVENT_FLAG_VISIBLE = 1 << 1, /* Since: 3.22 */
+ GS_PLUGIN_EVENT_FLAG_WARNING = 1 << 2, /* Since: 3.22 */
+ GS_PLUGIN_EVENT_FLAG_INTERACTIVE = 1 << 3, /* Since: 3.30 */
+ /*< private >*/
+ GS_PLUGIN_EVENT_FLAG_LAST
+} GsPluginEventFlag;
+
+GsPluginEvent *gs_plugin_event_new (void);
+
+const gchar *gs_plugin_event_get_unique_id (GsPluginEvent *event);
+
+void gs_plugin_event_set_app (GsPluginEvent *event,
+ GsApp *app);
+GsApp *gs_plugin_event_get_app (GsPluginEvent *event);
+void gs_plugin_event_set_origin (GsPluginEvent *event,
+ GsApp *origin);
+GsApp *gs_plugin_event_get_origin (GsPluginEvent *event);
+void gs_plugin_event_set_action (GsPluginEvent *event,
+ GsPluginAction action);
+GsPluginAction gs_plugin_event_get_action (GsPluginEvent *event);
+
+void gs_plugin_event_set_error (GsPluginEvent *event,
+ const GError *error);
+const GError *gs_plugin_event_get_error (GsPluginEvent *event);
+
+void gs_plugin_event_add_flag (GsPluginEvent *event,
+ GsPluginEventFlag flag);
+void gs_plugin_event_remove_flag (GsPluginEvent *event,
+ GsPluginEventFlag flag);
+gboolean gs_plugin_event_has_flag (GsPluginEvent *event,
+ GsPluginEventFlag flag);
+
+G_END_DECLS
diff --git a/lib/gs-plugin-job-private.h b/lib/gs-plugin-job-private.h
new file mode 100644
index 0000000..863d890
--- /dev/null
+++ b/lib/gs-plugin-job-private.h
@@ -0,0 +1,44 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2017 Richard Hughes <richard@hughsie.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+#pragma once
+
+#include <glib-object.h>
+
+#include "gs-plugin-job.h"
+
+G_BEGIN_DECLS
+
+GsPluginAction gs_plugin_job_get_action (GsPluginJob *self);
+GsPluginRefineFlags gs_plugin_job_get_filter_flags (GsPluginJob *self);
+GsAppListFilterFlags gs_plugin_job_get_dedupe_flags (GsPluginJob *self);
+GsPluginRefineFlags gs_plugin_job_get_refine_flags (GsPluginJob *self);
+gboolean gs_plugin_job_has_refine_flags (GsPluginJob *self,
+ GsPluginRefineFlags refine_flags);
+void gs_plugin_job_add_refine_flags (GsPluginJob *self,
+ GsPluginRefineFlags refine_flags);
+void gs_plugin_job_remove_refine_flags (GsPluginJob *self,
+ GsPluginRefineFlags refine_flags);
+gboolean gs_plugin_job_get_interactive (GsPluginJob *self);
+guint gs_plugin_job_get_max_results (GsPluginJob *self);
+guint gs_plugin_job_get_timeout (GsPluginJob *self);
+guint64 gs_plugin_job_get_age (GsPluginJob *self);
+GsAppListSortFunc gs_plugin_job_get_sort_func (GsPluginJob *self);
+gpointer gs_plugin_job_get_sort_func_data (GsPluginJob *self);
+const gchar *gs_plugin_job_get_search (GsPluginJob *self);
+GsApp *gs_plugin_job_get_app (GsPluginJob *self);
+GsAppList *gs_plugin_job_get_list (GsPluginJob *self);
+GFile *gs_plugin_job_get_file (GsPluginJob *self);
+GsPlugin *gs_plugin_job_get_plugin (GsPluginJob *self);
+GsCategory *gs_plugin_job_get_category (GsPluginJob *self);
+AsReview *gs_plugin_job_get_review (GsPluginJob *self);
+gchar *gs_plugin_job_to_string (GsPluginJob *self);
+void gs_plugin_job_set_action (GsPluginJob *self,
+ GsPluginAction action);
+
+G_END_DECLS
diff --git a/lib/gs-plugin-job.c b/lib/gs-plugin-job.c
new file mode 100644
index 0000000..1ce320f
--- /dev/null
+++ b/lib/gs-plugin-job.c
@@ -0,0 +1,618 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2017-2018 Richard Hughes <richard@hughsie.com>
+ * Copyright (C) 2018 Kalev Lember <klember@redhat.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+#include "config.h"
+
+#include <glib.h>
+
+#include "gs-plugin-private.h"
+#include "gs-plugin-job-private.h"
+
+struct _GsPluginJob
+{
+ GObject parent_instance;
+ GsPluginRefineFlags refine_flags;
+ GsPluginRefineFlags filter_flags;
+ GsAppListFilterFlags dedupe_flags;
+ gboolean interactive;
+ guint max_results;
+ guint timeout;
+ guint64 age;
+ GsPlugin *plugin;
+ GsPluginAction action;
+ GsAppListSortFunc sort_func;
+ gpointer sort_func_data;
+ gchar *search;
+ GsApp *app;
+ GsAppList *list;
+ GFile *file;
+ GsCategory *category;
+ AsReview *review;
+ gint64 time_created;
+};
+
+enum {
+ PROP_0,
+ PROP_ACTION,
+ PROP_AGE,
+ PROP_SEARCH,
+ PROP_REFINE_FLAGS,
+ PROP_FILTER_FLAGS,
+ PROP_DEDUPE_FLAGS,
+ PROP_INTERACTIVE,
+ PROP_APP,
+ PROP_LIST,
+ PROP_FILE,
+ PROP_CATEGORY,
+ PROP_REVIEW,
+ PROP_MAX_RESULTS,
+ PROP_TIMEOUT,
+ PROP_LAST
+};
+
+G_DEFINE_TYPE (GsPluginJob, gs_plugin_job, G_TYPE_OBJECT)
+
+gchar *
+gs_plugin_job_to_string (GsPluginJob *self)
+{
+ GString *str = g_string_new (NULL);
+ gint64 time_now = g_get_monotonic_time ();
+ g_string_append_printf (str, "running %s",
+ gs_plugin_action_to_string (self->action));
+ if (self->plugin != NULL) {
+ g_string_append_printf (str, " on plugin=%s",
+ gs_plugin_get_name (self->plugin));
+ }
+ if (self->filter_flags > 0) {
+ g_autofree gchar *tmp = gs_plugin_refine_flags_to_string (self->filter_flags);
+ g_string_append_printf (str, " with filter-flags=%s", tmp);
+ }
+ if (self->dedupe_flags > 0)
+ g_string_append_printf (str, " with dedupe-flags=%" G_GUINT64_FORMAT, self->dedupe_flags);
+ if (self->refine_flags > 0) {
+ g_autofree gchar *tmp = gs_plugin_refine_flags_to_string (self->refine_flags);
+ g_string_append_printf (str, " with refine-flags=%s", tmp);
+ }
+ if (self->interactive)
+ g_string_append_printf (str, " with interactive=True");
+ if (self->timeout > 0)
+ g_string_append_printf (str, " with timeout=%u", self->timeout);
+ if (self->max_results > 0)
+ g_string_append_printf (str, " with max-results=%u", self->max_results);
+ if (self->age != 0) {
+ if (self->age == G_MAXUINT) {
+ g_string_append (str, " with cache age=any");
+ } else {
+ g_string_append_printf (str, " with cache age=%" G_GUINT64_FORMAT,
+ self->age);
+ }
+ }
+ if (self->search != NULL) {
+ g_string_append_printf (str, " with search=%s",
+ self->search);
+ }
+ if (self->category != NULL) {
+ GsCategory *parent = gs_category_get_parent (self->category);
+ if (parent != NULL) {
+ g_string_append_printf (str, " with category=%s/%s",
+ gs_category_get_id (parent),
+ gs_category_get_id (self->category));
+ } else {
+ g_string_append_printf (str, " with category=%s",
+ gs_category_get_id (self->category));
+ }
+ }
+ if (self->review != NULL) {
+ g_string_append_printf (str, " with review=%s",
+ as_review_get_id (self->review));
+ }
+ if (self->file != NULL) {
+ g_autofree gchar *path = g_file_get_path (self->file);
+ g_string_append_printf (str, " with file=%s", path);
+ }
+ if (self->list != NULL && gs_app_list_length (self->list) > 0) {
+ g_autofree const gchar **unique_ids = NULL;
+ g_autofree gchar *unique_ids_str = NULL;
+ unique_ids = g_new0 (const gchar *, gs_app_list_length (self->list) + 1);
+ for (guint i = 0; i < gs_app_list_length (self->list); i++) {
+ GsApp *app = gs_app_list_index (self->list, i);
+ unique_ids[i] = gs_app_get_unique_id (app);
+ }
+ unique_ids_str = g_strjoinv (",", (gchar**) unique_ids);
+ g_string_append_printf (str, " on apps %s", unique_ids_str);
+ }
+ if (time_now - self->time_created > 1000) {
+ g_string_append_printf (str, ", elapsed time since creation %" G_GINT64_FORMAT "ms",
+ (time_now - self->time_created) / 1000);
+ }
+ return g_string_free (str, FALSE);
+}
+
+void
+gs_plugin_job_set_refine_flags (GsPluginJob *self, GsPluginRefineFlags refine_flags)
+{
+ g_return_if_fail (GS_IS_PLUGIN_JOB (self));
+ self->refine_flags = refine_flags;
+}
+
+void
+gs_plugin_job_set_filter_flags (GsPluginJob *self, GsPluginRefineFlags filter_flags)
+{
+ g_return_if_fail (GS_IS_PLUGIN_JOB (self));
+ self->filter_flags = filter_flags;
+}
+
+void
+gs_plugin_job_set_dedupe_flags (GsPluginJob *self, GsAppListFilterFlags dedupe_flags)
+{
+ g_return_if_fail (GS_IS_PLUGIN_JOB (self));
+ self->dedupe_flags = dedupe_flags;
+}
+
+GsPluginRefineFlags
+gs_plugin_job_get_refine_flags (GsPluginJob *self)
+{
+ g_return_val_if_fail (GS_IS_PLUGIN_JOB (self), 0);
+ return self->refine_flags;
+}
+
+GsPluginRefineFlags
+gs_plugin_job_get_filter_flags (GsPluginJob *self)
+{
+ g_return_val_if_fail (GS_IS_PLUGIN_JOB (self), 0);
+ return self->filter_flags;
+}
+
+GsAppListFilterFlags
+gs_plugin_job_get_dedupe_flags (GsPluginJob *self)
+{
+ g_return_val_if_fail (GS_IS_PLUGIN_JOB (self), 0);
+ return self->dedupe_flags;
+}
+
+gboolean
+gs_plugin_job_has_refine_flags (GsPluginJob *self, GsPluginRefineFlags refine_flags)
+{
+ g_return_val_if_fail (GS_IS_PLUGIN_JOB (self), FALSE);
+ return (self->refine_flags & refine_flags) > 0;
+}
+
+void
+gs_plugin_job_add_refine_flags (GsPluginJob *self, GsPluginRefineFlags refine_flags)
+{
+ g_return_if_fail (GS_IS_PLUGIN_JOB (self));
+ self->refine_flags |= refine_flags;
+}
+
+void
+gs_plugin_job_remove_refine_flags (GsPluginJob *self, GsPluginRefineFlags refine_flags)
+{
+ g_return_if_fail (GS_IS_PLUGIN_JOB (self));
+ self->refine_flags &= ~refine_flags;
+}
+
+void
+gs_plugin_job_set_interactive (GsPluginJob *self, gboolean interactive)
+{
+ g_return_if_fail (GS_IS_PLUGIN_JOB (self));
+ self->interactive = interactive;
+}
+
+gboolean
+gs_plugin_job_get_interactive (GsPluginJob *self)
+{
+ g_return_val_if_fail (GS_IS_PLUGIN_JOB (self), FALSE);
+ return self->interactive;
+}
+
+void
+gs_plugin_job_set_max_results (GsPluginJob *self, guint max_results)
+{
+ g_return_if_fail (GS_IS_PLUGIN_JOB (self));
+ self->max_results = max_results;
+}
+
+guint
+gs_plugin_job_get_max_results (GsPluginJob *self)
+{
+ g_return_val_if_fail (GS_IS_PLUGIN_JOB (self), 0);
+ return self->max_results;
+}
+
+void
+gs_plugin_job_set_timeout (GsPluginJob *self, guint timeout)
+{
+ g_return_if_fail (GS_IS_PLUGIN_JOB (self));
+ self->timeout = timeout;
+}
+
+guint
+gs_plugin_job_get_timeout (GsPluginJob *self)
+{
+ g_return_val_if_fail (GS_IS_PLUGIN_JOB (self), 0);
+ return self->timeout;
+}
+
+void
+gs_plugin_job_set_age (GsPluginJob *self, guint64 age)
+{
+ g_return_if_fail (GS_IS_PLUGIN_JOB (self));
+ self->age = age;
+}
+
+guint64
+gs_plugin_job_get_age (GsPluginJob *self)
+{
+ g_return_val_if_fail (GS_IS_PLUGIN_JOB (self), 0);
+ return self->age;
+}
+
+void
+gs_plugin_job_set_action (GsPluginJob *self, GsPluginAction action)
+{
+ g_return_if_fail (GS_IS_PLUGIN_JOB (self));
+ self->action = action;
+}
+
+GsPluginAction
+gs_plugin_job_get_action (GsPluginJob *self)
+{
+ g_return_val_if_fail (GS_IS_PLUGIN_JOB (self), 0);
+ return self->action;
+}
+
+void
+gs_plugin_job_set_sort_func (GsPluginJob *self, GsAppListSortFunc sort_func)
+{
+ g_return_if_fail (GS_IS_PLUGIN_JOB (self));
+ self->sort_func = sort_func;
+}
+
+GsAppListSortFunc
+gs_plugin_job_get_sort_func (GsPluginJob *self)
+{
+ g_return_val_if_fail (GS_IS_PLUGIN_JOB (self), 0);
+ return self->sort_func;
+}
+
+void
+gs_plugin_job_set_sort_func_data (GsPluginJob *self, gpointer sort_func_data)
+{
+ g_return_if_fail (GS_IS_PLUGIN_JOB (self));
+ self->sort_func_data = sort_func_data;
+}
+
+gpointer
+gs_plugin_job_get_sort_func_data (GsPluginJob *self)
+{
+ g_return_val_if_fail (GS_IS_PLUGIN_JOB (self), NULL);
+ return self->sort_func_data;
+}
+
+void
+gs_plugin_job_set_search (GsPluginJob *self, const gchar *search)
+{
+ g_return_if_fail (GS_IS_PLUGIN_JOB (self));
+ g_free (self->search);
+ self->search = g_strdup (search);
+}
+
+const gchar *
+gs_plugin_job_get_search (GsPluginJob *self)
+{
+ g_return_val_if_fail (GS_IS_PLUGIN_JOB (self), NULL);
+ return self->search;
+}
+
+void
+gs_plugin_job_set_app (GsPluginJob *self, GsApp *app)
+{
+ g_return_if_fail (GS_IS_PLUGIN_JOB (self));
+ g_set_object (&self->app, app);
+
+ /* ensure we can always operate on a list object */
+ if (self->list != NULL && app != NULL && gs_app_list_length (self->list) == 0)
+ gs_app_list_add (self->list, self->app);
+}
+
+GsApp *
+gs_plugin_job_get_app (GsPluginJob *self)
+{
+ g_return_val_if_fail (GS_IS_PLUGIN_JOB (self), NULL);
+ return self->app;
+}
+
+void
+gs_plugin_job_set_list (GsPluginJob *self, GsAppList *list)
+{
+ g_return_if_fail (GS_IS_PLUGIN_JOB (self));
+ if (list == NULL)
+ g_warning ("trying to set list to NULL, not a good idea");
+ g_set_object (&self->list, list);
+}
+
+GsAppList *
+gs_plugin_job_get_list (GsPluginJob *self)
+{
+ g_return_val_if_fail (GS_IS_PLUGIN_JOB (self), NULL);
+ return self->list;
+}
+
+void
+gs_plugin_job_set_file (GsPluginJob *self, GFile *file)
+{
+ g_return_if_fail (GS_IS_PLUGIN_JOB (self));
+ g_set_object (&self->file, file);
+}
+
+GFile *
+gs_plugin_job_get_file (GsPluginJob *self)
+{
+ g_return_val_if_fail (GS_IS_PLUGIN_JOB (self), NULL);
+ return self->file;
+}
+
+void
+gs_plugin_job_set_plugin (GsPluginJob *self, GsPlugin *plugin)
+{
+ g_return_if_fail (GS_IS_PLUGIN_JOB (self));
+ g_set_object (&self->plugin, plugin);
+}
+
+GsPlugin *
+gs_plugin_job_get_plugin (GsPluginJob *self)
+{
+ g_return_val_if_fail (GS_IS_PLUGIN_JOB (self), NULL);
+ return self->plugin;
+}
+
+void
+gs_plugin_job_set_category (GsPluginJob *self, GsCategory *category)
+{
+ g_return_if_fail (GS_IS_PLUGIN_JOB (self));
+ g_set_object (&self->category, category);
+}
+
+GsCategory *
+gs_plugin_job_get_category (GsPluginJob *self)
+{
+ g_return_val_if_fail (GS_IS_PLUGIN_JOB (self), NULL);
+ return self->category;
+}
+
+void
+gs_plugin_job_set_review (GsPluginJob *self, AsReview *review)
+{
+ g_return_if_fail (GS_IS_PLUGIN_JOB (self));
+ g_set_object (&self->review, review);
+}
+
+AsReview *
+gs_plugin_job_get_review (GsPluginJob *self)
+{
+ g_return_val_if_fail (GS_IS_PLUGIN_JOB (self), NULL);
+ return self->review;
+}
+
+static void
+gs_plugin_job_get_property (GObject *obj, guint prop_id, GValue *value, GParamSpec *pspec)
+{
+ GsPluginJob *self = GS_PLUGIN_JOB (obj);
+
+ switch (prop_id) {
+ case PROP_ACTION:
+ g_value_set_uint (value, self->action);
+ break;
+ case PROP_AGE:
+ g_value_set_uint64 (value, self->age);
+ break;
+ case PROP_REFINE_FLAGS:
+ g_value_set_uint64 (value, self->refine_flags);
+ break;
+ case PROP_FILTER_FLAGS:
+ g_value_set_uint64 (value, self->filter_flags);
+ break;
+ case PROP_DEDUPE_FLAGS:
+ g_value_set_uint64 (value, self->dedupe_flags);
+ break;
+ case PROP_INTERACTIVE:
+ g_value_set_boolean (value, self->interactive);
+ break;
+ case PROP_SEARCH:
+ g_value_set_string (value, self->search);
+ break;
+ case PROP_APP:
+ g_value_set_object (value, self->app);
+ break;
+ case PROP_LIST:
+ g_value_set_object (value, self->list);
+ break;
+ case PROP_FILE:
+ g_value_set_object (value, self->file);
+ break;
+ case PROP_CATEGORY:
+ g_value_set_object (value, self->category);
+ break;
+ case PROP_REVIEW:
+ g_value_set_object (value, self->review);
+ break;
+ case PROP_MAX_RESULTS:
+ g_value_set_uint (value, self->max_results);
+ break;
+ case PROP_TIMEOUT:
+ g_value_set_uint (value, self->timeout);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+gs_plugin_job_set_property (GObject *obj, guint prop_id, const GValue *value, GParamSpec *pspec)
+{
+ GsPluginJob *self = GS_PLUGIN_JOB (obj);
+
+ switch (prop_id) {
+ case PROP_ACTION:
+ gs_plugin_job_set_action (self, g_value_get_uint (value));
+ break;
+ case PROP_AGE:
+ gs_plugin_job_set_age (self, g_value_get_uint64 (value));
+ break;
+ case PROP_REFINE_FLAGS:
+ gs_plugin_job_set_refine_flags (self, g_value_get_uint64 (value));
+ break;
+ case PROP_FILTER_FLAGS:
+ gs_plugin_job_set_filter_flags (self, g_value_get_uint64 (value));
+ break;
+ case PROP_DEDUPE_FLAGS:
+ gs_plugin_job_set_dedupe_flags (self, g_value_get_uint64 (value));
+ break;
+ case PROP_INTERACTIVE:
+ gs_plugin_job_set_interactive (self, g_value_get_boolean (value));
+ break;
+ case PROP_SEARCH:
+ gs_plugin_job_set_search (self, g_value_get_string (value));
+ break;
+ case PROP_APP:
+ gs_plugin_job_set_app (self, g_value_get_object (value));
+ break;
+ case PROP_LIST:
+ gs_plugin_job_set_list (self, g_value_get_object (value));
+ break;
+ case PROP_FILE:
+ gs_plugin_job_set_file (self, g_value_get_object (value));
+ break;
+ case PROP_CATEGORY:
+ gs_plugin_job_set_category (self, g_value_get_object (value));
+ break;
+ case PROP_REVIEW:
+ gs_plugin_job_set_review (self, g_value_get_object (value));
+ break;
+ case PROP_MAX_RESULTS:
+ gs_plugin_job_set_max_results (self, g_value_get_uint (value));
+ break;
+ case PROP_TIMEOUT:
+ gs_plugin_job_set_timeout (self, g_value_get_uint (value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+gs_plugin_job_finalize (GObject *obj)
+{
+ GsPluginJob *self = GS_PLUGIN_JOB (obj);
+ g_free (self->search);
+ g_clear_object (&self->app);
+ g_clear_object (&self->list);
+ g_clear_object (&self->file);
+ g_clear_object (&self->plugin);
+ g_clear_object (&self->category);
+ g_clear_object (&self->review);
+ G_OBJECT_CLASS (gs_plugin_job_parent_class)->finalize (obj);
+}
+
+static void
+gs_plugin_job_class_init (GsPluginJobClass *klass)
+{
+ GParamSpec *pspec;
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ object_class->finalize = gs_plugin_job_finalize;
+ object_class->get_property = gs_plugin_job_get_property;
+ object_class->set_property = gs_plugin_job_set_property;
+
+ pspec = g_param_spec_uint ("action", NULL, NULL,
+ GS_PLUGIN_ACTION_UNKNOWN,
+ GS_PLUGIN_ACTION_LAST,
+ GS_PLUGIN_ACTION_UNKNOWN,
+ G_PARAM_READWRITE);
+ g_object_class_install_property (object_class, PROP_ACTION, pspec);
+
+ pspec = g_param_spec_uint64 ("age", NULL, NULL,
+ 0, G_MAXUINT64, 0,
+ G_PARAM_READWRITE);
+ g_object_class_install_property (object_class, PROP_AGE, pspec);
+
+ pspec = g_param_spec_uint64 ("refine-flags", NULL, NULL,
+ 0, G_MAXUINT64, 0,
+ G_PARAM_READWRITE);
+ g_object_class_install_property (object_class, PROP_REFINE_FLAGS, pspec);
+
+ pspec = g_param_spec_uint64 ("filter-flags", NULL, NULL,
+ 0, G_MAXUINT64, 0,
+ G_PARAM_READWRITE);
+ g_object_class_install_property (object_class, PROP_FILTER_FLAGS, pspec);
+
+ pspec = g_param_spec_uint64 ("dedupe-flags", NULL, NULL,
+ 0, G_MAXUINT64, 0,
+ G_PARAM_READWRITE);
+ g_object_class_install_property (object_class, PROP_DEDUPE_FLAGS, pspec);
+
+ pspec = g_param_spec_boolean ("interactive", NULL, NULL,
+ FALSE,
+ G_PARAM_READWRITE);
+
+ g_object_class_install_property (object_class, PROP_INTERACTIVE, pspec);
+
+ pspec = g_param_spec_string ("search", NULL, NULL,
+ NULL,
+ G_PARAM_READWRITE);
+ g_object_class_install_property (object_class, PROP_SEARCH, pspec);
+
+ pspec = g_param_spec_object ("app", NULL, NULL,
+ GS_TYPE_APP,
+ G_PARAM_READWRITE);
+ g_object_class_install_property (object_class, PROP_APP, pspec);
+
+ pspec = g_param_spec_object ("list", NULL, NULL,
+ GS_TYPE_APP_LIST,
+ G_PARAM_READWRITE);
+ g_object_class_install_property (object_class, PROP_LIST, pspec);
+
+ pspec = g_param_spec_object ("file", NULL, NULL,
+ G_TYPE_FILE,
+ G_PARAM_READWRITE);
+ g_object_class_install_property (object_class, PROP_FILE, pspec);
+
+ pspec = g_param_spec_object ("category", NULL, NULL,
+ GS_TYPE_CATEGORY,
+ G_PARAM_READWRITE);
+ g_object_class_install_property (object_class, PROP_CATEGORY, pspec);
+
+ pspec = g_param_spec_object ("review", NULL, NULL,
+ AS_TYPE_REVIEW,
+ G_PARAM_READWRITE);
+ g_object_class_install_property (object_class, PROP_REVIEW, pspec);
+
+ pspec = g_param_spec_uint ("max-results", NULL, NULL,
+ 0, G_MAXUINT, 0,
+ G_PARAM_READWRITE);
+ g_object_class_install_property (object_class, PROP_MAX_RESULTS, pspec);
+
+ pspec = g_param_spec_uint ("timeout", NULL, NULL,
+ 0, G_MAXUINT, 60,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT);
+ g_object_class_install_property (object_class, PROP_TIMEOUT, pspec);
+}
+
+static void
+gs_plugin_job_init (GsPluginJob *self)
+{
+ self->refine_flags = GS_PLUGIN_REFINE_FLAGS_DEFAULT;
+ self->filter_flags = GS_PLUGIN_REFINE_FLAGS_DEFAULT;
+ self->dedupe_flags = GS_APP_LIST_FILTER_FLAG_KEY_ID |
+ GS_APP_LIST_FILTER_FLAG_KEY_SOURCE |
+ GS_APP_LIST_FILTER_FLAG_KEY_VERSION;
+ self->list = gs_app_list_new ();
+ self->time_created = g_get_monotonic_time ();
+}
diff --git a/lib/gs-plugin-job.h b/lib/gs-plugin-job.h
new file mode 100644
index 0000000..885be1c
--- /dev/null
+++ b/lib/gs-plugin-job.h
@@ -0,0 +1,58 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2017-2018 Richard Hughes <richard@hughsie.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+#pragma once
+
+#include <glib-object.h>
+
+#include "gs-app-list-private.h"
+#include "gs-category.h"
+#include "gs-plugin-types.h"
+
+G_BEGIN_DECLS
+
+#define GS_TYPE_PLUGIN_JOB (gs_plugin_job_get_type ())
+
+G_DECLARE_FINAL_TYPE (GsPluginJob, gs_plugin_job, GS, PLUGIN_JOB, GObject)
+
+void gs_plugin_job_set_refine_flags (GsPluginJob *self,
+ GsPluginRefineFlags refine_flags);
+void gs_plugin_job_set_filter_flags (GsPluginJob *self,
+ GsPluginRefineFlags filter_flags);
+void gs_plugin_job_set_dedupe_flags (GsPluginJob *self,
+ GsAppListFilterFlags dedupe_flags);
+void gs_plugin_job_set_interactive (GsPluginJob *self,
+ gboolean interactive);
+void gs_plugin_job_set_max_results (GsPluginJob *self,
+ guint max_results);
+void gs_plugin_job_set_timeout (GsPluginJob *self,
+ guint timeout);
+void gs_plugin_job_set_age (GsPluginJob *self,
+ guint64 age);
+void gs_plugin_job_set_sort_func (GsPluginJob *self,
+ GsAppListSortFunc sort_func);
+void gs_plugin_job_set_sort_func_data (GsPluginJob *self,
+ gpointer sort_func_data);
+void gs_plugin_job_set_search (GsPluginJob *self,
+ const gchar *search);
+void gs_plugin_job_set_app (GsPluginJob *self,
+ GsApp *app);
+void gs_plugin_job_set_list (GsPluginJob *self,
+ GsAppList *list);
+void gs_plugin_job_set_file (GsPluginJob *self,
+ GFile *file);
+void gs_plugin_job_set_plugin (GsPluginJob *self,
+ GsPlugin *plugin);
+void gs_plugin_job_set_category (GsPluginJob *self,
+ GsCategory *category);
+void gs_plugin_job_set_review (GsPluginJob *self,
+ AsReview *review);
+
+#define gs_plugin_job_newv(a,...) GS_PLUGIN_JOB(g_object_new(GS_TYPE_PLUGIN_JOB, "action", a, __VA_ARGS__))
+
+G_END_DECLS
diff --git a/lib/gs-plugin-loader-sync.c b/lib/gs-plugin-loader-sync.c
new file mode 100644
index 0000000..bf6f076
--- /dev/null
+++ b/lib/gs-plugin-loader-sync.c
@@ -0,0 +1,208 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2012-2017 Richard Hughes <richard@hughsie.com>
+ * Copyright (C) 2017 Kalev Lember <klember@redhat.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+#include "config.h"
+
+#include "gs-plugin-loader-sync.h"
+
+/* tiny helper to help us do the async operation */
+typedef struct {
+ GAsyncResult *res;
+ GMainContext *context;
+ GMainLoop *loop;
+} GsPluginLoaderHelper;
+
+static void
+_job_process_finish_sync (GsPluginLoader *plugin_loader,
+ GAsyncResult *res,
+ GsPluginLoaderHelper *helper)
+{
+ helper->res = g_object_ref (res);
+ g_main_loop_quit (helper->loop);
+}
+
+GsAppList *
+gs_plugin_loader_job_process (GsPluginLoader *plugin_loader,
+ GsPluginJob *plugin_job,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GsPluginLoaderHelper helper;
+ GsAppList *list;
+
+ /* create temp object */
+ helper.res = NULL;
+ helper.context = g_main_context_new ();
+ helper.loop = g_main_loop_new (helper.context, FALSE);
+
+ g_main_context_push_thread_default (helper.context);
+
+ /* run async method */
+ gs_plugin_loader_job_process_async (plugin_loader,
+ plugin_job,
+ cancellable,
+ (GAsyncReadyCallback) _job_process_finish_sync,
+ &helper);
+ g_main_loop_run (helper.loop);
+ list = gs_plugin_loader_job_process_finish (plugin_loader,
+ helper.res,
+ error);
+
+ g_main_context_pop_thread_default (helper.context);
+
+ g_main_loop_unref (helper.loop);
+ g_main_context_unref (helper.context);
+ if (helper.res != NULL)
+ g_object_unref (helper.res);
+
+ return list;
+}
+
+static void
+_job_get_categories_finish_sync (GsPluginLoader *plugin_loader,
+ GAsyncResult *res,
+ GsPluginLoaderHelper *helper)
+{
+ helper->res = g_object_ref (res);
+ g_main_loop_quit (helper->loop);
+}
+
+GPtrArray *
+gs_plugin_loader_job_get_categories (GsPluginLoader *plugin_loader,
+ GsPluginJob *plugin_job,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GsPluginLoaderHelper helper;
+ GPtrArray *catlist;
+
+ /* create temp object */
+ helper.res = NULL;
+ helper.context = g_main_context_new ();
+ helper.loop = g_main_loop_new (helper.context, FALSE);
+
+ g_main_context_push_thread_default (helper.context);
+
+ /* run async method */
+ gs_plugin_loader_job_get_categories_async (plugin_loader,
+ plugin_job,
+ cancellable,
+ (GAsyncReadyCallback) _job_get_categories_finish_sync,
+ &helper);
+ g_main_loop_run (helper.loop);
+ catlist = gs_plugin_loader_job_get_categories_finish (plugin_loader,
+ helper.res,
+ error);
+
+ g_main_context_pop_thread_default (helper.context);
+
+ g_main_loop_unref (helper.loop);
+ g_main_context_unref (helper.context);
+ if (helper.res != NULL)
+ g_object_unref (helper.res);
+
+ return catlist;
+}
+
+static void
+_job_action_finish_sync (GsPluginLoader *plugin_loader,
+ GAsyncResult *res,
+ GsPluginLoaderHelper *helper)
+{
+ helper->res = g_object_ref (res);
+ g_main_loop_quit (helper->loop);
+}
+
+gboolean
+gs_plugin_loader_job_action (GsPluginLoader *plugin_loader,
+ GsPluginJob *plugin_job,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GsPluginLoaderHelper helper;
+ gboolean ret;
+
+ /* create temp object */
+ helper.res = NULL;
+ helper.context = g_main_context_new ();
+ helper.loop = g_main_loop_new (helper.context, FALSE);
+
+ g_main_context_push_thread_default (helper.context);
+
+ /* run async method */
+ gs_plugin_loader_job_process_async (plugin_loader,
+ plugin_job,
+ cancellable,
+ (GAsyncReadyCallback) _job_action_finish_sync,
+ &helper);
+ g_main_loop_run (helper.loop);
+ ret = gs_plugin_loader_job_action_finish (plugin_loader,
+ helper.res,
+ error);
+
+ g_main_context_pop_thread_default (helper.context);
+
+ g_main_loop_unref (helper.loop);
+ g_main_context_unref (helper.context);
+ if (helper.res != NULL)
+ g_object_unref (helper.res);
+
+ return ret;
+}
+
+static void
+_job_process_app_finish_sync (GObject *source_object,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ GsPluginLoaderHelper *helper = (GsPluginLoaderHelper *) user_data;
+
+ helper->res = g_object_ref (res);
+ g_main_loop_quit (helper->loop);
+}
+
+GsApp *
+gs_plugin_loader_job_process_app (GsPluginLoader *plugin_loader,
+ GsPluginJob *plugin_job,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GsPluginLoaderHelper helper;
+ g_autoptr(GsAppList) list = NULL;
+ GsApp *app = NULL;
+
+ /* create temp object */
+ helper.res = NULL;
+ helper.context = g_main_context_new ();
+ helper.loop = g_main_loop_new (helper.context, FALSE);
+
+ g_main_context_push_thread_default (helper.context);
+
+ /* run async method */
+ gs_plugin_loader_job_process_async (plugin_loader,
+ plugin_job,
+ cancellable,
+ _job_process_app_finish_sync,
+ &helper);
+ g_main_loop_run (helper.loop);
+ list = gs_plugin_loader_job_process_finish (plugin_loader,
+ helper.res,
+ error);
+ if (list != NULL)
+ app = g_object_ref (gs_app_list_index (list, 0));
+
+ g_main_context_pop_thread_default (helper.context);
+
+ g_main_loop_unref (helper.loop);
+ g_main_context_unref (helper.context);
+ if (helper.res != NULL)
+ g_object_unref (helper.res);
+
+ return app;
+}
diff --git a/lib/gs-plugin-loader-sync.h b/lib/gs-plugin-loader-sync.h
new file mode 100644
index 0000000..b428ba9
--- /dev/null
+++ b/lib/gs-plugin-loader-sync.h
@@ -0,0 +1,34 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2007-2017 Richard Hughes <richard@hughsie.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+#pragma once
+
+#include <glib-object.h>
+
+#include "gs-plugin-loader.h"
+
+G_BEGIN_DECLS
+
+GsAppList *gs_plugin_loader_job_process (GsPluginLoader *plugin_loader,
+ GsPluginJob *plugin_job,
+ GCancellable *cancellable,
+ GError **error);
+GsApp *gs_plugin_loader_job_process_app (GsPluginLoader *plugin_loader,
+ GsPluginJob *plugin_job,
+ GCancellable *cancellable,
+ GError **error);
+gboolean gs_plugin_loader_job_action (GsPluginLoader *plugin_loader,
+ GsPluginJob *plugin_job,
+ GCancellable *cancellable,
+ GError **error);
+GPtrArray *gs_plugin_loader_job_get_categories (GsPluginLoader *plugin_loader,
+ GsPluginJob *plugin_job,
+ GCancellable *cancellable,
+ GError **error);
+
+G_END_DECLS
diff --git a/lib/gs-plugin-loader.c b/lib/gs-plugin-loader.c
new file mode 100644
index 0000000..7783f77
--- /dev/null
+++ b/lib/gs-plugin-loader.c
@@ -0,0 +1,3968 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2007-2018 Richard Hughes <richard@hughsie.com>
+ * Copyright (C) 2014-2020 Kalev Lember <klember@redhat.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+#include "config.h"
+
+#include <locale.h>
+#include <glib/gi18n.h>
+#include <appstream-glib.h>
+#include <math.h>
+
+#ifdef HAVE_SYSPROF
+#include <sysprof-capture.h>
+#endif
+
+#include "gs-app-collation.h"
+#include "gs-app-private.h"
+#include "gs-app-list-private.h"
+#include "gs-category-private.h"
+#include "gs-ioprio.h"
+#include "gs-plugin-loader.h"
+#include "gs-plugin.h"
+#include "gs-plugin-event.h"
+#include "gs-plugin-job-private.h"
+#include "gs-plugin-private.h"
+#include "gs-utils.h"
+
+#define GS_PLUGIN_LOADER_UPDATES_CHANGED_DELAY 3 /* s */
+#define GS_PLUGIN_LOADER_RELOAD_DELAY 5 /* s */
+
+typedef struct
+{
+ GPtrArray *plugins;
+ GPtrArray *locations;
+ gchar *locale;
+ gchar *language;
+ gboolean plugin_dir_dirty;
+ SoupSession *soup_session;
+ GPtrArray *file_monitors;
+ GsPluginStatus global_status_last;
+
+ GMutex pending_apps_mutex;
+ GPtrArray *pending_apps;
+
+ GThreadPool *queued_ops_pool;
+
+ GSettings *settings;
+
+ GMutex events_by_id_mutex;
+ GHashTable *events_by_id; /* unique-id : GsPluginEvent */
+
+ gchar **compatible_projects;
+ guint scale;
+
+ guint updates_changed_id;
+ guint updates_changed_cnt;
+ guint reload_id;
+ GHashTable *disallow_updates; /* GsPlugin : const char *name */
+
+ GNetworkMonitor *network_monitor;
+ gulong network_changed_handler;
+ gulong network_available_notify_handler;
+ gulong network_metered_notify_handler;
+
+#ifdef HAVE_SYSPROF
+ SysprofCaptureWriter *sysprof_writer; /* (owned) (nullable) */
+#endif
+} GsPluginLoaderPrivate;
+
+static void gs_plugin_loader_monitor_network (GsPluginLoader *plugin_loader);
+static void add_app_to_install_queue (GsPluginLoader *plugin_loader, GsApp *app);
+static void gs_plugin_loader_process_in_thread_pool_cb (gpointer data, gpointer user_data);
+
+G_DEFINE_TYPE_WITH_PRIVATE (GsPluginLoader, gs_plugin_loader, G_TYPE_OBJECT)
+
+enum {
+ SIGNAL_STATUS_CHANGED,
+ SIGNAL_PENDING_APPS_CHANGED,
+ SIGNAL_UPDATES_CHANGED,
+ SIGNAL_RELOAD,
+ SIGNAL_BASIC_AUTH_START,
+ SIGNAL_LAST
+};
+
+enum {
+ PROP_0,
+ PROP_EVENTS,
+ PROP_ALLOW_UPDATES,
+ PROP_NETWORK_AVAILABLE,
+ PROP_NETWORK_METERED,
+ PROP_LAST
+};
+
+static guint signals [SIGNAL_LAST] = { 0 };
+
+typedef void (*GsPluginFunc) (GsPlugin *plugin);
+typedef gboolean (*GsPluginSetupFunc) (GsPlugin *plugin,
+ GCancellable *cancellable,
+ GError **error);
+typedef gboolean (*GsPluginSearchFunc) (GsPlugin *plugin,
+ gchar **value,
+ GsAppList *list,
+ GCancellable *cancellable,
+ GError **error);
+typedef gboolean (*GsPluginAlternatesFunc) (GsPlugin *plugin,
+ GsApp *app,
+ GsAppList *list,
+ GCancellable *cancellable,
+ GError **error);
+typedef gboolean (*GsPluginCategoryFunc) (GsPlugin *plugin,
+ GsCategory *category,
+ GsAppList *list,
+ GCancellable *cancellable,
+ GError **error);
+typedef gboolean (*GsPluginGetRecentFunc) (GsPlugin *plugin,
+ GsAppList *list,
+ guint64 age,
+ GCancellable *cancellable,
+ GError **error);
+typedef gboolean (*GsPluginResultsFunc) (GsPlugin *plugin,
+ GsAppList *list,
+ GCancellable *cancellable,
+ GError **error);
+typedef gboolean (*GsPluginCategoriesFunc) (GsPlugin *plugin,
+ GPtrArray *list,
+ GCancellable *cancellable,
+ GError **error);
+typedef gboolean (*GsPluginActionFunc) (GsPlugin *plugin,
+ GsApp *app,
+ GCancellable *cancellable,
+ GError **error);
+typedef gboolean (*GsPluginReviewFunc) (GsPlugin *plugin,
+ GsApp *app,
+ AsReview *review,
+ GCancellable *cancellable,
+ GError **error);
+typedef gboolean (*GsPluginRefineFunc) (GsPlugin *plugin,
+ GsAppList *list,
+ GsPluginRefineFlags refine_flags,
+ GCancellable *cancellable,
+ GError **error);
+typedef gboolean (*GsPluginRefineAppFunc) (GsPlugin *plugin,
+ GsApp *app,
+ GsPluginRefineFlags refine_flags,
+ GCancellable *cancellable,
+ GError **error);
+typedef gboolean (*GsPluginRefineWildcardFunc) (GsPlugin *plugin,
+ GsApp *app,
+ GsAppList *list,
+ GsPluginRefineFlags refine_flags,
+ GCancellable *cancellable,
+ GError **error);
+typedef gboolean (*GsPluginRefreshFunc) (GsPlugin *plugin,
+ guint cache_age,
+ GCancellable *cancellable,
+ GError **error);
+typedef gboolean (*GsPluginFileToAppFunc) (GsPlugin *plugin,
+ GsAppList *list,
+ GFile *file,
+ GCancellable *cancellable,
+ GError **error);
+typedef gboolean (*GsPluginUrlToAppFunc) (GsPlugin *plugin,
+ GsAppList *list,
+ const gchar *url,
+ GCancellable *cancellable,
+ GError **error);
+typedef gboolean (*GsPluginUpdateFunc) (GsPlugin *plugin,
+ GsAppList *apps,
+ GCancellable *cancellable,
+ GError **error);
+typedef void (*GsPluginAdoptAppFunc) (GsPlugin *plugin,
+ GsApp *app);
+typedef gboolean (*GsPluginGetLangPacksFunc) (GsPlugin *plugin,
+ GsAppList *list,
+ const gchar *locale,
+ GCancellable *cancellable,
+ GError **error);
+
+
+/* async helper */
+typedef struct {
+ GsPluginLoader *plugin_loader;
+ GCancellable *cancellable;
+ GCancellable *cancellable_caller;
+ gulong cancellable_id;
+ const gchar *function_name;
+ const gchar *function_name_parent;
+ GPtrArray *catlist;
+ GsPluginJob *plugin_job;
+ gboolean anything_ran;
+ guint timeout_id;
+ gboolean timeout_triggered;
+ gchar **tokens;
+} GsPluginLoaderHelper;
+
+static GsPluginLoaderHelper *
+gs_plugin_loader_helper_new (GsPluginLoader *plugin_loader, GsPluginJob *plugin_job)
+{
+ GsPluginLoaderHelper *helper = g_slice_new0 (GsPluginLoaderHelper);
+ GsPluginAction action = gs_plugin_job_get_action (plugin_job);
+ helper->plugin_loader = g_object_ref (plugin_loader);
+ helper->plugin_job = g_object_ref (plugin_job);
+ helper->function_name = gs_plugin_action_to_function_name (action);
+ return helper;
+}
+
+static void
+reset_app_progress (GsApp *app)
+{
+ GsAppList *addons = gs_app_get_addons (app);
+ GsAppList *related = gs_app_get_related (app);
+
+ gs_app_set_progress (app, GS_APP_PROGRESS_UNKNOWN);
+
+ for (guint i = 0; i < gs_app_list_length (addons); i++) {
+ GsApp *app_addons = gs_app_list_index (addons, i);
+ gs_app_set_progress (app_addons, GS_APP_PROGRESS_UNKNOWN);
+ }
+ for (guint i = 0; i < gs_app_list_length (related); i++) {
+ GsApp *app_related = gs_app_list_index (related, i);
+ gs_app_set_progress (app_related, GS_APP_PROGRESS_UNKNOWN);
+ }
+}
+
+static void
+gs_plugin_loader_helper_free (GsPluginLoaderHelper *helper)
+{
+ /* reset progress */
+ switch (gs_plugin_job_get_action (helper->plugin_job)) {
+ case GS_PLUGIN_ACTION_INSTALL:
+ case GS_PLUGIN_ACTION_REMOVE:
+ case GS_PLUGIN_ACTION_UPDATE:
+ case GS_PLUGIN_ACTION_DOWNLOAD:
+ {
+ GsApp *app;
+ GsAppList *list;
+
+ app = gs_plugin_job_get_app (helper->plugin_job);
+ if (app != NULL)
+ reset_app_progress (app);
+
+ list = gs_plugin_job_get_list (helper->plugin_job);
+ for (guint i = 0; i < gs_app_list_length (list); i++) {
+ GsApp *app_tmp = gs_app_list_index (list, i);
+ reset_app_progress (app_tmp);
+ }
+ }
+ break;
+ default:
+ break;
+ }
+
+ if (helper->cancellable_id > 0) {
+ g_debug ("Disconnecting cancellable %p", helper->cancellable_caller);
+ g_cancellable_disconnect (helper->cancellable_caller,
+ helper->cancellable_id);
+ }
+ g_object_unref (helper->plugin_loader);
+ if (helper->timeout_id != 0)
+ g_source_remove (helper->timeout_id);
+ if (helper->plugin_job != NULL)
+ g_object_unref (helper->plugin_job);
+ if (helper->cancellable != NULL)
+ g_object_unref (helper->cancellable);
+ if (helper->cancellable_caller != NULL)
+ g_object_unref (helper->cancellable_caller);
+ if (helper->catlist != NULL)
+ g_ptr_array_unref (helper->catlist);
+ g_strfreev (helper->tokens);
+ g_slice_free (GsPluginLoaderHelper, helper);
+}
+
+static void
+gs_plugin_loader_job_debug (GsPluginLoaderHelper *helper)
+{
+ g_autofree gchar *str = gs_plugin_job_to_string (helper->plugin_job);
+ g_debug ("%s", str);
+}
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC(GsPluginLoaderHelper, gs_plugin_loader_helper_free)
+
+static gint
+gs_plugin_loader_app_sort_name_cb (GsApp *app1, GsApp *app2, gpointer user_data)
+{
+ return gs_utils_sort_strcmp (gs_app_get_name (app1), gs_app_get_name (app2));
+}
+
+GsPlugin *
+gs_plugin_loader_find_plugin (GsPluginLoader *plugin_loader,
+ const gchar *plugin_name)
+{
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+
+ for (guint i = 0; i < priv->plugins->len; i++) {
+ GsPlugin *plugin = g_ptr_array_index (priv->plugins, i);
+ if (g_strcmp0 (gs_plugin_get_name (plugin), plugin_name) == 0)
+ return plugin;
+ }
+ return NULL;
+}
+
+static gboolean
+gs_plugin_loader_notify_idle_cb (gpointer user_data)
+{
+ GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (user_data);
+ g_object_notify (G_OBJECT (plugin_loader), "events");
+ return FALSE;
+}
+
+static void
+gs_plugin_loader_add_event (GsPluginLoader *plugin_loader, GsPluginEvent *event)
+{
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+ g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->events_by_id_mutex);
+
+ /* events should always have a unique ID, either constructed from the
+ * app they are processing or preferably from the GError message */
+ if (gs_plugin_event_get_unique_id (event) == NULL) {
+ g_warning ("failed to add event from action %s",
+ gs_plugin_action_to_string (gs_plugin_event_get_action (event)));
+ return;
+ }
+
+ g_hash_table_insert (priv->events_by_id,
+ g_strdup (gs_plugin_event_get_unique_id (event)),
+ g_object_ref (event));
+ g_idle_add (gs_plugin_loader_notify_idle_cb, plugin_loader);
+}
+
+static GsPluginEvent *
+gs_plugin_job_to_failed_event (GsPluginJob *plugin_job, const GError *error)
+{
+ GsPluginEvent *event;
+ g_autoptr(GError) error_copy = NULL;
+
+ g_return_val_if_fail (error != NULL, NULL);
+
+ /* invalid */
+ if (error->domain != GS_PLUGIN_ERROR) {
+ g_warning ("not GsPlugin error %s:%i: %s",
+ g_quark_to_string (error->domain),
+ error->code,
+ error->message);
+ g_set_error_literal (&error_copy,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ error->message);
+ } else {
+ error_copy = g_error_copy (error);
+ }
+
+ /* create plugin event */
+ event = gs_plugin_event_new ();
+ gs_plugin_event_set_error (event, error_copy);
+ gs_plugin_event_set_action (event, gs_plugin_job_get_action (plugin_job));
+ if (gs_plugin_job_get_app (plugin_job) != NULL)
+ gs_plugin_event_set_app (event, gs_plugin_job_get_app (plugin_job));
+ if (gs_plugin_job_get_interactive (plugin_job))
+ gs_plugin_event_add_flag (event, GS_PLUGIN_EVENT_FLAG_INTERACTIVE);
+ gs_plugin_event_add_flag (event, GS_PLUGIN_EVENT_FLAG_WARNING);
+ return event;
+}
+
+static gboolean
+gs_plugin_loader_is_error_fatal (const GError *err)
+{
+ if (g_error_matches (err, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_TIMED_OUT))
+ return TRUE;
+ if (g_error_matches (err, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_AUTH_REQUIRED))
+ return TRUE;
+ if (g_error_matches (err, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_AUTH_INVALID))
+ return TRUE;
+ return FALSE;
+}
+
+static gboolean
+gs_plugin_error_handle_failure (GsPluginLoaderHelper *helper,
+ GsPlugin *plugin,
+ const GError *error_local,
+ GError **error)
+{
+ g_autofree gchar *app_id = NULL;
+ g_autofree gchar *origin_id = NULL;
+ g_autoptr(GsPluginEvent) event = NULL;
+
+ /* badly behaved plugin */
+ if (error_local == NULL) {
+ g_critical ("%s did not set error for %s",
+ gs_plugin_get_name (plugin),
+ helper->function_name);
+ return TRUE;
+ }
+
+ /* this is only ever informational */
+ if (g_error_matches (error_local, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED) ||
+ g_error_matches (error_local, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
+ g_debug ("ignoring error cancelled: %s", error_local->message);
+ return TRUE;
+ }
+
+ /* find and strip any unique IDs from the error message */
+ for (guint i = 0; i < 2; i++) {
+ if (app_id == NULL)
+ app_id = gs_utils_error_strip_app_id (error_local);
+ if (origin_id == NULL)
+ origin_id = gs_utils_error_strip_origin_id (error_local);
+ }
+
+ /* fatal error */
+ if (gs_plugin_job_get_action (helper->plugin_job) == GS_PLUGIN_ACTION_SETUP ||
+ gs_plugin_loader_is_error_fatal (error_local) ||
+ g_getenv ("GS_SELF_TEST_PLUGIN_ERROR_FAIL_HARD") != NULL) {
+ if (error != NULL)
+ *error = g_error_copy (error_local);
+ return FALSE;
+ }
+
+ /* create event which is handled by the GsShell */
+ event = gs_plugin_job_to_failed_event (helper->plugin_job, error_local);
+
+ /* set the app and origin IDs if we managed to scrape them from the error above */
+ if (as_utils_unique_id_valid (app_id)) {
+ g_autoptr(GsApp) app = gs_plugin_cache_lookup (plugin, app_id);
+ if (app != NULL) {
+ g_debug ("found app %s in error", origin_id);
+ gs_plugin_event_set_app (event, app);
+ } else {
+ g_debug ("no unique ID found for app %s", app_id);
+ }
+ }
+ if (as_utils_unique_id_valid (origin_id)) {
+ g_autoptr(GsApp) origin = gs_plugin_cache_lookup (plugin, origin_id);
+ if (origin != NULL) {
+ g_debug ("found origin %s in error", origin_id);
+ gs_plugin_event_set_origin (event, origin);
+ } else {
+ g_debug ("no unique ID found for origin %s", origin_id);
+ }
+ }
+
+ /* add event to queue */
+ gs_plugin_loader_add_event (helper->plugin_loader, event);
+ return TRUE;
+}
+
+static void
+gs_plugin_loader_run_adopt (GsPluginLoader *plugin_loader, GsAppList *list)
+{
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+ guint i;
+ guint j;
+
+ /* go through each plugin in order */
+ for (i = 0; i < priv->plugins->len; i++) {
+ GsPluginAdoptAppFunc adopt_app_func = NULL;
+ GsPlugin *plugin = g_ptr_array_index (priv->plugins, i);
+ adopt_app_func = gs_plugin_get_symbol (plugin, "gs_plugin_adopt_app");
+ if (adopt_app_func == NULL)
+ continue;
+ for (j = 0; j < gs_app_list_length (list); j++) {
+ GsApp *app = gs_app_list_index (list, j);
+ if (gs_app_get_management_plugin (app) != NULL)
+ continue;
+ if (gs_app_has_quirk (app, GS_APP_QUIRK_IS_WILDCARD))
+ continue;
+ adopt_app_func (plugin, app);
+ if (gs_app_get_management_plugin (app) != NULL) {
+ g_debug ("%s adopted %s",
+ gs_plugin_get_name (plugin),
+ gs_app_get_unique_id (app));
+ }
+ }
+ }
+ for (j = 0; j < gs_app_list_length (list); j++) {
+ GsApp *app = gs_app_list_index (list, j);
+ if (gs_app_get_management_plugin (app) != NULL)
+ continue;
+ if (gs_app_has_quirk (app, GS_APP_QUIRK_IS_WILDCARD))
+ continue;
+ g_debug ("nothing adopted %s", gs_app_get_unique_id (app));
+ }
+}
+
+static gint
+gs_plugin_loader_review_score_sort_cb (gconstpointer a, gconstpointer b)
+{
+ AsReview *ra = *((AsReview **) a);
+ AsReview *rb = *((AsReview **) b);
+ if (as_review_get_priority (ra) < as_review_get_priority (rb))
+ return 1;
+ if (as_review_get_priority (ra) > as_review_get_priority (rb))
+ return -1;
+ return 0;
+}
+
+static gboolean
+gs_plugin_loader_call_vfunc (GsPluginLoaderHelper *helper,
+ GsPlugin *plugin,
+ GsApp *app,
+ GsAppList *list,
+ GsPluginRefineFlags refine_flags,
+ GCancellable *cancellable,
+ GError **error)
+{
+#ifdef HAVE_SYSPROF
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (helper->plugin_loader);
+#endif
+ GsPluginAction action = gs_plugin_job_get_action (helper->plugin_job);
+ gboolean ret = TRUE;
+ gpointer func = NULL;
+ g_autoptr(GError) error_local = NULL;
+ g_autoptr(GTimer) timer = g_timer_new ();
+#ifdef HAVE_SYSPROF
+ gint64 begin_time_nsec = SYSPROF_CAPTURE_CURRENT_TIME;
+#endif
+
+ /* load the possible symbol */
+ func = gs_plugin_get_symbol (plugin, helper->function_name);
+ if (func == NULL)
+ return TRUE;
+
+ /* fallback if unset */
+ if (app == NULL)
+ app = gs_plugin_job_get_app (helper->plugin_job);
+ if (list == NULL)
+ list = gs_plugin_job_get_list (helper->plugin_job);
+ if (refine_flags == GS_PLUGIN_REFINE_FLAGS_DEFAULT)
+ refine_flags = gs_plugin_job_get_refine_flags (helper->plugin_job);
+
+ /* set what plugin is running on the job */
+ gs_plugin_job_set_plugin (helper->plugin_job, plugin);
+
+ /* run the correct vfunc */
+ if (gs_plugin_job_get_interactive (helper->plugin_job))
+ gs_plugin_interactive_inc (plugin);
+ switch (action) {
+ case GS_PLUGIN_ACTION_INITIALIZE:
+ case GS_PLUGIN_ACTION_DESTROY:
+ {
+ GsPluginFunc plugin_func = func;
+ plugin_func (plugin);
+ }
+ break;
+ case GS_PLUGIN_ACTION_SETUP:
+ {
+ GsPluginSetupFunc plugin_func = func;
+ ret = plugin_func (plugin, cancellable, &error_local);
+ }
+ break;
+ case GS_PLUGIN_ACTION_REFINE:
+ if (g_strcmp0 (helper->function_name, "gs_plugin_refine_wildcard") == 0) {
+ GsPluginRefineWildcardFunc plugin_func = func;
+ ret = plugin_func (plugin, app, list, refine_flags, cancellable, &error_local);
+ } else if (g_strcmp0 (helper->function_name, "gs_plugin_refine") == 0) {
+ GsPluginRefineFunc plugin_func = func;
+ ret = plugin_func (plugin, list, refine_flags, cancellable, &error_local);
+ } else {
+ g_critical ("function_name %s invalid for %s",
+ helper->function_name,
+ gs_plugin_action_to_string (action));
+ }
+ break;
+ case GS_PLUGIN_ACTION_UPDATE:
+ if (g_strcmp0 (helper->function_name, "gs_plugin_update_app") == 0) {
+ GsPluginActionFunc plugin_func = func;
+ ret = plugin_func (plugin, app, cancellable, &error_local);
+ } else if (g_strcmp0 (helper->function_name, "gs_plugin_update") == 0) {
+ GsPluginUpdateFunc plugin_func = func;
+ ret = plugin_func (plugin, list, cancellable, &error_local);
+ } else {
+ g_critical ("function_name %s invalid for %s",
+ helper->function_name,
+ gs_plugin_action_to_string (action));
+ }
+ break;
+ case GS_PLUGIN_ACTION_DOWNLOAD:
+ if (g_strcmp0 (helper->function_name, "gs_plugin_download_app") == 0) {
+ GsPluginActionFunc plugin_func = func;
+ ret = plugin_func (plugin, app, cancellable, &error_local);
+ } else if (g_strcmp0 (helper->function_name, "gs_plugin_download") == 0) {
+ GsPluginUpdateFunc plugin_func = func;
+ ret = plugin_func (plugin, list, cancellable, &error_local);
+ } else {
+ g_critical ("function_name %s invalid for %s",
+ helper->function_name,
+ gs_plugin_action_to_string (action));
+ }
+ break;
+ case GS_PLUGIN_ACTION_INSTALL:
+ case GS_PLUGIN_ACTION_REMOVE:
+ case GS_PLUGIN_ACTION_SET_RATING:
+ case GS_PLUGIN_ACTION_UPGRADE_DOWNLOAD:
+ case GS_PLUGIN_ACTION_UPGRADE_TRIGGER:
+ case GS_PLUGIN_ACTION_LAUNCH:
+ case GS_PLUGIN_ACTION_UPDATE_CANCEL:
+ case GS_PLUGIN_ACTION_ADD_SHORTCUT:
+ case GS_PLUGIN_ACTION_REMOVE_SHORTCUT:
+ {
+ GsPluginActionFunc plugin_func = func;
+ ret = plugin_func (plugin, app, cancellable, &error_local);
+ }
+ break;
+ case GS_PLUGIN_ACTION_REVIEW_SUBMIT:
+ case GS_PLUGIN_ACTION_REVIEW_UPVOTE:
+ case GS_PLUGIN_ACTION_REVIEW_DOWNVOTE:
+ case GS_PLUGIN_ACTION_REVIEW_REPORT:
+ case GS_PLUGIN_ACTION_REVIEW_REMOVE:
+ case GS_PLUGIN_ACTION_REVIEW_DISMISS:
+ {
+ GsPluginReviewFunc plugin_func = func;
+ ret = plugin_func (plugin, app,
+ gs_plugin_job_get_review (helper->plugin_job),
+ cancellable, &error_local);
+ }
+ break;
+ case GS_PLUGIN_ACTION_GET_RECENT:
+ {
+ GsPluginGetRecentFunc plugin_func = func;
+ ret = plugin_func (plugin, list,
+ gs_plugin_job_get_age (helper->plugin_job),
+ cancellable, &error_local);
+ }
+ break;
+ case GS_PLUGIN_ACTION_GET_UPDATES:
+ case GS_PLUGIN_ACTION_GET_UPDATES_HISTORICAL:
+ case GS_PLUGIN_ACTION_GET_DISTRO_UPDATES:
+ case GS_PLUGIN_ACTION_GET_UNVOTED_REVIEWS:
+ case GS_PLUGIN_ACTION_GET_SOURCES:
+ case GS_PLUGIN_ACTION_GET_INSTALLED:
+ case GS_PLUGIN_ACTION_GET_POPULAR:
+ case GS_PLUGIN_ACTION_GET_FEATURED:
+ {
+ GsPluginResultsFunc plugin_func = func;
+ ret = plugin_func (plugin, list, cancellable, &error_local);
+ }
+ break;
+ case GS_PLUGIN_ACTION_SEARCH:
+ {
+ GsPluginSearchFunc plugin_func = func;
+ ret = plugin_func (plugin, helper->tokens, list,
+ cancellable, &error_local);
+ }
+ break;
+ case GS_PLUGIN_ACTION_SEARCH_FILES:
+ case GS_PLUGIN_ACTION_SEARCH_PROVIDES:
+ {
+ GsPluginSearchFunc plugin_func = func;
+ gchar *search[2] = { gs_plugin_job_get_search (helper->plugin_job), NULL };
+ ret = plugin_func (plugin, search, list,
+ cancellable, &error_local);
+ }
+ break;
+ case GS_PLUGIN_ACTION_GET_ALTERNATES:
+ {
+ GsPluginAlternatesFunc plugin_func = func;
+ ret = plugin_func (plugin, app, list,
+ cancellable, &error_local);
+ }
+ break;
+ case GS_PLUGIN_ACTION_GET_CATEGORIES:
+ {
+ GsPluginCategoriesFunc plugin_func = func;
+ ret = plugin_func (plugin, helper->catlist,
+ cancellable, &error_local);
+ }
+ break;
+ case GS_PLUGIN_ACTION_GET_CATEGORY_APPS:
+ {
+ GsPluginCategoryFunc plugin_func = func;
+ ret = plugin_func (plugin,
+ gs_plugin_job_get_category (helper->plugin_job),
+ list,
+ cancellable, &error_local);
+ }
+ break;
+ case GS_PLUGIN_ACTION_REFRESH:
+ {
+ GsPluginRefreshFunc plugin_func = func;
+ ret = plugin_func (plugin,
+ gs_plugin_job_get_age (helper->plugin_job),
+ cancellable, &error_local);
+ }
+ break;
+ case GS_PLUGIN_ACTION_FILE_TO_APP:
+ {
+ GsPluginFileToAppFunc plugin_func = func;
+ ret = plugin_func (plugin, list,
+ gs_plugin_job_get_file (helper->plugin_job),
+ cancellable, &error_local);
+ }
+ break;
+ case GS_PLUGIN_ACTION_URL_TO_APP:
+ {
+ GsPluginUrlToAppFunc plugin_func = func;
+ ret = plugin_func (plugin, list,
+ gs_plugin_job_get_search (helper->plugin_job),
+ cancellable, &error_local);
+ }
+ break;
+ case GS_PLUGIN_ACTION_GET_LANGPACKS:
+ {
+ GsPluginGetLangPacksFunc plugin_func = func;
+ ret = plugin_func (plugin, list,
+ gs_plugin_job_get_search (helper->plugin_job),
+ cancellable, &error_local);
+ }
+ break;
+ default:
+ g_critical ("no handler for %s", helper->function_name);
+ break;
+ }
+ if (gs_plugin_job_get_interactive (helper->plugin_job))
+ gs_plugin_interactive_dec (plugin);
+
+ /* plugin did not return error on cancellable abort */
+ if (ret && g_cancellable_set_error_if_cancelled (cancellable, &error_local)) {
+ g_debug ("plugin %s did not return error with cancellable set",
+ gs_plugin_get_name (plugin));
+ gs_utils_error_convert_gio (&error_local);
+ ret = FALSE;
+ }
+
+ /* failed */
+ if (!ret) {
+ /* we returned cancelled, but this was because of a timeout,
+ * so re-create error, throwing the plugin under the bus */
+ if (helper->timeout_triggered &&
+ g_error_matches (error_local, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_CANCELLED)) {
+ g_debug ("converting cancelled to timeout");
+ g_clear_error (&error_local);
+ g_set_error (&error_local,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_TIMED_OUT,
+ "Timeout was reached as %s took "
+ "too long to return results",
+ gs_plugin_get_name (plugin));
+ }
+ return gs_plugin_error_handle_failure (helper,
+ plugin,
+ error_local,
+ error);
+ }
+
+ /* add app to the pending installation queue if necessary */
+ if (action == GS_PLUGIN_ACTION_INSTALL &&
+ app != NULL && gs_app_get_state (app) == AS_APP_STATE_QUEUED_FOR_INSTALL) {
+ add_app_to_install_queue (helper->plugin_loader, app);
+ }
+
+#ifdef HAVE_SYSPROF
+ if (priv->sysprof_writer != NULL) {
+ g_autofree gchar *sysprof_name = NULL;
+ g_autofree gchar *sysprof_message = NULL;
+
+ sysprof_name = g_strconcat ("vfunc:", gs_plugin_action_to_string (action), NULL);
+ sysprof_message = gs_plugin_job_to_string (helper->plugin_job);
+ sysprof_capture_writer_add_mark (priv->sysprof_writer,
+ begin_time_nsec,
+ sched_getcpu (),
+ getpid (),
+ SYSPROF_CAPTURE_CURRENT_TIME - begin_time_nsec,
+ "gnome-software",
+ sysprof_name,
+ sysprof_message);
+ }
+#endif /* HAVE_SYSPROF */
+
+ /* check the plugin didn't take too long */
+ if (g_timer_elapsed (timer, NULL) > 1.0f) {
+ GLogLevelFlags log_level;
+
+ switch (action) {
+ case GS_PLUGIN_ACTION_INITIALIZE:
+ case GS_PLUGIN_ACTION_DESTROY:
+ case GS_PLUGIN_ACTION_SETUP:
+ if (g_getenv ("GS_SELF_TEST_PLUGIN_ERROR_FAIL_HARD") == NULL)
+ log_level = G_LOG_LEVEL_WARNING;
+ else
+ log_level = G_LOG_LEVEL_DEBUG;
+ break;
+ default:
+ log_level = G_LOG_LEVEL_DEBUG;
+ break;
+ }
+
+ g_log_structured_standard (G_LOG_DOMAIN, log_level,
+ __FILE__, G_STRINGIFY (__LINE__),
+ G_STRFUNC,
+ "plugin %s took %.1f seconds to do %s",
+ gs_plugin_get_name (plugin),
+ g_timer_elapsed (timer, NULL),
+ gs_plugin_action_to_string (action));
+ }
+
+ /* success */
+ helper->anything_ran = TRUE;
+ return TRUE;
+}
+
+static gboolean
+gs_plugin_loader_app_is_non_wildcard (GsApp *app, gpointer user_data)
+{
+ return !gs_app_has_quirk (app, GS_APP_QUIRK_IS_WILDCARD);
+}
+
+static gboolean
+gs_plugin_loader_run_refine_filter (GsPluginLoaderHelper *helper,
+ GsAppList *list,
+ GsPluginRefineFlags refine_flags,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (helper->plugin_loader);
+
+ /* run each plugin */
+ for (guint i = 0; i < priv->plugins->len; i++) {
+ GsPlugin *plugin = g_ptr_array_index (priv->plugins, i);
+ g_autoptr(GsAppList) app_list = NULL;
+
+ /* run the batched plugin symbol then refine wildcards per-app */
+ helper->function_name = "gs_plugin_refine";
+ if (!gs_plugin_loader_call_vfunc (helper, plugin, NULL, list,
+ refine_flags, cancellable, error)) {
+ return FALSE;
+ }
+
+ if (gs_plugin_get_symbol (plugin, "gs_plugin_refine_wildcard") != NULL) {
+ /* use a copy of the list for the loop because a function called
+ * on the plugin may affect the list which can lead to problems
+ * (e.g. inserting an app in the list on every call results in
+ * an infinite loop) */
+ app_list = gs_app_list_copy (list);
+ helper->function_name = "gs_plugin_refine_wildcard";
+
+ for (guint j = 0; j < gs_app_list_length (app_list); j++) {
+ GsApp *app = gs_app_list_index (app_list, j);
+ if (gs_app_has_quirk (app, GS_APP_QUIRK_IS_WILDCARD) &&
+ !gs_plugin_loader_call_vfunc (helper, plugin, app, NULL,
+ refine_flags, cancellable, error)) {
+ return FALSE;
+ }
+ }
+ }
+
+ gs_plugin_status_update (plugin, NULL, GS_PLUGIN_STATUS_FINISHED);
+ }
+
+
+ /* filter any wildcard apps left in the list */
+ gs_app_list_filter (list, gs_plugin_loader_app_is_non_wildcard, NULL);
+ return TRUE;
+}
+
+static gboolean
+gs_plugin_loader_run_refine_internal (GsPluginLoaderHelper *helper,
+ GsAppList *list,
+ GCancellable *cancellable,
+ GError **error)
+{
+ /* try to adopt each application with a plugin */
+ gs_plugin_loader_run_adopt (helper->plugin_loader, list);
+
+ /* run each plugin */
+ if (!gs_plugin_loader_run_refine_filter (helper, list,
+ GS_PLUGIN_REFINE_FLAGS_DEFAULT,
+ cancellable, error))
+ return FALSE;
+
+ /* ensure these are sorted by score */
+ if (gs_plugin_job_has_refine_flags (helper->plugin_job,
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_REVIEWS)) {
+ GPtrArray *reviews;
+ for (guint i = 0; i < gs_app_list_length (list); i++) {
+ GsApp *app = gs_app_list_index (list, i);
+ reviews = gs_app_get_reviews (app);
+ g_ptr_array_sort (reviews,
+ gs_plugin_loader_review_score_sort_cb);
+ }
+ }
+
+ /* refine addons one layer deep */
+ if (gs_plugin_job_has_refine_flags (helper->plugin_job,
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_ADDONS)) {
+ g_autoptr(GsAppList) addons_list = NULL;
+ gs_plugin_job_remove_refine_flags (helper->plugin_job,
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_ADDONS |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_REVIEWS |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_REVIEW_RATINGS);
+ addons_list = gs_app_list_new ();
+ for (guint i = 0; i < gs_app_list_length (list); i++) {
+ GsApp *app = gs_app_list_index (list, i);
+ GsAppList *addons = gs_app_get_addons (app);
+ for (guint j = 0; j < gs_app_list_length (addons); j++) {
+ GsApp *addon = gs_app_list_index (addons, j);
+ g_debug ("refining app %s addon %s",
+ gs_app_get_id (app),
+ gs_app_get_id (addon));
+ gs_app_list_add (addons_list, addon);
+ }
+ }
+ if (gs_app_list_length (addons_list) > 0) {
+ if (!gs_plugin_loader_run_refine_internal (helper,
+ addons_list,
+ cancellable,
+ error)) {
+ return FALSE;
+ }
+ }
+ }
+
+ /* also do runtime */
+ if (gs_plugin_job_has_refine_flags (helper->plugin_job,
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_RUNTIME)) {
+ g_autoptr(GsAppList) list2 = gs_app_list_new ();
+ for (guint i = 0; i < gs_app_list_length (list); i++) {
+ GsApp *runtime;
+ GsApp *app = gs_app_list_index (list, i);
+ runtime = gs_app_get_runtime (app);
+ if (runtime != NULL)
+ gs_app_list_add (list2, runtime);
+ }
+ if (gs_app_list_length (list2) > 0) {
+ if (!gs_plugin_loader_run_refine_internal (helper,
+ list2,
+ cancellable,
+ error)) {
+ return FALSE;
+ }
+ }
+ }
+
+ /* also do related packages one layer deep */
+ if (gs_plugin_job_has_refine_flags (helper->plugin_job,
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_RELATED)) {
+ g_autoptr(GsAppList) related_list = NULL;
+ gs_plugin_job_remove_refine_flags (helper->plugin_job,
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_RELATED);
+ related_list = gs_app_list_new ();
+ for (guint i = 0; i < gs_app_list_length (list); i++) {
+ GsApp *app = gs_app_list_index (list, i);
+ GsAppList *related = gs_app_get_related (app);
+ for (guint j = 0; j < gs_app_list_length (related); j++) {
+ GsApp *app2 = gs_app_list_index (related, j);
+ g_debug ("refining related: %s[%s]",
+ gs_app_get_id (app2),
+ gs_app_get_source_default (app2));
+ gs_app_list_add (related_list, app2);
+ }
+ }
+ if (gs_app_list_length (related_list) > 0) {
+ if (!gs_plugin_loader_run_refine_internal (helper,
+ related_list,
+ cancellable,
+ error)) {
+ return FALSE;
+ }
+ }
+ }
+
+ /* success */
+ return TRUE;
+}
+
+static gboolean
+app_thaw_notify_idle (gpointer data)
+{
+ GsApp *app = GS_APP (data);
+ g_object_thaw_notify (G_OBJECT (app));
+ g_object_unref (app);
+ return G_SOURCE_REMOVE;
+}
+
+static gboolean
+gs_plugin_loader_run_refine (GsPluginLoaderHelper *helper,
+ GsAppList *list,
+ GCancellable *cancellable,
+ GError **error)
+{
+ gboolean ret;
+ g_autoptr(GsAppList) freeze_list = NULL;
+ g_autoptr(GsPluginLoaderHelper) helper2 = NULL;
+ g_autoptr(GsPluginJob) plugin_job = NULL;
+
+ /* nothing to do */
+ if (gs_app_list_length (list) == 0)
+ return TRUE;
+
+ /* freeze all apps */
+ freeze_list = gs_app_list_copy (list);
+ for (guint i = 0; i < gs_app_list_length (freeze_list); i++) {
+ GsApp *app = gs_app_list_index (freeze_list, i);
+ g_object_freeze_notify (G_OBJECT (app));
+ }
+
+ /* first pass */
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REFINE,
+ "list", list,
+ "refine-flags", gs_plugin_job_get_refine_flags (helper->plugin_job),
+ NULL);
+ helper2 = gs_plugin_loader_helper_new (helper->plugin_loader, plugin_job);
+ helper2->function_name_parent = helper->function_name;
+ ret = gs_plugin_loader_run_refine_internal (helper2, list, cancellable, error);
+ if (!ret)
+ goto out;
+
+ /* remove any addons that have the same source as the parent app */
+ for (guint i = 0; i < gs_app_list_length (list); i++) {
+ g_autoptr(GPtrArray) to_remove = g_ptr_array_new ();
+ GsApp *app = gs_app_list_index (list, i);
+ GsAppList *addons = gs_app_get_addons (app);
+
+ /* find any apps with the same source */
+ const gchar *pkgname_parent = gs_app_get_source_default (app);
+ if (pkgname_parent == NULL)
+ continue;
+ for (guint j = 0; j < gs_app_list_length (addons); j++) {
+ GsApp *addon = gs_app_list_index (addons, j);
+ if (g_strcmp0 (gs_app_get_source_default (addon),
+ pkgname_parent) == 0) {
+ g_debug ("%s has the same pkgname of %s as %s",
+ gs_app_get_unique_id (app),
+ pkgname_parent,
+ gs_app_get_unique_id (addon));
+ g_ptr_array_add (to_remove, addon);
+ }
+ }
+
+ /* remove any addons with the same source */
+ for (guint j = 0; j < to_remove->len; j++) {
+ GsApp *addon = g_ptr_array_index (to_remove, j);
+ gs_app_remove_addon (app, addon);
+ }
+ }
+
+out:
+ /* now emit all the changed signals */
+ for (guint i = 0; i < gs_app_list_length (freeze_list); i++) {
+ GsApp *app = gs_app_list_index (freeze_list, i);
+ g_idle_add (app_thaw_notify_idle, g_object_ref (app));
+ }
+ return ret;
+}
+
+static void
+gs_plugin_loader_job_sorted_truncation_again (GsPluginLoaderHelper *helper)
+{
+ GsAppListSortFunc sort_func;
+ gpointer sort_func_data;
+
+ /* not valid */
+ if (gs_plugin_job_get_list (helper->plugin_job) == NULL)
+ return;
+
+ /* unset */
+ sort_func = gs_plugin_job_get_sort_func (helper->plugin_job);
+ if (sort_func == NULL)
+ return;
+ sort_func_data = gs_plugin_job_get_sort_func_data (helper->plugin_job);
+ gs_app_list_sort (gs_plugin_job_get_list (helper->plugin_job), sort_func, sort_func_data);
+}
+
+static void
+gs_plugin_loader_job_sorted_truncation (GsPluginLoaderHelper *helper)
+{
+ GsAppListSortFunc sort_func;
+ guint max_results;
+ GsAppList *list = gs_plugin_job_get_list (helper->plugin_job);
+
+ /* not valid */
+ if (list == NULL)
+ return;
+
+ /* unset */
+ max_results = gs_plugin_job_get_max_results (helper->plugin_job);
+ if (max_results == 0)
+ return;
+
+ /* already small enough */
+ if (gs_app_list_length (list) <= max_results)
+ return;
+
+ /* nothing set */
+ g_debug ("truncating results to %u from %u",
+ max_results, gs_app_list_length (list));
+ sort_func = gs_plugin_job_get_sort_func (helper->plugin_job);
+ if (sort_func == NULL) {
+ GsPluginAction action = gs_plugin_job_get_action (helper->plugin_job);
+ g_debug ("no ->sort_func() set for %s, using random!",
+ gs_plugin_action_to_string (action));
+ gs_app_list_randomize (list);
+ } else {
+ gpointer sort_func_data;
+ sort_func_data = gs_plugin_job_get_sort_func_data (helper->plugin_job);
+ gs_app_list_sort (list, sort_func, sort_func_data);
+ }
+ gs_app_list_truncate (list, max_results);
+}
+
+static gboolean
+gs_plugin_loader_run_results (GsPluginLoaderHelper *helper,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (helper->plugin_loader);
+#ifdef HAVE_SYSPROF
+ gint64 begin_time_nsec G_GNUC_UNUSED = SYSPROF_CAPTURE_CURRENT_TIME;
+#endif
+
+ /* run each plugin */
+ for (guint i = 0; i < priv->plugins->len; i++) {
+ GsPlugin *plugin = g_ptr_array_index (priv->plugins, i);
+ if (g_cancellable_set_error_if_cancelled (cancellable, error)) {
+ gs_utils_error_convert_gio (error);
+ return FALSE;
+ }
+ if (!gs_plugin_loader_call_vfunc (helper, plugin, NULL, NULL,
+ GS_PLUGIN_REFINE_FLAGS_DEFAULT,
+ cancellable, error)) {
+ return FALSE;
+ }
+ gs_plugin_status_update (plugin, NULL, GS_PLUGIN_STATUS_FINISHED);
+ }
+
+#ifdef HAVE_SYSPROF
+ if (priv->sysprof_writer != NULL) {
+ g_autofree gchar *sysprof_name = NULL;
+ g_autofree gchar *sysprof_message = NULL;
+
+ sysprof_name = g_strconcat ("run-results:",
+ gs_plugin_action_to_string (gs_plugin_job_get_action (helper->plugin_job)),
+ NULL);
+ sysprof_message = gs_plugin_job_to_string (helper->plugin_job);
+ sysprof_capture_writer_add_mark (priv->sysprof_writer,
+ begin_time_nsec,
+ sched_getcpu (),
+ getpid (),
+ SYSPROF_CAPTURE_CURRENT_TIME - begin_time_nsec,
+ "gnome-software",
+ sysprof_name,
+ sysprof_message);
+ }
+#endif /* HAVE_SYSPROF */
+
+ return TRUE;
+}
+
+static const gchar *
+gs_plugin_loader_get_app_str (GsApp *app)
+{
+ const gchar *id;
+
+ /* first try the actual id */
+ id = gs_app_get_unique_id (app);
+ if (id != NULL)
+ return id;
+
+ /* then try the source */
+ id = gs_app_get_source_default (app);
+ if (id != NULL)
+ return id;
+
+ /* lastly try the source id */
+ id = gs_app_get_source_id_default (app);
+ if (id != NULL)
+ return id;
+
+ /* urmmm */
+ return "<invalid>";
+}
+
+static gboolean
+gs_plugin_loader_app_set_prio (GsApp *app, gpointer user_data)
+{
+ GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (user_data);
+ GsPlugin *plugin;
+ const gchar *tmp;
+
+ /* if set, copy the priority */
+ tmp = gs_app_get_management_plugin (app);
+ if (tmp == NULL)
+ return TRUE;
+ plugin = gs_plugin_loader_find_plugin (plugin_loader, tmp);
+ if (plugin == NULL)
+ return TRUE;
+ gs_app_set_priority (app, gs_plugin_get_priority (plugin));
+ return TRUE;
+}
+
+static gboolean
+gs_plugin_loader_app_is_valid_installed (GsApp *app, gpointer user_data)
+{
+ /* even without AppData, show things in progress */
+ switch (gs_app_get_state (app)) {
+ case AS_APP_STATE_INSTALLING:
+ case AS_APP_STATE_REMOVING:
+ return TRUE;
+ break;
+ default:
+ break;
+ }
+
+ switch (gs_app_get_kind (app)) {
+ case AS_APP_KIND_OS_UPGRADE:
+ case AS_APP_KIND_CODEC:
+ case AS_APP_KIND_FONT:
+ g_debug ("app invalid as %s: %s",
+ as_app_kind_to_string (gs_app_get_kind (app)),
+ gs_plugin_loader_get_app_str (app));
+ return FALSE;
+ break;
+ default:
+ break;
+ }
+
+ /* sanity check */
+ if (!gs_app_is_installed (app)) {
+ g_autofree gchar *tmp = gs_app_to_string (app);
+ g_warning ("ignoring non-installed app %s", tmp);
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+static gboolean
+gs_plugin_loader_app_is_valid (GsApp *app, gpointer user_data)
+{
+ GsPluginLoaderHelper *helper = (GsPluginLoaderHelper *) user_data;
+
+ /* never show addons */
+ if (gs_app_get_kind (app) == AS_APP_KIND_ADDON) {
+ g_debug ("app invalid as addon %s",
+ gs_plugin_loader_get_app_str (app));
+ return FALSE;
+ }
+
+ /* never show CLI apps */
+ if (gs_app_get_kind (app) == AS_APP_KIND_CONSOLE) {
+ g_debug ("app invalid as console %s",
+ gs_plugin_loader_get_app_str (app));
+ return FALSE;
+ }
+
+ /* don't show unknown state */
+ if (gs_app_get_state (app) == AS_APP_STATE_UNKNOWN) {
+ g_debug ("app invalid as state unknown %s",
+ gs_plugin_loader_get_app_str (app));
+ return FALSE;
+ }
+
+ /* don't show unconverted unavailables */
+ if (gs_app_get_kind (app) == AS_APP_KIND_UNKNOWN &&
+ gs_app_get_state (app) == AS_APP_STATE_UNAVAILABLE) {
+ g_debug ("app invalid as unconverted unavailable %s",
+ gs_plugin_loader_get_app_str (app));
+ return FALSE;
+ }
+
+ /* don't show blocklisted apps */
+ if (gs_app_has_quirk (app, GS_APP_QUIRK_HIDE_EVERYWHERE)) {
+ g_debug ("app invalid as blocklisted %s",
+ gs_plugin_loader_get_app_str (app));
+ return FALSE;
+ }
+
+ /* Don’t show parentally filtered apps unless they’re already
+ * installed. See the comments in gs-details-page.c for details. */
+ if (!gs_app_is_installed (app) &&
+ gs_app_has_quirk (app, GS_APP_QUIRK_PARENTAL_FILTER)) {
+ g_debug ("app invalid as parentally filtered %s",
+ gs_plugin_loader_get_app_str (app));
+ return FALSE;
+ }
+
+ /* don't show apps with hide-from-search quirk, unless they are already installed */
+ if (!gs_app_is_installed (app) &&
+ gs_app_has_quirk (app, GS_APP_QUIRK_HIDE_FROM_SEARCH)) {
+ g_debug ("app invalid as hide-from-search quirk set %s",
+ gs_plugin_loader_get_app_str (app));
+ return FALSE;
+ }
+
+ /* don't show sources */
+ if (gs_app_get_kind (app) == AS_APP_KIND_SOURCE) {
+ g_debug ("app invalid as source %s",
+ gs_plugin_loader_get_app_str (app));
+ return FALSE;
+ }
+
+ /* don't show unknown kind */
+ if (gs_app_get_kind (app) == AS_APP_KIND_UNKNOWN) {
+ g_debug ("app invalid as kind unknown %s",
+ gs_plugin_loader_get_app_str (app));
+ return FALSE;
+ }
+
+ /* don't show unconverted packages in the application view */
+ if (!gs_plugin_job_has_refine_flags (helper->plugin_job,
+ GS_PLUGIN_REFINE_FLAGS_ALLOW_PACKAGES) &&
+ (gs_app_get_kind (app) == AS_APP_KIND_GENERIC)) {
+ g_debug ("app invalid as only a %s: %s",
+ as_app_kind_to_string (gs_app_get_kind (app)),
+ gs_plugin_loader_get_app_str (app));
+ return FALSE;
+ }
+
+ /* don't show apps that do not have the required details */
+ if (gs_app_get_name (app) == NULL) {
+ g_debug ("app invalid as no name %s",
+ gs_plugin_loader_get_app_str (app));
+ return FALSE;
+ }
+ if (gs_app_get_summary (app) == NULL) {
+ g_debug ("app invalid as no summary %s",
+ gs_plugin_loader_get_app_str (app));
+ return FALSE;
+ }
+
+ /* ignore this crazy application */
+ if (g_strcmp0 (gs_app_get_id (app), "gnome-system-monitor-kde.desktop") == 0) {
+ g_debug ("Ignoring KDE version of %s", gs_app_get_id (app));
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static gboolean
+gs_plugin_loader_app_is_valid_updatable (GsApp *app, gpointer user_data)
+{
+ return gs_plugin_loader_app_is_valid (app, user_data) &&
+ gs_app_is_updatable (app);
+}
+
+static gboolean
+gs_plugin_loader_filter_qt_for_gtk (GsApp *app, gpointer user_data)
+{
+ /* hide the QT versions in preference to the GTK ones */
+ if (g_strcmp0 (gs_app_get_id (app), "transmission-qt.desktop") == 0 ||
+ g_strcmp0 (gs_app_get_id (app), "nntpgrab_qt.desktop") == 0 ||
+ g_strcmp0 (gs_app_get_id (app), "gimagereader-qt4.desktop") == 0 ||
+ g_strcmp0 (gs_app_get_id (app), "gimagereader-qt5.desktop") == 0 ||
+ g_strcmp0 (gs_app_get_id (app), "nntpgrab_server_qt.desktop") == 0 ||
+ g_strcmp0 (gs_app_get_id (app), "hotot-qt.desktop") == 0) {
+ g_debug ("removing QT version of %s",
+ gs_plugin_loader_get_app_str (app));
+ return FALSE;
+ }
+
+ /* hide the KDE version in preference to the GTK one */
+ if (g_strcmp0 (gs_app_get_id (app), "qalculate_kde.desktop") == 0) {
+ g_debug ("removing KDE version of %s",
+ gs_plugin_loader_get_app_str (app));
+ return FALSE;
+ }
+
+ /* hide the KDE version in preference to the Qt one */
+ if (g_strcmp0 (gs_app_get_id (app), "kid3.desktop") == 0 ||
+ g_strcmp0 (gs_app_get_id (app), "kchmviewer.desktop") == 0) {
+ g_debug ("removing KDE version of %s",
+ gs_plugin_loader_get_app_str (app));
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static gboolean
+gs_plugin_loader_app_is_non_compulsory (GsApp *app, gpointer user_data)
+{
+ return !gs_app_has_quirk (app, GS_APP_QUIRK_COMPULSORY);
+}
+
+static gboolean
+gs_plugin_loader_get_app_is_compatible (GsApp *app, gpointer user_data)
+{
+ GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (user_data);
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+ const gchar *tmp;
+ guint i;
+
+ /* search for any compatible projects */
+ tmp = gs_app_get_project_group (app);
+ if (tmp == NULL)
+ return TRUE;
+ for (i = 0; priv->compatible_projects[i] != NULL; i++) {
+ if (g_strcmp0 (tmp, priv->compatible_projects[i]) == 0)
+ return TRUE;
+ }
+ g_debug ("removing incompatible %s from project group %s",
+ gs_app_get_id (app), gs_app_get_project_group (app));
+ return FALSE;
+}
+
+/******************************************************************************/
+
+static gboolean
+gs_plugin_loader_featured_debug (GsApp *app, gpointer user_data)
+{
+ if (g_strcmp0 (gs_app_get_id (app),
+ g_getenv ("GNOME_SOFTWARE_FEATURED")) == 0)
+ return TRUE;
+ return FALSE;
+}
+
+static gint
+gs_plugin_loader_app_sort_kind_cb (GsApp *app1, GsApp *app2, gpointer user_data)
+{
+ if (gs_app_get_kind (app1) == AS_APP_KIND_DESKTOP)
+ return -1;
+ if (gs_app_get_kind (app2) == AS_APP_KIND_DESKTOP)
+ return 1;
+ return 0;
+}
+
+static gint
+gs_plugin_loader_app_sort_match_value_cb (GsApp *app1, GsApp *app2, gpointer user_data)
+{
+ if (gs_app_get_match_value (app1) > gs_app_get_match_value (app2))
+ return -1;
+ if (gs_app_get_match_value (app1) < gs_app_get_match_value (app2))
+ return 1;
+ return 0;
+}
+
+static gint
+gs_plugin_loader_app_sort_prio_cb (GsApp *app1, GsApp *app2, gpointer user_data)
+{
+ return gs_app_compare_priority (app1, app2);
+}
+
+static gint
+gs_plugin_loader_app_sort_version_cb (GsApp *app1, GsApp *app2, gpointer user_data)
+{
+#if AS_CHECK_VERSION(0,7,15)
+ return as_utils_vercmp_full (gs_app_get_version (app1),
+ gs_app_get_version (app2),
+ AS_VERSION_COMPARE_FLAG_NONE);
+#else
+ return as_utils_vercmp (gs_app_get_version (app1),
+ gs_app_get_version (app2));
+#endif
+}
+
+/******************************************************************************/
+
+/**
+ * gs_plugin_loader_job_process_finish:
+ * @plugin_loader: A #GsPluginLoader
+ * @res: a #GAsyncResult
+ * @error: A #GError, or %NULL
+ *
+ * Return value: (element-type GsApp) (transfer full): A list of applications
+ **/
+GsAppList *
+gs_plugin_loader_job_process_finish (GsPluginLoader *plugin_loader,
+ GAsyncResult *res,
+ GError **error)
+{
+ GTask *task;
+ GsAppList *list = NULL;
+
+ g_return_val_if_fail (GS_IS_PLUGIN_LOADER (plugin_loader), NULL);
+ g_return_val_if_fail (G_IS_TASK (res), NULL);
+ g_return_val_if_fail (g_task_is_valid (res, plugin_loader), NULL);
+ g_return_val_if_fail (error == NULL || *error == NULL, NULL);
+
+ task = G_TASK (res);
+
+ /* Return cancelled if the task was cancelled and there is no other error set.
+ *
+ * This is needed because we set the task `check_cancellable` to FALSE,
+ * to be able to catch other errors such as timeout, but that means
+ * g_task_propagate_pointer() will ignore if the task was cancelled and only
+ * check if there was an error (i.e. g_task_return_*error*).
+ *
+ * We only do this if there is no error already set in the task (e.g.
+ * timeout) because in that case we want to return the existing error.
+ */
+ if (!g_task_had_error (task)) {
+ GCancellable *cancellable = g_task_get_cancellable (task);
+
+ if (g_cancellable_set_error_if_cancelled (cancellable, error)) {
+ gs_utils_error_convert_gio (error);
+ return NULL;
+ }
+ }
+ list = g_task_propagate_pointer (task, error);
+ gs_utils_error_convert_gio (error);
+ return list;
+}
+
+/**
+ * gs_plugin_loader_job_action_finish:
+ * @plugin_loader: A #GsPluginLoader
+ * @res: a #GAsyncResult
+ * @error: A #GError, or %NULL
+ *
+ * Return value: success
+ **/
+gboolean
+gs_plugin_loader_job_action_finish (GsPluginLoader *plugin_loader,
+ GAsyncResult *res,
+ GError **error)
+{
+ g_autoptr(GsAppList) list = NULL;
+
+ g_return_val_if_fail (GS_IS_PLUGIN_LOADER (plugin_loader), FALSE);
+ g_return_val_if_fail (G_IS_TASK (res), FALSE);
+ g_return_val_if_fail (g_task_is_valid (res, plugin_loader), FALSE);
+ g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
+
+ list = g_task_propagate_pointer (G_TASK (res), error);
+ return list != NULL;
+}
+
+/******************************************************************************/
+
+static gint
+gs_plugin_loader_category_sort_cb (gconstpointer a, gconstpointer b)
+{
+ GsCategory *cata = GS_CATEGORY (*(GsCategory **) a);
+ GsCategory *catb = GS_CATEGORY (*(GsCategory **) b);
+ if (gs_category_get_score (cata) < gs_category_get_score (catb))
+ return 1;
+ if (gs_category_get_score (cata) > gs_category_get_score (catb))
+ return -1;
+ return gs_utils_sort_strcmp (gs_category_get_name (cata),
+ gs_category_get_name (catb));
+}
+
+static void
+gs_plugin_loader_fix_category_all (GsCategory *category)
+{
+ GPtrArray *children;
+ GsCategory *cat_all;
+ guint i, j;
+
+ /* set correct size */
+ cat_all = gs_category_find_child (category, "all");
+ if (cat_all == NULL)
+ return;
+ gs_category_set_size (cat_all, gs_category_get_size (category));
+
+ /* add the desktop groups from all children */
+ children = gs_category_get_children (category);
+ for (i = 0; i < children->len; i++) {
+ GPtrArray *desktop_groups;
+ GsCategory *child;
+
+ /* ignore the all category */
+ child = g_ptr_array_index (children, i);
+ if (g_strcmp0 (gs_category_get_id (child), "all") == 0)
+ continue;
+
+ /* add all desktop groups */
+ desktop_groups = gs_category_get_desktop_groups (child);
+ for (j = 0; j < desktop_groups->len; j++) {
+ const gchar *tmp = g_ptr_array_index (desktop_groups, j);
+ gs_category_add_desktop_group (cat_all, tmp);
+ }
+ }
+}
+
+static void
+gs_plugin_loader_job_get_categories_thread_cb (GTask *task,
+ gpointer object,
+ gpointer task_data,
+ GCancellable *cancellable)
+{
+ GError *error = NULL;
+ GsPluginLoaderHelper *helper = (GsPluginLoaderHelper *) task_data;
+#ifdef HAVE_SYSPROF
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (helper->plugin_loader);
+ gint64 begin_time_nsec G_GNUC_UNUSED = SYSPROF_CAPTURE_CURRENT_TIME;
+#endif
+
+ /* run each plugin */
+ if (!gs_plugin_loader_run_results (helper, cancellable, &error)) {
+ g_task_return_error (task, error);
+ return;
+ }
+
+ /* make sure 'All' has the right categories */
+ for (guint i = 0; i < helper->catlist->len; i++) {
+ GsCategory *cat = g_ptr_array_index (helper->catlist, i);
+ gs_plugin_loader_fix_category_all (cat);
+ }
+
+ /* sort by name */
+ g_ptr_array_sort (helper->catlist, gs_plugin_loader_category_sort_cb);
+ for (guint i = 0; i < helper->catlist->len; i++) {
+ GsCategory *cat = GS_CATEGORY (g_ptr_array_index (helper->catlist, i));
+ gs_category_sort_children (cat);
+ }
+
+#ifdef HAVE_SYSPROF
+ if (priv->sysprof_writer != NULL) {
+ g_autofree gchar *sysprof_message = gs_plugin_job_to_string (helper->plugin_job);
+ sysprof_capture_writer_add_mark (priv->sysprof_writer,
+ begin_time_nsec,
+ sched_getcpu (),
+ getpid (),
+ SYSPROF_CAPTURE_CURRENT_TIME - begin_time_nsec,
+ "gnome-software",
+ "get-categories",
+ sysprof_message);
+ }
+#endif /* HAVE_SYSPROF */
+
+ /* show elapsed time */
+ gs_plugin_loader_job_debug (helper);
+
+ /* success */
+ if (helper->catlist->len == 0)
+ g_task_return_new_error (task,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_NOT_SUPPORTED,
+ "no categories to show");
+ else
+ g_task_return_pointer (task, g_ptr_array_ref (helper->catlist), (GDestroyNotify) g_ptr_array_unref);
+}
+
+/**
+ * gs_plugin_loader_job_get_categories_async:
+ * @plugin_loader: A #GsPluginLoader
+ * @plugin_job: job to process
+ * @cancellable: a #GCancellable, or %NULL
+ * @callback: function to call when complete
+ * @user_data: user data to pass to @callback
+ *
+ * This method calls all plugins that implement the gs_plugin_add_categories()
+ * function. The plugins return #GsCategory objects.
+ **/
+void
+gs_plugin_loader_job_get_categories_async (GsPluginLoader *plugin_loader,
+ GsPluginJob *plugin_job,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ GsPluginLoaderHelper *helper;
+ g_autoptr(GTask) task = NULL;
+
+ g_return_if_fail (GS_IS_PLUGIN_LOADER (plugin_loader));
+ g_return_if_fail (GS_IS_PLUGIN_JOB (plugin_job));
+ g_return_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable));
+
+ /* save helper */
+ helper = gs_plugin_loader_helper_new (plugin_loader, plugin_job);
+ helper->catlist = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref);
+
+ /* run in a thread */
+ task = g_task_new (plugin_loader, cancellable, callback, user_data);
+ g_task_set_source_tag (task, gs_plugin_loader_job_get_categories_async);
+ g_task_set_task_data (task, helper, (GDestroyNotify) gs_plugin_loader_helper_free);
+ g_task_run_in_thread (task, gs_plugin_loader_job_get_categories_thread_cb);
+}
+
+/**
+ * gs_plugin_loader_job_get_categories_finish:
+ * @plugin_loader: A #GsPluginLoader
+ * @res: a #GAsyncResult
+ * @error: A #GError, or %NULL
+ *
+ * Return value: (element-type GsCategory) (transfer full): A list of applications
+ **/
+GPtrArray *
+gs_plugin_loader_job_get_categories_finish (GsPluginLoader *plugin_loader,
+ GAsyncResult *res,
+ GError **error)
+{
+ GPtrArray *array;
+
+ g_return_val_if_fail (GS_IS_PLUGIN_LOADER (plugin_loader), NULL);
+ g_return_val_if_fail (G_IS_TASK (res), NULL);
+ g_return_val_if_fail (g_task_is_valid (res, plugin_loader), NULL);
+ g_return_val_if_fail (error == NULL || *error == NULL, NULL);
+
+ array = g_task_propagate_pointer (G_TASK (res), error);
+ gs_utils_error_convert_gio (error);
+ return array;
+}
+
+/******************************************************************************/
+
+static gboolean
+emit_pending_apps_idle (gpointer loader)
+{
+ g_signal_emit (loader, signals[SIGNAL_PENDING_APPS_CHANGED], 0);
+ g_object_unref (loader);
+
+ return G_SOURCE_REMOVE;
+}
+
+static void
+gs_plugin_loader_pending_apps_add (GsPluginLoader *plugin_loader,
+ GsPluginLoaderHelper *helper)
+{
+ GsAppList *list = gs_plugin_job_get_list (helper->plugin_job);
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+ g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->pending_apps_mutex);
+
+ g_assert (gs_app_list_length (list) > 0);
+ for (guint i = 0; i < gs_app_list_length (list); i++) {
+ GsApp *app = gs_app_list_index (list, i);
+ g_ptr_array_add (priv->pending_apps, g_object_ref (app));
+ /* make sure the progress is properly initialized */
+ gs_app_set_progress (app, GS_APP_PROGRESS_UNKNOWN);
+ }
+ g_idle_add (emit_pending_apps_idle, g_object_ref (plugin_loader));
+}
+
+static void
+gs_plugin_loader_pending_apps_remove (GsPluginLoader *plugin_loader,
+ GsPluginLoaderHelper *helper)
+{
+ GsAppList *list = gs_plugin_job_get_list (helper->plugin_job);
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+ g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->pending_apps_mutex);
+
+ g_assert (gs_app_list_length (list) > 0);
+ for (guint i = 0; i < gs_app_list_length (list); i++) {
+ GsApp *app = gs_app_list_index (list, i);
+ g_ptr_array_remove (priv->pending_apps, app);
+
+ /* check the app is not still in an action helper */
+ switch (gs_app_get_state (app)) {
+ case AS_APP_STATE_INSTALLING:
+ case AS_APP_STATE_REMOVING:
+ g_warning ("application %s left in %s helper",
+ gs_app_get_unique_id (app),
+ as_app_state_to_string (gs_app_get_state (app)));
+ gs_app_set_state (app, AS_APP_STATE_UNKNOWN);
+ break;
+ default:
+ break;
+ }
+
+ }
+ g_idle_add (emit_pending_apps_idle, g_object_ref (plugin_loader));
+}
+
+static gboolean
+load_install_queue (GsPluginLoader *plugin_loader, GError **error)
+{
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+ g_autofree gchar *contents = NULL;
+ g_autofree gchar *file = NULL;
+ g_auto(GStrv) names = NULL;
+ g_autoptr(GsAppList) list = NULL;
+
+ /* load from file */
+ file = g_build_filename (g_get_user_data_dir (),
+ "gnome-software",
+ "install-queue",
+ NULL);
+ if (!g_file_test (file, G_FILE_TEST_EXISTS))
+ return TRUE;
+ g_debug ("loading install queue from %s", file);
+ if (!g_file_get_contents (file, &contents, NULL, error))
+ return FALSE;
+
+ /* add to GsAppList, deduplicating if required */
+ list = gs_app_list_new ();
+ names = g_strsplit (contents, "\n", 0);
+ for (guint i = 0; names[i] != NULL; i++) {
+ g_autoptr(GsApp) app = NULL;
+ if (strlen (names[i]) == 0)
+ continue;
+ app = gs_app_new (names[i]);
+ gs_app_set_state (app, AS_APP_STATE_QUEUED_FOR_INSTALL);
+ gs_app_list_add (list, app);
+ }
+
+ /* add to pending list */
+ g_mutex_lock (&priv->pending_apps_mutex);
+ for (guint i = 0; i < gs_app_list_length (list); i++) {
+ GsApp *app = gs_app_list_index (list, i);
+ g_debug ("adding pending app %s", gs_app_get_unique_id (app));
+ g_ptr_array_add (priv->pending_apps, g_object_ref (app));
+ }
+ g_mutex_unlock (&priv->pending_apps_mutex);
+
+ /* refine */
+ if (gs_app_list_length (list) > 0) {
+ g_autoptr(GsPluginLoaderHelper) helper = NULL;
+ g_autoptr(GsPluginJob) plugin_job = NULL;
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REFINE, NULL);
+ helper = gs_plugin_loader_helper_new (plugin_loader, plugin_job);
+ if (!gs_plugin_loader_run_refine (helper, list, NULL, error))
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static void
+save_install_queue (GsPluginLoader *plugin_loader)
+{
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+ GPtrArray *pending_apps;
+ gboolean ret;
+ gint i;
+ g_autoptr(GError) error = NULL;
+ g_autoptr(GString) s = NULL;
+ g_autofree gchar *file = NULL;
+
+ s = g_string_new ("");
+ pending_apps = priv->pending_apps;
+ g_mutex_lock (&priv->pending_apps_mutex);
+ for (i = (gint) pending_apps->len - 1; i >= 0; i--) {
+ GsApp *app;
+ app = g_ptr_array_index (pending_apps, i);
+ if (gs_app_get_state (app) == AS_APP_STATE_QUEUED_FOR_INSTALL) {
+ g_string_append (s, gs_app_get_id (app));
+ g_string_append_c (s, '\n');
+ }
+ }
+ g_mutex_unlock (&priv->pending_apps_mutex);
+
+ /* save file */
+ file = g_build_filename (g_get_user_data_dir (),
+ "gnome-software",
+ "install-queue",
+ NULL);
+ if (!gs_mkdir_parent (file, &error)) {
+ g_warning ("failed to create dir for %s: %s",
+ file, error->message);
+ return;
+ }
+ g_debug ("saving install queue to %s", file);
+ ret = g_file_set_contents (file, s->str, (gssize) s->len, &error);
+ if (!ret)
+ g_warning ("failed to save install queue: %s", error->message);
+}
+
+static void
+add_app_to_install_queue (GsPluginLoader *plugin_loader, GsApp *app)
+{
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+ GsAppList *addons;
+ guint i;
+ guint id;
+
+ /* queue the app itself */
+ g_mutex_lock (&priv->pending_apps_mutex);
+ g_ptr_array_add (priv->pending_apps, g_object_ref (app));
+ g_mutex_unlock (&priv->pending_apps_mutex);
+
+ gs_app_set_state (app, AS_APP_STATE_QUEUED_FOR_INSTALL);
+ id = g_idle_add (emit_pending_apps_idle, g_object_ref (plugin_loader));
+ g_source_set_name_by_id (id, "[gnome-software] emit_pending_apps_idle");
+ save_install_queue (plugin_loader);
+
+ /* recursively queue any addons */
+ addons = gs_app_get_addons (app);
+ for (i = 0; i < gs_app_list_length (addons); i++) {
+ GsApp *addon = gs_app_list_index (addons, i);
+ if (gs_app_get_to_be_installed (addon))
+ add_app_to_install_queue (plugin_loader, addon);
+ }
+}
+
+static gboolean
+remove_app_from_install_queue (GsPluginLoader *plugin_loader, GsApp *app)
+{
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+ GsAppList *addons;
+ gboolean ret;
+ guint i;
+ guint id;
+
+ g_mutex_lock (&priv->pending_apps_mutex);
+ ret = g_ptr_array_remove (priv->pending_apps, app);
+ g_mutex_unlock (&priv->pending_apps_mutex);
+
+ if (ret) {
+ gs_app_set_state (app, AS_APP_STATE_AVAILABLE);
+ id = g_idle_add (emit_pending_apps_idle, g_object_ref (plugin_loader));
+ g_source_set_name_by_id (id, "[gnome-software] emit_pending_apps_idle");
+ save_install_queue (plugin_loader);
+
+ /* recursively remove any queued addons */
+ addons = gs_app_get_addons (app);
+ for (i = 0; i < gs_app_list_length (addons); i++) {
+ GsApp *addon = gs_app_list_index (addons, i);
+ remove_app_from_install_queue (plugin_loader, addon);
+ }
+ }
+
+ return ret;
+}
+
+/******************************************************************************/
+
+gboolean
+gs_plugin_loader_get_allow_updates (GsPluginLoader *plugin_loader)
+{
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+ GHashTableIter iter;
+ gpointer value;
+
+ /* nothing */
+ if (g_hash_table_size (priv->disallow_updates) == 0)
+ return TRUE;
+
+ /* list */
+ g_hash_table_iter_init (&iter, priv->disallow_updates);
+ while (g_hash_table_iter_next (&iter, NULL, &value)) {
+ const gchar *reason = value;
+ g_debug ("managed updates inhibited by %s", reason);
+ }
+ return FALSE;
+}
+
+GsAppList *
+gs_plugin_loader_get_pending (GsPluginLoader *plugin_loader)
+{
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+ GsAppList *array;
+ guint i;
+
+ array = gs_app_list_new ();
+ g_mutex_lock (&priv->pending_apps_mutex);
+ for (i = 0; i < priv->pending_apps->len; i++) {
+ GsApp *app = g_ptr_array_index (priv->pending_apps, i);
+ gs_app_list_add (array, app);
+ }
+ g_mutex_unlock (&priv->pending_apps_mutex);
+
+ return array;
+}
+
+gboolean
+gs_plugin_loader_get_enabled (GsPluginLoader *plugin_loader,
+ const gchar *plugin_name)
+{
+ GsPlugin *plugin;
+ plugin = gs_plugin_loader_find_plugin (plugin_loader, plugin_name);
+ if (plugin == NULL)
+ return FALSE;
+ return gs_plugin_get_enabled (plugin);
+}
+
+/**
+ * gs_plugin_loader_get_events:
+ * @plugin_loader: A #GsPluginLoader
+ *
+ * Gets all plugin events, even ones that are not active or visible anymore.
+ *
+ * Returns: (transfer container) (element-type GsPluginEvent): events
+ **/
+GPtrArray *
+gs_plugin_loader_get_events (GsPluginLoader *plugin_loader)
+{
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+ GPtrArray *events = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref);
+ g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->events_by_id_mutex);
+ GHashTableIter iter;
+ gpointer key, value;
+
+ /* just add everything */
+ g_hash_table_iter_init (&iter, priv->events_by_id);
+ while (g_hash_table_iter_next (&iter, &key, &value)) {
+ const gchar *id = key;
+ GsPluginEvent *event = value;
+ if (event == NULL) {
+ g_warning ("failed to get event for '%s'", id);
+ continue;
+ }
+ g_ptr_array_add (events, g_object_ref (event));
+ }
+ return events;
+}
+
+/**
+ * gs_plugin_loader_get_event_default:
+ * @plugin_loader: A #GsPluginLoader
+ *
+ * Gets an active plugin event where active means that it was not been
+ * already dismissed by the user.
+ *
+ * Returns: (transfer full): a #GsPluginEvent, or %NULL for none
+ **/
+GsPluginEvent *
+gs_plugin_loader_get_event_default (GsPluginLoader *plugin_loader)
+{
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+ g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->events_by_id_mutex);
+ GHashTableIter iter;
+ gpointer key, value;
+
+ /* just add everything */
+ g_hash_table_iter_init (&iter, priv->events_by_id);
+ while (g_hash_table_iter_next (&iter, &key, &value)) {
+ const gchar *id = key;
+ GsPluginEvent *event = value;
+ if (event == NULL) {
+ g_warning ("failed to get event for '%s'", id);
+ continue;
+ }
+ if (!gs_plugin_event_has_flag (event, GS_PLUGIN_EVENT_FLAG_INVALID))
+ return g_object_ref (event);
+ }
+ return NULL;
+}
+
+/**
+ * gs_plugin_loader_remove_events:
+ * @plugin_loader: A #GsPluginLoader
+ *
+ * Removes all plugin events from the loader. This function should only be
+ * called from the self tests.
+ **/
+void
+gs_plugin_loader_remove_events (GsPluginLoader *plugin_loader)
+{
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+ g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->events_by_id_mutex);
+ g_hash_table_remove_all (priv->events_by_id);
+}
+
+static void
+gs_plugin_loader_report_event_cb (GsPlugin *plugin,
+ GsPluginEvent *event,
+ GsPluginLoader *plugin_loader)
+{
+ if (gs_plugin_has_flags (plugin, GS_PLUGIN_FLAGS_INTERACTIVE))
+ gs_plugin_event_add_flag (event, GS_PLUGIN_EVENT_FLAG_INTERACTIVE);
+ gs_plugin_loader_add_event (plugin_loader, event);
+}
+
+static void
+gs_plugin_loader_allow_updates_cb (GsPlugin *plugin,
+ gboolean allow_updates,
+ GsPluginLoader *plugin_loader)
+{
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+ gboolean changed = FALSE;
+
+ /* plugin now allowing gnome-software to show updates panel */
+ if (allow_updates) {
+ if (g_hash_table_remove (priv->disallow_updates, plugin)) {
+ g_debug ("plugin %s no longer inhibited managed updates",
+ gs_plugin_get_name (plugin));
+ changed = TRUE;
+ }
+
+ /* plugin preventing the updates panel from being shown */
+ } else {
+ if (g_hash_table_replace (priv->disallow_updates,
+ (gpointer) plugin,
+ (gpointer) gs_plugin_get_name (plugin))) {
+ g_debug ("plugin %s inhibited managed updates",
+ gs_plugin_get_name (plugin));
+ changed = TRUE;
+ }
+ }
+
+ /* notify display layer */
+ if (changed)
+ g_object_notify (G_OBJECT (plugin_loader), "allow-updates");
+}
+
+static void
+gs_plugin_loader_status_changed_cb (GsPlugin *plugin,
+ GsApp *app,
+ GsPluginStatus status,
+ GsPluginLoader *plugin_loader)
+{
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+
+ /* nothing specific */
+ if (app == NULL || gs_app_get_id (app) == NULL) {
+ if (priv->global_status_last != status) {
+ g_debug ("emitting global %s",
+ gs_plugin_status_to_string (status));
+ g_signal_emit (plugin_loader,
+ signals[SIGNAL_STATUS_CHANGED],
+ 0, app, status);
+ priv->global_status_last = status;
+ }
+ return;
+ }
+
+ /* a specific app */
+ g_debug ("emitting %s(%s)",
+ gs_plugin_status_to_string (status),
+ gs_app_get_id (app));
+ g_signal_emit (plugin_loader,
+ signals[SIGNAL_STATUS_CHANGED],
+ 0, app, status);
+}
+
+static void
+gs_plugin_loader_basic_auth_start_cb (GsPlugin *plugin,
+ const gchar *remote,
+ const gchar *realm,
+ GCallback callback,
+ gpointer user_data,
+ GsPluginLoader *plugin_loader)
+{
+ g_debug ("emitting basic-auth-start %s", realm);
+ g_signal_emit (plugin_loader,
+ signals[SIGNAL_BASIC_AUTH_START], 0,
+ remote,
+ realm,
+ callback,
+ user_data);
+}
+
+static gboolean
+gs_plugin_loader_job_actions_changed_delay_cb (gpointer user_data)
+{
+ GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (user_data);
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+
+ /* notify shells */
+ g_debug ("updates-changed");
+ g_signal_emit (plugin_loader, signals[SIGNAL_UPDATES_CHANGED], 0);
+ priv->updates_changed_id = 0;
+ priv->updates_changed_cnt = 0;
+
+ g_object_unref (plugin_loader);
+ return FALSE;
+}
+
+static void
+gs_plugin_loader_job_actions_changed_cb (GsPlugin *plugin, GsPluginLoader *plugin_loader)
+{
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+ priv->updates_changed_cnt++;
+}
+
+static void
+gs_plugin_loader_updates_changed (GsPluginLoader *plugin_loader)
+{
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+ if (priv->updates_changed_id != 0)
+ return;
+ priv->updates_changed_id =
+ g_timeout_add_seconds (GS_PLUGIN_LOADER_UPDATES_CHANGED_DELAY,
+ gs_plugin_loader_job_actions_changed_delay_cb,
+ g_object_ref (plugin_loader));
+}
+
+static gboolean
+gs_plugin_loader_reload_delay_cb (gpointer user_data)
+{
+ GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (user_data);
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+
+ /* notify shells */
+ g_debug ("emitting ::reload");
+ g_signal_emit (plugin_loader, signals[SIGNAL_RELOAD], 0);
+ priv->reload_id = 0;
+
+ g_object_unref (plugin_loader);
+ return FALSE;
+}
+
+static void
+gs_plugin_loader_reload_cb (GsPlugin *plugin,
+ GsPluginLoader *plugin_loader)
+{
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+ if (priv->reload_id != 0)
+ return;
+ priv->reload_id =
+ g_timeout_add_seconds (GS_PLUGIN_LOADER_RELOAD_DELAY,
+ gs_plugin_loader_reload_delay_cb,
+ g_object_ref (plugin_loader));
+}
+
+static void
+gs_plugin_loader_open_plugin (GsPluginLoader *plugin_loader,
+ const gchar *filename)
+{
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+ GsPlugin *plugin;
+ g_autoptr(GError) error = NULL;
+
+ /* create plugin from file */
+ plugin = gs_plugin_create (filename, &error);
+ if (plugin == NULL) {
+ g_warning ("Failed to load %s: %s", filename, error->message);
+ return;
+ }
+ g_signal_connect (plugin, "updates-changed",
+ G_CALLBACK (gs_plugin_loader_job_actions_changed_cb),
+ plugin_loader);
+ g_signal_connect (plugin, "reload",
+ G_CALLBACK (gs_plugin_loader_reload_cb),
+ plugin_loader);
+ g_signal_connect (plugin, "status-changed",
+ G_CALLBACK (gs_plugin_loader_status_changed_cb),
+ plugin_loader);
+ g_signal_connect (plugin, "basic-auth-start",
+ G_CALLBACK (gs_plugin_loader_basic_auth_start_cb),
+ plugin_loader);
+ g_signal_connect (plugin, "report-event",
+ G_CALLBACK (gs_plugin_loader_report_event_cb),
+ plugin_loader);
+ g_signal_connect (plugin, "allow-updates",
+ G_CALLBACK (gs_plugin_loader_allow_updates_cb),
+ plugin_loader);
+ gs_plugin_set_soup_session (plugin, priv->soup_session);
+ gs_plugin_set_locale (plugin, priv->locale);
+ gs_plugin_set_language (plugin, priv->language);
+ gs_plugin_set_scale (plugin, gs_plugin_loader_get_scale (plugin_loader));
+ gs_plugin_set_network_monitor (plugin, priv->network_monitor);
+ g_debug ("opened plugin %s: %s", filename, gs_plugin_get_name (plugin));
+
+ /* add to array */
+ g_ptr_array_add (priv->plugins, plugin);
+}
+
+void
+gs_plugin_loader_set_scale (GsPluginLoader *plugin_loader, guint scale)
+{
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+
+ /* save globally, and update each plugin */
+ priv->scale = scale;
+ for (guint i = 0; i < priv->plugins->len; i++) {
+ GsPlugin *plugin = g_ptr_array_index (priv->plugins, i);
+ gs_plugin_set_scale (plugin, scale);
+ }
+}
+
+guint
+gs_plugin_loader_get_scale (GsPluginLoader *plugin_loader)
+{
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+ return priv->scale;
+}
+
+void
+gs_plugin_loader_add_location (GsPluginLoader *plugin_loader, const gchar *location)
+{
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+ for (guint i = 0; i < priv->locations->len; i++) {
+ const gchar *location_tmp = g_ptr_array_index (priv->locations, i);
+ if (g_strcmp0 (location_tmp, location) == 0)
+ return;
+ }
+ g_info ("adding plugin location %s", location);
+ g_ptr_array_add (priv->locations, g_strdup (location));
+}
+
+static gint
+gs_plugin_loader_plugin_sort_fn (gconstpointer a, gconstpointer b)
+{
+ GsPlugin *pa = *((GsPlugin **) a);
+ GsPlugin *pb = *((GsPlugin **) b);
+ if (gs_plugin_get_order (pa) < gs_plugin_get_order (pb))
+ return -1;
+ if (gs_plugin_get_order (pa) > gs_plugin_get_order (pb))
+ return 1;
+ return g_strcmp0 (gs_plugin_get_name (pa), gs_plugin_get_name (pb));
+}
+
+static void
+gs_plugin_loader_plugin_dir_changed_cb (GFileMonitor *monitor,
+ GFile *file,
+ GFile *other_file,
+ GFileMonitorEvent event_type,
+ GsPluginLoader *plugin_loader)
+{
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+ g_autoptr(GsApp) app = NULL;
+ g_autoptr(GsPluginEvent) event = gs_plugin_event_new ();
+ g_autoptr(GError) error = NULL;
+
+ /* already triggered */
+ if (priv->plugin_dir_dirty)
+ return;
+
+ /* add app */
+ gs_plugin_event_set_action (event, GS_PLUGIN_ACTION_SETUP);
+ app = gs_plugin_loader_app_create (plugin_loader,
+ "system/*/*/*/org.gnome.Software.desktop/*");
+ if (app != NULL)
+ gs_plugin_event_set_app (event, app);
+
+ /* add error */
+ g_set_error_literal (&error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_RESTART_REQUIRED,
+ "A restart is required");
+ gs_plugin_event_set_error (event, error);
+ gs_plugin_loader_add_event (plugin_loader, event);
+ priv->plugin_dir_dirty = TRUE;
+}
+
+void
+gs_plugin_loader_clear_caches (GsPluginLoader *plugin_loader)
+{
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+ for (guint i = 0; i < priv->plugins->len; i++) {
+ GsPlugin *plugin = g_ptr_array_index (priv->plugins, i);
+ gs_plugin_cache_invalidate (plugin);
+ }
+}
+
+/**
+ * gs_plugin_loader_setup_again:
+ * @plugin_loader: a #GsPluginLoader
+ *
+ * Calls setup on each plugin. This should only be used from the self tests
+ * and in a controlled way.
+ */
+void
+gs_plugin_loader_setup_again (GsPluginLoader *plugin_loader)
+{
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+ GsPluginAction actions[] = {
+ GS_PLUGIN_ACTION_DESTROY,
+ GS_PLUGIN_ACTION_INITIALIZE,
+ GS_PLUGIN_ACTION_SETUP,
+ GS_PLUGIN_ACTION_UNKNOWN };
+#ifdef HAVE_SYSPROF
+ gint64 begin_time_nsec G_GNUC_UNUSED = SYSPROF_CAPTURE_CURRENT_TIME;
+#endif
+
+ /* clear global cache */
+ gs_plugin_loader_clear_caches (plugin_loader);
+
+ /* remove any events */
+ gs_plugin_loader_remove_events (plugin_loader);
+
+ /* call in order */
+ for (guint j = 0; actions[j] != GS_PLUGIN_ACTION_UNKNOWN; j++) {
+ for (guint i = 0; i < priv->plugins->len; i++) {
+ g_autoptr(GError) error_local = NULL;
+ g_autoptr(GsPluginLoaderHelper) helper = NULL;
+ g_autoptr(GsPluginJob) plugin_job = NULL;
+ GsPlugin *plugin = g_ptr_array_index (priv->plugins, i);
+ if (!gs_plugin_get_enabled (plugin))
+ continue;
+
+ plugin_job = gs_plugin_job_newv (actions[j], NULL);
+ helper = gs_plugin_loader_helper_new (plugin_loader, plugin_job);
+ if (!gs_plugin_loader_call_vfunc (helper, plugin, NULL, NULL,
+ GS_PLUGIN_REFINE_FLAGS_DEFAULT,
+ NULL, &error_local)) {
+ g_warning ("resetup of %s failed: %s",
+ gs_plugin_get_name (plugin),
+ error_local->message);
+ break;
+ }
+ if (actions[j] == GS_PLUGIN_ACTION_DESTROY)
+ gs_plugin_clear_data (plugin);
+ }
+ }
+
+#ifdef HAVE_SYSPROF
+ if (priv->sysprof_writer != NULL) {
+ sysprof_capture_writer_add_mark (priv->sysprof_writer,
+ begin_time_nsec,
+ sched_getcpu (),
+ getpid (),
+ SYSPROF_CAPTURE_CURRENT_TIME - begin_time_nsec,
+ "gnome-software",
+ "setup-again",
+ NULL);
+ }
+#endif /* HAVE_SYSPROF */
+}
+
+static gint
+gs_plugin_loader_path_sort_fn (gconstpointer a, gconstpointer b)
+{
+ const gchar *sa = *((const gchar **) a);
+ const gchar *sb = *((const gchar **) b);
+ return g_strcmp0 (sa, sb);
+}
+
+static GPtrArray *
+gs_plugin_loader_find_plugins (const gchar *path, GError **error)
+{
+ const gchar *fn_tmp;
+ g_autoptr(GPtrArray) fns = g_ptr_array_new_with_free_func (g_free);
+ g_autoptr(GDir) dir = g_dir_open (path, 0, error);
+ if (dir == NULL)
+ return NULL;
+ while ((fn_tmp = g_dir_read_name (dir)) != NULL) {
+ if (!g_str_has_suffix (fn_tmp, ".so"))
+ continue;
+ g_ptr_array_add (fns, g_build_filename (path, fn_tmp, NULL));
+ }
+ g_ptr_array_sort (fns, gs_plugin_loader_path_sort_fn);
+ return g_steal_pointer (&fns);
+}
+
+/**
+ * gs_plugin_loader_setup:
+ * @plugin_loader: a #GsPluginLoader
+ * @allowlist: list of plugin names, or %NULL
+ * @blocklist: list of plugin names, or %NULL
+ * @cancellable: A #GCancellable, or %NULL
+ * @error: A #GError, or %NULL
+ *
+ * Sets up the plugin loader ready for use.
+ *
+ * Returns: %TRUE for success
+ */
+gboolean
+gs_plugin_loader_setup (GsPluginLoader *plugin_loader,
+ gchar **allowlist,
+ gchar **blocklist,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+ const gchar *plugin_name;
+ gboolean changes;
+ GPtrArray *deps;
+ GsPlugin *dep;
+ GsPlugin *plugin;
+ guint dep_loop_check = 0;
+ guint i;
+ guint j;
+ g_autoptr(GsPluginLoaderHelper) helper = NULL;
+ g_autoptr(GsPluginJob) plugin_job = NULL;
+#ifdef HAVE_SYSPROF
+ gint64 begin_time_nsec G_GNUC_UNUSED = SYSPROF_CAPTURE_CURRENT_TIME;
+#endif
+
+ /* use the default, but this requires a 'make install' */
+ if (priv->locations->len == 0) {
+ g_autofree gchar *filename = NULL;
+ filename = g_strdup_printf ("gs-plugins-%s", GS_PLUGIN_API_VERSION);
+ g_ptr_array_add (priv->locations, g_build_filename (LIBDIR, filename, NULL));
+ }
+
+ for (i = 0; i < priv->locations->len; i++) {
+ GFileMonitor *monitor;
+ const gchar *location = g_ptr_array_index (priv->locations, i);
+ g_autoptr(GFile) plugin_dir = g_file_new_for_path (location);
+ monitor = g_file_monitor_directory (plugin_dir,
+ G_FILE_MONITOR_NONE,
+ cancellable,
+ error);
+ if (monitor == NULL)
+ return FALSE;
+ g_signal_connect (monitor, "changed",
+ G_CALLBACK (gs_plugin_loader_plugin_dir_changed_cb), plugin_loader);
+ g_ptr_array_add (priv->file_monitors, monitor);
+ }
+
+ /* search for plugins */
+ for (i = 0; i < priv->locations->len; i++) {
+ const gchar *location = g_ptr_array_index (priv->locations, i);
+ g_autoptr(GPtrArray) fns = NULL;
+
+ /* search in the plugin directory for plugins */
+ g_debug ("searching for plugins in %s", location);
+ fns = gs_plugin_loader_find_plugins (location, error);
+ if (fns == NULL)
+ return FALSE;
+ for (j = 0; j < fns->len; j++) {
+ const gchar *fn = g_ptr_array_index (fns, j);
+ gs_plugin_loader_open_plugin (plugin_loader, fn);
+ }
+ }
+
+ /* optional allowlist */
+ if (allowlist != NULL) {
+ for (i = 0; i < priv->plugins->len; i++) {
+ gboolean ret;
+ plugin = g_ptr_array_index (priv->plugins, i);
+ if (!gs_plugin_get_enabled (plugin))
+ continue;
+ ret = g_strv_contains ((const gchar * const *) allowlist,
+ gs_plugin_get_name (plugin));
+ if (!ret) {
+ g_debug ("%s not in allowlist, disabling",
+ gs_plugin_get_name (plugin));
+ }
+ gs_plugin_set_enabled (plugin, ret);
+ }
+ }
+
+ /* optional blocklist */
+ if (blocklist != NULL) {
+ for (i = 0; i < priv->plugins->len; i++) {
+ gboolean ret;
+ plugin = g_ptr_array_index (priv->plugins, i);
+ if (!gs_plugin_get_enabled (plugin))
+ continue;
+ ret = g_strv_contains ((const gchar * const *) blocklist,
+ gs_plugin_get_name (plugin));
+ if (ret)
+ gs_plugin_set_enabled (plugin, FALSE);
+ }
+ }
+
+ /* run the plugins */
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_INITIALIZE, NULL);
+ helper = gs_plugin_loader_helper_new (plugin_loader, plugin_job);
+ if (!gs_plugin_loader_run_results (helper, cancellable, error))
+ return FALSE;
+
+ /* order by deps */
+ do {
+ changes = FALSE;
+ for (i = 0; i < priv->plugins->len; i++) {
+ plugin = g_ptr_array_index (priv->plugins, i);
+ deps = gs_plugin_get_rules (plugin, GS_PLUGIN_RULE_RUN_AFTER);
+ for (j = 0; j < deps->len && !changes; j++) {
+ plugin_name = g_ptr_array_index (deps, j);
+ dep = gs_plugin_loader_find_plugin (plugin_loader,
+ plugin_name);
+ if (dep == NULL) {
+ g_debug ("cannot find plugin '%s' "
+ "requested by '%s'",
+ plugin_name,
+ gs_plugin_get_name (plugin));
+ continue;
+ }
+ if (!gs_plugin_get_enabled (dep))
+ continue;
+ if (gs_plugin_get_order (plugin) <= gs_plugin_get_order (dep)) {
+ gs_plugin_set_order (plugin, gs_plugin_get_order (dep) + 1);
+ changes = TRUE;
+ }
+ }
+ }
+ for (i = 0; i < priv->plugins->len; i++) {
+ plugin = g_ptr_array_index (priv->plugins, i);
+ deps = gs_plugin_get_rules (plugin, GS_PLUGIN_RULE_RUN_BEFORE);
+ for (j = 0; j < deps->len && !changes; j++) {
+ plugin_name = g_ptr_array_index (deps, j);
+ dep = gs_plugin_loader_find_plugin (plugin_loader,
+ plugin_name);
+ if (dep == NULL) {
+ g_debug ("cannot find plugin '%s' "
+ "requested by '%s'",
+ plugin_name,
+ gs_plugin_get_name (plugin));
+ continue;
+ }
+ if (!gs_plugin_get_enabled (dep))
+ continue;
+ if (gs_plugin_get_order (plugin) >= gs_plugin_get_order (dep)) {
+ gs_plugin_set_order (dep, gs_plugin_get_order (plugin) + 1);
+ changes = TRUE;
+ }
+ }
+ }
+
+ /* check we're not stuck */
+ if (dep_loop_check++ > 100) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_PLUGIN_DEPSOLVE_FAILED,
+ "got stuck in dep loop");
+ return FALSE;
+ }
+ } while (changes);
+
+ /* check for conflicts */
+ for (i = 0; i < priv->plugins->len; i++) {
+ plugin = g_ptr_array_index (priv->plugins, i);
+ if (!gs_plugin_get_enabled (plugin))
+ continue;
+ deps = gs_plugin_get_rules (plugin, GS_PLUGIN_RULE_CONFLICTS);
+ for (j = 0; j < deps->len && !changes; j++) {
+ plugin_name = g_ptr_array_index (deps, j);
+ dep = gs_plugin_loader_find_plugin (plugin_loader,
+ plugin_name);
+ if (dep == NULL)
+ continue;
+ if (!gs_plugin_get_enabled (dep))
+ continue;
+ g_debug ("disabling %s as conflicts with %s",
+ gs_plugin_get_name (dep),
+ gs_plugin_get_name (plugin));
+ gs_plugin_set_enabled (dep, FALSE);
+ }
+ }
+
+ /* sort by order */
+ g_ptr_array_sort (priv->plugins,
+ gs_plugin_loader_plugin_sort_fn);
+
+ /* assign priority values */
+ do {
+ changes = FALSE;
+ for (i = 0; i < priv->plugins->len; i++) {
+ plugin = g_ptr_array_index (priv->plugins, i);
+ deps = gs_plugin_get_rules (plugin, GS_PLUGIN_RULE_BETTER_THAN);
+ for (j = 0; j < deps->len && !changes; j++) {
+ plugin_name = g_ptr_array_index (deps, j);
+ dep = gs_plugin_loader_find_plugin (plugin_loader,
+ plugin_name);
+ if (dep == NULL) {
+ g_debug ("cannot find plugin '%s' "
+ "requested by '%s'",
+ plugin_name,
+ gs_plugin_get_name (plugin));
+ continue;
+ }
+ if (!gs_plugin_get_enabled (dep))
+ continue;
+ if (gs_plugin_get_priority (plugin) <= gs_plugin_get_priority (dep)) {
+ gs_plugin_set_priority (plugin, gs_plugin_get_priority (dep) + 1);
+ changes = TRUE;
+ }
+ }
+ }
+
+ /* check we're not stuck */
+ if (dep_loop_check++ > 100) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_PLUGIN_DEPSOLVE_FAILED,
+ "got stuck in priority loop");
+ return FALSE;
+ }
+ } while (changes);
+
+ /* run setup */
+ gs_plugin_job_set_action (helper->plugin_job, GS_PLUGIN_ACTION_SETUP);
+ helper->function_name = "gs_plugin_setup";
+ for (i = 0; i < priv->plugins->len; i++) {
+ g_autoptr(GError) error_local = NULL;
+ plugin = g_ptr_array_index (priv->plugins, i);
+ if (!gs_plugin_loader_call_vfunc (helper, plugin, NULL, NULL,
+ GS_PLUGIN_REFINE_FLAGS_DEFAULT,
+ cancellable, &error_local)) {
+ g_debug ("disabling %s as setup failed: %s",
+ gs_plugin_get_name (plugin),
+ error_local->message);
+ gs_plugin_set_enabled (plugin, FALSE);
+ }
+ }
+
+ /* now we can load the install-queue */
+ if (!load_install_queue (plugin_loader, error))
+ return FALSE;
+
+#ifdef HAVE_SYSPROF
+ if (priv->sysprof_writer != NULL) {
+ sysprof_capture_writer_add_mark (priv->sysprof_writer,
+ begin_time_nsec,
+ sched_getcpu (),
+ getpid (),
+ SYSPROF_CAPTURE_CURRENT_TIME - begin_time_nsec,
+ "gnome-software",
+ "setup",
+ NULL);
+ }
+#endif /* HAVE_SYSPROF */
+
+ return TRUE;
+}
+
+void
+gs_plugin_loader_dump_state (GsPluginLoader *plugin_loader)
+{
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+ g_autoptr(GString) str_enabled = g_string_new (NULL);
+ g_autoptr(GString) str_disabled = g_string_new (NULL);
+
+ /* print what the priorities are if verbose */
+ for (guint i = 0; i < priv->plugins->len; i++) {
+ GsPlugin *plugin = g_ptr_array_index (priv->plugins, i);
+ GString *str = gs_plugin_get_enabled (plugin) ? str_enabled : str_disabled;
+ g_string_append_printf (str, "%s, ", gs_plugin_get_name (plugin));
+ g_debug ("[%s]\t%u\t->\t%s",
+ gs_plugin_get_enabled (plugin) ? "enabled" : "disabld",
+ gs_plugin_get_order (plugin),
+ gs_plugin_get_name (plugin));
+ }
+ if (str_enabled->len > 2)
+ g_string_truncate (str_enabled, str_enabled->len - 2);
+ if (str_disabled->len > 2)
+ g_string_truncate (str_disabled, str_disabled->len - 2);
+ g_info ("enabled plugins: %s", str_enabled->str);
+ g_info ("disabled plugins: %s", str_disabled->str);
+}
+
+static void
+gs_plugin_loader_get_property (GObject *object, guint prop_id,
+ GValue *value, GParamSpec *pspec)
+{
+ GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (object);
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+
+ switch (prop_id) {
+ case PROP_EVENTS:
+ g_value_set_pointer (value, priv->events_by_id);
+ break;
+ case PROP_ALLOW_UPDATES:
+ g_value_set_boolean (value, gs_plugin_loader_get_allow_updates (plugin_loader));
+ break;
+ case PROP_NETWORK_AVAILABLE:
+ g_value_set_boolean (value, gs_plugin_loader_get_network_available (plugin_loader));
+ break;
+ case PROP_NETWORK_METERED:
+ g_value_set_boolean (value, gs_plugin_loader_get_network_metered (plugin_loader));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+gs_plugin_loader_set_property (GObject *object, guint prop_id,
+ const GValue *value, GParamSpec *pspec)
+{
+ switch (prop_id) {
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+gs_plugin_loader_dispose (GObject *object)
+{
+ GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (object);
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+
+ if (priv->plugins != NULL) {
+ g_autoptr(GsPluginLoaderHelper) helper = NULL;
+ g_autoptr(GsPluginJob) plugin_job = NULL;
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_DESTROY, NULL);
+ helper = gs_plugin_loader_helper_new (plugin_loader, plugin_job);
+ gs_plugin_loader_run_results (helper, NULL, NULL);
+ g_clear_pointer (&priv->plugins, g_ptr_array_unref);
+ }
+ if (priv->updates_changed_id != 0) {
+ g_source_remove (priv->updates_changed_id);
+ priv->updates_changed_id = 0;
+ }
+ if (priv->network_changed_handler != 0) {
+ g_signal_handler_disconnect (priv->network_monitor,
+ priv->network_changed_handler);
+ priv->network_changed_handler = 0;
+ }
+ if (priv->network_available_notify_handler != 0) {
+ g_signal_handler_disconnect (priv->network_monitor,
+ priv->network_available_notify_handler);
+ priv->network_available_notify_handler = 0;
+ }
+ if (priv->network_metered_notify_handler != 0) {
+ g_signal_handler_disconnect (priv->network_monitor,
+ priv->network_metered_notify_handler);
+ priv->network_metered_notify_handler = 0;
+ }
+ if (priv->queued_ops_pool != NULL) {
+ /* stop accepting more requests and wait until any currently
+ * running ones are finished */
+ g_thread_pool_free (priv->queued_ops_pool, TRUE, TRUE);
+ priv->queued_ops_pool = NULL;
+ }
+ g_clear_object (&priv->network_monitor);
+ g_clear_object (&priv->soup_session);
+ g_clear_object (&priv->settings);
+ g_clear_pointer (&priv->pending_apps, g_ptr_array_unref);
+#ifdef HAVE_SYSPROF
+ g_clear_pointer (&priv->sysprof_writer, sysprof_capture_writer_unref);
+#endif
+
+ G_OBJECT_CLASS (gs_plugin_loader_parent_class)->dispose (object);
+}
+
+static void
+gs_plugin_loader_finalize (GObject *object)
+{
+ GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (object);
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+
+ g_strfreev (priv->compatible_projects);
+ g_ptr_array_unref (priv->locations);
+ g_free (priv->locale);
+ g_free (priv->language);
+ g_ptr_array_unref (priv->file_monitors);
+ g_hash_table_unref (priv->events_by_id);
+ g_hash_table_unref (priv->disallow_updates);
+
+ g_mutex_clear (&priv->pending_apps_mutex);
+ g_mutex_clear (&priv->events_by_id_mutex);
+
+ G_OBJECT_CLASS (gs_plugin_loader_parent_class)->finalize (object);
+}
+
+static void
+gs_plugin_loader_class_init (GsPluginLoaderClass *klass)
+{
+ GParamSpec *pspec;
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->get_property = gs_plugin_loader_get_property;
+ object_class->set_property = gs_plugin_loader_set_property;
+ object_class->dispose = gs_plugin_loader_dispose;
+ object_class->finalize = gs_plugin_loader_finalize;
+
+ pspec = g_param_spec_string ("events", NULL, NULL,
+ NULL,
+ G_PARAM_READABLE);
+ g_object_class_install_property (object_class, PROP_EVENTS, pspec);
+
+ pspec = g_param_spec_boolean ("allow-updates", NULL, NULL,
+ TRUE,
+ G_PARAM_READABLE);
+ g_object_class_install_property (object_class, PROP_ALLOW_UPDATES, pspec);
+
+ pspec = g_param_spec_boolean ("network-available", NULL, NULL,
+ FALSE,
+ G_PARAM_READABLE);
+ g_object_class_install_property (object_class, PROP_NETWORK_AVAILABLE, pspec);
+
+ pspec = g_param_spec_boolean ("network-metered", NULL, NULL,
+ FALSE,
+ G_PARAM_READABLE);
+ g_object_class_install_property (object_class, PROP_NETWORK_METERED, pspec);
+
+ signals [SIGNAL_STATUS_CHANGED] =
+ g_signal_new ("status-changed",
+ G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (GsPluginLoaderClass, status_changed),
+ NULL, NULL, g_cclosure_marshal_generic,
+ G_TYPE_NONE, 2, G_TYPE_POINTER, G_TYPE_UINT);
+ signals [SIGNAL_PENDING_APPS_CHANGED] =
+ g_signal_new ("pending-apps-changed",
+ G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (GsPluginLoaderClass, pending_apps_changed),
+ NULL, NULL, g_cclosure_marshal_VOID__VOID,
+ G_TYPE_NONE, 0);
+ signals [SIGNAL_UPDATES_CHANGED] =
+ g_signal_new ("updates-changed",
+ G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (GsPluginLoaderClass, updates_changed),
+ NULL, NULL, g_cclosure_marshal_VOID__VOID,
+ G_TYPE_NONE, 0);
+ signals [SIGNAL_RELOAD] =
+ g_signal_new ("reload",
+ G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (GsPluginLoaderClass, reload),
+ NULL, NULL, g_cclosure_marshal_VOID__VOID,
+ G_TYPE_NONE, 0);
+ signals [SIGNAL_BASIC_AUTH_START] =
+ g_signal_new ("basic-auth-start",
+ G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (GsPluginLoaderClass, basic_auth_start),
+ NULL, NULL, g_cclosure_marshal_generic,
+ G_TYPE_NONE, 4, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_POINTER, G_TYPE_POINTER);
+}
+
+static void
+gs_plugin_loader_allow_updates_recheck (GsPluginLoader *plugin_loader)
+{
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+ if (g_settings_get_boolean (priv->settings, "allow-updates")) {
+ g_hash_table_remove (priv->disallow_updates, plugin_loader);
+ } else {
+ g_hash_table_insert (priv->disallow_updates,
+ (gpointer) plugin_loader,
+ (gpointer) "GSettings");
+ }
+}
+
+static void
+gs_plugin_loader_settings_changed_cb (GSettings *settings,
+ const gchar *key,
+ GsPluginLoader *plugin_loader)
+{
+ if (g_strcmp0 (key, "allow-updates") == 0)
+ gs_plugin_loader_allow_updates_recheck (plugin_loader);
+}
+
+static gint
+get_max_parallel_ops (void)
+{
+ guint mem_total = gs_utils_get_memory_total ();
+ if (mem_total == 0)
+ return 8;
+ /* allow 1 op per GB of memory */
+ return (gint) MAX (round((gdouble) mem_total / 1024), 1.0);
+}
+
+static void
+gs_plugin_loader_init (GsPluginLoader *plugin_loader)
+{
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+ const gchar *tmp;
+ gchar *match;
+ gchar **projects;
+ guint i;
+
+#ifdef HAVE_SYSPROF
+ priv->sysprof_writer = sysprof_capture_writer_new_from_env (0);
+#endif /* HAVE_SYSPROF */
+
+ priv->scale = 1;
+ priv->plugins = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref);
+ priv->pending_apps = g_ptr_array_new_with_free_func ((GFreeFunc) g_object_unref);
+ priv->queued_ops_pool = g_thread_pool_new (gs_plugin_loader_process_in_thread_pool_cb,
+ NULL,
+ get_max_parallel_ops (),
+ FALSE,
+ NULL);
+ priv->file_monitors = g_ptr_array_new_with_free_func ((GFreeFunc) g_object_unref);
+ priv->locations = g_ptr_array_new_with_free_func (g_free);
+ priv->settings = g_settings_new ("org.gnome.software");
+ g_signal_connect (priv->settings, "changed",
+ G_CALLBACK (gs_plugin_loader_settings_changed_cb), plugin_loader);
+ priv->events_by_id = g_hash_table_new_full ((GHashFunc) as_utils_unique_id_hash,
+ (GEqualFunc) as_utils_unique_id_equal,
+ g_free,
+ (GDestroyNotify) g_object_unref);
+
+ /* share a soup session (also disable the double-compression) */
+ priv->soup_session = soup_session_new_with_options (SOUP_SESSION_USER_AGENT, gs_user_agent (),
+ SOUP_SESSION_TIMEOUT, 10,
+ NULL);
+
+ /* get the locale */
+ tmp = g_getenv ("GS_SELF_TEST_LOCALE");
+ if (tmp != NULL) {
+ g_debug ("using self test locale of %s", tmp);
+ priv->locale = g_strdup (tmp);
+ } else {
+ priv->locale = g_strdup (setlocale (LC_MESSAGES, NULL));
+ }
+
+ /* the settings key sets the initial override */
+ priv->disallow_updates = g_hash_table_new (g_direct_hash, g_direct_equal);
+ gs_plugin_loader_allow_updates_recheck (plugin_loader);
+
+ /* get the language from the locale (i.e. strip the territory, codeset
+ * and modifier) */
+ priv->language = g_strdup (priv->locale);
+ match = strpbrk (priv->language, "._@");
+ if (match != NULL)
+ *match = '\0';
+
+ g_debug ("Using locale = %s, language = %s", priv->locale, priv->language);
+
+ g_mutex_init (&priv->pending_apps_mutex);
+ g_mutex_init (&priv->events_by_id_mutex);
+
+ /* monitor the network as the many UI operations need the network */
+ gs_plugin_loader_monitor_network (plugin_loader);
+
+ /* by default we only show project-less apps or compatible projects */
+ tmp = g_getenv ("GNOME_SOFTWARE_COMPATIBLE_PROJECTS");
+ if (tmp == NULL) {
+ projects = g_settings_get_strv (priv->settings,
+ "compatible-projects");
+ } else {
+ projects = g_strsplit (tmp, ",", -1);
+ }
+ for (i = 0; projects[i] != NULL; i++)
+ g_debug ("compatible-project: %s", projects[i]);
+ priv->compatible_projects = projects;
+}
+
+/**
+ * gs_plugin_loader_new:
+ *
+ * Return value: a new GsPluginLoader object.
+ **/
+GsPluginLoader *
+gs_plugin_loader_new (void)
+{
+ GsPluginLoader *plugin_loader;
+ plugin_loader = g_object_new (GS_TYPE_PLUGIN_LOADER, NULL);
+ return GS_PLUGIN_LOADER (plugin_loader);
+}
+
+static void
+gs_plugin_loader_app_installed_cb (GObject *source,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (source);
+ gboolean ret;
+ g_autoptr(GError) error = NULL;
+ g_autoptr(GsApp) app = GS_APP (user_data);
+
+ ret = gs_plugin_loader_job_action_finish (plugin_loader,
+ res,
+ &error);
+ if (!ret) {
+ remove_app_from_install_queue (plugin_loader, app);
+ g_warning ("failed to install %s: %s",
+ gs_app_get_unique_id (app), error->message);
+ }
+}
+
+gboolean
+gs_plugin_loader_get_network_available (GsPluginLoader *plugin_loader)
+{
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+ if (priv->network_monitor == NULL) {
+ g_debug ("no network monitor, so returning network-available=TRUE");
+ return TRUE;
+ }
+ return g_network_monitor_get_network_available (priv->network_monitor);
+}
+
+gboolean
+gs_plugin_loader_get_network_metered (GsPluginLoader *plugin_loader)
+{
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+ if (priv->network_monitor == NULL) {
+ g_debug ("no network monitor, so returning network-metered=FALSE");
+ return FALSE;
+ }
+ return g_network_monitor_get_network_metered (priv->network_monitor);
+}
+
+static void
+gs_plugin_loader_network_changed_cb (GNetworkMonitor *monitor,
+ gboolean available,
+ GsPluginLoader *plugin_loader)
+{
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+ gboolean metered = g_network_monitor_get_network_metered (priv->network_monitor);
+
+ g_debug ("network status change: %s [%s]",
+ available ? "online" : "offline",
+ metered ? "metered" : "unmetered");
+
+ g_object_notify (G_OBJECT (plugin_loader), "network-available");
+ g_object_notify (G_OBJECT (plugin_loader), "network-metered");
+
+ if (available && !metered) {
+ g_autoptr(GsAppList) queue = NULL;
+ g_mutex_lock (&priv->pending_apps_mutex);
+ queue = gs_app_list_new ();
+ for (guint i = 0; i < priv->pending_apps->len; i++) {
+ GsApp *app = g_ptr_array_index (priv->pending_apps, i);
+ if (gs_app_get_state (app) == AS_APP_STATE_QUEUED_FOR_INSTALL)
+ gs_app_list_add (queue, app);
+ }
+ g_mutex_unlock (&priv->pending_apps_mutex);
+ for (guint i = 0; i < gs_app_list_length (queue); i++) {
+ GsApp *app = gs_app_list_index (queue, i);
+ g_autoptr(GsPluginJob) plugin_job = NULL;
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_INSTALL,
+ "app", app,
+ NULL);
+ gs_plugin_loader_job_process_async (plugin_loader, plugin_job,
+ NULL,
+ gs_plugin_loader_app_installed_cb,
+ g_object_ref (app));
+ }
+ }
+}
+
+static void
+gs_plugin_loader_network_available_notify_cb (GObject *obj,
+ GParamSpec *pspec,
+ gpointer user_data)
+{
+ GNetworkMonitor *monitor = G_NETWORK_MONITOR (obj);
+ GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (user_data);
+
+ gs_plugin_loader_network_changed_cb (monitor, g_network_monitor_get_network_available (monitor), plugin_loader);
+}
+
+static void
+gs_plugin_loader_network_metered_notify_cb (GObject *obj,
+ GParamSpec *pspec,
+ gpointer user_data)
+{
+ GNetworkMonitor *monitor = G_NETWORK_MONITOR (obj);
+ GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (user_data);
+
+ gs_plugin_loader_network_changed_cb (monitor, g_network_monitor_get_network_available (monitor), plugin_loader);
+}
+
+static void
+gs_plugin_loader_monitor_network (GsPluginLoader *plugin_loader)
+{
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+ GNetworkMonitor *network_monitor;
+
+ network_monitor = g_network_monitor_get_default ();
+ if (network_monitor == NULL || priv->network_changed_handler != 0)
+ return;
+ priv->network_monitor = g_object_ref (network_monitor);
+
+ priv->network_changed_handler =
+ g_signal_connect (priv->network_monitor, "network-changed",
+ G_CALLBACK (gs_plugin_loader_network_changed_cb), plugin_loader);
+ priv->network_available_notify_handler =
+ g_signal_connect (priv->network_monitor, "notify::network-available",
+ G_CALLBACK (gs_plugin_loader_network_available_notify_cb), plugin_loader);
+ priv->network_metered_notify_handler =
+ g_signal_connect (priv->network_monitor, "notify::network-metered",
+ G_CALLBACK (gs_plugin_loader_network_metered_notify_cb), plugin_loader);
+
+ gs_plugin_loader_network_changed_cb (priv->network_monitor,
+ g_network_monitor_get_network_available (priv->network_monitor),
+ plugin_loader);
+}
+
+/******************************************************************************/
+
+static AsIcon *
+_gs_app_get_icon_by_kind (GsApp *app, AsIconKind kind)
+{
+ GPtrArray *icons = gs_app_get_icons (app);
+ guint i;
+ for (i = 0; i < icons->len; i++) {
+ AsIcon *ic = g_ptr_array_index (icons, i);
+ if (as_icon_get_kind (ic) == kind)
+ return ic;
+ }
+ return NULL;
+}
+
+static void
+generic_update_cancelled_cb (GCancellable *cancellable, gpointer data)
+{
+ GCancellable *app_cancellable = G_CANCELLABLE (data);
+ g_cancellable_cancel (app_cancellable);
+}
+
+static gboolean
+gs_plugin_loader_generic_update (GsPluginLoader *plugin_loader,
+ GsPluginLoaderHelper *helper,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+ guint cancel_handler_id = 0;
+ GsAppList *list;
+
+ /* run each plugin, per-app version */
+ list = gs_plugin_job_get_list (helper->plugin_job);
+ for (guint i = 0; i < priv->plugins->len; i++) {
+ GsPluginActionFunc plugin_app_func = NULL;
+ GsPlugin *plugin = g_ptr_array_index (priv->plugins, i);
+ if (g_cancellable_set_error_if_cancelled (cancellable, error)) {
+ gs_utils_error_convert_gio (error);
+ return FALSE;
+ }
+ plugin_app_func = gs_plugin_get_symbol (plugin, helper->function_name);
+ if (plugin_app_func == NULL)
+ continue;
+
+ /* for each app */
+ for (guint j = 0; j < gs_app_list_length (list); j++) {
+ GCancellable *app_cancellable;
+ GsApp *app = gs_app_list_index (list, j);
+ gboolean ret;
+ g_autoptr(GError) error_local = NULL;
+
+ /* if the whole operation should be cancelled */
+ if (g_cancellable_set_error_if_cancelled (cancellable, error))
+ return FALSE;
+
+ /* already installed? */
+ if (gs_app_get_state (app) == AS_APP_STATE_INSTALLED)
+ continue;
+
+ /* make sure that the app update is cancelled when the whole op is cancelled */
+ app_cancellable = gs_app_get_cancellable (app);
+ cancel_handler_id = g_cancellable_connect (cancellable,
+ G_CALLBACK (generic_update_cancelled_cb),
+ g_object_ref (app_cancellable),
+ g_object_unref);
+
+ gs_plugin_job_set_app (helper->plugin_job, app);
+ ret = plugin_app_func (plugin, app, app_cancellable, &error_local);
+ g_cancellable_disconnect (cancellable, cancel_handler_id);
+
+ if (!ret) {
+ if (!gs_plugin_error_handle_failure (helper,
+ plugin,
+ error_local,
+ error)) {
+ return FALSE;
+ }
+ }
+ }
+ helper->anything_ran = TRUE;
+ gs_plugin_status_update (plugin, NULL, GS_PLUGIN_STATUS_FINISHED);
+ }
+
+ gs_utils_set_online_updates_timestamp (priv->settings);
+ return TRUE;
+}
+
+static void
+gs_plugin_loader_process_thread_cb (GTask *task,
+ gpointer object,
+ gpointer task_data,
+ GCancellable *cancellable)
+{
+ GError *error = NULL;
+ GsPluginLoaderHelper *helper = (GsPluginLoaderHelper *) task_data;
+ GsAppListFilterFlags dedupe_flags;
+ GsAppList *list = gs_plugin_job_get_list (helper->plugin_job);
+ GsPluginAction action = gs_plugin_job_get_action (helper->plugin_job);
+ GsPluginLoader *plugin_loader = GS_PLUGIN_LOADER (object);
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+ GsPluginRefineFlags filter_flags;
+ GsPluginRefineFlags refine_flags;
+ gboolean add_to_pending_array = FALSE;
+ guint max_results;
+ GsAppListSortFunc sort_func;
+#ifdef HAVE_SYSPROF
+ gint64 begin_time_nsec G_GNUC_UNUSED = SYSPROF_CAPTURE_CURRENT_TIME;
+#endif
+
+ /* these change the pending count on the installed panel */
+ switch (action) {
+ case GS_PLUGIN_ACTION_INSTALL:
+ case GS_PLUGIN_ACTION_REMOVE:
+ add_to_pending_array = TRUE;
+ break;
+ default:
+ break;
+ }
+
+ /* add to pending list */
+ if (add_to_pending_array)
+ gs_plugin_loader_pending_apps_add (plugin_loader, helper);
+
+ /* run each plugin */
+ if (action != GS_PLUGIN_ACTION_REFINE) {
+ if (!gs_plugin_loader_run_results (helper, cancellable, &error)) {
+ if (add_to_pending_array) {
+ gs_app_set_state_recover (gs_plugin_job_get_app (helper->plugin_job));
+ gs_plugin_loader_pending_apps_remove (plugin_loader, helper);
+ }
+ gs_utils_error_convert_gio (&error);
+ g_task_return_error (task, error);
+ return;
+ }
+ }
+
+ /* run per-app version */
+ if (action == GS_PLUGIN_ACTION_UPDATE) {
+ helper->function_name = "gs_plugin_update_app";
+ if (!gs_plugin_loader_generic_update (plugin_loader, helper,
+ cancellable, &error)) {
+ gs_utils_error_convert_gio (&error);
+ g_task_return_error (task, error);
+ return;
+ }
+ } else if (action == GS_PLUGIN_ACTION_DOWNLOAD) {
+ helper->function_name = "gs_plugin_download_app";
+ if (!gs_plugin_loader_generic_update (plugin_loader, helper,
+ cancellable, &error)) {
+ gs_utils_error_convert_gio (&error);
+ g_task_return_error (task, error);
+ return;
+ }
+ }
+
+ if (action == GS_PLUGIN_ACTION_UPGRADE_TRIGGER)
+ gs_utils_set_online_updates_timestamp (priv->settings);
+
+ /* remove from pending list */
+ if (add_to_pending_array)
+ gs_plugin_loader_pending_apps_remove (plugin_loader, helper);
+
+ /* some functions are really required for proper operation */
+ switch (action) {
+ case GS_PLUGIN_ACTION_DESTROY:
+ case GS_PLUGIN_ACTION_GET_INSTALLED:
+ case GS_PLUGIN_ACTION_GET_UPDATES:
+ case GS_PLUGIN_ACTION_INITIALIZE:
+ case GS_PLUGIN_ACTION_INSTALL:
+ case GS_PLUGIN_ACTION_DOWNLOAD:
+ case GS_PLUGIN_ACTION_LAUNCH:
+ case GS_PLUGIN_ACTION_REFRESH:
+ case GS_PLUGIN_ACTION_REMOVE:
+ case GS_PLUGIN_ACTION_SEARCH:
+ case GS_PLUGIN_ACTION_SETUP:
+ case GS_PLUGIN_ACTION_UPDATE:
+ if (!helper->anything_ran) {
+ g_set_error (&error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_NOT_SUPPORTED,
+ "no plugin could handle %s",
+ gs_plugin_action_to_string (action));
+ g_task_return_error (task, error);
+ return;
+ }
+ break;
+ case GS_PLUGIN_ACTION_REFINE:
+ break;
+ default:
+ if (!helper->anything_ran) {
+ g_debug ("no plugin could handle %s",
+ gs_plugin_action_to_string (action));
+ }
+ break;
+ }
+
+ /* unstage addons */
+ if (add_to_pending_array) {
+ GsAppList *addons;
+ addons = gs_app_get_addons (gs_plugin_job_get_app (helper->plugin_job));
+ for (guint i = 0; i < gs_app_list_length (addons); i++) {
+ GsApp *addon = gs_app_list_index (addons, i);
+ if (gs_app_get_to_be_installed (addon))
+ gs_app_set_to_be_installed (addon, FALSE);
+ }
+ }
+
+ /* modify the local app */
+ switch (action) {
+ case GS_PLUGIN_ACTION_REVIEW_SUBMIT:
+ gs_app_add_review (gs_plugin_job_get_app (helper->plugin_job), gs_plugin_job_get_review (helper->plugin_job));
+ break;
+ case GS_PLUGIN_ACTION_REVIEW_REMOVE:
+ gs_app_remove_review (gs_plugin_job_get_app (helper->plugin_job), gs_plugin_job_get_review (helper->plugin_job));
+ break;
+ default:
+ break;
+ }
+
+ /* refine with enough data so that the sort_func in
+ * gs_plugin_loader_job_sorted_truncation() can do what it needs */
+ filter_flags = gs_plugin_job_get_filter_flags (helper->plugin_job);
+ max_results = gs_plugin_job_get_max_results (helper->plugin_job);
+ sort_func = gs_plugin_job_get_sort_func (helper->plugin_job);
+ if (filter_flags > 0 && max_results > 0 && sort_func != NULL) {
+ g_autoptr(GsPluginLoaderHelper) helper2 = NULL;
+ g_autoptr(GsPluginJob) plugin_job = NULL;
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REFINE,
+ "list", list,
+ "refine-flags", filter_flags,
+ NULL);
+ helper2 = gs_plugin_loader_helper_new (helper->plugin_loader, plugin_job);
+ helper2->function_name_parent = helper->function_name;
+ g_debug ("running filter flags with early refine");
+ if (!gs_plugin_loader_run_refine_filter (helper2, list,
+ filter_flags,
+ cancellable, &error)) {
+ gs_utils_error_convert_gio (&error);
+ g_task_return_error (task, error);
+ return;
+ }
+ }
+
+ /* filter to reduce to a sane set */
+ gs_plugin_loader_job_sorted_truncation (helper);
+
+ /* set the local file on any of the returned results */
+ switch (action) {
+ case GS_PLUGIN_ACTION_FILE_TO_APP:
+ for (guint j = 0; j < gs_app_list_length (list); j++) {
+ GsApp *app = gs_app_list_index (list, j);
+ if (gs_app_get_local_file (app) == NULL)
+ gs_app_set_local_file (app, gs_plugin_job_get_file (helper->plugin_job));
+ }
+ default:
+ break;
+ }
+
+ /* pick up new source id */
+ switch (action) {
+ case GS_PLUGIN_ACTION_INSTALL:
+ case GS_PLUGIN_ACTION_REMOVE:
+ gs_plugin_job_add_refine_flags (helper->plugin_job,
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_SETUP_ACTION);
+ break;
+ default:
+ break;
+ }
+
+ /* run refine() on each one if required */
+ if (gs_plugin_job_get_refine_flags (helper->plugin_job) != 0) {
+ if (!gs_plugin_loader_run_refine (helper, list, cancellable, &error)) {
+ gs_utils_error_convert_gio (&error);
+ g_task_return_error (task, error);
+ return;
+ }
+ } else {
+ g_debug ("no refine flags set for transaction");
+ }
+
+ /* check the local files have an icon set */
+ switch (action) {
+ case GS_PLUGIN_ACTION_URL_TO_APP:
+ case GS_PLUGIN_ACTION_FILE_TO_APP:
+ for (guint j = 0; j < gs_app_list_length (list); j++) {
+ GsApp *app = gs_app_list_index (list, j);
+ if (_gs_app_get_icon_by_kind (app, AS_ICON_KIND_STOCK) == NULL &&
+ _gs_app_get_icon_by_kind (app, AS_ICON_KIND_LOCAL) == NULL &&
+ _gs_app_get_icon_by_kind (app, AS_ICON_KIND_CACHED) == NULL) {
+ g_autoptr(AsIcon) ic = as_icon_new ();
+ as_icon_set_kind (ic, AS_ICON_KIND_STOCK);
+ if (gs_app_has_quirk (app, GS_APP_QUIRK_HAS_SOURCE))
+ as_icon_set_name (ic, "x-package-repository");
+ else
+ as_icon_set_name (ic, "application-x-executable");
+ gs_app_add_icon (app, ic);
+ }
+ }
+
+ /* run refine() on each one again to pick up any icons */
+ refine_flags = gs_plugin_job_get_refine_flags (helper->plugin_job);
+ gs_plugin_job_set_refine_flags (helper->plugin_job,
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON);
+ if (!gs_plugin_loader_run_refine (helper, list, cancellable, &error)) {
+ gs_utils_error_convert_gio (&error);
+ g_task_return_error (task, error);
+ return;
+ }
+ /* restore the refine flags so that gs_app_list_filter sees the right thing */
+ gs_plugin_job_set_refine_flags (helper->plugin_job, refine_flags);
+ break;
+ default:
+ break;
+ }
+
+ /* filter package list */
+ switch (action) {
+ case GS_PLUGIN_ACTION_URL_TO_APP:
+ gs_app_list_filter (list, gs_plugin_loader_app_is_valid, helper);
+ break;
+ case GS_PLUGIN_ACTION_SEARCH:
+ case GS_PLUGIN_ACTION_SEARCH_FILES:
+ case GS_PLUGIN_ACTION_SEARCH_PROVIDES:
+ case GS_PLUGIN_ACTION_GET_ALTERNATES:
+ gs_app_list_filter (list, gs_plugin_loader_app_is_valid, helper);
+ gs_app_list_filter (list, gs_plugin_loader_filter_qt_for_gtk, NULL);
+ gs_app_list_filter (list, gs_plugin_loader_get_app_is_compatible, plugin_loader);
+ break;
+ case GS_PLUGIN_ACTION_GET_CATEGORY_APPS:
+ gs_app_list_filter (list, gs_plugin_loader_app_is_valid, helper);
+ gs_app_list_filter (list, gs_plugin_loader_filter_qt_for_gtk, NULL);
+ gs_app_list_filter (list, gs_plugin_loader_get_app_is_compatible, plugin_loader);
+ break;
+ case GS_PLUGIN_ACTION_GET_INSTALLED:
+ gs_app_list_filter (list, gs_plugin_loader_app_is_valid, helper);
+ gs_app_list_filter (list, gs_plugin_loader_app_is_valid_installed, helper);
+ break;
+ case GS_PLUGIN_ACTION_GET_FEATURED:
+ if (g_getenv ("GNOME_SOFTWARE_FEATURED") != NULL) {
+ gs_app_list_filter (list, gs_plugin_loader_featured_debug, NULL);
+ } else {
+ gs_app_list_filter (list, gs_plugin_loader_app_is_valid, helper);
+ gs_app_list_filter (list, gs_plugin_loader_get_app_is_compatible, plugin_loader);
+ }
+ break;
+ case GS_PLUGIN_ACTION_GET_UPDATES:
+ gs_app_list_filter (list, gs_plugin_loader_app_is_valid_updatable, helper);
+ break;
+ case GS_PLUGIN_ACTION_GET_RECENT:
+ gs_app_list_filter (list, gs_plugin_loader_app_is_non_compulsory, NULL);
+ gs_app_list_filter (list, gs_plugin_loader_app_is_valid, helper);
+ gs_app_list_filter (list, gs_plugin_loader_filter_qt_for_gtk, NULL);
+ gs_app_list_filter (list, gs_plugin_loader_get_app_is_compatible, plugin_loader);
+ break;
+ case GS_PLUGIN_ACTION_REFINE:
+ gs_app_list_filter (list, gs_plugin_loader_app_is_valid, helper);
+ break;
+ case GS_PLUGIN_ACTION_GET_POPULAR:
+ gs_app_list_filter (list, gs_plugin_loader_app_is_valid, helper);
+ gs_app_list_filter (list, gs_plugin_loader_filter_qt_for_gtk, NULL);
+ gs_app_list_filter (list, gs_plugin_loader_get_app_is_compatible, plugin_loader);
+ break;
+ default:
+ break;
+ }
+
+ /* only allow one result */
+ if (action == GS_PLUGIN_ACTION_URL_TO_APP ||
+ action == GS_PLUGIN_ACTION_FILE_TO_APP) {
+ if (gs_app_list_length (list) == 0) {
+ g_autofree gchar *str = gs_plugin_job_to_string (helper->plugin_job);
+ g_autoptr(GError) error_local = NULL;
+ g_autoptr(GsPluginEvent) event = NULL;
+ g_set_error (&error_local,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_NOT_SUPPORTED,
+ "no application was created for %s", str);
+ event = gs_plugin_job_to_failed_event (helper->plugin_job, error_local);
+ gs_plugin_loader_add_event (plugin_loader, event);
+ g_task_return_error (task, g_steal_pointer (&error_local));
+ return;
+ }
+ if (gs_app_list_length (list) > 1) {
+ g_autofree gchar *str = gs_plugin_job_to_string (helper->plugin_job);
+ g_debug ("more than one application was created for %s", str);
+ }
+ }
+
+ /* filter duplicates with priority, taking into account the source name
+ * & version, so we combine available updates with the installed app */
+ gs_app_list_filter (list, gs_plugin_loader_app_set_prio, plugin_loader);
+ dedupe_flags = gs_plugin_job_get_dedupe_flags (helper->plugin_job);
+ if (dedupe_flags != GS_APP_LIST_FILTER_FLAG_NONE)
+ gs_app_list_filter_duplicates (list, dedupe_flags);
+
+ /* sort these again as the refine may have added useful metadata */
+ gs_plugin_loader_job_sorted_truncation_again (helper);
+
+ /* if the plugin used updates-changed actually schedule it now */
+ if (priv->updates_changed_cnt > 0)
+ gs_plugin_loader_updates_changed (plugin_loader);
+
+#ifdef HAVE_SYSPROF
+ if (priv->sysprof_writer != NULL) {
+ g_autofree gchar *sysprof_name = g_strconcat ("process-thread:", gs_plugin_action_to_string (action), NULL);
+ g_autofree gchar *sysprof_message = gs_plugin_job_to_string (helper->plugin_job);
+ sysprof_capture_writer_add_mark (priv->sysprof_writer,
+ begin_time_nsec,
+ sched_getcpu (),
+ getpid (),
+ SYSPROF_CAPTURE_CURRENT_TIME - begin_time_nsec,
+ "gnome-software",
+ sysprof_name,
+ sysprof_message);
+ }
+#endif /* HAVE_SYSPROF */
+
+ /* show elapsed time */
+ gs_plugin_loader_job_debug (helper);
+
+ /* success */
+ g_task_return_pointer (task, g_object_ref (list), (GDestroyNotify) g_object_unref);
+}
+
+static void
+gs_plugin_loader_process_in_thread_pool_cb (gpointer data,
+ gpointer user_data)
+{
+ GTask *task = data;
+ gpointer source_object = g_task_get_source_object (task);
+ gpointer task_data = g_task_get_task_data (task);
+ GCancellable *cancellable = g_task_get_cancellable (task);
+
+ gs_ioprio_init ();
+
+ gs_plugin_loader_process_thread_cb (task, source_object, task_data, cancellable);
+ g_object_unref (task);
+}
+
+static gboolean
+gs_plugin_loader_job_timeout_cb (gpointer user_data)
+{
+ GsPluginLoaderHelper *helper = (GsPluginLoaderHelper *) user_data;
+
+ /* call the cancellable */
+ g_debug ("cancelling job %s as it took longer than %u seconds",
+ helper->function_name,
+ gs_plugin_job_get_timeout (helper->plugin_job));
+ g_cancellable_cancel (helper->cancellable);
+
+ /* failed */
+ helper->timeout_triggered = TRUE;
+ helper->timeout_id = 0;
+ return G_SOURCE_REMOVE;
+}
+
+static void
+gs_plugin_loader_cancelled_cb (GCancellable *cancellable, GsPluginLoaderHelper *helper)
+{
+ /* just proxy this forward */
+ g_debug ("Cancelling job with cancellable %p", helper->cancellable);
+ g_cancellable_cancel (helper->cancellable);
+}
+
+static void
+gs_plugin_loader_schedule_task (GsPluginLoader *plugin_loader,
+ GTask *task)
+{
+ GsPluginLoaderHelper *helper = g_task_get_task_data (task);
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+ GsApp *app = gs_plugin_job_get_app (helper->plugin_job);
+
+ if (app != NULL) {
+ /* set the pending-action to the app */
+ GsPluginAction action = gs_plugin_job_get_action (helper->plugin_job);
+ gs_app_set_pending_action (app, action);
+ }
+ g_thread_pool_push (priv->queued_ops_pool, g_object_ref (task), NULL);
+}
+
+/**
+ * gs_plugin_loader_job_process_async:
+ * @plugin_loader: A #GsPluginLoader
+ * @plugin_job: job to process
+ * @cancellable: a #GCancellable, or %NULL
+ * @callback: function to call when complete
+ * @user_data: user data to pass to @callback
+ *
+ * This method calls all plugins.
+ **/
+void
+gs_plugin_loader_job_process_async (GsPluginLoader *plugin_loader,
+ GsPluginJob *plugin_job,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ GsPluginAction action;
+ GsPluginLoaderHelper *helper;
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+ g_autoptr(GTask) task = NULL;
+ g_autoptr(GCancellable) cancellable_job = g_cancellable_new ();
+#if GLIB_CHECK_VERSION(2, 60, 0)
+ g_autofree gchar *task_name = NULL;
+#endif
+
+ g_return_if_fail (GS_IS_PLUGIN_LOADER (plugin_loader));
+ g_return_if_fail (GS_IS_PLUGIN_JOB (plugin_job));
+ g_return_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable));
+
+ action = gs_plugin_job_get_action (plugin_job);
+#if GLIB_CHECK_VERSION(2, 60, 0)
+ task_name = g_strdup_printf ("%s %s", G_STRFUNC, gs_plugin_action_to_string (action));
+#endif
+
+ /* check job has valid action */
+ if (action == GS_PLUGIN_ACTION_UNKNOWN) {
+ g_autofree gchar *job_str = gs_plugin_job_to_string (plugin_job);
+ task = g_task_new (plugin_loader, cancellable_job, callback, user_data);
+#if GLIB_CHECK_VERSION(2, 60, 0)
+ g_task_set_name (task, task_name);
+#endif
+ g_task_return_new_error (task,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_NOT_SUPPORTED,
+ "job has no valid action: %s", job_str);
+ return;
+ }
+
+ /* deal with the install queue */
+ if (action == GS_PLUGIN_ACTION_REMOVE) {
+ if (remove_app_from_install_queue (plugin_loader, gs_plugin_job_get_app (plugin_job))) {
+ GsAppList *list = gs_plugin_job_get_list (plugin_job);
+ task = g_task_new (plugin_loader, cancellable, callback, user_data);
+#if GLIB_CHECK_VERSION(2, 60, 0)
+ g_task_set_name (task, task_name);
+#endif
+ g_task_return_pointer (task, g_object_ref (list), (GDestroyNotify) g_object_unref);
+ return;
+ }
+ }
+
+ /* hardcoded, so resolve a set list */
+ if (action == GS_PLUGIN_ACTION_GET_POPULAR) {
+ g_auto(GStrv) apps = NULL;
+ if (g_getenv ("GNOME_SOFTWARE_POPULAR") != NULL) {
+ apps = g_strsplit (g_getenv ("GNOME_SOFTWARE_POPULAR"), ",", 0);
+ } else {
+ apps = g_settings_get_strv (priv->settings, "popular-overrides");
+ }
+ if (apps != NULL && g_strv_length (apps) > 0) {
+ GsAppList *list = gs_plugin_job_get_list (plugin_job);
+ for (guint i = 0; apps[i] != NULL; i++) {
+ g_autoptr(GsApp) app = gs_app_new (apps[i]);
+ gs_app_add_quirk (app, GS_APP_QUIRK_IS_WILDCARD);
+ gs_app_list_add (list, app);
+ }
+ gs_plugin_job_set_action (plugin_job, GS_PLUGIN_ACTION_REFINE);
+ }
+ }
+
+ /* FIXME: the plugins should specify this, rather than hardcoding */
+ if (gs_plugin_job_has_refine_flags (plugin_job,
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_KEY_COLORS)) {
+ gs_plugin_job_add_refine_flags (plugin_job,
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON);
+ }
+ if (gs_plugin_job_has_refine_flags (plugin_job,
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN_UI)) {
+ gs_plugin_job_add_refine_flags (plugin_job,
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN);
+ }
+ if (gs_plugin_job_has_refine_flags (plugin_job,
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_MENU_PATH)) {
+ gs_plugin_job_add_refine_flags (plugin_job,
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_CATEGORIES);
+ }
+ if (gs_plugin_job_has_refine_flags (plugin_job,
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN_HOSTNAME)) {
+ gs_plugin_job_add_refine_flags (plugin_job,
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN);
+ }
+ if (gs_plugin_job_has_refine_flags (plugin_job,
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_SIZE)) {
+ gs_plugin_job_add_refine_flags (plugin_job,
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_RUNTIME);
+ }
+
+ /* FIXME: this is probably a bug */
+ if (action == GS_PLUGIN_ACTION_GET_DISTRO_UPDATES ||
+ action == GS_PLUGIN_ACTION_GET_SOURCES) {
+ gs_plugin_job_add_refine_flags (plugin_job,
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_SETUP_ACTION);
+ }
+
+ /* get alternates is unusual in that it needs an app input and a list
+ * output -- so undo the helpful app add in gs_plugin_job_set_app() */
+ if (action == GS_PLUGIN_ACTION_GET_ALTERNATES) {
+ GsAppList *list = gs_plugin_job_get_list (plugin_job);
+ gs_app_list_remove_all (list);
+ }
+
+ /* check required args */
+ task = g_task_new (plugin_loader, cancellable_job, callback, user_data);
+#if GLIB_CHECK_VERSION(2, 60, 0)
+ g_task_set_name (task, task_name);
+#endif
+
+ switch (action) {
+ case GS_PLUGIN_ACTION_SEARCH:
+ case GS_PLUGIN_ACTION_SEARCH_FILES:
+ case GS_PLUGIN_ACTION_SEARCH_PROVIDES:
+ case GS_PLUGIN_ACTION_URL_TO_APP:
+ if (gs_plugin_job_get_search (plugin_job) == NULL) {
+ g_task_return_new_error (task,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_NOT_SUPPORTED,
+ "no valid search terms");
+ return;
+ }
+ break;
+ case GS_PLUGIN_ACTION_REVIEW_SUBMIT:
+ case GS_PLUGIN_ACTION_REVIEW_UPVOTE:
+ case GS_PLUGIN_ACTION_REVIEW_DOWNVOTE:
+ case GS_PLUGIN_ACTION_REVIEW_REPORT:
+ case GS_PLUGIN_ACTION_REVIEW_REMOVE:
+ case GS_PLUGIN_ACTION_REVIEW_DISMISS:
+ if (gs_plugin_job_get_review (plugin_job) == NULL) {
+ g_task_return_new_error (task,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_NOT_SUPPORTED,
+ "no valid review object");
+ return;
+ }
+ break;
+ default:
+ break;
+ }
+
+ /* sorting fallbacks */
+ switch (action) {
+ case GS_PLUGIN_ACTION_SEARCH:
+ if (gs_plugin_job_get_sort_func (plugin_job) == NULL) {
+ gs_plugin_job_set_sort_func (plugin_job,
+ gs_plugin_loader_app_sort_match_value_cb);
+ }
+ break;
+ case GS_PLUGIN_ACTION_GET_RECENT:
+ if (gs_plugin_job_get_sort_func (plugin_job) == NULL) {
+ gs_plugin_job_set_sort_func (plugin_job,
+ gs_plugin_loader_app_sort_kind_cb);
+ }
+ break;
+ case GS_PLUGIN_ACTION_GET_CATEGORY_APPS:
+ if (gs_plugin_job_get_sort_func (plugin_job) == NULL) {
+ gs_plugin_job_set_sort_func (plugin_job,
+ gs_plugin_loader_app_sort_name_cb);
+ }
+ break;
+ case GS_PLUGIN_ACTION_GET_ALTERNATES:
+ if (gs_plugin_job_get_sort_func (plugin_job) == NULL) {
+ gs_plugin_job_set_sort_func (plugin_job,
+ gs_plugin_loader_app_sort_prio_cb);
+ }
+ break;
+ case GS_PLUGIN_ACTION_GET_DISTRO_UPDATES:
+ if (gs_plugin_job_get_sort_func (plugin_job) == NULL) {
+ gs_plugin_job_set_sort_func (plugin_job,
+ gs_plugin_loader_app_sort_version_cb);
+ }
+ break;
+ default:
+ break;
+ }
+
+ /* save helper */
+ helper = gs_plugin_loader_helper_new (plugin_loader, plugin_job);
+ g_task_set_task_data (task, helper, (GDestroyNotify) gs_plugin_loader_helper_free);
+
+ /* let the task cancel itself */
+ g_task_set_check_cancellable (task, FALSE);
+ g_task_set_return_on_cancel (task, FALSE);
+
+ /* pre-tokenize search */
+ if (action == GS_PLUGIN_ACTION_SEARCH) {
+ const gchar *search = gs_plugin_job_get_search (plugin_job);
+ helper->tokens = as_utils_search_tokenize (search);
+ if (helper->tokens == NULL) {
+ g_task_return_new_error (task,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_NOT_SUPPORTED,
+ "failed to tokenize %s", search);
+ return;
+ }
+ }
+
+ /* jobs always have a valid cancellable, so proxy the caller */
+ helper->cancellable = g_object_ref (cancellable_job);
+ g_debug ("Chaining cancellation from %p to %p", cancellable, cancellable_job);
+ if (cancellable != NULL) {
+ helper->cancellable_caller = g_object_ref (cancellable);
+ helper->cancellable_id =
+ g_cancellable_connect (helper->cancellable_caller,
+ G_CALLBACK (gs_plugin_loader_cancelled_cb),
+ helper, NULL);
+ }
+
+ /* set up a hang handler */
+ switch (action) {
+ case GS_PLUGIN_ACTION_GET_ALTERNATES:
+ case GS_PLUGIN_ACTION_GET_CATEGORY_APPS:
+ case GS_PLUGIN_ACTION_GET_FEATURED:
+ case GS_PLUGIN_ACTION_GET_INSTALLED:
+ case GS_PLUGIN_ACTION_GET_POPULAR:
+ case GS_PLUGIN_ACTION_GET_RECENT:
+ case GS_PLUGIN_ACTION_SEARCH:
+ case GS_PLUGIN_ACTION_SEARCH_FILES:
+ case GS_PLUGIN_ACTION_SEARCH_PROVIDES:
+ helper->timeout_id =
+ g_timeout_add_seconds (gs_plugin_job_get_timeout (plugin_job),
+ gs_plugin_loader_job_timeout_cb,
+ helper);
+ break;
+ default:
+ break;
+ }
+
+ switch (action) {
+ case GS_PLUGIN_ACTION_INSTALL:
+ case GS_PLUGIN_ACTION_UPDATE:
+ case GS_PLUGIN_ACTION_UPGRADE_DOWNLOAD:
+ /* these actions must be performed by the thread pool because we
+ * want to limit the number of them running in parallel */
+ gs_plugin_loader_schedule_task (plugin_loader, task);
+ return;
+ default:
+ break;
+ }
+
+ /* run in a thread */
+ g_task_run_in_thread (task, gs_plugin_loader_process_thread_cb);
+}
+
+/******************************************************************************/
+
+/**
+ * gs_plugin_loader_get_plugin_supported:
+ * @plugin_loader: A #GsPluginLoader
+ * @function_name: a function name
+ *
+ * This function returns TRUE if the symbol is found in any enabled plugin.
+ */
+gboolean
+gs_plugin_loader_get_plugin_supported (GsPluginLoader *plugin_loader,
+ const gchar *function_name)
+{
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+ for (guint i = 0; i < priv->plugins->len; i++) {
+ GsPlugin *plugin = g_ptr_array_index (priv->plugins, i);
+ if (gs_plugin_get_symbol (plugin, function_name) != NULL)
+ return TRUE;
+ }
+ return FALSE;
+}
+
+/**
+ * gs_plugin_loader_app_create:
+ * @plugin_loader: a #GsPluginLoader
+ * @unique_id: a unique_id
+ *
+ * Returns an application from the global cache, creating if required.
+ *
+ * Returns: (transfer full): a #GsApp
+ **/
+GsApp *
+gs_plugin_loader_app_create (GsPluginLoader *plugin_loader, const gchar *unique_id)
+{
+ g_autoptr(GError) error = NULL;
+ g_autoptr(GsApp) app = NULL;
+ g_autoptr(GsAppList) list = gs_app_list_new ();
+ g_autoptr(GsPluginJob) plugin_job = NULL;
+ g_autoptr(GsPluginLoaderHelper) helper = NULL;
+
+ /* use the plugin loader to convert a wildcard app*/
+ app = gs_app_new (NULL);
+ gs_app_add_quirk (app, GS_APP_QUIRK_IS_WILDCARD);
+ gs_app_set_from_unique_id (app, unique_id);
+ gs_app_list_add (list, app);
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_REFINE, NULL);
+ helper = gs_plugin_loader_helper_new (plugin_loader, plugin_job);
+ if (!gs_plugin_loader_run_refine (helper, list, NULL, &error)) {
+ g_warning ("%s", error->message);
+ return NULL;
+ }
+
+ /* return the matching GsApp */
+ for (guint i = 0; i < gs_app_list_length (list); i++) {
+ GsApp *app_tmp = gs_app_list_index (list, i);
+ if (g_strcmp0 (unique_id, gs_app_get_unique_id (app_tmp)) == 0)
+ return g_object_ref (app_tmp);
+ }
+
+ /* return the first returned app that's not a wildcard */
+ for (guint i = 0; i < gs_app_list_length (list); i++) {
+ GsApp *app_tmp = gs_app_list_index (list, i);
+ if (!gs_app_has_quirk (app_tmp, GS_APP_QUIRK_IS_WILDCARD)) {
+ g_debug ("returning imperfect match: %s != %s",
+ unique_id, gs_app_get_unique_id (app_tmp));
+ return g_object_ref (app_tmp);
+ }
+ }
+
+ /* does not exist */
+ g_warning ("failed to create an app for %s", unique_id);
+ return NULL;
+}
+
+/**
+ * gs_plugin_loader_get_system_app:
+ * @plugin_loader: a #GsPluginLoader
+ *
+ * Returns the application that represents the currently installed OS.
+ *
+ * Returns: (transfer full): a #GsApp
+ **/
+GsApp *
+gs_plugin_loader_get_system_app (GsPluginLoader *plugin_loader)
+{
+ return gs_plugin_loader_app_create (plugin_loader, "*/*/*/*/system/*");
+}
+
+/**
+ * gs_plugin_loader_set_max_parallel_ops:
+ * @plugin_loader: a #GsPluginLoader
+ * @max_ops: the maximum number of parallel operations
+ *
+ * Sets the number of maximum number of queued operations (install/update/upgrade-download)
+ * to be processed at a time. If @max_ops is 0, then it will set the default maximum number.
+ */
+void
+gs_plugin_loader_set_max_parallel_ops (GsPluginLoader *plugin_loader,
+ guint max_ops)
+{
+ g_autoptr(GError) error = NULL;
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+ if (max_ops == 0)
+ max_ops = get_max_parallel_ops ();
+ if (!g_thread_pool_set_max_threads (priv->queued_ops_pool, max_ops, &error))
+ g_warning ("Failed to set the maximum number of ops in parallel: %s",
+ error->message);
+}
+
+const gchar *
+gs_plugin_loader_get_locale (GsPluginLoader *plugin_loader)
+{
+ GsPluginLoaderPrivate *priv = gs_plugin_loader_get_instance_private (plugin_loader);
+ return priv->locale;
+}
diff --git a/lib/gs-plugin-loader.h b/lib/gs-plugin-loader.h
new file mode 100644
index 0000000..f729572
--- /dev/null
+++ b/lib/gs-plugin-loader.h
@@ -0,0 +1,100 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2007-2017 Richard Hughes <richard@hughsie.com>
+ * Copyright (C) 2015-2020 Kalev Lember <klember@redhat.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+#pragma once
+
+#include <glib-object.h>
+
+#include "gs-app.h"
+#include "gs-category.h"
+#include "gs-plugin-event.h"
+#include "gs-plugin-private.h"
+#include "gs-plugin-job.h"
+
+G_BEGIN_DECLS
+
+#define GS_TYPE_PLUGIN_LOADER (gs_plugin_loader_get_type ())
+
+G_DECLARE_DERIVABLE_TYPE (GsPluginLoader, gs_plugin_loader, GS, PLUGIN_LOADER, GObject)
+
+struct _GsPluginLoaderClass
+{
+ GObjectClass parent_class;
+ void (*status_changed) (GsPluginLoader *plugin_loader,
+ GsApp *app,
+ GsPluginStatus status);
+ void (*pending_apps_changed) (GsPluginLoader *plugin_loader);
+ void (*updates_changed) (GsPluginLoader *plugin_loader);
+ void (*reload) (GsPluginLoader *plugin_loader);
+ void (*basic_auth_start) (GsPluginLoader *plugin_loader,
+ const gchar *remote,
+ const gchar *realm,
+ GCallback callback,
+ gpointer user_data);
+};
+
+GsPluginLoader *gs_plugin_loader_new (void);
+void gs_plugin_loader_job_process_async (GsPluginLoader *plugin_loader,
+ GsPluginJob *plugin_job,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+GsAppList *gs_plugin_loader_job_process_finish (GsPluginLoader *plugin_loader,
+ GAsyncResult *res,
+ GError **error);
+gboolean gs_plugin_loader_job_action_finish (GsPluginLoader *plugin_loader,
+ GAsyncResult *res,
+ GError **error);
+void gs_plugin_loader_job_get_categories_async (GsPluginLoader *plugin_loader,
+ GsPluginJob *plugin_job,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data);
+GPtrArray *gs_plugin_loader_job_get_categories_finish (GsPluginLoader *plugin_loader,
+ GAsyncResult *res,
+ GError **error);
+gboolean gs_plugin_loader_setup (GsPluginLoader *plugin_loader,
+ gchar **allowlist,
+ gchar **blocklist,
+ GCancellable *cancellable,
+ GError **error);
+void gs_plugin_loader_dump_state (GsPluginLoader *plugin_loader);
+gboolean gs_plugin_loader_get_enabled (GsPluginLoader *plugin_loader,
+ const gchar *plugin_name);
+void gs_plugin_loader_add_location (GsPluginLoader *plugin_loader,
+ const gchar *location);
+guint gs_plugin_loader_get_scale (GsPluginLoader *plugin_loader);
+void gs_plugin_loader_set_scale (GsPluginLoader *plugin_loader,
+ guint scale);
+GsAppList *gs_plugin_loader_get_pending (GsPluginLoader *plugin_loader);
+gboolean gs_plugin_loader_get_allow_updates (GsPluginLoader *plugin_loader);
+gboolean gs_plugin_loader_get_network_available (GsPluginLoader *plugin_loader);
+gboolean gs_plugin_loader_get_network_metered (GsPluginLoader *plugin_loader);
+gboolean gs_plugin_loader_get_plugin_supported (GsPluginLoader *plugin_loader,
+ const gchar *function_name);
+
+GPtrArray *gs_plugin_loader_get_events (GsPluginLoader *plugin_loader);
+GsPluginEvent *gs_plugin_loader_get_event_default (GsPluginLoader *plugin_loader);
+void gs_plugin_loader_remove_events (GsPluginLoader *plugin_loader);
+
+GsApp *gs_plugin_loader_app_create (GsPluginLoader *plugin_loader,
+ const gchar *unique_id);
+GsApp *gs_plugin_loader_get_system_app (GsPluginLoader *plugin_loader);
+
+/* only useful from the self tests */
+void gs_plugin_loader_setup_again (GsPluginLoader *plugin_loader);
+void gs_plugin_loader_clear_caches (GsPluginLoader *plugin_loader);
+GsPlugin *gs_plugin_loader_find_plugin (GsPluginLoader *plugin_loader,
+ const gchar *plugin_name);
+void gs_plugin_loader_set_max_parallel_ops (GsPluginLoader *plugin_loader,
+ guint max_ops);
+
+const gchar *gs_plugin_loader_get_locale (GsPluginLoader *plugin_loader);
+
+G_END_DECLS
diff --git a/lib/gs-plugin-private.h b/lib/gs-plugin-private.h
new file mode 100644
index 0000000..1defc98
--- /dev/null
+++ b/lib/gs-plugin-private.h
@@ -0,0 +1,55 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2016 Richard Hughes <richard@hughsie.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+#pragma once
+
+#include <appstream-glib.h>
+#include <glib-object.h>
+#include <gmodule.h>
+#include <libsoup/soup.h>
+
+#include "gs-plugin.h"
+
+G_BEGIN_DECLS
+
+GsPlugin *gs_plugin_new (void);
+GsPlugin *gs_plugin_create (const gchar *filename,
+ GError **error);
+const gchar *gs_plugin_error_to_string (GsPluginError error);
+const gchar *gs_plugin_action_to_string (GsPluginAction action);
+GsPluginAction gs_plugin_action_from_string (const gchar *action);
+const gchar *gs_plugin_action_to_function_name (GsPluginAction action);
+
+void gs_plugin_clear_data (GsPlugin *plugin);
+void gs_plugin_set_scale (GsPlugin *plugin,
+ guint scale);
+guint gs_plugin_get_order (GsPlugin *plugin);
+void gs_plugin_set_order (GsPlugin *plugin,
+ guint order);
+guint gs_plugin_get_priority (GsPlugin *plugin);
+void gs_plugin_set_priority (GsPlugin *plugin,
+ guint priority);
+void gs_plugin_set_name (GsPlugin *plugin,
+ const gchar *name);
+void gs_plugin_set_locale (GsPlugin *plugin,
+ const gchar *locale);
+void gs_plugin_set_language (GsPlugin *plugin,
+ const gchar *language);
+void gs_plugin_set_auth_array (GsPlugin *plugin,
+ GPtrArray *auth_array);
+GPtrArray *gs_plugin_get_rules (GsPlugin *plugin,
+ GsPluginRule rule);
+gpointer gs_plugin_get_symbol (GsPlugin *plugin,
+ const gchar *function_name);
+void gs_plugin_interactive_inc (GsPlugin *plugin);
+void gs_plugin_interactive_dec (GsPlugin *plugin);
+gchar *gs_plugin_refine_flags_to_string (GsPluginRefineFlags refine_flags);
+void gs_plugin_set_network_monitor (GsPlugin *plugin,
+ GNetworkMonitor *monitor);
+
+G_END_DECLS
diff --git a/lib/gs-plugin-types.h b/lib/gs-plugin-types.h
new file mode 100644
index 0000000..b4a3e3b
--- /dev/null
+++ b/lib/gs-plugin-types.h
@@ -0,0 +1,280 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2012-2018 Richard Hughes <richard@hughsie.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+#pragma once
+
+#include <glib-object.h>
+
+G_BEGIN_DECLS
+
+/**
+ * GsPluginStatus:
+ * @GS_PLUGIN_STATUS_UNKNOWN: Unknown status
+ * @GS_PLUGIN_STATUS_WAITING: Waiting
+ * @GS_PLUGIN_STATUS_FINISHED: Finished
+ * @GS_PLUGIN_STATUS_SETUP: Setup in progress
+ * @GS_PLUGIN_STATUS_DOWNLOADING: Downloading in progress
+ * @GS_PLUGIN_STATUS_QUERYING: Querying in progress
+ * @GS_PLUGIN_STATUS_INSTALLING: Installing in progress
+ * @GS_PLUGIN_STATUS_REMOVING: Removing in progress
+ *
+ * The ststus of the plugin.
+ **/
+typedef enum {
+ GS_PLUGIN_STATUS_UNKNOWN,
+ GS_PLUGIN_STATUS_WAITING,
+ GS_PLUGIN_STATUS_FINISHED,
+ GS_PLUGIN_STATUS_SETUP,
+ GS_PLUGIN_STATUS_DOWNLOADING,
+ GS_PLUGIN_STATUS_QUERYING,
+ GS_PLUGIN_STATUS_INSTALLING,
+ GS_PLUGIN_STATUS_REMOVING,
+ /*< private >*/
+ GS_PLUGIN_STATUS_LAST
+} GsPluginStatus;
+
+/**
+ * GsPluginFlags:
+ * @GS_PLUGIN_FLAGS_NONE: No flags set
+ * @GS_PLUGIN_FLAGS_INTERACTIVE: User initiated the job
+ *
+ * The flags for the plugin at this point in time.
+ **/
+#define GS_PLUGIN_FLAGS_NONE (0u)
+#define GS_PLUGIN_FLAGS_INTERACTIVE (1u << 4)
+typedef guint64 GsPluginFlags;
+
+/**
+ * GsPluginError:
+ * @GS_PLUGIN_ERROR_FAILED: Generic failure
+ * @GS_PLUGIN_ERROR_NOT_SUPPORTED: Action not supported
+ * @GS_PLUGIN_ERROR_CANCELLED: Action was cancelled
+ * @GS_PLUGIN_ERROR_NO_NETWORK: No network connection available
+ * @GS_PLUGIN_ERROR_NO_SECURITY: Security policy forbid action
+ * @GS_PLUGIN_ERROR_NO_SPACE: No disk space to allow action
+ * @GS_PLUGIN_ERROR_AUTH_REQUIRED: Authentication was required
+ * @GS_PLUGIN_ERROR_AUTH_INVALID: Provided authentication was invalid
+ * @GS_PLUGIN_ERROR_PLUGIN_DEPSOLVE_FAILED: The plugins installed are incompatible
+ * @GS_PLUGIN_ERROR_DOWNLOAD_FAILED: The download action failed
+ * @GS_PLUGIN_ERROR_WRITE_FAILED: The save-to-disk failed
+ * @GS_PLUGIN_ERROR_INVALID_FORMAT: The data format is invalid
+ * @GS_PLUGIN_ERROR_DELETE_FAILED: The delete action failed
+ * @GS_PLUGIN_ERROR_RESTART_REQUIRED: A restart is required
+ * @GS_PLUGIN_ERROR_AC_POWER_REQUIRED: AC power is required
+ * @GS_PLUGIN_ERROR_TIMED_OUT: The job timed out
+ * @GS_PLUGIN_ERROR_BATTERY_LEVEL_TOO_LOW: The system battery level is too low
+ *
+ * The failure error types.
+ **/
+typedef enum {
+ GS_PLUGIN_ERROR_FAILED,
+ GS_PLUGIN_ERROR_NOT_SUPPORTED,
+ GS_PLUGIN_ERROR_CANCELLED,
+ GS_PLUGIN_ERROR_NO_NETWORK,
+ GS_PLUGIN_ERROR_NO_SECURITY,
+ GS_PLUGIN_ERROR_NO_SPACE,
+ GS_PLUGIN_ERROR_AUTH_REQUIRED,
+ GS_PLUGIN_ERROR_AUTH_INVALID,
+ GS_PLUGIN_ERROR_PLUGIN_DEPSOLVE_FAILED,
+ GS_PLUGIN_ERROR_DOWNLOAD_FAILED,
+ GS_PLUGIN_ERROR_WRITE_FAILED,
+ GS_PLUGIN_ERROR_INVALID_FORMAT,
+ GS_PLUGIN_ERROR_DELETE_FAILED,
+ GS_PLUGIN_ERROR_RESTART_REQUIRED,
+ GS_PLUGIN_ERROR_AC_POWER_REQUIRED,
+ GS_PLUGIN_ERROR_TIMED_OUT,
+ GS_PLUGIN_ERROR_BATTERY_LEVEL_TOO_LOW,
+ /*< private >*/
+ GS_PLUGIN_ERROR_LAST
+} GsPluginError;
+
+/**
+ * GsPluginRefineFlags:
+ * @GS_PLUGIN_REFINE_FLAGS_DEFAULT: No explicit flags set
+ * @GS_PLUGIN_REFINE_FLAGS_USE_HISTORY: Get the historical view (unused)
+ * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_LICENSE: Require the license
+ * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_URL: Require the URL
+ * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_DESCRIPTION: Require the long description
+ * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_SIZE: Require the installed and download sizes
+ * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_RATING: Require the rating
+ * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_VERSION: Require the version
+ * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_HISTORY: Require the history
+ * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_SETUP_ACTION: Require enough to install or remove the package
+ * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_UPDATE_DETAILS: Require update details
+ * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN: Require the origin
+ * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_RELATED: Require related packages
+ * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_MENU_PATH: Require the menu path
+ * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_ADDONS: Require available addons
+ * @GS_PLUGIN_REFINE_FLAGS_ALLOW_PACKAGES: Allow packages to be returned
+ * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_UPDATE_SEVERITY: Require update severity
+ * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_UPGRADE_REMOVED: Require distro upgrades
+ * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_PROVENANCE: Require the provenance
+ * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_REVIEWS: Require user-reviews
+ * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_REVIEW_RATINGS: Require user-ratings
+ * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_KEY_COLORS: Require the key colors
+ * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON: Require the icon to be loaded
+ * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_PERMISSIONS: Require the needed permissions
+ * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN_HOSTNAME: Require the origin hostname
+ * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN_UI: Require the origin for UI
+ * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_RUNTIME: Require the runtime
+ * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_SCREENSHOTS: Require screenshot information
+ * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_CATEGORIES: Require categories
+ * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_PROJECT_GROUP: Require project group
+ * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_DEVELOPER_NAME: Require developer name
+ * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_KUDOS: Require kudos
+ * @GS_PLUGIN_REFINE_FLAGS_REQUIRE_CONTENT_RATING: Require content rating
+ *
+ * The refine flags.
+ **/
+#define GS_PLUGIN_REFINE_FLAGS_DEFAULT ((guint64) 0)
+#define GS_PLUGIN_REFINE_FLAGS_USE_HISTORY ((guint64) 1 << 0) /* unused, TODO: perhaps ->STATE */
+#define GS_PLUGIN_REFINE_FLAGS_REQUIRE_LICENSE ((guint64) 1 << 1)
+#define GS_PLUGIN_REFINE_FLAGS_REQUIRE_URL ((guint64) 1 << 2)
+#define GS_PLUGIN_REFINE_FLAGS_REQUIRE_DESCRIPTION ((guint64) 1 << 3)
+#define GS_PLUGIN_REFINE_FLAGS_REQUIRE_SIZE ((guint64) 1 << 4)
+#define GS_PLUGIN_REFINE_FLAGS_REQUIRE_RATING ((guint64) 1 << 5)
+#define GS_PLUGIN_REFINE_FLAGS_REQUIRE_VERSION ((guint64) 1 << 6)
+#define GS_PLUGIN_REFINE_FLAGS_REQUIRE_HISTORY ((guint64) 1 << 7)
+#define GS_PLUGIN_REFINE_FLAGS_REQUIRE_SETUP_ACTION ((guint64) 1 << 8)
+#define GS_PLUGIN_REFINE_FLAGS_REQUIRE_UPDATE_DETAILS ((guint64) 1 << 9)
+#define GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN ((guint64) 1 << 10)
+#define GS_PLUGIN_REFINE_FLAGS_REQUIRE_RELATED ((guint64) 1 << 11)
+#define GS_PLUGIN_REFINE_FLAGS_REQUIRE_MENU_PATH ((guint64) 1 << 12)
+#define GS_PLUGIN_REFINE_FLAGS_REQUIRE_ADDONS ((guint64) 1 << 13)
+#define GS_PLUGIN_REFINE_FLAGS_ALLOW_PACKAGES ((guint64) 1 << 14) /* TODO: move to request */
+#define GS_PLUGIN_REFINE_FLAGS_REQUIRE_UPDATE_SEVERITY ((guint64) 1 << 15)
+#define GS_PLUGIN_REFINE_FLAGS_REQUIRE_UPGRADE_REMOVED ((guint64) 1 << 16)
+#define GS_PLUGIN_REFINE_FLAGS_REQUIRE_PROVENANCE ((guint64) 1 << 17)
+#define GS_PLUGIN_REFINE_FLAGS_REQUIRE_REVIEWS ((guint64) 1 << 18)
+#define GS_PLUGIN_REFINE_FLAGS_REQUIRE_REVIEW_RATINGS ((guint64) 1 << 19)
+#define GS_PLUGIN_REFINE_FLAGS_REQUIRE_KEY_COLORS ((guint64) 1 << 20)
+#define GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON ((guint64) 1 << 21)
+#define GS_PLUGIN_REFINE_FLAGS_REQUIRE_PERMISSIONS ((guint64) 1 << 22)
+#define GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN_HOSTNAME ((guint64) 1 << 23)
+#define GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN_UI ((guint64) 1 << 24)
+#define GS_PLUGIN_REFINE_FLAGS_REQUIRE_RUNTIME ((guint64) 1 << 25)
+#define GS_PLUGIN_REFINE_FLAGS_REQUIRE_SCREENSHOTS ((guint64) 1 << 26)
+#define GS_PLUGIN_REFINE_FLAGS_REQUIRE_CATEGORIES ((guint64) 1 << 27)
+#define GS_PLUGIN_REFINE_FLAGS_REQUIRE_PROJECT_GROUP ((guint64) 1 << 28)
+#define GS_PLUGIN_REFINE_FLAGS_REQUIRE_DEVELOPER_NAME ((guint64) 1 << 29)
+#define GS_PLUGIN_REFINE_FLAGS_REQUIRE_KUDOS ((guint64) 1 << 30)
+#define GS_PLUGIN_REFINE_FLAGS_REQUIRE_CONTENT_RATING ((guint64) 1 << 31)
+typedef guint64 GsPluginRefineFlags;
+
+/**
+ * GsPluginRule:
+ * @GS_PLUGIN_RULE_CONFLICTS: The plugin conflicts with another
+ * @GS_PLUGIN_RULE_RUN_AFTER: Order the plugin after another
+ * @GS_PLUGIN_RULE_RUN_BEFORE: Order the plugin before another
+ * @GS_PLUGIN_RULE_BETTER_THAN: Results are better than another
+ *
+ * The rules used for ordering plugins.
+ * Plugins are expected to add rules in gs_plugin_initialize().
+ **/
+typedef enum {
+ GS_PLUGIN_RULE_CONFLICTS,
+ GS_PLUGIN_RULE_RUN_AFTER,
+ GS_PLUGIN_RULE_RUN_BEFORE,
+ GS_PLUGIN_RULE_BETTER_THAN,
+ /*< private >*/
+ GS_PLUGIN_RULE_LAST
+} GsPluginRule;
+
+/**
+ * GsPluginAction:
+ * @GS_PLUGIN_ACTION_UNKNOWN: Action is unknown
+ * @GS_PLUGIN_ACTION_SETUP: Plugin setup (internal)
+ * @GS_PLUGIN_ACTION_INSTALL: Install an application
+ * @GS_PLUGIN_ACTION_REMOVE: Remove an application
+ * @GS_PLUGIN_ACTION_UPDATE: Update an application
+ * @GS_PLUGIN_ACTION_SET_RATING: Set rating on an application
+ * @GS_PLUGIN_ACTION_UPGRADE_DOWNLOAD: Download a distro upgrade
+ * @GS_PLUGIN_ACTION_UPGRADE_TRIGGER: Trigger a distro upgrade
+ * @GS_PLUGIN_ACTION_LAUNCH: Launch an application
+ * @GS_PLUGIN_ACTION_UPDATE_CANCEL: Cancel the update
+ * @GS_PLUGIN_ACTION_ADD_SHORTCUT: Add a shortcut to an application
+ * @GS_PLUGIN_ACTION_REMOVE_SHORTCUT: Remove a shortcut to an application
+ * @GS_PLUGIN_ACTION_REVIEW_SUBMIT: Submit a new review
+ * @GS_PLUGIN_ACTION_REVIEW_UPVOTE: Upvote an existing review
+ * @GS_PLUGIN_ACTION_REVIEW_DOWNVOTE: Downvote an existing review
+ * @GS_PLUGIN_ACTION_REVIEW_REPORT: Report an existing review
+ * @GS_PLUGIN_ACTION_REVIEW_REMOVE: Remove a review written by the user
+ * @GS_PLUGIN_ACTION_REVIEW_DISMISS: Dismiss (ignore) a review when moderating
+ * @GS_PLUGIN_ACTION_GET_UPDATES: Get the list of updates
+ * @GS_PLUGIN_ACTION_GET_DISTRO_UPDATES: Get the list of distro updates
+ * @GS_PLUGIN_ACTION_GET_UNVOTED_REVIEWS: Get the list of moderatable reviews
+ * @GS_PLUGIN_ACTION_GET_SOURCES: Get the list of sources
+ * @GS_PLUGIN_ACTION_GET_INSTALLED: Get the list of installed applications
+ * @GS_PLUGIN_ACTION_GET_POPULAR: Get the list of popular applications
+ * @GS_PLUGIN_ACTION_GET_FEATURED: Get the list of featured applications
+ * @GS_PLUGIN_ACTION_SEARCH: Get the search results for a query
+ * @GS_PLUGIN_ACTION_SEARCH_FILES: Get the search results for a file query
+ * @GS_PLUGIN_ACTION_SEARCH_PROVIDES: Get the search results for a provide query
+ * @GS_PLUGIN_ACTION_GET_CATEGORIES: Get the list of categories
+ * @GS_PLUGIN_ACTION_GET_CATEGORY_APPS: Get the apps for a specific category
+ * @GS_PLUGIN_ACTION_REFINE: Refine the application
+ * @GS_PLUGIN_ACTION_REFRESH: Refresh all the sources
+ * @GS_PLUGIN_ACTION_FILE_TO_APP: Convert the file to an application
+ * @GS_PLUGIN_ACTION_URL_TO_APP: Convert the URI to an application
+ * @GS_PLUGIN_ACTION_GET_RECENT: Get the apps recently released
+ * @GS_PLUGIN_ACTION_GET_UPDATES_HISTORICAL: Get the list of historical updates
+ * @GS_PLUGIN_ACTION_INITIALIZE: Initialize the plugin
+ * @GS_PLUGIN_ACTION_DESTROY: Destroy the plugin
+ * @GS_PLUGIN_ACTION_DOWNLOAD: Download an application
+ * @GS_PLUGIN_ACTION_GET_ALTERNATES: Get the alternates for a specific application
+ * @GS_PLUGIN_ACTION_GET_LANGPACKS: Get appropriate language pack
+ *
+ * The plugin action.
+ **/
+typedef enum {
+ GS_PLUGIN_ACTION_UNKNOWN,
+ GS_PLUGIN_ACTION_SETUP,
+ GS_PLUGIN_ACTION_INSTALL,
+ GS_PLUGIN_ACTION_REMOVE,
+ GS_PLUGIN_ACTION_UPDATE,
+ GS_PLUGIN_ACTION_SET_RATING,
+ GS_PLUGIN_ACTION_UPGRADE_DOWNLOAD,
+ GS_PLUGIN_ACTION_UPGRADE_TRIGGER,
+ GS_PLUGIN_ACTION_LAUNCH,
+ GS_PLUGIN_ACTION_UPDATE_CANCEL,
+ GS_PLUGIN_ACTION_ADD_SHORTCUT,
+ GS_PLUGIN_ACTION_REMOVE_SHORTCUT,
+ GS_PLUGIN_ACTION_REVIEW_SUBMIT,
+ GS_PLUGIN_ACTION_REVIEW_UPVOTE,
+ GS_PLUGIN_ACTION_REVIEW_DOWNVOTE,
+ GS_PLUGIN_ACTION_REVIEW_REPORT,
+ GS_PLUGIN_ACTION_REVIEW_REMOVE,
+ GS_PLUGIN_ACTION_REVIEW_DISMISS,
+ GS_PLUGIN_ACTION_GET_UPDATES,
+ GS_PLUGIN_ACTION_GET_DISTRO_UPDATES,
+ GS_PLUGIN_ACTION_GET_UNVOTED_REVIEWS,
+ GS_PLUGIN_ACTION_GET_SOURCES,
+ GS_PLUGIN_ACTION_GET_INSTALLED,
+ GS_PLUGIN_ACTION_GET_POPULAR,
+ GS_PLUGIN_ACTION_GET_FEATURED,
+ GS_PLUGIN_ACTION_SEARCH,
+ GS_PLUGIN_ACTION_SEARCH_FILES,
+ GS_PLUGIN_ACTION_SEARCH_PROVIDES,
+ GS_PLUGIN_ACTION_GET_CATEGORIES,
+ GS_PLUGIN_ACTION_GET_CATEGORY_APPS,
+ GS_PLUGIN_ACTION_REFINE,
+ GS_PLUGIN_ACTION_REFRESH,
+ GS_PLUGIN_ACTION_FILE_TO_APP,
+ GS_PLUGIN_ACTION_URL_TO_APP,
+ GS_PLUGIN_ACTION_GET_RECENT,
+ GS_PLUGIN_ACTION_GET_UPDATES_HISTORICAL,
+ GS_PLUGIN_ACTION_INITIALIZE,
+ GS_PLUGIN_ACTION_DESTROY,
+ GS_PLUGIN_ACTION_DOWNLOAD,
+ GS_PLUGIN_ACTION_GET_ALTERNATES,
+ GS_PLUGIN_ACTION_GET_LANGPACKS,
+ /*< private >*/
+ GS_PLUGIN_ACTION_LAST
+} GsPluginAction;
+
+G_END_DECLS
diff --git a/lib/gs-plugin-vfuncs.h b/lib/gs-plugin-vfuncs.h
new file mode 100644
index 0000000..05772f2
--- /dev/null
+++ b/lib/gs-plugin-vfuncs.h
@@ -0,0 +1,942 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2012-2017 Richard Hughes <richard@hughsie.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+#pragma once
+
+/**
+ * SECTION:gs-plugin-vfuncs
+ * @title: GsPlugin Exports
+ * @include: gnome-software.h
+ * @stability: Unstable
+ * @short_description: Vfuncs that plugins can implement
+ */
+
+#include <appstream-glib.h>
+#include <glib-object.h>
+#include <gmodule.h>
+#include <gio/gio.h>
+#include <libsoup/soup.h>
+
+#include "gs-app.h"
+#include "gs-app-list.h"
+#include "gs-category.h"
+
+G_BEGIN_DECLS
+
+/**
+ * gs_plugin_initialize:
+ * @plugin: a #GsPlugin
+ *
+ * Checks if the plugin should run, and if initializes it. If the plugin should
+ * not be run then gs_plugin_set_enabled() should be called.
+ * This is also the place to call gs_plugin_alloc_data() if private data is
+ * required for the plugin.
+ *
+ * NOTE: Do not do any failable actions in this function; use gs_plugin_setup()
+ * instead.
+ **/
+void gs_plugin_initialize (GsPlugin *plugin);
+
+/**
+ * gs_plugin_destroy:
+ * @plugin: a #GsPlugin
+ *
+ * Called when the plugin should destroy any private data.
+ **/
+void gs_plugin_destroy (GsPlugin *plugin);
+
+/**
+ * gs_plugin_adopt_app:
+ * @plugin: a #GsPlugin
+ * @app: a #GsApp
+ *
+ * Called when an #GsApp has not been claimed (i.e. a management plugin has not
+ * been set).
+ *
+ * A claimed application means other plugins will not try to perform actions
+ * such as install, remove or update. Most applications are claimed when they
+ * are created.
+ *
+ * If a plugin can adopt this application then it should call
+ * gs_app_set_management_plugin() on @app.
+ **/
+void gs_plugin_adopt_app (GsPlugin *plugin,
+ GsApp *app);
+
+/**
+ * gs_plugin_add_search:
+ * @plugin: a #GsPlugin
+ * @values: a NULL terminated list of search terms, e.g. [ "gnome", "software" ]
+ * @list: a #GsAppList
+ * @cancellable: a #GCancellable, or %NULL
+ * @error: a #GError, or %NULL
+ *
+ * Get search results for a specific query.
+ *
+ * Plugins are expected to add new apps using gs_app_list_add().
+ *
+ * Returns: %TRUE for success or if not relevant
+ **/
+gboolean gs_plugin_add_search (GsPlugin *plugin,
+ gchar **values,
+ GsAppList *list,
+ GCancellable *cancellable,
+ GError **error);
+
+/**
+ * gs_plugin_add_search_files:
+ * @plugin: a #GsPlugin
+ * @values: a list of filenames, e.g. [ "/usr/share/help/gimp/index.html" ]
+ * @list: a #GsAppList
+ * @cancellable: a #GCancellable, or %NULL
+ * @error: a #GError, or %NULL
+ *
+ * Called when searching for an application that provides a specific filename
+ * on the filesystem.
+ *
+ * Plugins are expected to add new apps using gs_app_list_add().
+ *
+ * Returns: %TRUE for success or if not relevant
+ **/
+gboolean gs_plugin_add_search_files (GsPlugin *plugin,
+ gchar **values,
+ GsAppList *list,
+ GCancellable *cancellable,
+ GError **error);
+
+/**
+ * gs_plugin_add_search_what_provides
+ * @plugin: a list of tags, e.g. [ "text/rtf" ]
+ * @values: a #GStrv
+ * @list: a #GsAppList
+ * @cancellable: a #GCancellable, or %NULL
+ * @error: a #GError, or %NULL
+ *
+ * Called when searching for an application that provides specific defined tags,
+ * for instance a codec string or mime-type.
+ *
+ * Plugins are expected to add new apps using gs_app_list_add().
+ *
+ * Returns: %TRUE for success or if not relevant
+ **/
+gboolean gs_plugin_add_search_what_provides (GsPlugin *plugin,
+ gchar **values,
+ GsAppList *list,
+ GCancellable *cancellable,
+ GError **error);
+
+/**
+ * gs_plugin_add_alternates
+ * @plugin: a #GsPlugin
+ * @app: a #GsApp
+ * @list: a #GsAppList
+ * @cancellable: a #GCancellable, or %NULL
+ * @error: a #GError, or %NULL
+ *
+ * Called when trying to find alternates to a specific app, for instance
+ * finding a flatpak version of an existing distro packaged application.
+ *
+ * Plugins are expected to add new apps using gs_app_list_add().
+ *
+ * Returns: %TRUE for success or if not relevant
+ **/
+gboolean gs_plugin_add_alternates (GsPlugin *plugin,
+ GsApp *app,
+ GsAppList *list,
+ GCancellable *cancellable,
+ GError **error);
+
+/**
+ * gs_plugin_setup:
+ * @plugin: a #GsPlugin
+ * @cancellable: a #GCancellable, or %NULL
+ * @error: a #GError, or %NULL
+ *
+ * Called when the plugin should set up the initial state, and with the write
+ * lock held.
+ *
+ * All functions can block, but should sent progress notifications, e.g. using
+ * gs_app_set_progress() if they will take more than tens of milliseconds
+ * to complete.
+ *
+ * This function will also not be called if gs_plugin_initialize() self-disabled.
+ *
+ * Returns: %TRUE for success
+ **/
+gboolean gs_plugin_setup (GsPlugin *plugin,
+ GCancellable *cancellable,
+ GError **error);
+
+/**
+ * gs_plugin_add_installed:
+ * @plugin: a #GsPlugin
+ * @list: a #GsAppList
+ * @cancellable: a #GCancellable, or %NULL
+ * @error: a #GError, or %NULL
+ *
+ * Get the list of installed applications.
+ *
+ * Plugins are expected to add new apps using gs_app_list_add().
+ *
+ * Returns: %TRUE for success or if not relevant
+ **/
+gboolean gs_plugin_add_installed (GsPlugin *plugin,
+ GsAppList *list,
+ GCancellable *cancellable,
+ GError **error);
+
+/**
+ * gs_plugin_add_updates:
+ * @plugin: a #GsPlugin
+ * @list: a #GsAppList
+ * @cancellable: a #GCancellable, or %NULL
+ * @error: a #GError, or %NULL
+ *
+ * Get the list of updates.
+ *
+ * NOTE: Actually downloading the updates can be done in gs_plugin_download_app()
+ * or in gs_plugin_download().
+ *
+ * Plugins are expected to add new apps using gs_app_list_add().
+ *
+ * Returns: %TRUE for success or if not relevant
+ **/
+gboolean gs_plugin_add_updates (GsPlugin *plugin,
+ GsAppList *list,
+ GCancellable *cancellable,
+ GError **error);
+
+/**
+ * gs_plugin_add_distro_upgrades:
+ * @plugin: a #GsPlugin
+ * @list: a #GsAppList
+ * @cancellable: a #GCancellable, or %NULL
+ * @error: a #GError, or %NULL
+ *
+ * Get the list of distribution upgrades. Due to the download size, these
+ * should not be downloaded until the user has explicitly opted-in.
+ *
+ * Plugins are expected to add new apps using gs_app_list_add() of type
+ * %AS_APP_KIND_OS_UPGRADE.
+ *
+ * Returns: %TRUE for success or if not relevant
+ **/
+gboolean gs_plugin_add_distro_upgrades (GsPlugin *plugin,
+ GsAppList *list,
+ GCancellable *cancellable,
+ GError **error);
+
+/**
+ * gs_plugin_add_sources:
+ * @plugin: a #GsPlugin
+ * @list: a #GsAppList
+ * @cancellable: a #GCancellable, or %NULL
+ * @error: a #GError, or %NULL
+ *
+ * Get the list of sources, for example the repos listed in /etc/yum.repos.d
+ * or the remotes configured in flatpak.
+ *
+ * Plugins are expected to add new apps using gs_app_list_add() of type
+ * %AS_APP_KIND_SOURCE.
+ *
+ * Returns: %TRUE for success or if not relevant
+ **/
+gboolean gs_plugin_add_sources (GsPlugin *plugin,
+ GsAppList *list,
+ GCancellable *cancellable,
+ GError **error);
+
+/**
+ * gs_plugin_add_updates_historical
+ * @plugin: a #GsPlugin
+ * @list: a #GsAppList
+ * @cancellable: a #GCancellable, or %NULL
+ * @error: a #GError, or %NULL
+ *
+ * Get the list of historical updates, i.e. the updates that have just been
+ * installed.
+ *
+ * Plugins are expected to add new apps using gs_app_list_add().
+ *
+ * Returns: %TRUE for success or if not relevant
+ **/
+gboolean gs_plugin_add_updates_historical (GsPlugin *plugin,
+ GsAppList *list,
+ GCancellable *cancellable,
+ GError **error);
+
+/**
+ * gs_plugin_add_categories:
+ * @plugin: a #GsPlugin
+ * @list: (element-type GsCategory): a #GPtrArray
+ * @cancellable: a #GCancellable, or %NULL
+ * @error: a #GError, or %NULL
+ *
+ * Get the category tree, for instance Games->Action or Internet->Email.
+ *
+ * Plugins are expected to add new categories using g_ptr_array_add().
+ *
+ * Returns: %TRUE for success or if not relevant
+ **/
+gboolean gs_plugin_add_categories (GsPlugin *plugin,
+ GPtrArray *list,
+ GCancellable *cancellable,
+ GError **error);
+
+/**
+ * gs_plugin_add_category_apps:
+ * @plugin: a #GsPlugin
+ * @category: a #GsCategory
+ * @list: a #GsAppList
+ * @cancellable: a #GCancellable, or %NULL
+ * @error: a #GError, or %NULL
+ *
+ * Get all the applications that match a specific category.
+ *
+ * Plugins are expected to add new apps using gs_app_list_add().
+ *
+ * Returns: %TRUE for success or if not relevant
+ **/
+gboolean gs_plugin_add_category_apps (GsPlugin *plugin,
+ GsCategory *category,
+ GsAppList *list,
+ GCancellable *cancellable,
+ GError **error);
+
+/**
+ * gs_plugin_add_recent:
+ * @plugin: a #GsPlugin
+ * @list: a #GsAppList
+ * @age: a number of seconds
+ * @cancellable: a #GCancellable, or %NULL
+ * @error: a #GError, or %NULL
+ *
+ * Return all the applications that have had upstream releases recently.
+ *
+ * Plugins are expected to add new apps using gs_app_list_add().
+ *
+ * Returns: %TRUE for success or if not relevant
+ **/
+gboolean gs_plugin_add_recent (GsPlugin *plugin,
+ GsAppList *list,
+ guint64 age,
+ GCancellable *cancellable,
+ GError **error);
+
+/**
+ * gs_plugin_add_popular:
+ * @plugin: a #GsPlugin
+ * @list: a #GsAppList
+ * @cancellable: a #GCancellable, or %NULL
+ * @error: a #GError, or %NULL
+ *
+ * Get popular applications that should be featured on the main page as
+ * "Editors Picks".
+ * This is expected to be a curated list of applications that are high quality
+ * and feature-complete.
+ *
+ * The returned list of popular applications are not sorted, but each #GsApp has
+ * to be valid, for instance having a known state and a valid icon.
+ * If an insufficient number of applications are added by plugins then the
+ * section on the overview shell may be hidden.
+ *
+ * Plugins are expected to add new apps using gs_app_list_add().
+ *
+ * Returns: %TRUE for success or if not relevant
+ **/
+gboolean gs_plugin_add_popular (GsPlugin *plugin,
+ GsAppList *list,
+ GCancellable *cancellable,
+ GError **error);
+
+/**
+ * gs_plugin_add_featured:
+ * @plugin: a #GsPlugin
+ * @list: a #GsAppList
+ * @cancellable: a #GCancellable, or %NULL
+ * @error: a #GError, or %NULL
+ *
+ * Get applications that should be featured as a large full-width banner on the
+ * overview page.
+ * This is expected to be a curated list of applications that are high quality
+ * and feature-complete.
+ *
+ * The returned list of popular applications are randomized in a way so that
+ * the same application is featured for the entire calendar day.
+ *
+ * NOTE: The UI code may expect that applications have additional metadata set on
+ * results, for instance <code>GnomeSoftware::FeatureTile-css</code>.
+ *
+ * Plugins are expected to add new apps using gs_app_list_add().
+ *
+ * Returns: %TRUE for success or if not relevant
+ **/
+gboolean gs_plugin_add_featured (GsPlugin *plugin,
+ GsAppList *list,
+ GCancellable *cancellable,
+ GError **error);
+
+/**
+ * gs_plugin_add_unvoted_reviews:
+ * @plugin: a #GsPlugin
+ * @list: a #GsAppList
+ * @cancellable: a #GCancellable, or %NULL
+ * @error: a #GError, or %NULL
+ *
+ * Gets the list of unvoted reviews. Only applications should be returned where
+ * there are reviews, and where the user has not previously moderated them.
+ * This function is supposed to be used to display a moderation panel for
+ * reviewers.
+ *
+ * Plugins are expected to add new apps using gs_app_list_add().
+ *
+ * Returns: %TRUE for success or if not relevant
+ **/
+gboolean gs_plugin_add_unvoted_reviews (GsPlugin *plugin,
+ GsAppList *list,
+ GCancellable *cancellable,
+ GError **error);
+
+/**
+ * gs_plugin_refine:
+ * @plugin: a #GsPlugin
+ * @list: a #GsAppList
+ * @flags: a #GsPluginRefineFlags, e.g. %GS_PLUGIN_REFINE_FLAGS_REQUIRE_LICENSE
+ * @cancellable: a #GCancellable, or %NULL
+ * @error: a #GError, or %NULL
+ *
+ * Adds required information to a list of #GsApp's. It allows requests to be
+ * batched up, which allows better performance than individual calls per app.
+ *
+ * An example for when this is useful would be in the PackageKit plugin where
+ * we want to do one transaction of GetDetails with multiple source-ids rather
+ * than scheduling a large number of pending requests.
+ *
+ * Returns: %TRUE for success or if not relevant
+ **/
+gboolean gs_plugin_refine (GsPlugin *plugin,
+ GsAppList *list,
+ GsPluginRefineFlags flags,
+ GCancellable *cancellable,
+ GError **error);
+
+/**
+ * gs_plugin_refine_wildcard:
+ * @plugin: a #GsPlugin
+ * @app: a #GsApp
+ * @list: a #GsAppList
+ * @flags: a #GsPluginRefineFlags, e.g. %GS_PLUGIN_REFINE_FLAGS_REQUIRE_LICENSE
+ * @cancellable: a #GCancellable, or %NULL
+ * @error: a #GError, or %NULL
+ *
+ * Adds applications that match the wildcard specified in @app.
+ *
+ * The general idea is that plugins create and add *new* applications rather
+ * than all trying to fight over the wildcard application.
+ * This allows the plugin loader to filter using the #GsApp priority value.
+ *
+ * Returns: %TRUE for success or if not relevant
+ **/
+gboolean gs_plugin_refine_wildcard (GsPlugin *plugin,
+ GsApp *app,
+ GsAppList *list,
+ GsPluginRefineFlags flags,
+ GCancellable *cancellable,
+ GError **error);
+
+/**
+ * gs_plugin_launch:
+ * @plugin: a #GsPlugin
+ * @app: a #GsApp
+ * @cancellable: a #GCancellable, or %NULL
+ * @error: a #GError, or %NULL
+ *
+ * Launch the specified application using a plugin-specific method.
+ * This is normally setting some environment or launching a specific binary.
+ *
+ * Plugins can simply use gs_plugin_app_launch() if no plugin-specific
+ * functionality is required.
+ *
+ * Returns: %TRUE for success or if not relevant
+ **/
+gboolean gs_plugin_launch (GsPlugin *plugin,
+ GsApp *app,
+ GCancellable *cancellable,
+ GError **error);
+
+/**
+ * gs_plugin_add_shortcut:
+ * @plugin: a #GsPlugin
+ * @app: a #GsApp
+ * @cancellable: a #GCancellable, or %NULL
+ * @error: a #GError, or %NULL
+ *
+ * Adds a shortcut for the application in a desktop-defined location.
+ *
+ * Returns: %TRUE for success or if not relevant
+ **/
+gboolean gs_plugin_add_shortcut (GsPlugin *plugin,
+ GsApp *app,
+ GCancellable *cancellable,
+ GError **error);
+
+/**
+ * gs_plugin_remove_shortcut:
+ * @plugin: a #GsPlugin
+ * @app: a #GsApp
+ * @cancellable: a #GCancellable, or %NULL
+ * @error: a #GError, or %NULL
+ *
+ * Removes a shortcut for the application in a desktop-defined location.
+ *
+ * Returns: %TRUE for success or if not relevant
+ **/
+gboolean gs_plugin_remove_shortcut (GsPlugin *plugin,
+ GsApp *app,
+ GCancellable *cancellable,
+ GError **error);
+
+/**
+ * gs_plugin_update_cancel:
+ * @plugin: a #GsPlugin
+ * @app: a #GsApp
+ * @cancellable: a #GCancellable, or %NULL
+ * @error: a #GError, or %NULL
+ *
+ * Cancels the offline update of @app.
+ *
+ * Returns: %TRUE for success or if not relevant
+ **/
+gboolean gs_plugin_update_cancel (GsPlugin *plugin,
+ GsApp *app,
+ GCancellable *cancellable,
+ GError **error);
+
+/**
+ * gs_plugin_app_install:
+ * @plugin: a #GsPlugin
+ * @app: a #GsApp
+ * @cancellable: a #GCancellable, or %NULL
+ * @error: a #GError, or %NULL
+ *
+ * Install the application.
+ *
+ * Plugins are expected to send progress notifications to the UI using
+ * gs_app_set_progress() using the passed in @app.
+ *
+ * All functions can block, but should sent progress notifications, e.g. using
+ * gs_app_set_progress() if they will take more than tens of milliseconds
+ * to complete.
+ *
+ * On failure the error message returned will usually only be shown on the
+ * console, but they can also be retrieved using gs_plugin_loader_get_events().
+ *
+ * NOTE: Once the action is complete, the plugin must set the new state of @app
+ * to %AS_APP_STATE_INSTALLED.
+ *
+ * Returns: %TRUE for success or if not relevant
+ **/
+gboolean gs_plugin_app_install (GsPlugin *plugin,
+ GsApp *app,
+ GCancellable *cancellable,
+ GError **error);
+
+/**
+ * gs_plugin_app_remove:
+ * @plugin: a #GsPlugin
+ * @app: a #GsApp
+ * @cancellable: a #GCancellable, or %NULL
+ * @error: a #GError, or %NULL
+ *
+ * Remove the application.
+ *
+ * Plugins are expected to send progress notifications to the UI using
+ * gs_app_set_progress() using the passed in @app.
+ *
+ * All functions can block, but should sent progress notifications, e.g. using
+ * gs_app_set_progress() if they will take more than tens of milliseconds
+ * to complete.
+ *
+ * On failure the error message returned will usually only be shown on the
+ * console, but they can also be retrieved using gs_plugin_loader_get_events().
+ *
+ * NOTE: Once the action is complete, the plugin must set the new state of @app
+ * to %AS_APP_STATE_AVAILABLE or %AS_APP_STATE_UNKNOWN if not known.
+ *
+ * Returns: %TRUE for success or if not relevant
+ **/
+gboolean gs_plugin_app_remove (GsPlugin *plugin,
+ GsApp *app,
+ GCancellable *cancellable,
+ GError **error);
+
+/**
+ * gs_plugin_app_set_rating:
+ * @plugin: a #GsPlugin
+ * @app: a #GsApp
+ * @cancellable: a #GCancellable, or %NULL
+ * @error: a #GError, or %NULL
+ *
+ * Gets any ratings for the applications.
+ *
+ * Plugins are expected to call gs_app_set_rating() on @app.
+ *
+ * Returns: %TRUE for success or if not relevant
+ **/
+gboolean gs_plugin_app_set_rating (GsPlugin *plugin,
+ GsApp *app,
+ GCancellable *cancellable,
+ GError **error);
+
+/**
+ * gs_plugin_update_app:
+ * @plugin: a #GsPlugin
+ * @app: a #GsApp
+ * @cancellable: a #GCancellable, or %NULL
+ * @error: a #GError, or %NULL
+ *
+ * Update the application live.
+ *
+ * Plugins are expected to send progress notifications to the UI using
+ * gs_app_set_progress() using the passed in @app.
+ *
+ * All functions can block, but should sent progress notifications, e.g. using
+ * gs_app_set_progress() if they will take more than tens of milliseconds
+ * to complete.
+ *
+ * On failure the error message returned will usually only be shown on the
+ * console, but they can also be retrieved using gs_plugin_loader_get_events().
+ *
+ * NOTE: Once the action is complete, the plugin must set the new state of @app
+ * to %AS_APP_STATE_INSTALLED or %AS_APP_STATE_UNKNOWN if not known.
+ *
+ * If %GS_APP_QUIRK_IS_PROXY is set on the application then the actual #GsApp
+ * set in @app will be the related application of the parent. Plugins do not
+ * need to manually iterate on the related list of applications.
+ *
+ * Returns: %TRUE for success or if not relevant
+ **/
+gboolean gs_plugin_update_app (GsPlugin *plugin,
+ GsApp *app,
+ GCancellable *cancellable,
+ GError **error);
+
+/**
+ * gs_plugin_download_app:
+ * @plugin: a #GsPlugin
+ * @app: a #GsApp
+ * @cancellable: a #GCancellable, or %NULL
+ * @error: a #GError, or %NULL
+ *
+ * Downloads the application and any dependencies ready to be installed or
+ * updated.
+ *
+ * Plugins are expected to schedule downloads using the system download
+ * scheduler if appropriate (if the download is not guaranteed to be under a few
+ * hundred kilobytes, for example), so that the user’s metered data preferences
+ * are honoured.
+ *
+ * Plugins are expected to send progress notifications to the UI using
+ * gs_app_set_progress() using the passed in @app.
+ *
+ * All functions can block, but should sent progress notifications, e.g. using
+ * gs_app_set_progress() if they will take more than tens of milliseconds
+ * to complete.
+ *
+ * If the @app is already downloaded, do not return an error and return %TRUE.
+ *
+ * On failure the error message returned will usually only be shown on the
+ * console, but they can also be retrieved using gs_plugin_loader_get_events().
+ *
+ * Returns: %TRUE for success or if not relevant
+ **/
+gboolean gs_plugin_download_app (GsPlugin *plugin,
+ GsApp *app,
+ GCancellable *cancellable,
+ GError **error);
+
+/**
+ * gs_plugin_download:
+ * @plugin: a #GsPlugin
+ * @apps: a #GsAppList
+ * @cancellable: a #GCancellable, or %NULL
+ * @error: a #GError, or %NULL
+ *
+ * Downloads a list of applications ready to be installed or updated.
+ *
+ * Plugins are expected to schedule downloads using the system download
+ * scheduler if appropriate (if the download is not guaranteed to be under a few
+ * hundred kilobytes, for example), so that the user’s metered data preferences
+ * are honoured.
+ *
+ * Returns: %TRUE for success or if not relevant
+ **/
+gboolean gs_plugin_download (GsPlugin *plugin,
+ GsAppList *apps,
+ GCancellable *cancellable,
+ GError **error);
+
+/**
+ * gs_plugin_app_upgrade_download:
+ * @plugin: a #GsPlugin
+ * @app: a #GsApp, with kind %AS_APP_KIND_OS_UPGRADE
+ * @cancellable: a #GCancellable, or %NULL
+ * @error: a #GError, or %NULL
+ *
+ * Starts downloading a distribution upgrade in the background.
+ *
+ * All functions can block, but should sent progress notifications, e.g. using
+ * gs_app_set_progress() if they will take more than tens of milliseconds
+ * to complete.
+ *
+ * Returns: %TRUE for success or if not relevant
+ **/
+gboolean gs_plugin_app_upgrade_download (GsPlugin *plugin,
+ GsApp *app,
+ GCancellable *cancellable,
+ GError **error);
+
+/**
+ * gs_plugin_app_upgrade_trigger:
+ * @plugin: a #GsPlugin
+ * @app: a #GsApp, with kind %AS_APP_KIND_OS_UPGRADE
+ * @cancellable: a #GCancellable, or %NULL
+ * @error: a #GError, or %NULL
+ *
+ * Triggers the distribution upgrade to be installed on next boot.
+ *
+ * Returns: %TRUE for success or if not relevant
+ **/
+gboolean gs_plugin_app_upgrade_trigger (GsPlugin *plugin,
+ GsApp *app,
+ GCancellable *cancellable,
+ GError **error);
+
+/**
+ * gs_plugin_review_submit:
+ * @plugin: a #GsPlugin
+ * @app: a #GsApp
+ * @review: a #AsReview
+ * @cancellable: a #GCancellable, or %NULL
+ * @error: a #GError, or %NULL
+ *
+ * Submits a new end-user application review.
+ *
+ * Returns: %TRUE for success or if not relevant
+ **/
+gboolean gs_plugin_review_submit (GsPlugin *plugin,
+ GsApp *app,
+ AsReview *review,
+ GCancellable *cancellable,
+ GError **error);
+
+/**
+ * gs_plugin_review_upvote:
+ * @plugin: a #GsPlugin
+ * @app: a #GsApp
+ * @review: a #AsReview
+ * @cancellable: a #GCancellable, or %NULL
+ * @error: a #GError, or %NULL
+ *
+ * Upvote a specific review to indicate the review is helpful.
+ *
+ * Returns: %TRUE for success or if not relevant
+ **/
+gboolean gs_plugin_review_upvote (GsPlugin *plugin,
+ GsApp *app,
+ AsReview *review,
+ GCancellable *cancellable,
+ GError **error);
+
+/**
+ * gs_plugin_review_downvote:
+ * @plugin: a #GsPlugin
+ * @app: a #GsApp
+ * @review: a #AsReview
+ * @cancellable: a #GCancellable, or %NULL
+ * @error: a #GError, or %NULL
+ *
+ * Downvote a specific review to indicate the review is unhelpful.
+ *
+ * Plugins are expected to add new apps using gs_app_list_add().
+ *
+ * Returns: %TRUE for success or if not relevant
+ **/
+gboolean gs_plugin_review_downvote (GsPlugin *plugin,
+ GsApp *app,
+ AsReview *review,
+ GCancellable *cancellable,
+ GError **error);
+
+/**
+ * gs_plugin_review_report:
+ * @plugin: a #GsPlugin
+ * @app: a #GsApp
+ * @review: a #AsReview
+ * @cancellable: a #GCancellable, or %NULL
+ * @error: a #GError, or %NULL
+ *
+ * Report a review that is not suitable in some way.
+ * It is expected that this action flags a review to be checked by a moderator
+ * and that the review won't be shown to any users until this happens.
+ *
+ * Returns: %TRUE for success or if not relevant
+ **/
+gboolean gs_plugin_review_report (GsPlugin *plugin,
+ GsApp *app,
+ AsReview *review,
+ GCancellable *cancellable,
+ GError **error);
+
+/**
+ * gs_plugin_review_remove:
+ * @plugin: a #GsPlugin
+ * @app: a #GsApp
+ * @review: a #AsReview
+ * @cancellable: a #GCancellable, or %NULL
+ * @error: a #GError, or %NULL
+ *
+ * Remove a review that the user wrote.
+ * NOTE: Users should only be able to remove reviews with %AS_REVIEW_FLAG_SELF.
+ *
+ * Returns: %TRUE for success or if not relevant
+ **/
+gboolean gs_plugin_review_remove (GsPlugin *plugin,
+ GsApp *app,
+ AsReview *review,
+ GCancellable *cancellable,
+ GError **error);
+
+/**
+ * gs_plugin_review_dismiss:
+ * @plugin: a #GsPlugin
+ * @app: a #GsApp
+ * @review: a #AsReview
+ * @cancellable: a #GCancellable, or %NULL
+ * @error: a #GError, or %NULL
+ *
+ * Dismisses a review, i.e. hide it from future moderated views.
+ * This action is useful when the moderator is unable to speak the language of
+ * the review for example.
+ *
+ * Returns: %TRUE for success or if not relevant
+ **/
+gboolean gs_plugin_review_dismiss (GsPlugin *plugin,
+ GsApp *app,
+ AsReview *review,
+ GCancellable *cancellable,
+ GError **error);
+
+/**
+ * gs_plugin_refresh:
+ * @plugin: a #GsPlugin
+ * @cache_age: the acceptable cache age in seconds, or MAXUINT for "any"
+ * @cancellable: a #GCancellable, or %NULL
+ * @error: a #GError, or %NULL
+ *
+ * Refreshes the state of all the plugins. Plugins should make sure
+ * there's enough metadata to start the application, for example lists of
+ * available applications.
+ *
+ * All functions can block, but should sent progress notifications, e.g. using
+ * gs_app_set_progress() if they will take more than tens of milliseconds
+ * to complete.
+ *
+ * Returns: %TRUE for success or if not relevant
+ **/
+gboolean gs_plugin_refresh (GsPlugin *plugin,
+ guint cache_age,
+ GCancellable *cancellable,
+ GError **error);
+
+/**
+ * gs_plugin_file_to_app:
+ * @plugin: a #GsPlugin
+ * @list: a #GsAppList
+ * @file: a #GFile
+ * @cancellable: a #GCancellable, or %NULL
+ * @error: a #GError, or %NULL
+ *
+ * Converts a local file to a #GsApp. It's expected that only one plugin will
+ * match the mimetype of @file and that a single #GsApp will be in the returned
+ * list. If no plugins can handle the file, the list will be empty.
+ *
+ * For example, the PackageKit plugin can turn a .rpm file into a application
+ * of kind %AS_APP_KIND_UNKNOWN but that in some cases it will be further refined
+ * into a %AS_APP_KIND_DESKTOP (with all the extra metadata) by the appstream
+ * plugin.
+ *
+ * Plugins are expected to add new apps using gs_app_list_add().
+ *
+ * Returns: %TRUE for success or if not relevant
+ **/
+gboolean gs_plugin_file_to_app (GsPlugin *plugin,
+ GsAppList *list,
+ GFile *file,
+ GCancellable *cancellable,
+ GError **error);
+
+/**
+ * gs_plugin_url_to_app:
+ * @plugin: a #GsPlugin
+ * @list: a #GsAppList
+ * @url: a #URL, e.g. "apt://gimp"
+ * @cancellable: a #GCancellable, or %NULL
+ * @error: a #GError, or %NULL
+ *
+ * Converts a URL to a #GsApp. It's expected that only one plugin will
+ * match the scheme of @url and that a single #GsApp will be in the returned
+ * list. If no plugins can handle the file, the list will be empty.
+ *
+ * For example, the apt plugin can turn apt://gimp into a application.
+ *
+ * Plugins are expected to add new apps using gs_app_list_add().
+ *
+ * Returns: %TRUE for success or if not relevant
+ **/
+gboolean gs_plugin_url_to_app (GsPlugin *plugin,
+ GsAppList *list,
+ const gchar *url,
+ GCancellable *cancellable,
+ GError **error);
+
+/**
+ * gs_plugin_update:
+ * @plugin: a #GsPlugin
+ * @apps: a #GsAppList
+ * @cancellable: a #GCancellable, or %NULL
+ * @error: a #GError, or %NULL
+ *
+ * Updates a list of applications, typically scheduling them for offline update.
+ *
+ * Returns: %TRUE for success or if not relevant
+ **/
+gboolean gs_plugin_update (GsPlugin *plugin,
+ GsAppList *apps,
+ GCancellable *cancellable,
+ GError **error);
+
+/**
+ * gs_plugin_add_langpacks:
+ * @plugin: a #GsPlugin
+ * @list: a #GsAppList
+ * @locale: a #LANGUAGE_CODE or #LOCALE, e.g. "ja" or "ja_JP"
+ * @cancellable: a #GCancellable, or %NULL
+ * @error: a #GError, or %NULL
+ *
+ * Returns a list of language packs, as per input language code or locale.
+ *
+ * Returns: %TRUE for success or if not relevant
+ **/
+gboolean gs_plugin_add_langpacks (GsPlugin *plugin,
+ GsAppList *list,
+ const gchar *locale,
+ GCancellable *cancellable,
+ GError **error);
+
+G_END_DECLS
diff --git a/lib/gs-plugin.c b/lib/gs-plugin.c
new file mode 100644
index 0000000..c517de0
--- /dev/null
+++ b/lib/gs-plugin.c
@@ -0,0 +1,2069 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2013-2016 Richard Hughes <richard@hughsie.com>
+ * Copyright (C) 2014-2020 Kalev Lember <klember@redhat.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+/**
+ * SECTION:gs-plugin
+ * @title: GsPlugin Helpers
+ * @include: gnome-software.h
+ * @stability: Unstable
+ * @short_description: Runtime-loaded modules providing functionality
+ *
+ * Plugins are modules that are loaded at runtime to provide information
+ * about requests and to service user actions like installing, removing
+ * and updating.
+ * This allows different distributions to pick and choose how the
+ * application installer gathers data.
+ *
+ * Plugins also have a priority system where the largest number gets
+ * run first. That means if one plugin requires some property or
+ * metadata set by another plugin then it **must** depend on the other
+ * plugin to be run in the correct order.
+ *
+ * As a general rule, try to make plugins as small and self-contained
+ * as possible and remember to cache as much data as possible for speed.
+ * Memory is cheap, time less so.
+ */
+
+#include "config.h"
+
+#include <gio/gdesktopappinfo.h>
+#include <gdk/gdk.h>
+#include <string.h>
+
+#ifdef USE_VALGRIND
+#include <valgrind.h>
+#endif
+
+#include "gs-app-list-private.h"
+#include "gs-os-release.h"
+#include "gs-plugin-private.h"
+#include "gs-plugin.h"
+#include "gs-utils.h"
+
+typedef struct
+{
+ GHashTable *cache;
+ GMutex cache_mutex;
+ GModule *module;
+ GsPluginData *data; /* for gs-plugin-{name}.c */
+ GsPluginFlags flags;
+ SoupSession *soup_session;
+ GPtrArray *rules[GS_PLUGIN_RULE_LAST];
+ GHashTable *vfuncs; /* string:pointer */
+ GMutex vfuncs_mutex;
+ gboolean enabled;
+ guint interactive_cnt;
+ GMutex interactive_mutex;
+ gchar *locale; /* allow-none */
+ gchar *language; /* allow-none */
+ gchar *name;
+ gchar *appstream_id;
+ guint scale;
+ guint order;
+ guint priority;
+ guint timer_id;
+ GMutex timer_mutex;
+ GNetworkMonitor *network_monitor;
+} GsPluginPrivate;
+
+G_DEFINE_TYPE_WITH_PRIVATE (GsPlugin, gs_plugin, G_TYPE_OBJECT)
+
+G_DEFINE_QUARK (gs-plugin-error-quark, gs_plugin_error)
+
+enum {
+ PROP_0,
+ PROP_FLAGS,
+ PROP_LAST
+};
+
+enum {
+ SIGNAL_UPDATES_CHANGED,
+ SIGNAL_STATUS_CHANGED,
+ SIGNAL_RELOAD,
+ SIGNAL_REPORT_EVENT,
+ SIGNAL_ALLOW_UPDATES,
+ SIGNAL_BASIC_AUTH_START,
+ SIGNAL_LAST
+};
+
+static guint signals [SIGNAL_LAST] = { 0 };
+
+typedef const gchar **(*GsPluginGetDepsFunc) (GsPlugin *plugin);
+
+/**
+ * gs_plugin_status_to_string:
+ * @status: a #GsPluginStatus, e.g. %GS_PLUGIN_STATUS_DOWNLOADING
+ *
+ * Converts the #GsPluginStatus enum to a string.
+ *
+ * Returns: the string representation, or "unknown"
+ *
+ * Since: 3.22
+ **/
+const gchar *
+gs_plugin_status_to_string (GsPluginStatus status)
+{
+ if (status == GS_PLUGIN_STATUS_WAITING)
+ return "waiting";
+ if (status == GS_PLUGIN_STATUS_FINISHED)
+ return "finished";
+ if (status == GS_PLUGIN_STATUS_SETUP)
+ return "setup";
+ if (status == GS_PLUGIN_STATUS_DOWNLOADING)
+ return "downloading";
+ if (status == GS_PLUGIN_STATUS_QUERYING)
+ return "querying";
+ if (status == GS_PLUGIN_STATUS_INSTALLING)
+ return "installing";
+ if (status == GS_PLUGIN_STATUS_REMOVING)
+ return "removing";
+ return "unknown";
+}
+
+/**
+ * gs_plugin_set_name:
+ * @plugin: a #GsPlugin
+ * @name: a plugin name
+ *
+ * Sets the name of the plugin.
+ *
+ * Plugins are not required to set the plugin name as it is automatically set
+ * from the `.so` filename.
+ *
+ * Since: 3.26
+ **/
+void
+gs_plugin_set_name (GsPlugin *plugin, const gchar *name)
+{
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+ if (priv->name != NULL)
+ g_free (priv->name);
+ priv->name = g_strdup (name);
+}
+
+/**
+ * gs_plugin_create:
+ * @filename: an absolute filename
+ * @error: a #GError, or %NULL
+ *
+ * Creates a new plugin from an external module.
+ *
+ * Returns: the #GsPlugin or %NULL
+ *
+ * Since: 3.22
+ **/
+GsPlugin *
+gs_plugin_create (const gchar *filename, GError **error)
+{
+ GsPlugin *plugin = NULL;
+ GsPluginPrivate *priv;
+ g_autofree gchar *basename = NULL;
+
+ /* get the plugin name from the basename */
+ basename = g_path_get_basename (filename);
+ if (!g_str_has_prefix (basename, "libgs_plugin_")) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ "plugin filename has wrong prefix: %s",
+ filename);
+ return NULL;
+ }
+ g_strdelimit (basename, ".", '\0');
+
+ /* create new plugin */
+ plugin = gs_plugin_new ();
+ priv = gs_plugin_get_instance_private (plugin);
+ priv->module = g_module_open (filename, 0);
+ if (priv->module == NULL) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ "failed to open plugin %s: %s",
+ filename, g_module_error ());
+ return NULL;
+ }
+ gs_plugin_set_name (plugin, basename + 13);
+ return plugin;
+}
+
+static void
+gs_plugin_finalize (GObject *object)
+{
+ GsPlugin *plugin = GS_PLUGIN (object);
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+ guint i;
+
+ for (i = 0; i < GS_PLUGIN_RULE_LAST; i++)
+ g_ptr_array_unref (priv->rules[i]);
+
+ if (priv->timer_id > 0)
+ g_source_remove (priv->timer_id);
+ g_free (priv->name);
+ g_free (priv->appstream_id);
+ g_free (priv->data);
+ g_free (priv->locale);
+ g_free (priv->language);
+ if (priv->soup_session != NULL)
+ g_object_unref (priv->soup_session);
+ if (priv->network_monitor != NULL)
+ g_object_unref (priv->network_monitor);
+ g_hash_table_unref (priv->cache);
+ g_hash_table_unref (priv->vfuncs);
+ g_mutex_clear (&priv->cache_mutex);
+ g_mutex_clear (&priv->interactive_mutex);
+ g_mutex_clear (&priv->timer_mutex);
+ g_mutex_clear (&priv->vfuncs_mutex);
+#ifndef RUNNING_ON_VALGRIND
+ if (priv->module != NULL)
+ g_module_close (priv->module);
+#endif
+
+ G_OBJECT_CLASS (gs_plugin_parent_class)->finalize (object);
+}
+
+/**
+ * gs_plugin_get_data:
+ * @plugin: a #GsPlugin
+ *
+ * Gets the private data for the plugin if gs_plugin_alloc_data() has
+ * been called.
+ *
+ * Returns: the #GsPluginData, or %NULL
+ *
+ * Since: 3.22
+ **/
+GsPluginData *
+gs_plugin_get_data (GsPlugin *plugin)
+{
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+ g_assert (priv->data != NULL);
+ return priv->data;
+}
+
+/**
+ * gs_plugin_alloc_data:
+ * @plugin: a #GsPlugin
+ * @sz: the size of data to allocate, e.g. `sizeof(FooPluginPrivate)`
+ *
+ * Allocates a private data area for the plugin which can be retrieved
+ * using gs_plugin_get_data().
+ * This is normally called in gs_plugin_initialize() and the data should
+ * not be manually freed.
+ *
+ * Returns: the #GsPluginData, cleared to NUL bytes
+ *
+ * Since: 3.22
+ **/
+GsPluginData *
+gs_plugin_alloc_data (GsPlugin *plugin, gsize sz)
+{
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+ g_assert (priv->data == NULL);
+ priv->data = g_malloc0 (sz);
+ return priv->data;
+}
+
+/**
+ * gs_plugin_clear_data:
+ * @plugin: a #GsPlugin
+ *
+ * Clears and resets the private data. Only run this from the self tests.
+ **/
+void
+gs_plugin_clear_data (GsPlugin *plugin)
+{
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+ if (priv->data == NULL)
+ return;
+ g_clear_pointer (&priv->data, g_free);
+}
+
+/**
+ * gs_plugin_get_symbol: (skip)
+ * @plugin: a #GsPlugin
+ * @function_name: a symbol name
+ *
+ * Gets the symbol from the module that backs the plugin. If the plugin is not
+ * enabled then no symbol is returned.
+ *
+ * Returns: the pointer to the symbol, or %NULL
+ *
+ * Since: 3.22
+ **/
+gpointer
+gs_plugin_get_symbol (GsPlugin *plugin, const gchar *function_name)
+{
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+ gpointer func = NULL;
+ g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->vfuncs_mutex);
+
+ g_return_val_if_fail (function_name != NULL, NULL);
+
+ /* disabled plugins shouldn't be checked */
+ if (!priv->enabled)
+ return NULL;
+
+ /* look up the symbol from the cache */
+ if (g_hash_table_lookup_extended (priv->vfuncs, function_name, NULL, &func))
+ return func;
+
+ /* look up the symbol using the elf headers */
+ g_module_symbol (priv->module, function_name, &func);
+ g_hash_table_insert (priv->vfuncs, g_strdup (function_name), func);
+
+ return func;
+}
+
+/**
+ * gs_plugin_get_enabled:
+ * @plugin: a #GsPlugin
+ *
+ * Gets if the plugin is enabled.
+ *
+ * Returns: %TRUE if enabled
+ *
+ * Since: 3.22
+ **/
+gboolean
+gs_plugin_get_enabled (GsPlugin *plugin)
+{
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+ return priv->enabled;
+}
+
+/**
+ * gs_plugin_set_enabled:
+ * @plugin: a #GsPlugin
+ * @enabled: the enabled state
+ *
+ * Enables or disables a plugin.
+ * This is normally only called from gs_plugin_initialize().
+ *
+ * Since: 3.22
+ **/
+void
+gs_plugin_set_enabled (GsPlugin *plugin, gboolean enabled)
+{
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+ priv->enabled = enabled;
+}
+
+void
+gs_plugin_interactive_inc (GsPlugin *plugin)
+{
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+ g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->interactive_mutex);
+ priv->interactive_cnt++;
+ gs_plugin_add_flags (plugin, GS_PLUGIN_FLAGS_INTERACTIVE);
+}
+
+void
+gs_plugin_interactive_dec (GsPlugin *plugin)
+{
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+ g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&priv->interactive_mutex);
+ if (priv->interactive_cnt > 0)
+ priv->interactive_cnt--;
+ if (priv->interactive_cnt == 0)
+ gs_plugin_remove_flags (plugin, GS_PLUGIN_FLAGS_INTERACTIVE);
+}
+
+/**
+ * gs_plugin_get_name:
+ * @plugin: a #GsPlugin
+ *
+ * Gets the plugin name.
+ *
+ * Returns: a string, e.g. "fwupd"
+ *
+ * Since: 3.22
+ **/
+const gchar *
+gs_plugin_get_name (GsPlugin *plugin)
+{
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+ return priv->name;
+}
+
+/**
+ * gs_plugin_get_appstream_id:
+ * @plugin: a #GsPlugin
+ *
+ * Gets the plugin AppStream ID.
+ *
+ * Returns: a string, e.g. `org.gnome.Software.Plugin.Epiphany`
+ *
+ * Since: 3.24
+ **/
+const gchar *
+gs_plugin_get_appstream_id (GsPlugin *plugin)
+{
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+ return priv->appstream_id;
+}
+
+/**
+ * gs_plugin_set_appstream_id:
+ * @plugin: a #GsPlugin
+ * @appstream_id: an appstream ID, e.g. `org.gnome.Software.Plugin.Epiphany`
+ *
+ * Sets the plugin AppStream ID.
+ *
+ * Since: 3.24
+ **/
+void
+gs_plugin_set_appstream_id (GsPlugin *plugin, const gchar *appstream_id)
+{
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+ g_free (priv->appstream_id);
+ priv->appstream_id = g_strdup (appstream_id);
+}
+
+/**
+ * gs_plugin_get_scale:
+ * @plugin: a #GsPlugin
+ *
+ * Gets the window scale factor.
+ *
+ * Returns: the factor, usually 1 for standard screens or 2 for HiDPI
+ *
+ * Since: 3.22
+ **/
+guint
+gs_plugin_get_scale (GsPlugin *plugin)
+{
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+ return priv->scale;
+}
+
+/**
+ * gs_plugin_set_scale:
+ * @plugin: a #GsPlugin
+ * @scale: the window scale factor, usually 1 for standard screens or 2 for HiDPI
+ *
+ * Sets the window scale factor.
+ *
+ * Since: 3.22
+ **/
+void
+gs_plugin_set_scale (GsPlugin *plugin, guint scale)
+{
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+ priv->scale = scale;
+}
+
+/**
+ * gs_plugin_get_order:
+ * @plugin: a #GsPlugin
+ *
+ * Gets the plugin order, where higher numbers are run after lower
+ * numbers.
+ *
+ * Returns: the integer value
+ *
+ * Since: 3.22
+ **/
+guint
+gs_plugin_get_order (GsPlugin *plugin)
+{
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+ return priv->order;
+}
+
+/**
+ * gs_plugin_set_order:
+ * @plugin: a #GsPlugin
+ * @order: a integer value
+ *
+ * Sets the plugin order, where higher numbers are run after lower
+ * numbers.
+ *
+ * Since: 3.22
+ **/
+void
+gs_plugin_set_order (GsPlugin *plugin, guint order)
+{
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+ priv->order = order;
+}
+
+/**
+ * gs_plugin_get_priority:
+ * @plugin: a #GsPlugin
+ *
+ * Gets the plugin priority, where higher values will be chosen where
+ * multiple #GsApp's match a specific rule.
+ *
+ * Returns: the integer value
+ *
+ * Since: 3.22
+ **/
+guint
+gs_plugin_get_priority (GsPlugin *plugin)
+{
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+ return priv->priority;
+}
+
+/**
+ * gs_plugin_set_priority:
+ * @plugin: a #GsPlugin
+ * @priority: a integer value
+ *
+ * Sets the plugin priority, where higher values will be chosen where
+ * multiple #GsApp's match a specific rule.
+ *
+ * Since: 3.22
+ **/
+void
+gs_plugin_set_priority (GsPlugin *plugin, guint priority)
+{
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+ priv->priority = priority;
+}
+
+/**
+ * gs_plugin_get_locale:
+ * @plugin: a #GsPlugin
+ *
+ * Gets the user locale. This is in the form documented in `man 3 setlocale`:
+ * ```
+ * language[_territory][.codeset][@modifier]
+ * ```
+ * where `language` is an
+ * [ISO 639 language code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes),
+ * `territory` is an
+ * [ISO 3166 country code](https://en.wikipedia.org/wiki/ISO_3166-1), and
+ * `codeset` is a character set or encoding identifier like `ISO-8859-1` or
+ * `UTF-8`. For a list of all supported locales, run `locale -a`.
+ *
+ * Returns: the locale string, e.g. `en_GB` or `uz_UZ.utf8@cyrillic`
+ *
+ * Since: 3.22
+ **/
+const gchar *
+gs_plugin_get_locale (GsPlugin *plugin)
+{
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+ return priv->locale;
+}
+
+/**
+ * gs_plugin_get_language:
+ * @plugin: a #GsPlugin
+ *
+ * Gets the user language from the locale. This is the first component of the
+ * locale.
+ *
+ * Typically you should use the full locale rather than the language, as the
+ * same language can be used quite differently in different territories.
+ *
+ * Returns: the language string, e.g. `fr`
+ *
+ * Since: 3.22
+ **/
+const gchar *
+gs_plugin_get_language (GsPlugin *plugin)
+{
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+ return priv->language;
+}
+
+/**
+ * gs_plugin_set_locale:
+ * @plugin: a #GsPlugin
+ * @locale: a locale string, e.g. "en_GB"
+ *
+ * Sets the plugin locale.
+ *
+ * Since: 3.22
+ **/
+void
+gs_plugin_set_locale (GsPlugin *plugin, const gchar *locale)
+{
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+ g_free (priv->locale);
+ priv->locale = g_strdup (locale);
+}
+
+/**
+ * gs_plugin_set_language:
+ * @plugin: a #GsPlugin
+ * @language: a language string, e.g. "fr"
+ *
+ * Sets the plugin language.
+ *
+ * Since: 3.22
+ **/
+void
+gs_plugin_set_language (GsPlugin *plugin, const gchar *language)
+{
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+ g_free (priv->language);
+ priv->language = g_strdup (language);
+}
+
+/**
+ * gs_plugin_get_soup_session:
+ * @plugin: a #GsPlugin
+ *
+ * Gets the soup session that this plugin can use when downloading.
+ *
+ * Returns: the #SoupSession
+ *
+ * Since: 3.22
+ **/
+SoupSession *
+gs_plugin_get_soup_session (GsPlugin *plugin)
+{
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+ return priv->soup_session;
+}
+
+/**
+ * gs_plugin_set_soup_session:
+ * @plugin: a #GsPlugin
+ * @soup_session: a #SoupSession
+ *
+ * Sets the soup session that this plugin will use when downloading.
+ *
+ * Since: 3.22
+ **/
+void
+gs_plugin_set_soup_session (GsPlugin *plugin, SoupSession *soup_session)
+{
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+ g_set_object (&priv->soup_session, soup_session);
+}
+
+/**
+ * gs_plugin_set_network_monitor:
+ * @plugin: a #GsPlugin
+ * @monitor: a #GNetworkMonitor
+ *
+ * Sets the network monitor so that plugins can check the state of the network.
+ *
+ * Since: 3.28
+ **/
+void
+gs_plugin_set_network_monitor (GsPlugin *plugin, GNetworkMonitor *monitor)
+{
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+ g_set_object (&priv->network_monitor, monitor);
+}
+
+/**
+ * gs_plugin_get_network_available:
+ * @plugin: a #GsPlugin
+ *
+ * Gets whether a network connectivity is available.
+ *
+ * Returns: %TRUE if a network is available.
+ *
+ * Since: 3.28
+ **/
+gboolean
+gs_plugin_get_network_available (GsPlugin *plugin)
+{
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+ if (priv->network_monitor == NULL) {
+ g_debug ("no network monitor, so returning network-available=TRUE");
+ return TRUE;
+ }
+ return g_network_monitor_get_network_available (priv->network_monitor);
+}
+
+/**
+ * gs_plugin_has_flags:
+ * @plugin: a #GsPlugin
+ * @flags: a #GsPluginFlags, e.g. %GS_PLUGIN_FLAGS_RUNNING_SELF
+ *
+ * Finds out if a plugin has a specific flag set.
+ *
+ * Returns: TRUE if the flag is set
+ *
+ * Since: 3.22
+ **/
+gboolean
+gs_plugin_has_flags (GsPlugin *plugin, GsPluginFlags flags)
+{
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+ return (priv->flags & flags) > 0;
+}
+
+/**
+ * gs_plugin_add_flags:
+ * @plugin: a #GsPlugin
+ * @flags: a #GsPluginFlags, e.g. %GS_PLUGIN_FLAGS_RUNNING_SELF
+ *
+ * Adds specific flags to the plugin.
+ *
+ * Since: 3.22
+ **/
+void
+gs_plugin_add_flags (GsPlugin *plugin, GsPluginFlags flags)
+{
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+ priv->flags |= flags;
+}
+
+/**
+ * gs_plugin_remove_flags:
+ * @plugin: a #GsPlugin
+ * @flags: a #GsPluginFlags, e.g. %GS_PLUGIN_FLAGS_RUNNING_SELF
+ *
+ * Removes specific flags from the plugin.
+ *
+ * Since: 3.22
+ **/
+void
+gs_plugin_remove_flags (GsPlugin *plugin, GsPluginFlags flags)
+{
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+ priv->flags &= ~flags;
+}
+
+/**
+ * gs_plugin_add_rule:
+ * @plugin: a #GsPlugin
+ * @rule: a #GsPluginRule, e.g. %GS_PLUGIN_RULE_CONFLICTS
+ * @name: a plugin name, e.g. "appstream"
+ *
+ * If the plugin name is found, the rule will be used to sort the plugin list,
+ * for example the plugin specified by @name will be ordered after this plugin
+ * when %GS_PLUGIN_RULE_RUN_AFTER is used.
+ *
+ * NOTE: The depsolver is iterative and may not solve overly-complicated rules;
+ * If depsolving fails then gnome-software will not start.
+ *
+ * Since: 3.22
+ **/
+void
+gs_plugin_add_rule (GsPlugin *plugin, GsPluginRule rule, const gchar *name)
+{
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+ g_ptr_array_add (priv->rules[rule], g_strdup (name));
+}
+
+/**
+ * gs_plugin_get_rules:
+ * @plugin: a #GsPlugin
+ * @rule: a #GsPluginRule, e.g. %GS_PLUGIN_RULE_CONFLICTS
+ *
+ * Gets the plugin IDs that should be run after this plugin.
+ *
+ * Returns: (element-type utf8) (transfer none): the list of plugin names, e.g. ['appstream']
+ *
+ * Since: 3.22
+ **/
+GPtrArray *
+gs_plugin_get_rules (GsPlugin *plugin, GsPluginRule rule)
+{
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+ return priv->rules[rule];
+}
+
+/**
+ * gs_plugin_check_distro_id:
+ * @plugin: a #GsPlugin
+ * @distro_id: a distro ID, e.g. "fedora"
+ *
+ * Checks if the distro is compatible.
+ *
+ * Returns: %TRUE if compatible
+ *
+ * Since: 3.22
+ **/
+gboolean
+gs_plugin_check_distro_id (GsPlugin *plugin, const gchar *distro_id)
+{
+ g_autoptr(GError) error = NULL;
+ g_autoptr(GsOsRelease) os_release = NULL;
+ const gchar *id = NULL;
+
+ /* load /etc/os-release */
+ os_release = gs_os_release_new (&error);
+ if (os_release == NULL) {
+ g_debug ("could not parse os-release: %s", error->message);
+ return FALSE;
+ }
+
+ /* check that we are running on Fedora */
+ id = gs_os_release_get_id (os_release);
+ if (id == NULL) {
+ g_debug ("could not get distro ID");
+ return FALSE;
+ }
+ if (g_strcmp0 (id, distro_id) != 0)
+ return FALSE;
+ return TRUE;
+}
+
+typedef struct {
+ GsPlugin *plugin;
+ GsApp *app;
+ GsPluginStatus status;
+ guint percentage;
+} GsPluginStatusHelper;
+
+static gboolean
+gs_plugin_status_update_cb (gpointer user_data)
+{
+ GsPluginStatusHelper *helper = (GsPluginStatusHelper *) user_data;
+ g_signal_emit (helper->plugin,
+ signals[SIGNAL_STATUS_CHANGED], 0,
+ helper->app,
+ helper->status);
+ if (helper->app != NULL)
+ g_object_unref (helper->app);
+ g_slice_free (GsPluginStatusHelper, helper);
+ return FALSE;
+}
+
+/**
+ * gs_plugin_status_update:
+ * @plugin: a #GsPlugin
+ * @app: a #GsApp, or %NULL
+ * @status: a #GsPluginStatus, e.g. %GS_PLUGIN_STATUS_DOWNLOADING
+ *
+ * Update the state of the plugin so any UI can be updated.
+ *
+ * Since: 3.22
+ **/
+void
+gs_plugin_status_update (GsPlugin *plugin, GsApp *app, GsPluginStatus status)
+{
+ GsPluginStatusHelper *helper;
+ g_autoptr(GSource) idle_source = NULL;
+
+ helper = g_slice_new0 (GsPluginStatusHelper);
+ helper->plugin = plugin;
+ helper->status = status;
+ if (app != NULL)
+ helper->app = g_object_ref (app);
+ idle_source = g_idle_source_new ();
+ g_source_set_callback (idle_source, gs_plugin_status_update_cb, helper, NULL);
+ g_source_attach (idle_source, NULL);
+}
+
+typedef struct {
+ GsPlugin *plugin;
+ gchar *remote;
+ gchar *realm;
+ GCallback callback;
+ gpointer user_data;
+} GsPluginBasicAuthHelper;
+
+static gboolean
+gs_plugin_basic_auth_start_cb (gpointer user_data)
+{
+ GsPluginBasicAuthHelper *helper = user_data;
+ g_signal_emit (helper->plugin,
+ signals[SIGNAL_BASIC_AUTH_START], 0,
+ helper->remote,
+ helper->realm,
+ helper->callback,
+ helper->user_data);
+ g_free (helper->remote);
+ g_free (helper->realm);
+ g_slice_free (GsPluginBasicAuthHelper, helper);
+ return FALSE;
+}
+
+/**
+ * gs_plugin_basic_auth_start:
+ * @plugin: a #GsPlugin
+ * @remote: a string
+ * @realm: a string
+ * @callback: callback to invoke to submit the user/password
+ * @user_data: callback data to pass to the callback
+ *
+ * Emit the basic-auth-start signal in the main thread.
+ *
+ * Since: 3.38
+ **/
+void
+gs_plugin_basic_auth_start (GsPlugin *plugin,
+ const gchar *remote,
+ const gchar *realm,
+ GCallback callback,
+ gpointer user_data)
+{
+ GsPluginBasicAuthHelper *helper;
+ g_autoptr(GSource) idle_source = NULL;
+
+ helper = g_slice_new0 (GsPluginBasicAuthHelper);
+ helper->plugin = plugin;
+ helper->remote = g_strdup (remote);
+ helper->realm = g_strdup (realm);
+ helper->callback = callback;
+ helper->user_data = user_data;
+
+ idle_source = g_idle_source_new ();
+ g_source_set_callback (idle_source, gs_plugin_basic_auth_start_cb, helper, NULL);
+ g_source_attach (idle_source, NULL);
+}
+
+static gboolean
+gs_plugin_app_launch_cb (gpointer user_data)
+{
+ GAppInfo *appinfo = (GAppInfo *) user_data;
+ GdkDisplay *display;
+ g_autoptr(GAppLaunchContext) context = NULL;
+ g_autoptr(GError) error = NULL;
+
+ display = gdk_display_get_default ();
+ context = G_APP_LAUNCH_CONTEXT (gdk_display_get_app_launch_context (display));
+ if (!g_app_info_launch (appinfo, NULL, context, &error))
+ g_warning ("Failed to launch: %s", error->message);
+
+ return FALSE;
+}
+
+/**
+ * gs_plugin_app_launch:
+ * @plugin: a #GsPlugin
+ * @app: a #GsApp
+ * @error: a #GError, or %NULL
+ *
+ * Launches the application using #GAppInfo.
+ *
+ * Returns: %TRUE for success
+ *
+ * Since: 3.22
+ **/
+gboolean
+gs_plugin_app_launch (GsPlugin *plugin, GsApp *app, GError **error)
+{
+ const gchar *desktop_id;
+ g_autoptr(GAppInfo) appinfo = NULL;
+
+ desktop_id = gs_app_get_launchable (app, AS_LAUNCHABLE_KIND_DESKTOP_ID);
+ if (desktop_id == NULL)
+ desktop_id = gs_app_get_id (app);
+ if (desktop_id == NULL) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_NOT_SUPPORTED,
+ "no such desktop file: %s",
+ desktop_id);
+ return FALSE;
+ }
+ appinfo = G_APP_INFO (gs_utils_get_desktop_app_info (desktop_id));
+ if (appinfo == NULL) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_NOT_SUPPORTED,
+ "no such desktop file: %s",
+ desktop_id);
+ return FALSE;
+ }
+ g_idle_add_full (G_PRIORITY_DEFAULT,
+ gs_plugin_app_launch_cb,
+ g_object_ref (appinfo),
+ (GDestroyNotify) g_object_unref);
+ return TRUE;
+}
+
+static gboolean
+gs_plugin_updates_changed_cb (gpointer user_data)
+{
+ GsPlugin *plugin = GS_PLUGIN (user_data);
+ g_signal_emit (plugin, signals[SIGNAL_UPDATES_CHANGED], 0);
+ return FALSE;
+}
+
+/**
+ * gs_plugin_updates_changed:
+ * @plugin: a #GsPlugin
+ *
+ * Emit a signal that tells the plugin loader that the list of updates
+ * may have changed.
+ *
+ * Since: 3.22
+ **/
+void
+gs_plugin_updates_changed (GsPlugin *plugin)
+{
+ g_idle_add (gs_plugin_updates_changed_cb, plugin);
+}
+
+static gboolean
+gs_plugin_reload_cb (gpointer user_data)
+{
+ GsPlugin *plugin = GS_PLUGIN (user_data);
+ g_signal_emit (plugin, signals[SIGNAL_RELOAD], 0);
+ return FALSE;
+}
+
+/**
+ * gs_plugin_reload:
+ * @plugin: a #GsPlugin
+ *
+ * Plugins that call this function should expect that all panels will
+ * reload after a small delay, causing mush flashing, wailing and
+ * gnashing of teeth.
+ *
+ * Plugins should not call this unless absolutely required.
+ *
+ * Since: 3.22
+ **/
+void
+gs_plugin_reload (GsPlugin *plugin)
+{
+ g_debug ("emitting ::reload in idle");
+ g_idle_add (gs_plugin_reload_cb, plugin);
+}
+
+typedef struct {
+ GsPlugin *plugin;
+ GsApp *app;
+ GCancellable *cancellable;
+} GsPluginDownloadHelper;
+
+static void
+gs_plugin_download_chunk_cb (SoupMessage *msg, SoupBuffer *chunk,
+ GsPluginDownloadHelper *helper)
+{
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (helper->plugin);
+ guint percentage;
+ goffset header_size;
+ goffset body_length;
+
+ /* cancelled? */
+ if (g_cancellable_is_cancelled (helper->cancellable)) {
+ g_debug ("cancelling download of %s",
+ gs_app_get_id (helper->app));
+ soup_session_cancel_message (priv->soup_session,
+ msg,
+ SOUP_STATUS_CANCELLED);
+ return;
+ }
+
+ /* if it's returning "Found" or an error, ignore the percentage */
+ if (msg->status_code != SOUP_STATUS_OK) {
+ g_debug ("ignoring status code %u (%s)",
+ msg->status_code, msg->reason_phrase);
+ return;
+ }
+
+ /* get data */
+ body_length = msg->response_body->length;
+ header_size = soup_message_headers_get_content_length (msg->response_headers);
+
+ /* size is not known */
+ if (header_size < body_length)
+ return;
+
+ /* calculate percentage */
+ percentage = (guint) ((100 * body_length) / header_size);
+ g_debug ("%s progress: %u%%", gs_app_get_id (helper->app), percentage);
+ gs_app_set_progress (helper->app, percentage);
+ gs_plugin_status_update (helper->plugin,
+ helper->app,
+ GS_PLUGIN_STATUS_DOWNLOADING);
+}
+
+/**
+ * gs_plugin_download_data:
+ * @plugin: a #GsPlugin
+ * @app: a #GsApp, or %NULL
+ * @uri: a remote URI
+ * @cancellable: a #GCancellable, or %NULL
+ * @error: a #GError, or %NULL
+ *
+ * Downloads data.
+ *
+ * Returns: the downloaded data, or %NULL
+ *
+ * Since: 3.22
+ **/
+GBytes *
+gs_plugin_download_data (GsPlugin *plugin,
+ GsApp *app,
+ const gchar *uri,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+ GsPluginDownloadHelper helper;
+ guint status_code;
+ g_autoptr(SoupMessage) msg = NULL;
+
+ g_return_val_if_fail (GS_IS_PLUGIN (plugin), NULL);
+ g_return_val_if_fail (uri != NULL, NULL);
+ g_return_val_if_fail (error == NULL || *error == NULL, NULL);
+
+ /* local */
+ if (g_str_has_prefix (uri, "file://")) {
+ gsize length = 0;
+ g_autofree gchar *contents = NULL;
+ g_autoptr(GError) error_local = NULL;
+ g_debug ("copying %s from plugin %s", uri, priv->name);
+ if (!g_file_get_contents (uri + 7, &contents, &length, &error_local)) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_DOWNLOAD_FAILED,
+ "failed to copy %s: %s",
+ uri, error_local->message);
+ return NULL;
+ }
+ return g_bytes_new (contents, length);
+ }
+
+ /* remote */
+ g_debug ("downloading %s from plugin %s", uri, priv->name);
+ msg = soup_message_new (SOUP_METHOD_GET, uri);
+ if (app != NULL) {
+ helper.plugin = plugin;
+ helper.app = app;
+ helper.cancellable = cancellable;
+ g_signal_connect (msg, "got-chunk",
+ G_CALLBACK (gs_plugin_download_chunk_cb),
+ &helper);
+ }
+ status_code = soup_session_send_message (priv->soup_session, msg);
+ if (status_code != SOUP_STATUS_OK) {
+ g_autoptr(GString) str = g_string_new (NULL);
+ g_string_append (str, soup_status_get_phrase (status_code));
+ if (msg->response_body->data != NULL) {
+ g_string_append (str, ": ");
+ g_string_append (str, msg->response_body->data);
+ }
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_DOWNLOAD_FAILED,
+ "failed to download %s: %s",
+ uri, str->str);
+ return NULL;
+ }
+ return g_bytes_new (msg->response_body->data,
+ (gsize) msg->response_body->length);
+}
+
+/**
+ * gs_plugin_download_file:
+ * @plugin: a #GsPlugin
+ * @app: a #GsApp, or %NULL
+ * @uri: a remote URI
+ * @filename: a local filename
+ * @cancellable: a #GCancellable, or %NULL
+ * @error: a #GError, or %NULL
+ *
+ * Downloads data and saves it to a file.
+ *
+ * Returns: %TRUE for success
+ *
+ * Since: 3.22
+ **/
+gboolean
+gs_plugin_download_file (GsPlugin *plugin,
+ GsApp *app,
+ const gchar *uri,
+ const gchar *filename,
+ GCancellable *cancellable,
+ GError **error)
+{
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+ GsPluginDownloadHelper helper;
+ guint status_code;
+ g_autoptr(GError) error_local = NULL;
+ g_autoptr(SoupMessage) msg = NULL;
+
+ g_return_val_if_fail (GS_IS_PLUGIN (plugin), FALSE);
+ g_return_val_if_fail (uri != NULL, FALSE);
+ g_return_val_if_fail (filename != NULL, FALSE);
+ g_return_val_if_fail (error == NULL || *error == NULL, FALSE);
+
+ /* local */
+ if (g_str_has_prefix (uri, "file://")) {
+ gsize length = 0;
+ g_autofree gchar *contents = NULL;
+ g_debug ("copying %s from plugin %s", uri, priv->name);
+ if (!g_file_get_contents (uri + 7, &contents, &length, &error_local)) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_DOWNLOAD_FAILED,
+ "failed to copy %s: %s",
+ uri, error_local->message);
+ return FALSE;
+ }
+ if (!g_file_set_contents (filename, contents, length, &error_local)) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_WRITE_FAILED,
+ "Failed to save file: %s",
+ error_local->message);
+ return FALSE;
+ }
+ return TRUE;
+ }
+
+ /* remote */
+ g_debug ("downloading %s to %s from plugin %s", uri, filename, priv->name);
+ msg = soup_message_new (SOUP_METHOD_GET, uri);
+ if (msg == NULL) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_DOWNLOAD_FAILED,
+ "failed to parse URI %s", uri);
+ return FALSE;
+ }
+ if (app != NULL) {
+ helper.plugin = plugin;
+ helper.app = app;
+ helper.cancellable = cancellable;
+ g_signal_connect (msg, "got-chunk",
+ G_CALLBACK (gs_plugin_download_chunk_cb),
+ &helper);
+ }
+ status_code = soup_session_send_message (priv->soup_session, msg);
+ if (status_code != SOUP_STATUS_OK) {
+ g_autoptr(GString) str = g_string_new (NULL);
+ g_string_append (str, soup_status_get_phrase (status_code));
+ if (msg->response_body->data != NULL) {
+ g_string_append (str, ": ");
+ g_string_append (str, msg->response_body->data);
+ }
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_DOWNLOAD_FAILED,
+ "failed to download %s: %s",
+ uri, str->str);
+ return FALSE;
+ }
+ if (!gs_mkdir_parent (filename, error))
+ return FALSE;
+ if (!g_file_set_contents (filename,
+ msg->response_body->data,
+ msg->response_body->length,
+ &error_local)) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_WRITE_FAILED,
+ "Failed to save file: %s",
+ error_local->message);
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static gchar *
+gs_plugin_download_rewrite_resource_uri (GsPlugin *plugin,
+ GsApp *app,
+ const gchar *uri,
+ GCancellable *cancellable,
+ GError **error)
+{
+ g_autofree gchar *cachefn = NULL;
+
+ /* local files */
+ if (g_str_has_prefix (uri, "file://"))
+ uri += 7;
+ if (g_str_has_prefix (uri, "/")) {
+ if (!g_file_test (uri, G_FILE_TEST_EXISTS)) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_NOT_SUPPORTED,
+ "Failed to find file: %s", uri);
+ return NULL;
+ }
+ return g_strdup (uri);
+ }
+
+ /* get cache location */
+ cachefn = gs_utils_get_cache_filename ("cssresource", uri,
+ GS_UTILS_CACHE_FLAG_WRITEABLE |
+ GS_UTILS_CACHE_FLAG_USE_HASH,
+ error);
+ if (cachefn == NULL)
+ return NULL;
+
+ /* already exists */
+ if (g_file_test (cachefn, G_FILE_TEST_EXISTS))
+ return g_steal_pointer (&cachefn);
+
+ /* download */
+ if (!gs_plugin_download_file (plugin, app, uri, cachefn,
+ cancellable, error)) {
+ return NULL;
+ }
+ return g_steal_pointer (&cachefn);
+}
+
+/**
+ * gs_plugin_download_rewrite_resource:
+ * @plugin: a #GsPlugin
+ * @app: a #GsApp, or %NULL
+ * @resource: the CSS resource
+ * @cancellable: a #GCancellable, or %NULL
+ * @error: a #GError, or %NULL
+ *
+ * Downloads remote assets and rewrites a CSS resource to use cached local URIs.
+ *
+ * Returns: %TRUE for success
+ *
+ * Since: 3.26
+ **/
+gchar *
+gs_plugin_download_rewrite_resource (GsPlugin *plugin,
+ GsApp *app,
+ const gchar *resource,
+ GCancellable *cancellable,
+ GError **error)
+{
+ guint start = 0;
+ g_autoptr(GString) resource_str = g_string_new (resource);
+ g_autoptr(GString) str = g_string_new (NULL);
+
+ g_return_val_if_fail (GS_IS_PLUGIN (plugin), NULL);
+ g_return_val_if_fail (resource != NULL, NULL);
+ g_return_val_if_fail (error == NULL || *error == NULL, NULL);
+
+ /* replace datadir */
+ as_utils_string_replace (resource_str, "@datadir@", DATADIR);
+ resource = resource_str->str;
+
+ /* look in string for any url() links */
+ for (guint i = 0; resource[i] != '\0'; i++) {
+ if (i > 4 && strncmp (resource + i - 4, "url(", 4) == 0) {
+ start = i;
+ continue;
+ }
+ if (start == 0) {
+ g_string_append_c (str, resource[i]);
+ continue;
+ }
+ if (resource[i] == ')') {
+ guint len;
+ g_autofree gchar *cachefn = NULL;
+ g_autofree gchar *uri = NULL;
+
+ /* remove optional single quotes */
+ if (resource[start] == '\'' || resource[start] == '"')
+ start++;
+ len = i - start;
+ if (i > 0 && (resource[i - 1] == '\'' || resource[i - 1] == '"'))
+ len--;
+ uri = g_strndup (resource + start, len);
+
+ /* download them to per-user cache */
+ cachefn = gs_plugin_download_rewrite_resource_uri (plugin,
+ app,
+ uri,
+ cancellable,
+ error);
+ if (cachefn == NULL)
+ return NULL;
+ g_string_append_printf (str, "'%s'", cachefn);
+ g_string_append_c (str, resource[i]);
+ start = 0;
+ }
+ }
+ return g_strdup (str->str);
+}
+
+/**
+ * gs_plugin_cache_lookup:
+ * @plugin: a #GsPlugin
+ * @key: a string
+ *
+ * Looks up an application object from the per-plugin cache
+ *
+ * Returns: (transfer full) (nullable): the #GsApp, or %NULL
+ *
+ * Since: 3.22
+ **/
+GsApp *
+gs_plugin_cache_lookup (GsPlugin *plugin, const gchar *key)
+{
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+ GsApp *app;
+ g_autoptr(GMutexLocker) locker = NULL;
+
+ g_return_val_if_fail (GS_IS_PLUGIN (plugin), NULL);
+ g_return_val_if_fail (key != NULL, NULL);
+
+ locker = g_mutex_locker_new (&priv->cache_mutex);
+ app = g_hash_table_lookup (priv->cache, key);
+ if (app == NULL)
+ return NULL;
+ return g_object_ref (app);
+}
+
+/**
+ * gs_plugin_cache_remove:
+ * @plugin: a #GsPlugin
+ * @key: a key which matches
+ *
+ * Removes an application from the per-plugin cache.
+ *
+ * Since: 3.22
+ **/
+void
+gs_plugin_cache_remove (GsPlugin *plugin, const gchar *key)
+{
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+ g_autoptr(GMutexLocker) locker = NULL;
+
+ g_return_if_fail (GS_IS_PLUGIN (plugin));
+ g_return_if_fail (key != NULL);
+
+ locker = g_mutex_locker_new (&priv->cache_mutex);
+ g_hash_table_remove (priv->cache, key);
+}
+
+/**
+ * gs_plugin_cache_add:
+ * @plugin: a #GsPlugin
+ * @key: a string, or %NULL if the unique ID should be used
+ * @app: a #GsApp
+ *
+ * Adds an application to the per-plugin cache. This is optional,
+ * and the plugin can use the cache however it likes.
+ *
+ * Since: 3.22
+ **/
+void
+gs_plugin_cache_add (GsPlugin *plugin, const gchar *key, GsApp *app)
+{
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+ g_autoptr(GMutexLocker) locker = NULL;
+
+ g_return_if_fail (GS_IS_PLUGIN (plugin));
+ g_return_if_fail (GS_IS_APP (app));
+
+ locker = g_mutex_locker_new (&priv->cache_mutex);
+
+ /* the user probably doesn't want to do this */
+ if (gs_app_has_quirk (app, GS_APP_QUIRK_IS_WILDCARD)) {
+ g_warning ("adding wildcard app %s to plugin cache",
+ gs_app_get_unique_id (app));
+ }
+
+ /* default */
+ if (key == NULL)
+ key = gs_app_get_unique_id (app);
+
+ g_return_if_fail (key != NULL);
+
+ if (g_hash_table_lookup (priv->cache, key) == app)
+ return;
+ g_hash_table_insert (priv->cache, g_strdup (key), g_object_ref (app));
+}
+
+/**
+ * gs_plugin_cache_invalidate:
+ * @plugin: a #GsPlugin
+ *
+ * Invalidate the per-plugin cache by marking all entries as invalid.
+ * This is optional, and the plugin can evict the cache whenever it
+ * likes. Using this function may mean the front-end and the plugin
+ * may be operating on a different GsApp with the same cache ID.
+ *
+ * Most plugins do not need to call this function; if a suitable cache
+ * key is being used the old cache item can remain.
+ *
+ * Since: 3.22
+ **/
+void
+gs_plugin_cache_invalidate (GsPlugin *plugin)
+{
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+ g_autoptr(GMutexLocker) locker = NULL;
+
+ g_return_if_fail (GS_IS_PLUGIN (plugin));
+
+ locker = g_mutex_locker_new (&priv->cache_mutex);
+ g_hash_table_remove_all (priv->cache);
+}
+
+/**
+ * gs_plugin_report_event:
+ * @plugin: a #GsPlugin
+ * @event: a #GsPluginEvent
+ *
+ * Report a non-fatal event to the UI. Plugins should not assume that a
+ * specific event is actually shown to the user as it may be ignored
+ * automatically.
+ *
+ * Since: 3.24
+ **/
+void
+gs_plugin_report_event (GsPlugin *plugin, GsPluginEvent *event)
+{
+ g_return_if_fail (GS_IS_PLUGIN (plugin));
+ g_return_if_fail (GS_IS_PLUGIN_EVENT (event));
+ g_signal_emit (plugin, signals[SIGNAL_REPORT_EVENT], 0, event);
+}
+
+/**
+ * gs_plugin_set_allow_updates:
+ * @plugin: a #GsPlugin
+ * @allow_updates: boolean
+ *
+ * This allows plugins to inhibit the showing of the updates panel.
+ * This will typically be used when the required permissions are not possible
+ * to obtain, or when a LiveUSB image is low on space.
+ *
+ * By default, the updates panel is shown so plugins do not need to call this
+ * function unless they called gs_plugin_set_allow_updates() with %FALSE.
+ *
+ * Since: 3.24
+ **/
+void
+gs_plugin_set_allow_updates (GsPlugin *plugin, gboolean allow_updates)
+{
+ g_return_if_fail (GS_IS_PLUGIN (plugin));
+ g_signal_emit (plugin, signals[SIGNAL_ALLOW_UPDATES], 0, allow_updates);
+}
+
+/**
+ * gs_plugin_error_to_string:
+ * @error: a #GsPluginError, e.g. %GS_PLUGIN_ERROR_NO_NETWORK
+ *
+ * Converts the enumerated error to a string.
+ *
+ * Returns: a string, or %NULL for invalid
+ **/
+const gchar *
+gs_plugin_error_to_string (GsPluginError error)
+{
+ if (error == GS_PLUGIN_ERROR_FAILED)
+ return "failed";
+ if (error == GS_PLUGIN_ERROR_NOT_SUPPORTED)
+ return "not-supported";
+ if (error == GS_PLUGIN_ERROR_CANCELLED)
+ return "cancelled";
+ if (error == GS_PLUGIN_ERROR_NO_NETWORK)
+ return "no-network";
+ if (error == GS_PLUGIN_ERROR_NO_SECURITY)
+ return "no-security";
+ if (error == GS_PLUGIN_ERROR_NO_SPACE)
+ return "no-space";
+ if (error == GS_PLUGIN_ERROR_AUTH_REQUIRED)
+ return "auth-required";
+ if (error == GS_PLUGIN_ERROR_AUTH_INVALID)
+ return "auth-invalid";
+ if (error == GS_PLUGIN_ERROR_PLUGIN_DEPSOLVE_FAILED)
+ return "plugin-depsolve-failed";
+ if (error == GS_PLUGIN_ERROR_DOWNLOAD_FAILED)
+ return "download-failed";
+ if (error == GS_PLUGIN_ERROR_WRITE_FAILED)
+ return "write-failed";
+ if (error == GS_PLUGIN_ERROR_INVALID_FORMAT)
+ return "invalid-format";
+ if (error == GS_PLUGIN_ERROR_DELETE_FAILED)
+ return "delete-failed";
+ if (error == GS_PLUGIN_ERROR_RESTART_REQUIRED)
+ return "restart-required";
+ if (error == GS_PLUGIN_ERROR_AC_POWER_REQUIRED)
+ return "ac-power-required";
+ if (error == GS_PLUGIN_ERROR_BATTERY_LEVEL_TOO_LOW)
+ return "battery-level-too-low";
+ if (error == GS_PLUGIN_ERROR_TIMED_OUT)
+ return "timed-out";
+ return NULL;
+}
+
+/**
+ * gs_plugin_action_to_function_name: (skip)
+ * @action: a #GsPluginAction, e.g. %GS_PLUGIN_ERROR_NO_NETWORK
+ *
+ * Converts the enumerated action to the vfunc name.
+ *
+ * Returns: a string, or %NULL for invalid
+ **/
+const gchar *
+gs_plugin_action_to_function_name (GsPluginAction action)
+{
+ if (action == GS_PLUGIN_ACTION_REFRESH)
+ return "gs_plugin_refresh";
+ if (action == GS_PLUGIN_ACTION_REVIEW_SUBMIT)
+ return "gs_plugin_review_submit";
+ if (action == GS_PLUGIN_ACTION_REVIEW_UPVOTE)
+ return "gs_plugin_review_upvote";
+ if (action == GS_PLUGIN_ACTION_REVIEW_DOWNVOTE)
+ return "gs_plugin_review_downvote";
+ if (action == GS_PLUGIN_ACTION_REVIEW_REPORT)
+ return "gs_plugin_review_report";
+ if (action == GS_PLUGIN_ACTION_REVIEW_REMOVE)
+ return "gs_plugin_review_remove";
+ if (action == GS_PLUGIN_ACTION_REVIEW_DISMISS)
+ return "gs_plugin_review_dismiss";
+ if (action == GS_PLUGIN_ACTION_INSTALL)
+ return "gs_plugin_app_install";
+ if (action == GS_PLUGIN_ACTION_REMOVE)
+ return "gs_plugin_app_remove";
+ if (action == GS_PLUGIN_ACTION_SET_RATING)
+ return "gs_plugin_app_set_rating";
+ if (action == GS_PLUGIN_ACTION_UPGRADE_DOWNLOAD)
+ return "gs_plugin_app_upgrade_download";
+ if (action == GS_PLUGIN_ACTION_UPGRADE_TRIGGER)
+ return "gs_plugin_app_upgrade_trigger";
+ if (action == GS_PLUGIN_ACTION_LAUNCH)
+ return "gs_plugin_launch";
+ if (action == GS_PLUGIN_ACTION_UPDATE_CANCEL)
+ return "gs_plugin_update_cancel";
+ if (action == GS_PLUGIN_ACTION_ADD_SHORTCUT)
+ return "gs_plugin_add_shortcut";
+ if (action == GS_PLUGIN_ACTION_REMOVE_SHORTCUT)
+ return "gs_plugin_remove_shortcut";
+ if (action == GS_PLUGIN_ACTION_REFINE)
+ return "gs_plugin_refine";
+ if (action == GS_PLUGIN_ACTION_UPDATE)
+ return "gs_plugin_update";
+ if (action == GS_PLUGIN_ACTION_DOWNLOAD)
+ return "gs_plugin_download";
+ if (action == GS_PLUGIN_ACTION_FILE_TO_APP)
+ return "gs_plugin_file_to_app";
+ if (action == GS_PLUGIN_ACTION_URL_TO_APP)
+ return "gs_plugin_url_to_app";
+ if (action == GS_PLUGIN_ACTION_GET_DISTRO_UPDATES)
+ return "gs_plugin_add_distro_upgrades";
+ if (action == GS_PLUGIN_ACTION_GET_SOURCES)
+ return "gs_plugin_add_sources";
+ if (action == GS_PLUGIN_ACTION_GET_UNVOTED_REVIEWS)
+ return "gs_plugin_add_unvoted_reviews";
+ if (action == GS_PLUGIN_ACTION_GET_INSTALLED)
+ return "gs_plugin_add_installed";
+ if (action == GS_PLUGIN_ACTION_GET_FEATURED)
+ return "gs_plugin_add_featured";
+ if (action == GS_PLUGIN_ACTION_GET_UPDATES_HISTORICAL)
+ return "gs_plugin_add_updates_historical";
+ if (action == GS_PLUGIN_ACTION_GET_UPDATES)
+ return "gs_plugin_add_updates";
+ if (action == GS_PLUGIN_ACTION_GET_POPULAR)
+ return "gs_plugin_add_popular";
+ if (action == GS_PLUGIN_ACTION_GET_RECENT)
+ return "gs_plugin_add_recent";
+ if (action == GS_PLUGIN_ACTION_SEARCH)
+ return "gs_plugin_add_search";
+ if (action == GS_PLUGIN_ACTION_SEARCH_FILES)
+ return "gs_plugin_add_search_files";
+ if (action == GS_PLUGIN_ACTION_SEARCH_PROVIDES)
+ return "gs_plugin_add_search_what_provides";
+ if (action == GS_PLUGIN_ACTION_GET_CATEGORY_APPS)
+ return "gs_plugin_add_category_apps";
+ if (action == GS_PLUGIN_ACTION_GET_CATEGORIES)
+ return "gs_plugin_add_categories";
+ if (action == GS_PLUGIN_ACTION_SETUP)
+ return "gs_plugin_setup";
+ if (action == GS_PLUGIN_ACTION_INITIALIZE)
+ return "gs_plugin_initialize";
+ if (action == GS_PLUGIN_ACTION_DESTROY)
+ return "gs_plugin_destroy";
+ if (action == GS_PLUGIN_ACTION_GET_ALTERNATES)
+ return "gs_plugin_add_alternates";
+ if (action == GS_PLUGIN_ACTION_GET_LANGPACKS)
+ return "gs_plugin_add_langpacks";
+ return NULL;
+}
+
+/**
+ * gs_plugin_action_to_string:
+ * @action: a #GsPluginAction, e.g. %GS_PLUGIN_ERROR_NO_NETWORK
+ *
+ * Converts the enumerated action to a string.
+ *
+ * Returns: a string, or %NULL for invalid
+ **/
+const gchar *
+gs_plugin_action_to_string (GsPluginAction action)
+{
+ if (action == GS_PLUGIN_ACTION_UNKNOWN)
+ return "unknown";
+ if (action == GS_PLUGIN_ACTION_SETUP)
+ return "setup";
+ if (action == GS_PLUGIN_ACTION_INSTALL)
+ return "install";
+ if (action == GS_PLUGIN_ACTION_DOWNLOAD)
+ return "download";
+ if (action == GS_PLUGIN_ACTION_REMOVE)
+ return "remove";
+ if (action == GS_PLUGIN_ACTION_UPDATE)
+ return "update";
+ if (action == GS_PLUGIN_ACTION_SET_RATING)
+ return "set-rating";
+ if (action == GS_PLUGIN_ACTION_UPGRADE_DOWNLOAD)
+ return "upgrade-download";
+ if (action == GS_PLUGIN_ACTION_UPGRADE_TRIGGER)
+ return "upgrade-trigger";
+ if (action == GS_PLUGIN_ACTION_LAUNCH)
+ return "launch";
+ if (action == GS_PLUGIN_ACTION_UPDATE_CANCEL)
+ return "update-cancel";
+ if (action == GS_PLUGIN_ACTION_ADD_SHORTCUT)
+ return "add-shortcut";
+ if (action == GS_PLUGIN_ACTION_REMOVE_SHORTCUT)
+ return "remove-shortcut";
+ if (action == GS_PLUGIN_ACTION_REVIEW_SUBMIT)
+ return "review-submit";
+ if (action == GS_PLUGIN_ACTION_REVIEW_UPVOTE)
+ return "review-upvote";
+ if (action == GS_PLUGIN_ACTION_REVIEW_DOWNVOTE)
+ return "review-downvote";
+ if (action == GS_PLUGIN_ACTION_REVIEW_REPORT)
+ return "review-report";
+ if (action == GS_PLUGIN_ACTION_REVIEW_REMOVE)
+ return "review-remove";
+ if (action == GS_PLUGIN_ACTION_REVIEW_DISMISS)
+ return "review-dismiss";
+ if (action == GS_PLUGIN_ACTION_GET_UPDATES)
+ return "get-updates";
+ if (action == GS_PLUGIN_ACTION_GET_DISTRO_UPDATES)
+ return "get-distro-updates";
+ if (action == GS_PLUGIN_ACTION_GET_UNVOTED_REVIEWS)
+ return "get-unvoted-reviews";
+ if (action == GS_PLUGIN_ACTION_GET_SOURCES)
+ return "get-sources";
+ if (action == GS_PLUGIN_ACTION_GET_INSTALLED)
+ return "get-installed";
+ if (action == GS_PLUGIN_ACTION_GET_POPULAR)
+ return "get-popular";
+ if (action == GS_PLUGIN_ACTION_GET_FEATURED)
+ return "get-featured";
+ if (action == GS_PLUGIN_ACTION_SEARCH)
+ return "search";
+ if (action == GS_PLUGIN_ACTION_SEARCH_FILES)
+ return "search-files";
+ if (action == GS_PLUGIN_ACTION_SEARCH_PROVIDES)
+ return "search-provides";
+ if (action == GS_PLUGIN_ACTION_GET_CATEGORIES)
+ return "get-categories";
+ if (action == GS_PLUGIN_ACTION_GET_CATEGORY_APPS)
+ return "get-category-apps";
+ if (action == GS_PLUGIN_ACTION_REFINE)
+ return "refine";
+ if (action == GS_PLUGIN_ACTION_REFRESH)
+ return "refresh";
+ if (action == GS_PLUGIN_ACTION_FILE_TO_APP)
+ return "file-to-app";
+ if (action == GS_PLUGIN_ACTION_URL_TO_APP)
+ return "url-to-app";
+ if (action == GS_PLUGIN_ACTION_GET_RECENT)
+ return "get-recent";
+ if (action == GS_PLUGIN_ACTION_GET_UPDATES_HISTORICAL)
+ return "get-updates-historical";
+ if (action == GS_PLUGIN_ACTION_INITIALIZE)
+ return "initialize";
+ if (action == GS_PLUGIN_ACTION_DESTROY)
+ return "destroy";
+ if (action == GS_PLUGIN_ACTION_GET_ALTERNATES)
+ return "get-alternates";
+ if (action == GS_PLUGIN_ACTION_GET_LANGPACKS)
+ return "get-langpacks";
+ return NULL;
+}
+
+/**
+ * gs_plugin_action_from_string:
+ * @action: a #GsPluginAction, e.g. "install"
+ *
+ * Converts the string to an enumerated action.
+ *
+ * Returns: a GsPluginAction, e.g. %GS_PLUGIN_ACTION_INSTALL
+ *
+ * Since: 3.26
+ **/
+GsPluginAction
+gs_plugin_action_from_string (const gchar *action)
+{
+ if (g_strcmp0 (action, "setup") == 0)
+ return GS_PLUGIN_ACTION_SETUP;
+ if (g_strcmp0 (action, "install") == 0)
+ return GS_PLUGIN_ACTION_INSTALL;
+ if (g_strcmp0 (action, "download") == 0)
+ return GS_PLUGIN_ACTION_DOWNLOAD;
+ if (g_strcmp0 (action, "remove") == 0)
+ return GS_PLUGIN_ACTION_REMOVE;
+ if (g_strcmp0 (action, "update") == 0)
+ return GS_PLUGIN_ACTION_UPDATE;
+ if (g_strcmp0 (action, "set-rating") == 0)
+ return GS_PLUGIN_ACTION_SET_RATING;
+ if (g_strcmp0 (action, "upgrade-download") == 0)
+ return GS_PLUGIN_ACTION_UPGRADE_DOWNLOAD;
+ if (g_strcmp0 (action, "upgrade-trigger") == 0)
+ return GS_PLUGIN_ACTION_UPGRADE_TRIGGER;
+ if (g_strcmp0 (action, "launch") == 0)
+ return GS_PLUGIN_ACTION_LAUNCH;
+ if (g_strcmp0 (action, "update-cancel") == 0)
+ return GS_PLUGIN_ACTION_UPDATE_CANCEL;
+ if (g_strcmp0 (action, "add-shortcut") == 0)
+ return GS_PLUGIN_ACTION_ADD_SHORTCUT;
+ if (g_strcmp0 (action, "remove-shortcut") == 0)
+ return GS_PLUGIN_ACTION_REMOVE_SHORTCUT;
+ if (g_strcmp0 (action, "review-submit") == 0)
+ return GS_PLUGIN_ACTION_REVIEW_SUBMIT;
+ if (g_strcmp0 (action, "review-upvote") == 0)
+ return GS_PLUGIN_ACTION_REVIEW_UPVOTE;
+ if (g_strcmp0 (action, "review-downvote") == 0)
+ return GS_PLUGIN_ACTION_REVIEW_DOWNVOTE;
+ if (g_strcmp0 (action, "review-report") == 0)
+ return GS_PLUGIN_ACTION_REVIEW_REPORT;
+ if (g_strcmp0 (action, "review-remove") == 0)
+ return GS_PLUGIN_ACTION_REVIEW_REMOVE;
+ if (g_strcmp0 (action, "review-dismiss") == 0)
+ return GS_PLUGIN_ACTION_REVIEW_DISMISS;
+ if (g_strcmp0 (action, "get-updates") == 0)
+ return GS_PLUGIN_ACTION_GET_UPDATES;
+ if (g_strcmp0 (action, "get-distro-updates") == 0)
+ return GS_PLUGIN_ACTION_GET_DISTRO_UPDATES;
+ if (g_strcmp0 (action, "get-unvoted-reviews") == 0)
+ return GS_PLUGIN_ACTION_GET_UNVOTED_REVIEWS;
+ if (g_strcmp0 (action, "get-sources") == 0)
+ return GS_PLUGIN_ACTION_GET_SOURCES;
+ if (g_strcmp0 (action, "get-installed") == 0)
+ return GS_PLUGIN_ACTION_GET_INSTALLED;
+ if (g_strcmp0 (action, "get-popular") == 0)
+ return GS_PLUGIN_ACTION_GET_POPULAR;
+ if (g_strcmp0 (action, "get-featured") == 0)
+ return GS_PLUGIN_ACTION_GET_FEATURED;
+ if (g_strcmp0 (action, "search") == 0)
+ return GS_PLUGIN_ACTION_SEARCH;
+ if (g_strcmp0 (action, "search-files") == 0)
+ return GS_PLUGIN_ACTION_SEARCH_FILES;
+ if (g_strcmp0 (action, "search-provides") == 0)
+ return GS_PLUGIN_ACTION_SEARCH_PROVIDES;
+ if (g_strcmp0 (action, "get-categories") == 0)
+ return GS_PLUGIN_ACTION_GET_CATEGORIES;
+ if (g_strcmp0 (action, "get-category-apps") == 0)
+ return GS_PLUGIN_ACTION_GET_CATEGORY_APPS;
+ if (g_strcmp0 (action, "refine") == 0)
+ return GS_PLUGIN_ACTION_REFINE;
+ if (g_strcmp0 (action, "refresh") == 0)
+ return GS_PLUGIN_ACTION_REFRESH;
+ if (g_strcmp0 (action, "file-to-app") == 0)
+ return GS_PLUGIN_ACTION_FILE_TO_APP;
+ if (g_strcmp0 (action, "url-to-app") == 0)
+ return GS_PLUGIN_ACTION_URL_TO_APP;
+ if (g_strcmp0 (action, "get-recent") == 0)
+ return GS_PLUGIN_ACTION_GET_RECENT;
+ if (g_strcmp0 (action, "get-updates-historical") == 0)
+ return GS_PLUGIN_ACTION_GET_UPDATES_HISTORICAL;
+ if (g_strcmp0 (action, "initialize") == 0)
+ return GS_PLUGIN_ACTION_INITIALIZE;
+ if (g_strcmp0 (action, "destroy") == 0)
+ return GS_PLUGIN_ACTION_DESTROY;
+ if (g_strcmp0 (action, "get-alternates") == 0)
+ return GS_PLUGIN_ACTION_GET_ALTERNATES;
+ if (g_strcmp0 (action, "get-langpacks") == 0)
+ return GS_PLUGIN_ACTION_GET_LANGPACKS;
+ return GS_PLUGIN_ACTION_UNKNOWN;
+}
+
+/**
+ * gs_plugin_refine_flags_to_string:
+ * @refine_flags: some #GsPluginRefineFlags, e.g. %GS_PLUGIN_REFINE_FLAGS_REQUIRE_SIZE
+ *
+ * Converts the flags to a string.
+ *
+ * Returns: a string
+ **/
+gchar *
+gs_plugin_refine_flags_to_string (GsPluginRefineFlags refine_flags)
+{
+ g_autoptr(GPtrArray) cstrs = g_ptr_array_new ();
+ if (refine_flags & GS_PLUGIN_REFINE_FLAGS_USE_HISTORY)
+ g_ptr_array_add (cstrs, "use-history");
+ if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_LICENSE)
+ g_ptr_array_add (cstrs, "require-license");
+ if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_URL)
+ g_ptr_array_add (cstrs, "require-url");
+ if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_DESCRIPTION)
+ g_ptr_array_add (cstrs, "require-description");
+ if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_SIZE)
+ g_ptr_array_add (cstrs, "require-size");
+ if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_RATING)
+ g_ptr_array_add (cstrs, "require-rating");
+ if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_VERSION)
+ g_ptr_array_add (cstrs, "require-version");
+ if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_HISTORY)
+ g_ptr_array_add (cstrs, "require-history");
+ if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_SETUP_ACTION)
+ g_ptr_array_add (cstrs, "require-setup-action");
+ if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_UPDATE_DETAILS)
+ g_ptr_array_add (cstrs, "require-update-details");
+ if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN)
+ g_ptr_array_add (cstrs, "require-origin");
+ if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_RELATED)
+ g_ptr_array_add (cstrs, "require-related");
+ if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_MENU_PATH)
+ g_ptr_array_add (cstrs, "require-menu-path");
+ if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_ADDONS)
+ g_ptr_array_add (cstrs, "require-addons");
+ if (refine_flags & GS_PLUGIN_REFINE_FLAGS_ALLOW_PACKAGES)
+ g_ptr_array_add (cstrs, "require-allow-packages");
+ if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_UPDATE_SEVERITY)
+ g_ptr_array_add (cstrs, "require-update-severity");
+ if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_UPGRADE_REMOVED)
+ g_ptr_array_add (cstrs, "require-upgrade-removed");
+ if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_PROVENANCE)
+ g_ptr_array_add (cstrs, "require-provenance");
+ if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_REVIEWS)
+ g_ptr_array_add (cstrs, "require-reviews");
+ if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_REVIEW_RATINGS)
+ g_ptr_array_add (cstrs, "require-review-ratings");
+ if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_KEY_COLORS)
+ g_ptr_array_add (cstrs, "require-key-colors");
+ if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON)
+ g_ptr_array_add (cstrs, "require-icon");
+ if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_PERMISSIONS)
+ g_ptr_array_add (cstrs, "require-permissions");
+ if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN_HOSTNAME)
+ g_ptr_array_add (cstrs, "require-origin-hostname");
+ if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN_UI)
+ g_ptr_array_add (cstrs, "require-origin-ui");
+ if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_RUNTIME)
+ g_ptr_array_add (cstrs, "require-runtime");
+ if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_SCREENSHOTS)
+ g_ptr_array_add (cstrs, "require-screenshots");
+ if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_CATEGORIES)
+ g_ptr_array_add (cstrs, "require-categories");
+ if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_PROJECT_GROUP)
+ g_ptr_array_add (cstrs, "require-project-group");
+ if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_DEVELOPER_NAME)
+ g_ptr_array_add (cstrs, "require-developer-name");
+ if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_KUDOS)
+ g_ptr_array_add (cstrs, "require-kudos");
+ if (refine_flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_CONTENT_RATING)
+ g_ptr_array_add (cstrs, "content-rating");
+ if (cstrs->len == 0)
+ return g_strdup ("none");
+ g_ptr_array_add (cstrs, NULL);
+ return g_strjoinv (",", (gchar**) cstrs->pdata);
+}
+
+static void
+gs_plugin_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec)
+{
+ GsPlugin *plugin = GS_PLUGIN (object);
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+ switch (prop_id) {
+ case PROP_FLAGS:
+ priv->flags = g_value_get_uint64 (value);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+gs_plugin_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
+{
+ GsPlugin *plugin = GS_PLUGIN (object);
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+ switch (prop_id) {
+ case PROP_FLAGS:
+ g_value_set_uint64 (value, priv->flags);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+gs_plugin_class_init (GsPluginClass *klass)
+{
+ GParamSpec *pspec;
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->set_property = gs_plugin_set_property;
+ object_class->get_property = gs_plugin_get_property;
+ object_class->finalize = gs_plugin_finalize;
+
+ pspec = g_param_spec_uint64 ("flags", NULL, NULL,
+ 0, G_MAXUINT64, 0, G_PARAM_READWRITE);
+ g_object_class_install_property (object_class, PROP_FLAGS, pspec);
+
+ signals [SIGNAL_UPDATES_CHANGED] =
+ g_signal_new ("updates-changed",
+ G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (GsPluginClass, updates_changed),
+ NULL, NULL, g_cclosure_marshal_VOID__VOID,
+ G_TYPE_NONE, 0);
+
+ signals [SIGNAL_STATUS_CHANGED] =
+ g_signal_new ("status-changed",
+ G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (GsPluginClass, status_changed),
+ NULL, NULL, g_cclosure_marshal_generic,
+ G_TYPE_NONE, 2, GS_TYPE_APP, G_TYPE_UINT);
+
+ signals [SIGNAL_RELOAD] =
+ g_signal_new ("reload",
+ G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (GsPluginClass, reload),
+ NULL, NULL, g_cclosure_marshal_VOID__VOID,
+ G_TYPE_NONE, 0);
+
+ signals [SIGNAL_REPORT_EVENT] =
+ g_signal_new ("report-event",
+ G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (GsPluginClass, report_event),
+ NULL, NULL, g_cclosure_marshal_generic,
+ G_TYPE_NONE, 1, GS_TYPE_PLUGIN_EVENT);
+
+ signals [SIGNAL_ALLOW_UPDATES] =
+ g_signal_new ("allow-updates",
+ G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (GsPluginClass, allow_updates),
+ NULL, NULL, g_cclosure_marshal_VOID__BOOLEAN,
+ G_TYPE_NONE, 1, G_TYPE_BOOLEAN);
+
+ signals [SIGNAL_BASIC_AUTH_START] =
+ g_signal_new ("basic-auth-start",
+ G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST,
+ G_STRUCT_OFFSET (GsPluginClass, basic_auth_start),
+ NULL, NULL, g_cclosure_marshal_generic,
+ G_TYPE_NONE, 4, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_POINTER, G_TYPE_POINTER);
+}
+
+static void
+gs_plugin_init (GsPlugin *plugin)
+{
+ GsPluginPrivate *priv = gs_plugin_get_instance_private (plugin);
+ guint i;
+
+ for (i = 0; i < GS_PLUGIN_RULE_LAST; i++)
+ priv->rules[i] = g_ptr_array_new_with_free_func (g_free);
+
+ priv->enabled = TRUE;
+ priv->scale = 1;
+ priv->cache = g_hash_table_new_full ((GHashFunc) as_utils_unique_id_hash,
+ (GEqualFunc) as_utils_unique_id_equal,
+ g_free,
+ (GDestroyNotify) g_object_unref);
+ priv->vfuncs = g_hash_table_new_full (g_str_hash, g_str_equal,
+ g_free, NULL);
+ g_mutex_init (&priv->cache_mutex);
+ g_mutex_init (&priv->interactive_mutex);
+ g_mutex_init (&priv->timer_mutex);
+ g_mutex_init (&priv->vfuncs_mutex);
+}
+
+/**
+ * gs_plugin_new:
+ *
+ * Creates a new plugin.
+ *
+ * Returns: a #GsPlugin
+ *
+ * Since: 3.22
+ **/
+GsPlugin *
+gs_plugin_new (void)
+{
+ GsPlugin *plugin;
+ plugin = g_object_new (GS_TYPE_PLUGIN, NULL);
+ return plugin;
+}
diff --git a/lib/gs-plugin.h b/lib/gs-plugin.h
new file mode 100644
index 0000000..e211931
--- /dev/null
+++ b/lib/gs-plugin.h
@@ -0,0 +1,132 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2012-2016 Richard Hughes <richard@hughsie.com>
+ * Copyright (C) 2020 Kalev Lember <klember@redhat.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+#pragma once
+
+#include <appstream-glib.h>
+#include <glib-object.h>
+#include <gmodule.h>
+#include <gio/gio.h>
+#include <libsoup/soup.h>
+
+#include "gs-app.h"
+#include "gs-app-list.h"
+#include "gs-category.h"
+#include "gs-plugin-event.h"
+#include "gs-plugin-types.h"
+
+G_BEGIN_DECLS
+
+#define GS_TYPE_PLUGIN (gs_plugin_get_type ())
+
+G_DECLARE_DERIVABLE_TYPE (GsPlugin, gs_plugin, GS, PLUGIN, GObject)
+
+struct _GsPluginClass
+{
+ GObjectClass parent_class;
+ void (*updates_changed) (GsPlugin *plugin);
+ void (*status_changed) (GsPlugin *plugin,
+ GsApp *app,
+ guint status);
+ void (*reload) (GsPlugin *plugin);
+ void (*report_event) (GsPlugin *plugin,
+ GsPluginEvent *event);
+ void (*allow_updates) (GsPlugin *plugin,
+ gboolean allow_updates);
+ void (*basic_auth_start) (GsPlugin *plugin,
+ const gchar *remote,
+ const gchar *realm,
+ GCallback callback,
+ gpointer user_data);
+ gpointer padding[25];
+};
+
+typedef struct GsPluginData GsPluginData;
+
+/* helpers */
+#define GS_PLUGIN_ERROR gs_plugin_error_quark ()
+
+GQuark gs_plugin_error_quark (void);
+
+/* public getters and setters */
+GsPluginData *gs_plugin_alloc_data (GsPlugin *plugin,
+ gsize sz);
+GsPluginData *gs_plugin_get_data (GsPlugin *plugin);
+const gchar *gs_plugin_get_name (GsPlugin *plugin);
+const gchar *gs_plugin_get_appstream_id (GsPlugin *plugin);
+void gs_plugin_set_appstream_id (GsPlugin *plugin,
+ const gchar *appstream_id);
+gboolean gs_plugin_get_enabled (GsPlugin *plugin);
+void gs_plugin_set_enabled (GsPlugin *plugin,
+ gboolean enabled);
+gboolean gs_plugin_has_flags (GsPlugin *plugin,
+ GsPluginFlags flags);
+void gs_plugin_add_flags (GsPlugin *plugin,
+ GsPluginFlags flags);
+void gs_plugin_remove_flags (GsPlugin *plugin,
+ GsPluginFlags flags);
+guint gs_plugin_get_scale (GsPlugin *plugin);
+const gchar *gs_plugin_get_locale (GsPlugin *plugin);
+const gchar *gs_plugin_get_language (GsPlugin *plugin);
+SoupSession *gs_plugin_get_soup_session (GsPlugin *plugin);
+void gs_plugin_set_soup_session (GsPlugin *plugin,
+ SoupSession *soup_session);
+void gs_plugin_add_rule (GsPlugin *plugin,
+ GsPluginRule rule,
+ const gchar *name);
+
+/* helpers */
+GBytes *gs_plugin_download_data (GsPlugin *plugin,
+ GsApp *app,
+ const gchar *uri,
+ GCancellable *cancellable,
+ GError **error);
+gboolean gs_plugin_download_file (GsPlugin *plugin,
+ GsApp *app,
+ const gchar *uri,
+ const gchar *filename,
+ GCancellable *cancellable,
+ GError **error);
+gchar *gs_plugin_download_rewrite_resource (GsPlugin *plugin,
+ GsApp *app,
+ const gchar *resource,
+ GCancellable *cancellable,
+ GError **error);
+
+gboolean gs_plugin_check_distro_id (GsPlugin *plugin,
+ const gchar *distro_id);
+GsApp *gs_plugin_cache_lookup (GsPlugin *plugin,
+ const gchar *key);
+void gs_plugin_cache_add (GsPlugin *plugin,
+ const gchar *key,
+ GsApp *app);
+void gs_plugin_cache_remove (GsPlugin *plugin,
+ const gchar *key);
+void gs_plugin_cache_invalidate (GsPlugin *plugin);
+void gs_plugin_status_update (GsPlugin *plugin,
+ GsApp *app,
+ GsPluginStatus status);
+gboolean gs_plugin_app_launch (GsPlugin *plugin,
+ GsApp *app,
+ GError **error);
+void gs_plugin_updates_changed (GsPlugin *plugin);
+void gs_plugin_reload (GsPlugin *plugin);
+const gchar *gs_plugin_status_to_string (GsPluginStatus status);
+void gs_plugin_report_event (GsPlugin *plugin,
+ GsPluginEvent *event);
+void gs_plugin_set_allow_updates (GsPlugin *plugin,
+ gboolean allow_updates);
+gboolean gs_plugin_get_network_available (GsPlugin *plugin);
+void gs_plugin_basic_auth_start (GsPlugin *plugin,
+ const gchar *remote,
+ const gchar *realm,
+ GCallback callback,
+ gpointer user_data);
+
+G_END_DECLS
diff --git a/lib/gs-self-test.c b/lib/gs-self-test.c
new file mode 100644
index 0000000..c6c20ed
--- /dev/null
+++ b/lib/gs-self-test.c
@@ -0,0 +1,835 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2013-2017 Richard Hughes <richard@hughsie.com>
+ * Copyright (C) 2015-2018 Kalev Lember <klember@redhat.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+#include "config.h"
+
+#include "gnome-software-private.h"
+
+#include "gs-test.h"
+
+static gboolean
+gs_app_list_filter_cb (GsApp *app, gpointer user_data)
+{
+ if (g_strcmp0 (gs_app_get_id (app), "a") == 0)
+ return FALSE;
+ if (g_strcmp0 (gs_app_get_id (app), "c") == 0)
+ return FALSE;
+ return TRUE;
+}
+
+static void
+gs_utils_url_func (void)
+{
+ g_autofree gchar *path1 = NULL;
+ g_autofree gchar *path2 = NULL;
+ g_autofree gchar *path3 = NULL;
+ g_autofree gchar *scheme1 = NULL;
+ g_autofree gchar *scheme2 = NULL;
+
+ scheme1 = gs_utils_get_url_scheme ("appstream://gimp.desktop");
+ g_assert_cmpstr (scheme1, ==, "appstream");
+ scheme2 = gs_utils_get_url_scheme ("appstream:gimp.desktop");
+ g_assert_cmpstr (scheme2, ==, "appstream");
+
+ path1 = gs_utils_get_url_path ("appstream://gimp.desktop");
+ g_assert_cmpstr (path1, ==, "gimp.desktop");
+ path2 = gs_utils_get_url_path ("appstream:gimp.desktop");
+ g_assert_cmpstr (path2, ==, "gimp.desktop");
+ path3 = gs_utils_get_url_path ("apt:/gimp");
+ g_assert_cmpstr (path3, ==, "gimp");
+}
+
+static void
+gs_utils_wilson_func (void)
+{
+ g_assert_cmpint ((gint64) gs_utils_get_wilson_rating (0, 0, 0, 0, 0), ==, -1);
+ g_assert_cmpint ((gint64) gs_utils_get_wilson_rating (0, 0, 0, 0, 400), ==, 100);
+ g_assert_cmpint ((gint64) gs_utils_get_wilson_rating (10, 0, 0, 0, 400), ==, 98);
+ g_assert_cmpint ((gint64) gs_utils_get_wilson_rating (0, 0, 0, 0, 1), ==, 76);
+ g_assert_cmpint ((gint64) gs_utils_get_wilson_rating (5, 4, 20, 100, 400), ==, 93);
+}
+
+static void
+gs_os_release_func (void)
+{
+ g_autofree gchar *fn = NULL;
+ g_autoptr(GError) error = NULL;
+ g_autoptr(GsOsRelease) os_release = NULL;
+
+ fn = gs_test_get_filename (TESTDATADIR, "tests/os-release");
+ g_assert (fn != NULL);
+ g_setenv ("GS_SELF_TEST_OS_RELEASE_FILENAME", fn, TRUE);
+
+ os_release = gs_os_release_new (&error);
+ g_assert_no_error (error);
+ g_assert (os_release != NULL);
+ g_assert_cmpstr (gs_os_release_get_id (os_release), ==, "fedora");
+ g_assert_cmpstr (gs_os_release_get_name (os_release), ==, "Fedora");
+ g_assert_cmpstr (gs_os_release_get_version (os_release), ==, "25 (Workstation Edition)");
+ g_assert_cmpstr (gs_os_release_get_version_id (os_release), ==, "25");
+ g_assert_cmpstr (gs_os_release_get_pretty_name (os_release), ==, "Fedora 25 (Workstation Edition)");
+}
+
+static void
+gs_utils_append_kv_func (void)
+{
+ g_autoptr(GString) str = g_string_new (NULL);
+
+ /* normal */
+ gs_utils_append_key_value (str, 5, "key", "val");
+ g_assert_cmpstr (str->str, ==, "key: val\n");
+
+ /* oversize */
+ g_string_truncate (str, 0);
+ gs_utils_append_key_value (str, 5, "longkey", "val");
+ g_assert_cmpstr (str->str, ==, "longkey: val\n");
+
+ /* null key */
+ g_string_truncate (str, 0);
+ gs_utils_append_key_value (str, 5, NULL, "val");
+ g_assert_cmpstr (str->str, ==, " val\n");
+
+ /* zero align key */
+ g_string_truncate (str, 0);
+ gs_utils_append_key_value (str, 0, "key", "val");
+ g_assert_cmpstr (str->str, ==, "key: val\n");
+}
+
+static void
+gs_utils_cache_func (void)
+{
+ g_autofree gchar *fn1 = NULL;
+ g_autofree gchar *fn2 = NULL;
+ g_autoptr(GError) error = NULL;
+
+ fn1 = gs_utils_get_cache_filename ("test",
+ "http://www.foo.bar/baz",
+ GS_UTILS_CACHE_FLAG_WRITEABLE,
+ &error);
+ g_assert_no_error (error);
+ g_assert_cmpstr (fn1, !=, NULL);
+ g_assert (g_str_has_prefix (fn1, g_get_user_cache_dir ()));
+ g_assert (g_str_has_suffix (fn1, "test/baz"));
+
+ fn2 = gs_utils_get_cache_filename ("test",
+ "http://www.foo.bar/baz",
+ GS_UTILS_CACHE_FLAG_WRITEABLE |
+ GS_UTILS_CACHE_FLAG_USE_HASH,
+ &error);
+ g_assert_no_error (error);
+ g_assert_cmpstr (fn2, !=, NULL);
+ g_assert (g_str_has_prefix (fn2, g_get_user_cache_dir ()));
+ g_assert (g_str_has_suffix (fn2, "test/295099f59d12b3eb0b955325fcb699cd23792a89-baz"));
+}
+
+static void
+gs_utils_error_func (void)
+{
+ g_autofree gchar *app_id = NULL;
+ g_autofree gchar *origin_id = NULL;
+ g_autoptr(GError) error = NULL;
+ g_autoptr(GsApp) app = gs_app_new ("gimp.desktop");
+ g_autoptr(GsApp) origin = gs_app_new ("gimp-repo");
+
+ for (guint i = 0; i < GS_PLUGIN_ERROR_LAST; i++)
+ g_assert (gs_plugin_error_to_string (i) != NULL);
+
+ /* noop */
+ gs_utils_error_add_app_id (&error, app);
+ gs_utils_error_add_origin_id (&error, origin);
+
+ g_set_error (&error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_DOWNLOAD_FAILED,
+ "failed");
+ g_assert_cmpstr (error->message, ==, "failed");
+ gs_utils_error_add_app_id (&error, app);
+ gs_utils_error_add_origin_id (&error, origin);
+ g_assert_cmpstr (error->message, ==, "[*/*/*/*/gimp-repo/*] {*/*/*/*/gimp.desktop/*} failed");
+
+ /* find and strip any unique IDs from the error message */
+ for (guint i = 0; i < 2; i++) {
+ if (app_id == NULL)
+ app_id = gs_utils_error_strip_app_id (error);
+ if (origin_id == NULL)
+ origin_id = gs_utils_error_strip_origin_id (error);
+ }
+
+ g_assert_cmpstr (app_id, ==, "*/*/*/*/gimp.desktop/*");
+ g_assert_cmpstr (origin_id, ==, "*/*/*/*/gimp-repo/*");
+ g_assert_cmpstr (error->message, ==, "failed");
+}
+
+static void
+gs_utils_parse_evr_func (void)
+{
+ gboolean ret;
+
+ {
+ g_autofree gchar *epoch = NULL;
+ g_autofree gchar *version = NULL;
+ g_autofree gchar *release = NULL;
+
+ ret = gs_utils_parse_evr ("3.26.0-1.fc27", &epoch, &version, &release);
+ g_assert (ret);
+ g_assert_cmpstr (epoch, ==, "0");
+ g_assert_cmpstr (version, ==, "3.26.0");
+ g_assert_cmpstr (release, ==, "1.fc27");
+ }
+
+ {
+ g_autofree gchar *epoch = NULL;
+ g_autofree gchar *version = NULL;
+ g_autofree gchar *release = NULL;
+
+ ret = gs_utils_parse_evr ("1:3.26.0-1.fc27", &epoch, &version, &release);
+ g_assert (ret);
+ g_assert_cmpstr (epoch, ==, "1");
+ g_assert_cmpstr (version, ==, "3.26.0");
+ g_assert_cmpstr (release, ==, "1.fc27");
+ }
+
+ {
+ g_autofree gchar *epoch = NULL;
+ g_autofree gchar *version = NULL;
+ g_autofree gchar *release = NULL;
+
+ ret = gs_utils_parse_evr ("234", &epoch, &version, &release);
+ g_assert (ret);
+ g_assert_cmpstr (epoch, ==, "0");
+ g_assert_cmpstr (version, ==, "234");
+ g_assert_cmpstr (release, ==, "0");
+ }
+
+ {
+ g_autofree gchar *epoch = NULL;
+ g_autofree gchar *version = NULL;
+ g_autofree gchar *release = NULL;
+
+ ret = gs_utils_parse_evr ("3:1.6~git20131207+dfsg-2ubuntu1~14.04.3", &epoch, &version, &release);
+ g_assert (ret);
+ g_assert_cmpstr (epoch, ==, "3");
+ g_assert_cmpstr (version, ==, "1.6~git20131207+dfsg");
+ g_assert_cmpstr (release, ==, "2ubuntu1~14.04.3");
+ }
+
+ {
+ g_autofree gchar *epoch = NULL;
+ g_autofree gchar *version = NULL;
+ g_autofree gchar *release = NULL;
+
+ ret = gs_utils_parse_evr ("1-2-3-4-5-6", &epoch, &version, &release);
+ g_assert (!ret);
+ }
+
+ {
+ g_autofree gchar *epoch = NULL;
+ g_autofree gchar *version = NULL;
+ g_autofree gchar *release = NULL;
+
+ ret = gs_utils_parse_evr ("", &epoch, &version, &release);
+ g_assert (!ret);
+ }
+}
+
+static void
+gs_plugin_download_rewrite_func (void)
+{
+ g_autofree gchar *css = NULL;
+ g_autoptr(GError) error = NULL;
+ g_autoptr(GsPlugin) plugin = NULL;
+ const gchar *resource = "background:\n"
+ " url('file://" DATADIR "/gnome-software/featured-maps.png')\n"
+ " url('file://" DATADIR "/gnome-software/featured-maps-bg.png')\n"
+ " bottom center / contain no-repeat;\n";
+
+ /* only when installed */
+ if (!g_file_test (DATADIR "/gnome-software/featured-maps.png", G_FILE_TEST_EXISTS)) {
+ g_test_skip ("not installed");
+ return;
+ }
+
+ /* test rewrite */
+ plugin = gs_plugin_new ();
+ gs_plugin_set_name (plugin, "self-test");
+ css = gs_plugin_download_rewrite_resource (plugin,
+ NULL, /* app */
+ resource,
+ NULL,
+ &error);
+ g_assert_no_error (error);
+ g_assert (css != NULL);
+}
+
+static void
+gs_plugin_func (void)
+{
+ GsAppList *list;
+ GsAppList *list_dup;
+ GsAppList *list_remove;
+ GsApp *app;
+ g_autoptr(AsProvide) prov = as_provide_new ();
+
+ /* check enums converted */
+ for (guint i = 0; i < GS_PLUGIN_ACTION_LAST; i++) {
+ const gchar *tmp = gs_plugin_action_to_string (i);
+ if (tmp == NULL)
+ g_critical ("failed to convert %u", i);
+ g_assert_cmpint (gs_plugin_action_from_string (tmp), ==, i);
+ }
+ for (guint i = 1; i < GS_PLUGIN_ACTION_LAST; i++) {
+ const gchar *tmp = gs_plugin_action_to_function_name (i);
+ if (tmp == NULL)
+ g_critical ("failed to convert %u", i);
+ }
+
+ /* add a couple of duplicate IDs */
+ app = gs_app_new ("a");
+ list = gs_app_list_new ();
+ gs_app_list_add (list, app);
+ g_object_unref (app);
+
+ /* test refcounting */
+ g_assert_cmpstr (gs_app_get_id (gs_app_list_index (list, 0)), ==, "a");
+ list_dup = gs_app_list_copy (list);
+ g_object_unref (list);
+ g_assert_cmpint (gs_app_list_length (list_dup), ==, 1);
+ g_assert_cmpstr (gs_app_get_id (gs_app_list_index (list_dup, 0)), ==, "a");
+ g_object_unref (list_dup);
+
+ /* test removing objects */
+ app = gs_app_new ("a");
+ list_remove = gs_app_list_new ();
+ gs_app_list_add (list_remove, app);
+ g_object_unref (app);
+ app = gs_app_new ("b");
+ gs_app_list_add (list_remove, app);
+ g_object_unref (app);
+ app = gs_app_new ("c");
+ gs_app_list_add (list_remove, app);
+ g_object_unref (app);
+ g_assert_cmpint (gs_app_list_length (list_remove), ==, 3);
+ gs_app_list_filter (list_remove, gs_app_list_filter_cb, NULL);
+ g_assert_cmpint (gs_app_list_length (list_remove), ==, 1);
+ g_assert_cmpstr (gs_app_get_id (gs_app_list_index (list_remove, 0)), ==, "b");
+
+ /* test removing duplicates at runtime */
+ app = gs_app_new ("b");
+ gs_app_list_add (list_remove, app);
+ g_object_unref (app);
+ app = gs_app_new ("b");
+ gs_app_list_add (list_remove, app);
+ g_object_unref (app);
+ g_assert_cmpint (gs_app_list_length (list_remove), ==, 1);
+ g_assert_cmpstr (gs_app_get_id (gs_app_list_index (list_remove, 0)), ==, "b");
+ g_object_unref (list_remove);
+
+ /* test removing duplicates when lazy-loading */
+ list_remove = gs_app_list_new ();
+ app = gs_app_new (NULL);
+ gs_app_list_add (list_remove, app);
+ gs_app_set_id (app, "e");
+ g_object_unref (app);
+ app = gs_app_new (NULL);
+ gs_app_list_add (list_remove, app);
+ gs_app_set_id (app, "e");
+ g_object_unref (app);
+ g_assert_cmpint (gs_app_list_length (list_remove), ==, 2);
+ gs_app_list_filter_duplicates (list_remove, GS_APP_LIST_FILTER_FLAG_NONE);
+ g_assert_cmpint (gs_app_list_length (list_remove), ==, 1);
+ g_object_unref (list_remove);
+
+ /* test removing duplicates when some apps have no app ID */
+ list_remove = gs_app_list_new ();
+ app = gs_app_new (NULL);
+ gs_app_list_add (list_remove, app);
+ g_object_unref (app);
+ app = gs_app_new (NULL);
+ gs_app_list_add (list_remove, app);
+ g_object_unref (app);
+ app = gs_app_new (NULL);
+ gs_app_list_add (list_remove, app);
+ gs_app_set_id (app, "e");
+ g_object_unref (app);
+ g_assert_cmpint (gs_app_list_length (list_remove), ==, 3);
+ gs_app_list_filter_duplicates (list_remove, GS_APP_LIST_FILTER_FLAG_NONE);
+ g_assert_cmpint (gs_app_list_length (list_remove), ==, 3);
+ g_object_unref (list_remove);
+
+ /* remove lazy-loaded app */
+ list_remove = gs_app_list_new ();
+ app = gs_app_new (NULL);
+ gs_app_list_add (list_remove, app);
+ gs_app_list_remove (list_remove, app);
+ g_assert_cmpint (gs_app_list_length (list_remove), ==, 0);
+ g_object_unref (app);
+ g_object_unref (list_remove);
+
+ /* respect priority when deduplicating */
+ list = gs_app_list_new ();
+ app = gs_app_new ("e");
+ gs_app_set_unique_id (app, "user/foo/*/*/e/*");
+ gs_app_list_add (list, app);
+ gs_app_set_priority (app, 0);
+ g_object_unref (app);
+ app = gs_app_new ("e");
+ gs_app_set_unique_id (app, "user/bar/*/*/e/*");
+ gs_app_list_add (list, app);
+ gs_app_set_priority (app, 99);
+ g_object_unref (app);
+ app = gs_app_new ("e");
+ gs_app_set_unique_id (app, "user/baz/*/*/e/*");
+ gs_app_list_add (list, app);
+ gs_app_set_priority (app, 50);
+ g_object_unref (app);
+ g_assert_cmpint (gs_app_list_length (list), ==, 3);
+ gs_app_list_filter_duplicates (list, GS_APP_LIST_FILTER_FLAG_KEY_ID);
+ g_assert_cmpint (gs_app_list_length (list), ==, 1);
+ g_assert_cmpstr (gs_app_get_unique_id (gs_app_list_index (list, 0)), ==, "user/bar/*/*/e/*");
+ g_object_unref (list);
+
+ /* respect priority (using name and version) when deduplicating */
+ list = gs_app_list_new ();
+ app = gs_app_new ("e");
+ gs_app_add_source (app, "foo");
+ gs_app_set_version (app, "1.2.3");
+ gs_app_set_unique_id (app, "user/foo/repo/*/*/*");
+ gs_app_list_add (list, app);
+ gs_app_set_priority (app, 0);
+ g_object_unref (app);
+ app = gs_app_new ("e");
+ gs_app_add_source (app, "foo");
+ gs_app_set_version (app, "1.2.3");
+ gs_app_set_unique_id (app, "user/foo/repo-security/*/*/*");
+ gs_app_list_add (list, app);
+ gs_app_set_priority (app, 99);
+ g_object_unref (app);
+ app = gs_app_new ("e");
+ gs_app_add_source (app, "foo");
+ gs_app_set_version (app, "1.2.3");
+ gs_app_set_unique_id (app, "user/foo/repo-universe/*/*/*");
+ gs_app_list_add (list, app);
+ gs_app_set_priority (app, 50);
+ g_object_unref (app);
+ g_assert_cmpint (gs_app_list_length (list), ==, 3);
+ gs_app_list_filter_duplicates (list, GS_APP_LIST_FILTER_FLAG_KEY_ID |
+ GS_APP_LIST_FILTER_FLAG_KEY_SOURCE |
+ GS_APP_LIST_FILTER_FLAG_KEY_VERSION);
+ g_assert_cmpint (gs_app_list_length (list), ==, 1);
+ g_assert_cmpstr (gs_app_get_unique_id (gs_app_list_index (list, 0)), ==, "user/foo/repo-security/*/*/*");
+ g_object_unref (list);
+
+ /* prefer installed applications */
+ list = gs_app_list_new ();
+ app = gs_app_new ("e");
+ gs_app_set_state (app, AS_APP_STATE_INSTALLED);
+ gs_app_set_unique_id (app, "user/foo/*/*/e/*");
+ gs_app_set_priority (app, 0);
+ gs_app_list_add (list, app);
+ g_object_unref (app);
+ app = gs_app_new ("e");
+ gs_app_set_state (app, AS_APP_STATE_AVAILABLE);
+ gs_app_set_unique_id (app, "user/bar/*/*/e/*");
+ gs_app_set_priority (app, 100);
+ gs_app_list_add (list, app);
+ g_object_unref (app);
+ gs_app_list_filter_duplicates (list,
+ GS_APP_LIST_FILTER_FLAG_KEY_ID |
+ GS_APP_LIST_FILTER_FLAG_PREFER_INSTALLED);
+ g_assert_cmpint (gs_app_list_length (list), ==, 1);
+ g_assert_cmpstr (gs_app_get_unique_id (gs_app_list_index (list, 0)), ==, "user/foo/*/*/e/*");
+ g_object_unref (list);
+
+ /* use the provides ID to dedupe */
+ list = gs_app_list_new ();
+ app = gs_app_new ("gimp.desktop");
+ gs_app_set_unique_id (app, "user/fedora/*/*/gimp.desktop/*");
+ gs_app_set_priority (app, 0);
+ gs_app_list_add (list, app);
+ g_object_unref (app);
+ app = gs_app_new ("org.gimp.GIMP");
+ as_provide_set_kind (prov, AS_PROVIDE_KIND_ID);
+ as_provide_set_value (prov, "gimp.desktop");
+ gs_app_add_provide (app, prov);
+ gs_app_set_unique_id (app, "user/flathub/*/*/org.gimp.GIMP/*");
+ gs_app_set_priority (app, 100);
+ gs_app_list_add (list, app);
+ g_object_unref (app);
+ gs_app_list_filter_duplicates (list, GS_APP_LIST_FILTER_FLAG_KEY_ID_PROVIDES);
+ g_assert_cmpint (gs_app_list_length (list), ==, 1);
+ g_assert_cmpstr (gs_app_get_unique_id (gs_app_list_index (list, 0)), ==,
+ "user/flathub/*/*/org.gimp.GIMP/*");
+ g_object_unref (list);
+
+ /* use globs when adding */
+ list = gs_app_list_new ();
+ app = gs_app_new ("b");
+ gs_app_set_unique_id (app, "a/b/c/d/e/f");
+ gs_app_list_add (list, app);
+ g_object_unref (app);
+ app = gs_app_new ("b");
+ gs_app_set_unique_id (app, "a/b/c/*/e/f");
+ gs_app_list_add (list, app);
+ g_object_unref (app);
+ g_assert_cmpint (gs_app_list_length (list), ==, 1);
+ g_assert_cmpstr (gs_app_get_id (gs_app_list_index (list, 0)), ==, "b");
+ g_object_unref (list);
+
+ /* lookup with a wildcard */
+ list = gs_app_list_new ();
+ app = gs_app_new ("b");
+ gs_app_set_unique_id (app, "a/b/c/d/e/f");
+ gs_app_list_add (list, app);
+ g_object_unref (app);
+ g_assert (gs_app_list_lookup (list, "a/b/c/d/e/f") != NULL);
+ g_assert (gs_app_list_lookup (list, "a/b/c/d/e/*") != NULL);
+ g_assert (gs_app_list_lookup (list, "*/b/c/d/e/f") != NULL);
+ g_assert (gs_app_list_lookup (list, "x/x/x/x/x/x") == NULL);
+ g_object_unref (list);
+
+ /* allow duplicating a wildcard */
+ list = gs_app_list_new ();
+ app = gs_app_new ("gimp.desktop");
+ gs_app_add_quirk (app, GS_APP_QUIRK_IS_WILDCARD);
+ gs_app_list_add (list, app);
+ g_object_unref (app);
+ app = gs_app_new ("gimp.desktop");
+ gs_app_set_unique_id (app, "system/flatpak/*/*/gimp.desktop/stable");
+ gs_app_list_add (list, app);
+ g_object_unref (app);
+ g_assert_cmpint (gs_app_list_length (list), ==, 2);
+ g_object_unref (list);
+
+ /* allow duplicating a wildcard */
+ list = gs_app_list_new ();
+ app = gs_app_new ("gimp.desktop");
+ gs_app_add_quirk (app, GS_APP_QUIRK_IS_WILDCARD);
+ gs_app_list_add (list, app);
+ g_object_unref (app);
+ app = gs_app_new ("gimp.desktop");
+ gs_app_add_quirk (app, GS_APP_QUIRK_IS_WILDCARD);
+ gs_app_list_add (list, app);
+ g_object_unref (app);
+ g_assert_cmpint (gs_app_list_length (list), ==, 1);
+ g_object_unref (list);
+
+ /* add a list to a list */
+ list = gs_app_list_new ();
+ list_dup = gs_app_list_new ();
+ app = gs_app_new ("a");
+ gs_app_list_add (list, app);
+ g_object_unref (app);
+ app = gs_app_new ("b");
+ gs_app_list_add (list_dup, app);
+ g_object_unref (app);
+ gs_app_list_add_list (list, list_dup);
+ g_assert_cmpint (gs_app_list_length (list), ==, 2);
+ g_assert_cmpint (gs_app_list_length (list_dup), ==, 1);
+ g_object_unref (list);
+ g_object_unref (list_dup);
+
+ /* remove apps from the list */
+ list = gs_app_list_new ();
+ app = gs_app_new ("a");
+ gs_app_list_add (list, app);
+ gs_app_list_remove (list, app);
+ g_object_unref (app);
+ g_assert_cmpint (gs_app_list_length (list), ==, 0);
+ g_object_unref (list);
+
+ /* truncate list */
+ list = gs_app_list_new ();
+ app = gs_app_new ("a");
+ gs_app_list_add (list, app);
+ g_object_unref (app);
+ app = gs_app_new ("b");
+ gs_app_list_add (list, app);
+ g_object_unref (app);
+ app = gs_app_new ("c");
+ gs_app_list_add (list, app);
+ g_object_unref (app);
+ g_assert (!gs_app_list_has_flag (list, GS_APP_LIST_FLAG_IS_TRUNCATED));
+ g_assert_cmpint (gs_app_list_get_size_peak (list), ==, 3);
+ gs_app_list_truncate (list, 3);
+ g_assert_cmpint (gs_app_list_length (list), ==, 3);
+ g_assert (gs_app_list_has_flag (list, GS_APP_LIST_FLAG_IS_TRUNCATED));
+ g_assert_cmpint (gs_app_list_get_size_peak (list), ==, 3);
+ gs_app_list_truncate (list, 2);
+ g_assert_cmpint (gs_app_list_length (list), ==, 2);
+ gs_app_list_truncate (list, 1);
+ g_assert_cmpint (gs_app_list_length (list), ==, 1);
+ gs_app_list_truncate (list, 0);
+ g_assert_cmpint (gs_app_list_length (list), ==, 0);
+ g_assert_cmpint (gs_app_list_get_size_peak (list), ==, 3);
+ g_object_unref (list);
+}
+
+static gpointer
+gs_app_thread_cb (gpointer data)
+{
+ GsApp *app = GS_APP (data);
+ for (guint i = 0; i < 10000; i++) {
+ g_assert_cmpstr (gs_app_get_unique_id (app), !=, NULL);
+ gs_app_set_branch (app, "master");
+ g_assert_cmpstr (gs_app_get_unique_id (app), !=, NULL);
+ gs_app_set_branch (app, "stable");
+ }
+ return NULL;
+}
+
+static void
+gs_app_thread_func (void)
+{
+ GThread *thread1;
+ GThread *thread2;
+ g_autoptr(GsApp) app = gs_app_new ("gimp.desktop");
+
+ /* try really hard to cause a threading problem */
+ g_setenv ("G_MESSAGES_DEBUG", "", TRUE);
+ thread1 = g_thread_new ("thread1", gs_app_thread_cb, app);
+ thread2 = g_thread_new ("thread2", gs_app_thread_cb, app);
+ g_thread_join (thread1); /* consumes the reference */
+ g_thread_join (thread2);
+ g_setenv ("G_MESSAGES_DEBUG", "all", TRUE);
+}
+
+static void
+gs_app_unique_id_func (void)
+{
+ g_autoptr(GsApp) app = gs_app_new (NULL);
+ const gchar *unique_id;
+
+ unique_id = "system/flatpak/gnome/desktop/org.gnome.Software.desktop/master";
+ gs_app_set_from_unique_id (app, unique_id);
+ g_assert (GS_IS_APP (app));
+ g_assert_cmpint (gs_app_get_scope (app), ==, AS_APP_SCOPE_SYSTEM);
+ g_assert_cmpint (gs_app_get_bundle_kind (app), ==, AS_BUNDLE_KIND_FLATPAK);
+ g_assert_cmpstr (gs_app_get_origin (app), ==, "gnome");
+ g_assert_cmpint (gs_app_get_kind (app), ==, AS_APP_KIND_DESKTOP);
+ g_assert_cmpstr (gs_app_get_id (app), ==, "org.gnome.Software.desktop");
+ g_assert_cmpstr (gs_app_get_branch (app), ==, "master");
+}
+
+static void
+gs_app_addons_func (void)
+{
+ g_autoptr(GsApp) app = gs_app_new ("test.desktop");
+ GsApp *addon;
+
+ /* create, add then drop ref, so @app has the only refcount of addon */
+ addon = gs_app_new ("test.desktop");
+ gs_app_add_addon (app, addon);
+ g_object_unref (addon);
+
+ gs_app_remove_addon (app, addon);
+}
+
+static void
+gs_app_func (void)
+{
+ g_autoptr(GsApp) app = NULL;
+
+ app = gs_app_new ("gnome-software.desktop");
+ g_assert (GS_IS_APP (app));
+ g_assert_cmpstr (gs_app_get_id (app), ==, "gnome-software.desktop");
+
+ /* check we clean up the version, but not at the expense of having
+ * the same string as the update version */
+ gs_app_set_version (app, "2.8.6-3.fc20");
+ gs_app_set_update_version (app, "2.8.6-4.fc20");
+ g_assert_cmpstr (gs_app_get_version (app), ==, "2.8.6-3.fc20");
+ g_assert_cmpstr (gs_app_get_update_version (app), ==, "2.8.6-4.fc20");
+ g_assert_cmpstr (gs_app_get_version_ui (app), ==, "2.8.6-3");
+ g_assert_cmpstr (gs_app_get_update_version_ui (app), ==, "2.8.6-4");
+
+ /* check the quality stuff works */
+ gs_app_set_name (app, GS_APP_QUALITY_NORMAL, "dave");
+ g_assert_cmpstr (gs_app_get_name (app), ==, "dave");
+ gs_app_set_name (app, GS_APP_QUALITY_LOWEST, "brian");
+ g_assert_cmpstr (gs_app_get_name (app), ==, "dave");
+ gs_app_set_name (app, GS_APP_QUALITY_HIGHEST, "hugh");
+ g_assert_cmpstr (gs_app_get_name (app), ==, "hugh");
+
+ /* check non-transient state saving */
+ gs_app_set_state (app, AS_APP_STATE_INSTALLED);
+ g_assert_cmpint (gs_app_get_state (app), ==, AS_APP_STATE_INSTALLED);
+ gs_app_set_state (app, AS_APP_STATE_REMOVING);
+ g_assert_cmpint (gs_app_get_state (app), ==, AS_APP_STATE_REMOVING);
+ gs_app_set_state_recover (app); // simulate an error
+ g_assert_cmpint (gs_app_get_state (app), ==, AS_APP_STATE_INSTALLED);
+
+ /* try again */
+ gs_app_set_state (app, AS_APP_STATE_REMOVING);
+ g_assert_cmpint (gs_app_get_state (app), ==, AS_APP_STATE_REMOVING);
+ gs_app_set_state_recover (app); // simulate an error
+ g_assert_cmpint (gs_app_get_state (app), ==, AS_APP_STATE_INSTALLED);
+
+ /* correctly parse URL */
+ gs_app_set_origin_hostname (app, "https://mirrors.fedoraproject.org/metalink");
+ g_assert_cmpstr (gs_app_get_origin_hostname (app), ==, "fedoraproject.org");
+ gs_app_set_origin_hostname (app, "file:///home/hughsie");
+ g_assert_cmpstr (gs_app_get_origin_hostname (app), ==, "localhost");
+
+ /* check setting the progress */
+ gs_app_set_progress (app, 42);
+ g_assert_cmpuint (gs_app_get_progress (app), ==, 42);
+ gs_app_set_progress (app, 0);
+ g_assert_cmpuint (gs_app_get_progress (app), ==, 0);
+ gs_app_set_progress (app, GS_APP_PROGRESS_UNKNOWN);
+ g_assert_cmpuint (gs_app_get_progress (app), ==, GS_APP_PROGRESS_UNKNOWN);
+ g_assert_false ((gint) 0 <= (gint) GS_APP_PROGRESS_UNKNOWN && GS_APP_PROGRESS_UNKNOWN <= 100);
+
+ /* check pending action */
+ g_assert_cmpuint (gs_app_get_pending_action (app), ==, GS_PLUGIN_ACTION_UNKNOWN);
+ gs_app_set_state (app, AS_APP_STATE_UPDATABLE_LIVE);
+ gs_app_set_pending_action (app, GS_PLUGIN_ACTION_UPDATE);
+ g_assert_cmpuint (gs_app_get_pending_action (app), ==, GS_PLUGIN_ACTION_UPDATE);
+ gs_app_set_state (app, AS_APP_STATE_INSTALLING);
+ g_assert_cmpuint (gs_app_get_pending_action (app), ==, GS_PLUGIN_ACTION_UNKNOWN);
+ gs_app_set_state_recover (app);
+}
+
+static void
+gs_app_progress_clamping_func (void)
+{
+ g_autoptr(GsApp) app = NULL;
+
+ if (g_test_subprocess ()) {
+ app = gs_app_new ("gnome-software.desktop");
+ gs_app_set_progress (app, 142);
+ g_assert_cmpuint (gs_app_get_progress (app), ==, 100);
+ } else {
+ g_test_trap_subprocess (NULL, 0, 0);
+ g_test_trap_assert_failed ();
+ g_test_trap_assert_stderr ("*WARNING*cannot set 142% for *, setting instead: 100%*");
+ }
+}
+
+static void
+gs_app_list_wildcard_dedupe_func (void)
+{
+ g_autoptr(GsAppList) list = gs_app_list_new ();
+ g_autoptr(GsApp) app1 = gs_app_new ("app");
+ g_autoptr(GsApp) app2 = gs_app_new ("app");
+
+ gs_app_add_quirk (app1, GS_APP_QUIRK_IS_WILDCARD);
+ gs_app_list_add (list, app1);
+ gs_app_add_quirk (app2, GS_APP_QUIRK_IS_WILDCARD);
+ gs_app_list_add (list, app2);
+ g_assert_cmpint (gs_app_list_length (list), ==, 1);
+}
+
+static void
+gs_app_list_func (void)
+{
+ g_autoptr(GsAppList) list = gs_app_list_new ();
+ g_autoptr(GsApp) app1 = gs_app_new ("app1");
+ g_autoptr(GsApp) app2 = gs_app_new ("app2");
+
+ /* turn on */
+ gs_app_list_add_flag (list, GS_APP_LIST_FLAG_WATCH_APPS);
+
+ g_assert_cmpint (gs_app_list_get_progress (list), ==, 0);
+ g_assert_cmpint (gs_app_list_get_state (list), ==, AS_APP_STATE_UNKNOWN);
+ gs_app_list_add (list, app1);
+ gs_app_set_progress (app1, 75);
+ gs_app_set_state (app1, AS_APP_STATE_AVAILABLE);
+ gs_app_set_state (app1, AS_APP_STATE_INSTALLING);
+ gs_test_flush_main_context ();
+ g_assert_cmpint (gs_app_list_get_progress (list), ==, 75);
+ g_assert_cmpint (gs_app_list_get_state (list), ==, AS_APP_STATE_INSTALLING);
+
+ gs_app_list_add (list, app2);
+ gs_app_set_progress (app2, 25);
+ gs_test_flush_main_context ();
+ g_assert_cmpint (gs_app_list_get_progress (list), ==, 50);
+ g_assert_cmpint (gs_app_list_get_state (list), ==, AS_APP_STATE_INSTALLING);
+
+ gs_app_list_remove (list, app1);
+ g_assert_cmpint (gs_app_list_get_progress (list), ==, 25);
+ g_assert_cmpint (gs_app_list_get_state (list), ==, AS_APP_STATE_UNKNOWN);
+}
+
+static void
+gs_app_list_performance_func (void)
+{
+ g_autoptr(GPtrArray) apps = g_ptr_array_new_with_free_func ((GDestroyNotify) g_object_unref);
+ g_autoptr(GsAppList) list = gs_app_list_new ();
+ g_autoptr(GTimer) timer = NULL;
+
+ /* create a few apps */
+ for (guint i = 0; i < 500; i++) {
+ g_autofree gchar *id = g_strdup_printf ("%03u.desktop", i);
+ g_ptr_array_add (apps, gs_app_new (id));
+ }
+
+ /* add them to the list */
+ timer = g_timer_new ();
+ for (guint i = 0; i < apps->len; i++) {
+ GsApp *app = g_ptr_array_index (apps, i);
+ gs_app_list_add (list, app);
+ }
+ g_print ("%.2fms ", g_timer_elapsed (timer, NULL) * 1000);
+}
+
+static void
+gs_app_list_related_func (void)
+{
+ g_autoptr(GsAppList) list = gs_app_list_new ();
+ g_autoptr(GsApp) app = gs_app_new ("app");
+ g_autoptr(GsApp) related = gs_app_new ("related");
+
+ /* turn on */
+ gs_app_list_add_flag (list,
+ GS_APP_LIST_FLAG_WATCH_APPS |
+ GS_APP_LIST_FLAG_WATCH_APPS_RELATED);
+ gs_app_add_related (app, related);
+ gs_app_list_add (list, app);
+
+ gs_app_set_progress (app, 75);
+ gs_app_set_progress (related, 25);
+ gs_test_flush_main_context ();
+ g_assert_cmpint (gs_app_list_get_progress (list), ==, 50);
+}
+
+int
+main (int argc, char **argv)
+{
+ g_test_init (&argc, &argv,
+#if GLIB_CHECK_VERSION(2, 60, 0)
+ G_TEST_OPTION_ISOLATE_DIRS,
+#endif
+ NULL);
+ g_setenv ("G_MESSAGES_DEBUG", "all", TRUE);
+
+ /* only critical and error are fatal */
+ g_log_set_fatal_mask (NULL, G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL);
+
+ /* tests go here */
+ g_test_add_func ("/gnome-software/lib/utils{url}", gs_utils_url_func);
+ g_test_add_func ("/gnome-software/lib/utils{wilson}", gs_utils_wilson_func);
+ g_test_add_func ("/gnome-software/lib/utils{error}", gs_utils_error_func);
+ g_test_add_func ("/gnome-software/lib/utils{cache}", gs_utils_cache_func);
+ g_test_add_func ("/gnome-software/lib/utils{append-kv}", gs_utils_append_kv_func);
+ g_test_add_func ("/gnome-software/lib/utils{parse-evr}", gs_utils_parse_evr_func);
+ g_test_add_func ("/gnome-software/lib/os-release", gs_os_release_func);
+ g_test_add_func ("/gnome-software/lib/app", gs_app_func);
+ g_test_add_func ("/gnome-software/lib/app/progress-clamping", gs_app_progress_clamping_func);
+ g_test_add_func ("/gnome-software/lib/app{addons}", gs_app_addons_func);
+ g_test_add_func ("/gnome-software/lib/app{unique-id}", gs_app_unique_id_func);
+ g_test_add_func ("/gnome-software/lib/app{thread}", gs_app_thread_func);
+ g_test_add_func ("/gnome-software/lib/app{list}", gs_app_list_func);
+ g_test_add_func ("/gnome-software/lib/app{list-wildcard-dedupe}", gs_app_list_wildcard_dedupe_func);
+ g_test_add_func ("/gnome-software/lib/app{list-performance}", gs_app_list_performance_func);
+ g_test_add_func ("/gnome-software/lib/app{list-related}", gs_app_list_related_func);
+ g_test_add_func ("/gnome-software/lib/plugin", gs_plugin_func);
+ g_test_add_func ("/gnome-software/lib/plugin{download-rewrite}", gs_plugin_download_rewrite_func);
+
+ return g_test_run ();
+}
diff --git a/lib/gs-test.c b/lib/gs-test.c
new file mode 100644
index 0000000..1f79e6a
--- /dev/null
+++ b/lib/gs-test.c
@@ -0,0 +1,71 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2013-2016 Richard Hughes <richard@hughsie.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+#include <stdlib.h>
+
+#include "gs-test.h"
+
+gchar *
+gs_test_get_filename (const gchar *testdatadir, const gchar *filename)
+{
+ gchar *tmp;
+ char full_tmp[PATH_MAX];
+ g_autofree gchar *path = NULL;
+ path = g_build_filename (testdatadir, filename, NULL);
+ g_debug ("looking in %s", path);
+ tmp = realpath (path, full_tmp);
+ if (tmp == NULL)
+ return NULL;
+ return g_strdup (full_tmp);
+}
+
+void
+gs_test_flush_main_context (void)
+{
+ guint cnt = 0;
+ while (g_main_context_iteration (NULL, FALSE)) {
+ if (cnt == 0)
+ g_debug ("clearing pending events...");
+ cnt++;
+ }
+ if (cnt > 0)
+ g_debug ("cleared %u events", cnt);
+}
+
+/**
+ * gs_test_expose_icon_theme_paths:
+ *
+ * Calculate and set the `GS_SELF_TEST_ICON_THEME_PATH` environment variable
+ * to include the current system icon theme paths. This is designed to be called
+ * before calling `g_test_init()` with `G_TEST_OPTION_ISOLATE_DIRS`, which will
+ * clear the system icon theme paths.
+ *
+ * As this function calls `g_setenv()`, it must not be called after threads have
+ * been spawned.
+ *
+ * Calling this function is an explicit acknowledgement that the code under test
+ * should be accessing the icon theme.
+ *
+ * Since: 3.38
+ */
+void
+gs_test_expose_icon_theme_paths (void)
+{
+ const gchar * const *data_dirs;
+ g_autoptr(GString) data_dirs_str = NULL;
+ g_autofree gchar *data_dirs_joined = NULL;
+
+ data_dirs = g_get_system_data_dirs ();
+ data_dirs_str = g_string_new ("");
+ for (gsize i = 0; data_dirs[i] != NULL; i++)
+ g_string_append_printf (data_dirs_str, "%s%s/icons",
+ (data_dirs_str->len > 0) ? ":" : "",
+ data_dirs[i]);
+ data_dirs_joined = g_string_free (g_steal_pointer (&data_dirs_str), FALSE);
+ g_setenv ("GS_SELF_TEST_ICON_THEME_PATH", data_dirs_joined, TRUE);
+}
diff --git a/lib/gs-test.h b/lib/gs-test.h
new file mode 100644
index 0000000..82a33b4
--- /dev/null
+++ b/lib/gs-test.h
@@ -0,0 +1,20 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2013-2016 Richard Hughes <richard@hughsie.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+#pragma once
+
+#include "gs-app.h"
+
+G_BEGIN_DECLS
+
+void gs_test_flush_main_context (void);
+gchar *gs_test_get_filename (const gchar *testdatadir,
+ const gchar *filename);
+void gs_test_expose_icon_theme_paths (void);
+
+G_END_DECLS
diff --git a/lib/gs-utils.c b/lib/gs-utils.c
new file mode 100644
index 0000000..1ba5976
--- /dev/null
+++ b/lib/gs-utils.c
@@ -0,0 +1,1226 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2013-2017 Richard Hughes <richard@hughsie.com>
+ * Copyright (C) 2015-2018 Kalev Lember <klember@redhat.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+/**
+ * SECTION:gs-utils
+ * @title: GsUtils
+ * @include: gnome-software.h
+ * @stability: Unstable
+ * @short_description: Utilities that plugins can use
+ *
+ * These functions provide useful functionality that makes it easy to
+ * add new plugin functions.
+ */
+
+#include "config.h"
+
+#include <errno.h>
+#include <fnmatch.h>
+#include <math.h>
+#include <string.h>
+#include <glib/gstdio.h>
+#include <json-glib/json-glib.h>
+
+#if defined(__linux__)
+#include <sys/sysinfo.h>
+#elif defined(__FreeBSD__)
+#include <sys/types.h>
+#include <sys/sysctl.h>
+#endif
+
+#ifdef HAVE_POLKIT
+#include <polkit/polkit.h>
+#endif
+
+#include "gs-app.h"
+#include "gs-utils.h"
+#include "gs-plugin.h"
+
+#define MB_IN_BYTES (1024 * 1024)
+
+/**
+ * gs_mkdir_parent:
+ * @path: A full pathname
+ * @error: A #GError, or %NULL
+ *
+ * Creates any required directories, including any parent directories.
+ *
+ * Returns: %TRUE for success
+ **/
+gboolean
+gs_mkdir_parent (const gchar *path, GError **error)
+{
+ g_autofree gchar *parent = NULL;
+
+ parent = g_path_get_dirname (path);
+ if (g_mkdir_with_parents (parent, 0755) == -1) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_FAILED,
+ "Failed to create '%s': %s",
+ parent, g_strerror (errno));
+ return FALSE;
+ }
+ return TRUE;
+}
+
+/**
+ * gs_utils_get_file_age:
+ * @file: A #GFile
+ *
+ * Gets a file age.
+ *
+ * Returns: The time in seconds since the file was modified, or %G_MAXUINT for error
+ */
+guint
+gs_utils_get_file_age (GFile *file)
+{
+ guint64 now;
+ guint64 mtime;
+ g_autoptr(GFileInfo) info = NULL;
+
+ info = g_file_query_info (file,
+ G_FILE_ATTRIBUTE_TIME_MODIFIED,
+ G_FILE_QUERY_INFO_NONE,
+ NULL,
+ NULL);
+ if (info == NULL)
+ return G_MAXUINT;
+ mtime = g_file_info_get_attribute_uint64 (info, G_FILE_ATTRIBUTE_TIME_MODIFIED);
+ now = (guint64) g_get_real_time () / G_USEC_PER_SEC;
+ if (mtime > now)
+ return G_MAXUINT;
+ if (now - mtime > G_MAXUINT)
+ return G_MAXUINT;
+ return (guint) (now - mtime);
+}
+
+static gchar *
+gs_utils_filename_array_return_newest (GPtrArray *array)
+{
+ const gchar *filename_best = NULL;
+ guint age_lowest = G_MAXUINT;
+ guint i;
+ for (i = 0; i < array->len; i++) {
+ const gchar *fn = g_ptr_array_index (array, i);
+ g_autoptr(GFile) file = g_file_new_for_path (fn);
+ guint age_tmp = gs_utils_get_file_age (file);
+ if (age_tmp < age_lowest) {
+ age_lowest = age_tmp;
+ filename_best = fn;
+ }
+ }
+ return g_strdup (filename_best);
+}
+
+/**
+ * gs_utils_get_cache_filename:
+ * @kind: A cache kind, e.g. "fwupd" or "screenshots/123x456"
+ * @resource: A resource, e.g. "system.bin" or "http://foo.bar/baz.bin"
+ * @flags: Some #GsUtilsCacheFlags, e.g. %GS_UTILS_CACHE_FLAG_WRITEABLE
+ * @error: A #GError, or %NULL
+ *
+ * Returns a filename that points into the cache.
+ * This may be per-system or per-user, the latter being more likely
+ * when %GS_UTILS_CACHE_FLAG_WRITEABLE is specified in @flags.
+ *
+ * If %GS_UTILS_CACHE_FLAG_USE_HASH is set in @flags then the returned filename
+ * will contain the hashed version of @resource.
+ *
+ * If there is more than one match, the file that has been modified last is
+ * returned.
+ *
+ * If a plugin requests a file to be saved in the cache it is the plugins
+ * responsibility to remove the file when it is no longer valid or is too old
+ * -- gnome-software will not ever clean the cache for the plugin.
+ * For this reason it is a good idea to use the plugin name as @kind.
+ *
+ * Returns: The full path and filename, which may or may not exist, or %NULL
+ **/
+gchar *
+gs_utils_get_cache_filename (const gchar *kind,
+ const gchar *resource,
+ GsUtilsCacheFlags flags,
+ GError **error)
+{
+ const gchar *tmp;
+ g_autofree gchar *basename = NULL;
+ g_autofree gchar *cachedir = NULL;
+ g_autoptr(GFile) cachedir_file = NULL;
+ g_autoptr(GPtrArray) candidates = g_ptr_array_new_with_free_func (g_free);
+
+ /* in the self tests */
+ tmp = g_getenv ("GS_SELF_TEST_CACHEDIR");
+ if (tmp != NULL)
+ return g_build_filename (tmp, kind, resource, NULL);
+
+ /* get basename */
+ if (flags & GS_UTILS_CACHE_FLAG_USE_HASH) {
+ g_autofree gchar *basename_tmp = g_path_get_basename (resource);
+ g_autofree gchar *hash = g_compute_checksum_for_string (G_CHECKSUM_SHA1,
+ resource, -1);
+ basename = g_strdup_printf ("%s-%s", hash, basename_tmp);
+ } else {
+ basename = g_path_get_basename (resource);
+ }
+
+ /* not writable, so try the system cache first */
+ if ((flags & GS_UTILS_CACHE_FLAG_WRITEABLE) == 0) {
+ g_autofree gchar *cachefn = NULL;
+ cachefn = g_build_filename (LOCALSTATEDIR,
+ "cache",
+ "gnome-software",
+ kind,
+ basename,
+ NULL);
+ if (g_file_test (cachefn, G_FILE_TEST_EXISTS)) {
+ g_ptr_array_add (candidates,
+ g_steal_pointer (&cachefn));
+ }
+ }
+
+ /* not writable, so try the system cache first */
+ if ((flags & GS_UTILS_CACHE_FLAG_WRITEABLE) == 0) {
+ g_autofree gchar *cachefn = NULL;
+ cachefn = g_build_filename (DATADIR,
+ "gnome-software",
+ "cache",
+ kind,
+ basename,
+ NULL);
+ if (g_file_test (cachefn, G_FILE_TEST_EXISTS)) {
+ g_ptr_array_add (candidates,
+ g_steal_pointer (&cachefn));
+ }
+ }
+
+ /* create the cachedir in a per-release location, creating
+ * if it does not already exist */
+ cachedir = g_build_filename (g_get_user_cache_dir (),
+ "gnome-software",
+ kind,
+ NULL);
+ cachedir_file = g_file_new_for_path (cachedir);
+ if (g_file_query_exists (cachedir_file, NULL) &&
+ flags & GS_UTILS_CACHE_FLAG_ENSURE_EMPTY) {
+ if (!gs_utils_rmtree (cachedir, error))
+ return NULL;
+ }
+ if (!g_file_query_exists (cachedir_file, NULL) &&
+ !g_file_make_directory_with_parents (cachedir_file, NULL, error))
+ return NULL;
+ g_ptr_array_add (candidates, g_build_filename (cachedir, basename, NULL));
+
+ /* common case: we only have one option */
+ if (candidates->len == 1)
+ return g_strdup (g_ptr_array_index (candidates, 0));
+
+ /* return the newest (i.e. one with least age) */
+ return gs_utils_filename_array_return_newest (candidates);
+}
+
+/**
+ * gs_utils_get_user_hash:
+ * @error: A #GError, or %NULL
+ *
+ * This SHA1 hash is composed of the contents of machine-id and your
+ * username and is also salted with a hardcoded value.
+ *
+ * This provides an identifier that can be used to identify a specific
+ * user on a machine, allowing them to cast only one vote or perform
+ * one review on each application.
+ *
+ * There is no known way to calculate the machine ID or username from
+ * the machine hash and there should be no privacy issue.
+ *
+ * Returns: The user hash, or %NULL on error
+ */
+gchar *
+gs_utils_get_user_hash (GError **error)
+{
+ g_autofree gchar *data = NULL;
+ g_autofree gchar *salted = NULL;
+
+ if (!g_file_get_contents ("/etc/machine-id", &data, NULL, error))
+ return NULL;
+
+ salted = g_strdup_printf ("gnome-software[%s:%s]",
+ g_get_user_name (), data);
+ return g_compute_checksum_for_string (G_CHECKSUM_SHA1, salted, -1);
+}
+
+/**
+ * gs_utils_get_permission:
+ * @id: A PolicyKit ID, e.g. "org.gnome.Desktop"
+ * @cancellable: A #GCancellable, or %NULL
+ * @error: A #GError, or %NULL
+ *
+ * Gets a permission object for an ID.
+ *
+ * Returns: a #GPermission, or %NULL if this if not possible.
+ **/
+GPermission *
+gs_utils_get_permission (const gchar *id, GCancellable *cancellable, GError **error)
+{
+#ifdef HAVE_POLKIT
+ g_autoptr(GPermission) permission = NULL;
+ permission = polkit_permission_new_sync (id, NULL, cancellable, error);
+ if (permission == NULL) {
+ g_prefix_error (error, "failed to create permission %s: ", id);
+ gs_utils_error_convert_gio (error);
+ return NULL;
+ }
+ return g_steal_pointer (&permission);
+#else
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_NOT_SUPPORTED,
+ "no PolicyKit, so can't return GPermission for %s", id);
+ return NULL;
+#endif
+}
+
+/**
+ * gs_utils_get_content_type:
+ * @file: A GFile
+ * @cancellable: A #GCancellable, or %NULL
+ * @error: A #GError, or %NULL
+ *
+ * Gets the standard content type for a file.
+ *
+ * Returns: the content type, or %NULL, e.g. "text/plain"
+ */
+gchar *
+gs_utils_get_content_type (GFile *file,
+ GCancellable *cancellable,
+ GError **error)
+{
+ const gchar *tmp;
+ g_autoptr(GFileInfo) info = NULL;
+
+ /* get content type */
+ info = g_file_query_info (file,
+ G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE,
+ G_FILE_QUERY_INFO_NONE,
+ cancellable,
+ error);
+ if (info == NULL)
+ return NULL;
+ tmp = g_file_info_get_attribute_string (info, G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE);
+ if (tmp == NULL)
+ return NULL;
+ return g_strdup (tmp);
+}
+
+/**
+ * gs_utils_strv_fnmatch:
+ * @strv: A NUL-terminated list of strings
+ * @str: A string
+ *
+ * Matches a string against a list of globs.
+ *
+ * Returns: %TRUE if the list matches
+ */
+gboolean
+gs_utils_strv_fnmatch (gchar **strv, const gchar *str)
+{
+ guint i;
+
+ /* empty */
+ if (strv == NULL)
+ return FALSE;
+
+ /* look at each one */
+ for (i = 0; strv[i] != NULL; i++) {
+ if (fnmatch (strv[i], str, 0) == 0)
+ return TRUE;
+ }
+ return FALSE;
+}
+
+/**
+ * gs_utils_sort_key:
+ * @str: A string to convert to a sort key
+ *
+ * Useful to sort strings in a locale-sensitive, presentational way.
+ * Case is ignored and utf8 collation is used (e.g. accents are ignored).
+ *
+ * Returns: a newly allocated string sort key
+ */
+gchar *
+gs_utils_sort_key (const gchar *str)
+{
+ g_autofree gchar *casefolded = g_utf8_casefold (str, -1);
+ return g_utf8_collate_key (casefolded, -1);
+}
+
+/**
+ * gs_utils_sort_strcmp:
+ * @str1: (nullable): A string to compare
+ * @str2: (nullable): A string to compare
+ *
+ * Compares two strings in a locale-sensitive, presentational way.
+ * Case is ignored and utf8 collation is used (e.g. accents are ignored). %NULL
+ * is sorted before all non-%NULL strings, and %NULLs compare equal.
+ *
+ * Returns: < 0 if str1 is before str2, 0 if equal, > 0 if str1 is after str2
+ */
+gint
+gs_utils_sort_strcmp (const gchar *str1, const gchar *str2)
+{
+ g_autofree gchar *key1 = (str1 != NULL) ? gs_utils_sort_key (str1) : NULL;
+ g_autofree gchar *key2 = (str2 != NULL) ? gs_utils_sort_key (str2) : NULL;
+ return g_strcmp0 (key1, key2);
+}
+
+/**
+ * gs_utils_get_desktop_app_info:
+ * @id: A desktop ID, e.g. "gimp.desktop"
+ *
+ * Gets a a #GDesktopAppInfo taking into account the kde4- prefix.
+ * If the given @id doesn not have a ".desktop" suffix, it will add one to it
+ * for convenience.
+ *
+ * Returns: a #GDesktopAppInfo for a specific ID, or %NULL
+ */
+GDesktopAppInfo *
+gs_utils_get_desktop_app_info (const gchar *id)
+{
+ GDesktopAppInfo *app_info;
+ g_autofree gchar *desktop_id = NULL;
+
+ /* for convenience, if the given id doesn't have the required .desktop
+ * suffix, we add it here */
+ if (!g_str_has_suffix (id, ".desktop")) {
+ desktop_id = g_strconcat (id, ".desktop", NULL);
+ id = desktop_id;
+ }
+
+ /* try to get the standard app-id */
+ app_info = g_desktop_app_info_new (id);
+
+ /* KDE is a special project because it believes /usr/share/applications
+ * isn't KDE enough. For this reason we support falling back to the
+ * "kde4-" prefixed ID to avoid educating various self-righteous
+ * upstreams about the correct ID to use in the AppData file. */
+ if (app_info == NULL) {
+ g_autofree gchar *kde_id = NULL;
+ kde_id = g_strdup_printf ("%s-%s", "kde4", id);
+ app_info = g_desktop_app_info_new (kde_id);
+ }
+
+ return app_info;
+}
+
+/**
+ * gs_utils_symlink:
+ * @target: the full path of the symlink to create
+ * @linkpath: where the symlink should point to
+ * @error: A #GError, or %NULL
+ *
+ * Creates a symlink that can cross filesystem boundaries.
+ * Any parent directories needed for target to exist are also created.
+ *
+ * Returns: %TRUE for success
+ **/
+gboolean
+gs_utils_symlink (const gchar *target, const gchar *linkpath, GError **error)
+{
+ if (!gs_mkdir_parent (target, error))
+ return FALSE;
+ if (symlink (target, linkpath) != 0) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_WRITE_FAILED,
+ "failed to create symlink from %s to %s",
+ linkpath, target);
+ return FALSE;
+ }
+ return TRUE;
+}
+
+/**
+ * gs_utils_unlink:
+ * @filename: A full pathname to delete
+ * @error: A #GError, or %NULL
+ *
+ * Deletes a file from disk.
+ *
+ * Returns: %TRUE for success
+ **/
+gboolean
+gs_utils_unlink (const gchar *filename, GError **error)
+{
+ if (g_unlink (filename) != 0) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_DELETE_FAILED,
+ "failed to delete %s",
+ filename);
+ return FALSE;
+ }
+ return TRUE;
+}
+
+static gboolean
+gs_utils_rmtree_real (const gchar *directory, GError **error)
+{
+ const gchar *filename;
+ g_autoptr(GDir) dir = NULL;
+
+ /* try to open */
+ dir = g_dir_open (directory, 0, error);
+ if (dir == NULL)
+ return FALSE;
+
+ /* find each */
+ while ((filename = g_dir_read_name (dir))) {
+ g_autofree gchar *src = NULL;
+ src = g_build_filename (directory, filename, NULL);
+ if (g_file_test (src, G_FILE_TEST_IS_DIR) &&
+ !g_file_test (src, G_FILE_TEST_IS_SYMLINK)) {
+ if (!gs_utils_rmtree_real (src, error))
+ return FALSE;
+ } else {
+ if (g_unlink (src) != 0) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_DELETE_FAILED,
+ "Failed to delete: %s", src);
+ return FALSE;
+ }
+ }
+ }
+
+ if (g_rmdir (directory) != 0) {
+ g_set_error (error,
+ GS_PLUGIN_ERROR,
+ GS_PLUGIN_ERROR_DELETE_FAILED,
+ "Failed to remove: %s", directory);
+ return FALSE;
+ }
+ return TRUE;
+}
+
+/**
+ * gs_utils_rmtree:
+ * @directory: A full directory pathname to delete
+ * @error: A #GError, or %NULL
+ *
+ * Deletes a directory from disk and all its contents.
+ *
+ * Returns: %TRUE for success
+ **/
+gboolean
+gs_utils_rmtree (const gchar *directory, GError **error)
+{
+ g_debug ("recursively removing directory '%s'", directory);
+ return gs_utils_rmtree_real (directory, error);
+}
+
+static gdouble
+pnormaldist (gdouble qn)
+{
+ static gdouble b[11] = { 1.570796288, 0.03706987906, -0.8364353589e-3,
+ -0.2250947176e-3, 0.6841218299e-5, 0.5824238515e-5,
+ -0.104527497e-5, 0.8360937017e-7, -0.3231081277e-8,
+ 0.3657763036e-10, 0.6936233982e-12 };
+ gdouble w1, w3;
+ guint i;
+
+ if (qn < 0 || qn > 1)
+ return 0; // This is an error case
+ if (qn == 0.5)
+ return 0;
+
+ w1 = qn;
+ if (qn > 0.5)
+ w1 = 1.0 - w1;
+ w3 = -log (4.0 * w1 * (1.0 - w1));
+ w1 = b[0];
+ for (i = 1; i < 11; i++)
+ w1 = w1 + (b[i] * pow (w3, i));
+
+ if (qn > 0.5)
+ return sqrt (w1 * w3);
+ else
+ return -sqrt (w1 * w3);
+}
+
+static gdouble
+wilson_score (gdouble value, gdouble n, gdouble power)
+{
+ gdouble z, phat;
+ if (value == 0)
+ return 0;
+ z = pnormaldist (1 - power / 2);
+ phat = value / n;
+ return (phat + z * z / (2 * n) -
+ z * sqrt ((phat * (1 - phat) + z * z / (4 * n)) / n)) /
+ (1 + z * z / n);
+}
+
+/**
+ * gs_utils_get_wilson_rating:
+ * @star1: The number of 1 star reviews
+ * @star2: The number of 2 star reviews
+ * @star3: The number of 3 star reviews
+ * @star4: The number of 4 star reviews
+ * @star5: The number of 5 star reviews
+ *
+ * Returns the lower bound of Wilson score confidence interval for a
+ * Bernoulli parameter. This ensures small numbers of ratings don't give overly
+ * high scores.
+ * See https://en.wikipedia.org/wiki/Binomial_proportion_confidence_interval
+ * for details.
+ *
+ * Returns: Wilson rating percentage, or -1 for error
+ **/
+gint
+gs_utils_get_wilson_rating (guint64 star1,
+ guint64 star2,
+ guint64 star3,
+ guint64 star4,
+ guint64 star5)
+{
+ gdouble val;
+ guint64 star_sum = star1 + star2 + star3 + star4 + star5;
+ if (star_sum == 0)
+ return -1;
+
+ /* get score */
+ val = (wilson_score ((gdouble) star1, (gdouble) star_sum, 0.2) * -2);
+ val += (wilson_score ((gdouble) star2, (gdouble) star_sum, 0.2) * -1);
+ val += (wilson_score ((gdouble) star4, (gdouble) star_sum, 0.2) * 1);
+ val += (wilson_score ((gdouble) star5, (gdouble) star_sum, 0.2) * 2);
+
+ /* normalize from -2..+2 to 0..5 */
+ val += 3;
+
+ /* multiply to a percentage */
+ val *= 20;
+
+ /* return rounded up integer */
+ return (gint) ceil (val);
+}
+
+/**
+ * gs_utils_error_add_app_id:
+ * @error: a #GError
+ * @app: a #GsApp
+ *
+ * Adds app unique ID prefix to the error.
+ *
+ * Since: 3.30
+ **/
+void
+gs_utils_error_add_app_id (GError **error, GsApp *app)
+{
+ g_return_if_fail (GS_APP (app));
+ if (error == NULL || *error == NULL)
+ return;
+ g_prefix_error (error, "{%s} ", gs_app_get_unique_id (app));
+}
+
+/**
+ * gs_utils_error_add_origin_id:
+ * @error: a #GError
+ * @origin: a #GsApp
+ *
+ * Adds origin unique ID prefix to the error.
+ *
+ * Since: 3.30
+ **/
+void
+gs_utils_error_add_origin_id (GError **error, GsApp *origin)
+{
+ g_return_if_fail (GS_APP (origin));
+ if (error == NULL || *error == NULL)
+ return;
+ g_prefix_error (error, "[%s] ", gs_app_get_unique_id (origin));
+}
+
+/**
+ * gs_utils_error_strip_app_id:
+ * @error: a #GError
+ *
+ * Removes a possible app ID prefix from the error, and returns the removed
+ * app ID.
+ *
+ * Returns: A newly allocated string with the app ID
+ *
+ * Since: 3.30
+ **/
+gchar *
+gs_utils_error_strip_app_id (GError *error)
+{
+ g_autofree gchar *app_id = NULL;
+ g_autofree gchar *msg = NULL;
+
+ if (error == NULL || error->message == NULL)
+ return FALSE;
+
+ if (g_str_has_prefix (error->message, "{")) {
+ const gchar *endp = strstr (error->message + 1, "} ");
+ if (endp != NULL) {
+ app_id = g_strndup (error->message + 1,
+ endp - (error->message + 1));
+ msg = g_strdup (endp + 2);
+ }
+ }
+
+ if (msg != NULL) {
+ g_free (error->message);
+ error->message = g_steal_pointer (&msg);
+ }
+
+ return g_steal_pointer (&app_id);
+}
+
+/**
+ * gs_utils_error_strip_origin_id:
+ * @error: a #GError
+ *
+ * Removes a possible origin ID prefix from the error, and returns the removed
+ * origin ID.
+ *
+ * Returns: A newly allocated string with the origin ID
+ *
+ * Since: 3.30
+ **/
+gchar *
+gs_utils_error_strip_origin_id (GError *error)
+{
+ g_autofree gchar *origin_id = NULL;
+ g_autofree gchar *msg = NULL;
+
+ if (error == NULL || error->message == NULL)
+ return FALSE;
+
+ if (g_str_has_prefix (error->message, "[")) {
+ const gchar *endp = strstr (error->message + 1, "] ");
+ if (endp != NULL) {
+ origin_id = g_strndup (error->message + 1,
+ endp - (error->message + 1));
+ msg = g_strdup (endp + 2);
+ }
+ }
+
+ if (msg != NULL) {
+ g_free (error->message);
+ error->message = g_steal_pointer (&msg);
+ }
+
+ return g_steal_pointer (&origin_id);
+}
+
+/**
+ * gs_utils_error_convert_gdbus:
+ * @perror: a pointer to a #GError, or %NULL
+ *
+ * Converts the #GDBusError to an error with a GsPluginError domain.
+ *
+ * Returns: %TRUE if the error was converted, or already correct
+ **/
+gboolean
+gs_utils_error_convert_gdbus (GError **perror)
+{
+ GError *error = perror != NULL ? *perror : NULL;
+
+ /* not set */
+ if (error == NULL)
+ return FALSE;
+ if (error->domain == GS_PLUGIN_ERROR)
+ return TRUE;
+ if (error->domain != G_DBUS_ERROR)
+ return FALSE;
+ switch (error->code) {
+ case G_DBUS_ERROR_FAILED:
+ case G_DBUS_ERROR_NO_REPLY:
+ case G_DBUS_ERROR_TIMEOUT:
+ error->code = GS_PLUGIN_ERROR_FAILED;
+ break;
+ case G_DBUS_ERROR_IO_ERROR:
+ case G_DBUS_ERROR_NAME_HAS_NO_OWNER:
+ case G_DBUS_ERROR_NOT_SUPPORTED:
+ case G_DBUS_ERROR_SERVICE_UNKNOWN:
+ case G_DBUS_ERROR_UNKNOWN_INTERFACE:
+ case G_DBUS_ERROR_UNKNOWN_METHOD:
+ case G_DBUS_ERROR_UNKNOWN_OBJECT:
+ case G_DBUS_ERROR_UNKNOWN_PROPERTY:
+ error->code = GS_PLUGIN_ERROR_NOT_SUPPORTED;
+ break;
+ case G_DBUS_ERROR_NO_MEMORY:
+ error->code = GS_PLUGIN_ERROR_NO_SPACE;
+ break;
+ case G_DBUS_ERROR_ACCESS_DENIED:
+ case G_DBUS_ERROR_AUTH_FAILED:
+ error->code = GS_PLUGIN_ERROR_NO_SECURITY;
+ break;
+ case G_DBUS_ERROR_NO_NETWORK:
+ error->code = GS_PLUGIN_ERROR_NO_NETWORK;
+ break;
+ case G_DBUS_ERROR_INVALID_FILE_CONTENT:
+ error->code = GS_PLUGIN_ERROR_INVALID_FORMAT;
+ break;
+ default:
+ g_warning ("can't reliably fixup error code %i in domain %s",
+ error->code, g_quark_to_string (error->domain));
+ error->code = GS_PLUGIN_ERROR_FAILED;
+ break;
+ }
+ error->domain = GS_PLUGIN_ERROR;
+ return TRUE;
+}
+
+/**
+ * gs_utils_error_convert_gio:
+ * @perror: a pointer to a #GError, or %NULL
+ *
+ * Converts the #GIOError to an error with a GsPluginError domain.
+ *
+ * Returns: %TRUE if the error was converted, or already correct
+ **/
+gboolean
+gs_utils_error_convert_gio (GError **perror)
+{
+ GError *error = perror != NULL ? *perror : NULL;
+
+ /* not set */
+ if (error == NULL)
+ return FALSE;
+ if (error->domain == GS_PLUGIN_ERROR)
+ return TRUE;
+ if (error->domain != G_IO_ERROR)
+ return FALSE;
+ switch (error->code) {
+ case G_IO_ERROR_FAILED:
+ case G_IO_ERROR_NOT_FOUND:
+ case G_IO_ERROR_EXISTS:
+ error->code = GS_PLUGIN_ERROR_FAILED;
+ break;
+ case G_IO_ERROR_TIMED_OUT:
+ error->code = GS_PLUGIN_ERROR_TIMED_OUT;
+ break;
+ case G_IO_ERROR_NOT_SUPPORTED:
+ error->code = GS_PLUGIN_ERROR_NOT_SUPPORTED;
+ break;
+ case G_IO_ERROR_CANCELLED:
+ error->code = GS_PLUGIN_ERROR_CANCELLED;
+ break;
+ case G_IO_ERROR_NO_SPACE:
+ error->code = GS_PLUGIN_ERROR_NO_SPACE;
+ break;
+ case G_IO_ERROR_PERMISSION_DENIED:
+ error->code = GS_PLUGIN_ERROR_NO_SECURITY;
+ break;
+ case G_IO_ERROR_HOST_NOT_FOUND:
+ case G_IO_ERROR_HOST_UNREACHABLE:
+ case G_IO_ERROR_CONNECTION_REFUSED:
+ case G_IO_ERROR_PROXY_FAILED:
+ case G_IO_ERROR_PROXY_AUTH_FAILED:
+ case G_IO_ERROR_PROXY_NOT_ALLOWED:
+ error->code = GS_PLUGIN_ERROR_DOWNLOAD_FAILED;
+ break;
+ case G_IO_ERROR_NETWORK_UNREACHABLE:
+ error->code = GS_PLUGIN_ERROR_NO_NETWORK;
+ break;
+ default:
+ g_warning ("can't reliably fixup error code %i in domain %s",
+ error->code, g_quark_to_string (error->domain));
+ error->code = GS_PLUGIN_ERROR_FAILED;
+ break;
+ }
+ error->domain = GS_PLUGIN_ERROR;
+ return TRUE;
+}
+
+/**
+ * gs_utils_error_convert_gresolver:
+ * @perror: a pointer to a #GError, or %NULL
+ *
+ * Converts the #GResolverError to an error with a GsPluginError domain.
+ *
+ * Returns: %TRUE if the error was converted, or already correct
+ **/
+gboolean
+gs_utils_error_convert_gresolver (GError **perror)
+{
+ GError *error = perror != NULL ? *perror : NULL;
+
+ /* not set */
+ if (error == NULL)
+ return FALSE;
+ if (error->domain == GS_PLUGIN_ERROR)
+ return TRUE;
+ if (error->domain != G_RESOLVER_ERROR)
+ return FALSE;
+ switch (error->code) {
+ case G_RESOLVER_ERROR_INTERNAL:
+ error->code = GS_PLUGIN_ERROR_FAILED;
+ break;
+ case G_RESOLVER_ERROR_NOT_FOUND:
+ case G_RESOLVER_ERROR_TEMPORARY_FAILURE:
+ error->code = GS_PLUGIN_ERROR_DOWNLOAD_FAILED;
+ break;
+ default:
+ g_warning ("can't reliably fixup error code %i in domain %s",
+ error->code, g_quark_to_string (error->domain));
+ error->code = GS_PLUGIN_ERROR_FAILED;
+ break;
+ }
+ error->domain = GS_PLUGIN_ERROR;
+ return TRUE;
+}
+
+/**
+ * gs_utils_error_convert_gdk_pixbuf:
+ * @perror: a pointer to a #GError, or %NULL
+ *
+ * Converts the #GdkPixbufError to an error with a GsPluginError domain.
+ *
+ * Returns: %TRUE if the error was converted, or already correct
+ **/
+gboolean
+gs_utils_error_convert_gdk_pixbuf (GError **perror)
+{
+ GError *error = perror != NULL ? *perror : NULL;
+
+ /* not set */
+ if (error == NULL)
+ return FALSE;
+ if (error->domain == GS_PLUGIN_ERROR)
+ return TRUE;
+ if (error->domain != GDK_PIXBUF_ERROR)
+ return FALSE;
+ switch (error->code) {
+ case GDK_PIXBUF_ERROR_UNSUPPORTED_OPERATION:
+ case GDK_PIXBUF_ERROR_UNKNOWN_TYPE:
+ error->code = GS_PLUGIN_ERROR_NOT_SUPPORTED;
+ break;
+ case GDK_PIXBUF_ERROR_FAILED:
+ error->code = GS_PLUGIN_ERROR_FAILED;
+ break;
+ case GDK_PIXBUF_ERROR_CORRUPT_IMAGE:
+ error->code = GS_PLUGIN_ERROR_INVALID_FORMAT;
+ break;
+ default:
+ g_warning ("can't reliably fixup error code %i in domain %s",
+ error->code, g_quark_to_string (error->domain));
+ error->code = GS_PLUGIN_ERROR_FAILED;
+ break;
+ }
+ error->domain = GS_PLUGIN_ERROR;
+ return TRUE;
+}
+
+/**
+ * gs_utils_error_convert_json_glib:
+ * @perror: a pointer to a #GError, or %NULL
+ *
+ * Converts the #JsonParserError to an error with a GsPluginError domain.
+ *
+ * Returns: %TRUE if the error was converted, or already correct
+ **/
+gboolean
+gs_utils_error_convert_json_glib (GError **perror)
+{
+ GError *error = perror != NULL ? *perror : NULL;
+
+ /* not set */
+ if (error == NULL)
+ return FALSE;
+ if (error->domain == GS_PLUGIN_ERROR)
+ return TRUE;
+ if (error->domain != JSON_PARSER_ERROR)
+ return FALSE;
+ switch (error->code) {
+ case JSON_PARSER_ERROR_UNKNOWN:
+ error->code = GS_PLUGIN_ERROR_FAILED;
+ break;
+ default:
+ error->code = GS_PLUGIN_ERROR_INVALID_FORMAT;
+ break;
+ }
+ error->domain = GS_PLUGIN_ERROR;
+ return TRUE;
+}
+
+/**
+ * gs_utils_error_convert_appstream:
+ * @perror: a pointer to a #GError, or %NULL
+ *
+ * Converts the various AppStream error types to an error with a GsPluginError
+ * domain.
+ *
+ * Returns: %TRUE if the error was converted, or already correct
+ **/
+gboolean
+gs_utils_error_convert_appstream (GError **perror)
+{
+ GError *error = perror != NULL ? *perror : NULL;
+
+ /* not set */
+ if (error == NULL)
+ return FALSE;
+ if (error->domain == GS_PLUGIN_ERROR)
+ return TRUE;
+
+ /* custom to this plugin */
+ if (error->domain == AS_UTILS_ERROR) {
+ switch (error->code) {
+ case AS_UTILS_ERROR_INVALID_TYPE:
+ error->code = GS_PLUGIN_ERROR_INVALID_FORMAT;
+ break;
+ case AS_UTILS_ERROR_FAILED:
+ default:
+ error->code = GS_PLUGIN_ERROR_FAILED;
+ break;
+ }
+ } else if (error->domain == AS_STORE_ERROR) {
+ switch (error->code) {
+ case AS_UTILS_ERROR_FAILED:
+ default:
+ error->code = GS_PLUGIN_ERROR_FAILED;
+ break;
+ }
+ } else if (error->domain == AS_ICON_ERROR) {
+ switch (error->code) {
+ case AS_ICON_ERROR_FAILED:
+ default:
+ error->code = GS_PLUGIN_ERROR_FAILED;
+ break;
+ }
+ } else if (error->domain == G_FILE_ERROR) {
+ switch (error->code) {
+ case G_FILE_ERROR_EXIST:
+ case G_FILE_ERROR_ACCES:
+ case G_FILE_ERROR_PERM:
+ error->code = GS_PLUGIN_ERROR_NO_SECURITY;
+ break;
+ case G_FILE_ERROR_NOSPC:
+ error->code = GS_PLUGIN_ERROR_NO_SPACE;
+ break;
+ default:
+ error->code = GS_PLUGIN_ERROR_FAILED;
+ break;
+ }
+ } else {
+ g_warning ("can't reliably fixup error from domain %s",
+ g_quark_to_string (error->domain));
+ error->code = GS_PLUGIN_ERROR_FAILED;
+ }
+ error->domain = GS_PLUGIN_ERROR;
+ return TRUE;
+}
+
+/**
+ * gs_utils_get_url_scheme:
+ * @url: A URL, e.g. "appstream://gimp.desktop"
+ *
+ * Gets the scheme from the URL string.
+ *
+ * Returns: the URL scheme, e.g. "appstream"
+ */
+gchar *
+gs_utils_get_url_scheme (const gchar *url)
+{
+ g_autoptr(SoupURI) uri = NULL;
+
+ /* no data */
+ if (url == NULL)
+ return NULL;
+
+ /* create URI from URL */
+ uri = soup_uri_new (url);
+ if (!SOUP_URI_IS_VALID (uri))
+ return NULL;
+
+ /* success */
+ return g_strdup (soup_uri_get_scheme (uri));
+}
+
+/**
+ * gs_utils_get_url_path:
+ * @url: A URL, e.g. "appstream://gimp.desktop"
+ *
+ * Gets the path from the URL string, removing any leading slashes.
+ *
+ * Returns: the URL path, e.g. "gimp.desktop"
+ */
+gchar *
+gs_utils_get_url_path (const gchar *url)
+{
+ g_autoptr(SoupURI) uri = NULL;
+ const gchar *host;
+ const gchar *path;
+
+ uri = soup_uri_new (url);
+ if (!SOUP_URI_IS_VALID (uri))
+ return NULL;
+
+ /* foo://bar -> scheme: foo, host: bar, path: / */
+ /* foo:bar -> scheme: foo, host: (empty string), path: /bar */
+ host = soup_uri_get_host (uri);
+ path = soup_uri_get_path (uri);
+ if (host != NULL && (strlen (host) > 0))
+ path = host;
+
+ /* trim any leading slashes */
+ while (*path == '/')
+ path++;
+
+ /* success */
+ return g_strdup (path);
+}
+
+/**
+ * gs_user_agent:
+ *
+ * Gets the user agent to use for remote requests.
+ *
+ * Returns: the user-agent, e.g. "gnome-software/3.22.1"
+ */
+const gchar *
+gs_user_agent (void)
+{
+ return PACKAGE_NAME "/" PACKAGE_VERSION;
+}
+
+/**
+ * gs_utils_append_key_value:
+ * @str: A #GString
+ * @align_len: The alignment of the @value compared to the @key
+ * @key: The text to use as a title
+ * @value: The text to use as a value
+ *
+ * Adds a line to an existing string, padding the key to a set number of spaces.
+ *
+ * Since: 3.26
+ */
+void
+gs_utils_append_key_value (GString *str, gsize align_len,
+ const gchar *key, const gchar *value)
+{
+ gsize len = 0;
+
+ g_return_if_fail (str != NULL);
+ g_return_if_fail (value != NULL);
+
+ if (key != NULL) {
+ len = strlen (key) + 2;
+ g_string_append (str, key);
+ g_string_append (str, ": ");
+ }
+ for (gsize i = len; i < align_len + 1; i++)
+ g_string_append (str, " ");
+ g_string_append (str, value);
+ g_string_append (str, "\n");
+}
+
+guint
+gs_utils_get_memory_total (void)
+{
+#if defined(__linux__)
+ struct sysinfo si = { 0 };
+ sysinfo (&si);
+ if (si.mem_unit > 0)
+ return si.totalram / MB_IN_BYTES / si.mem_unit;
+ return 0;
+#elif defined(__FreeBSD__)
+ unsigned long physmem;
+ sysctl ((int[]){ CTL_HW, HW_PHYSMEM }, 2, &physmem, &(size_t){ sizeof (physmem) }, NULL, 0);
+ return physmem / MB_IN_BYTES;
+#else
+#error "Please implement gs_utils_get_memory_total for your system."
+#endif
+}
+
+/**
+ * gs_utils_parse_evr:
+ * @evr: an EVR version string
+ * @out_epoch: (out): return location for the epoch string
+ * @out_version: (out): return location for the version string
+ * @out_release: (out): return location for the release string
+ *
+ * Splits EVR into epoch-version-release strings.
+ *
+ * Returns: %TRUE for success
+ **/
+gboolean
+gs_utils_parse_evr (const gchar *evr,
+ gchar **out_epoch,
+ gchar **out_version,
+ gchar **out_release)
+{
+ const gchar *version_release;
+ g_auto(GStrv) split_colon = NULL;
+ g_auto(GStrv) split_dash = NULL;
+
+ /* split on : to get epoch */
+ split_colon = g_strsplit (evr, ":", -1);
+ switch (g_strv_length (split_colon)) {
+ case 1:
+ /* epoch is 0 when not set */
+ *out_epoch = g_strdup ("0");
+ version_release = split_colon[0];
+ break;
+ case 2:
+ /* epoch set */
+ *out_epoch = g_strdup (split_colon[0]);
+ version_release = split_colon[1];
+ break;
+ default:
+ /* error */
+ return FALSE;
+ }
+
+ /* split on - to get version and release */
+ split_dash = g_strsplit (version_release, "-", -1);
+ switch (g_strv_length (split_dash)) {
+ case 1:
+ /* all of the string is version */
+ *out_version = g_strdup (split_dash[0]);
+ *out_release = g_strdup ("0");
+ break;
+ case 2:
+ /* both version and release set */
+ *out_version = g_strdup (split_dash[0]);
+ *out_release = g_strdup (split_dash[1]);
+ break;
+ default:
+ /* error */
+ return FALSE;
+ }
+
+ g_assert (*out_epoch != NULL);
+ g_assert (*out_version != NULL);
+ g_assert (*out_release != NULL);
+ return TRUE;
+}
+
+/**
+ * gs_utils_set_online_updates_timestamp:
+ *
+ * Sets the value of online-updates-timestamp to current epoch. "online-updates-timestamp" represents
+ * the last time the system was online and got any updates.
+ *
+ **/
+void
+gs_utils_set_online_updates_timestamp (GSettings *settings)
+{
+ g_autoptr(GDateTime) now = NULL;
+
+ g_return_if_fail (settings != NULL);
+
+ now = g_date_time_new_now_local ();
+ g_settings_set (settings, "online-updates-timestamp", "x", g_date_time_to_unix (now));
+}
+
+/* vim: set noexpandtab: */
diff --git a/lib/gs-utils.h b/lib/gs-utils.h
new file mode 100644
index 0000000..3964f00
--- /dev/null
+++ b/lib/gs-utils.h
@@ -0,0 +1,96 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * Copyright (C) 2013-2016 Richard Hughes <richard@hughsie.com>
+ * Copyright (C) 2018 Kalev Lember <klember@redhat.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+#pragma once
+
+#include <gio/gdesktopappinfo.h>
+#include <gtk/gtk.h>
+
+#include "gs-app.h"
+
+G_BEGIN_DECLS
+
+/**
+ * GsUtilsCacheFlags:
+ * @GS_UTILS_CACHE_FLAG_NONE: No flags set
+ * @GS_UTILS_CACHE_FLAG_WRITEABLE: A writable directory is required
+ * @GS_UTILS_CACHE_FLAG_USE_HASH: Prefix a hash to the filename
+ * @GS_UTILS_CACHE_FLAG_ENSURE_EMPTY: Clear existing cached items
+ *
+ * The cache flags.
+ **/
+typedef enum {
+ GS_UTILS_CACHE_FLAG_NONE = 0,
+ GS_UTILS_CACHE_FLAG_WRITEABLE = 1 << 0,
+ GS_UTILS_CACHE_FLAG_USE_HASH = 1 << 1,
+ GS_UTILS_CACHE_FLAG_ENSURE_EMPTY = 1 << 2,
+ /*< private >*/
+ GS_UTILS_CACHE_FLAG_LAST
+} GsUtilsCacheFlags;
+
+guint gs_utils_get_file_age (GFile *file);
+gchar *gs_utils_get_content_type (GFile *file,
+ GCancellable *cancellable,
+ GError **error);
+gboolean gs_utils_symlink (const gchar *target,
+ const gchar *linkpath,
+ GError **error);
+gboolean gs_utils_unlink (const gchar *filename,
+ GError **error);
+gboolean gs_mkdir_parent (const gchar *path,
+ GError **error);
+gchar *gs_utils_get_cache_filename (const gchar *kind,
+ const gchar *resource,
+ GsUtilsCacheFlags flags,
+ GError **error);
+gchar *gs_utils_get_user_hash (GError **error);
+GPermission *gs_utils_get_permission (const gchar *id,
+ GCancellable *cancellable,
+ GError **error);
+gboolean gs_utils_strv_fnmatch (gchar **strv,
+ const gchar *str);
+gchar *gs_utils_sort_key (const gchar *str);
+gint gs_utils_sort_strcmp (const gchar *str1,
+ const gchar *str2);
+GDesktopAppInfo *gs_utils_get_desktop_app_info (const gchar *id);
+gboolean gs_utils_rmtree (const gchar *directory,
+ GError **error);
+gint gs_utils_get_wilson_rating (guint64 star1,
+ guint64 star2,
+ guint64 star3,
+ guint64 star4,
+ guint64 star5);
+void gs_utils_error_add_app_id (GError **error,
+ GsApp *app);
+void gs_utils_error_add_origin_id (GError **error,
+ GsApp *origin);
+gchar *gs_utils_error_strip_app_id (GError *error);
+gchar *gs_utils_error_strip_origin_id (GError *error);
+gboolean gs_utils_error_convert_gio (GError **perror);
+gboolean gs_utils_error_convert_gresolver (GError **perror);
+gboolean gs_utils_error_convert_gdbus (GError **perror);
+gboolean gs_utils_error_convert_gdk_pixbuf(GError **perror);
+gboolean gs_utils_error_convert_json_glib (GError **perror);
+gboolean gs_utils_error_convert_appstream (GError **perror);
+
+gchar *gs_utils_get_url_scheme (const gchar *url);
+gchar *gs_utils_get_url_path (const gchar *url);
+const gchar *gs_user_agent (void);
+void gs_utils_append_key_value (GString *str,
+ gsize align_len,
+ const gchar *key,
+ const gchar *value);
+guint gs_utils_get_memory_total (void);
+gboolean gs_utils_parse_evr (const gchar *evr,
+ gchar **out_epoch,
+ gchar **out_version,
+ gchar **out_release);
+void gs_utils_set_online_updates_timestamp (GSettings *settings);
+
+G_END_DECLS
diff --git a/lib/meson.build b/lib/meson.build
new file mode 100644
index 0000000..0ef4936
--- /dev/null
+++ b/lib/meson.build
@@ -0,0 +1,142 @@
+cargs = ['-DG_LOG_DOMAIN="Gs"']
+cargs += ['-DLOCALPLUGINDIR=""']
+
+install_headers([
+ 'gnome-software.h',
+ 'gs-app.h',
+ 'gs-app-collation.h',
+ 'gs-app-list.h',
+ 'gs-autocleanups.h',
+ 'gs-category.h',
+ 'gs-ioprio.h',
+ 'gs-metered.h',
+ 'gs-os-release.h',
+ 'gs-plugin.h',
+ 'gs-plugin-event.h',
+ 'gs-plugin-job.h',
+ 'gs-plugin-loader.h',
+ 'gs-plugin-loader-sync.h',
+ 'gs-plugin-types.h',
+ 'gs-plugin-vfuncs.h',
+ 'gs-utils.h'
+ ],
+ subdir : 'gnome-software'
+)
+
+librarydeps = [
+ appstream_glib,
+ gio_unix,
+ glib,
+ gmodule,
+ goa,
+ gtk,
+ json_glib,
+ libm,
+ libsoup,
+ libsysprof_capture_dep,
+ valgrind,
+]
+
+if get_option('mogwai')
+ librarydeps += mogwai_schedule_client
+endif
+
+if get_option('polkit')
+ librarydeps += polkit
+endif
+
+libgnomesoftware = static_library(
+ 'gnomesoftware',
+ sources : [
+ 'gs-app.c',
+ 'gs-app-list.c',
+ 'gs-category.c',
+ 'gs-debug.c',
+ 'gs-ioprio.c',
+ 'gs-ioprio.h',
+ 'gs-metered.c',
+ 'gs-os-release.c',
+ 'gs-plugin.c',
+ 'gs-plugin-event.c',
+ 'gs-plugin-job.c',
+ 'gs-plugin-loader.c',
+ 'gs-plugin-loader-sync.c',
+ 'gs-test.c',
+ 'gs-utils.c',
+ ],
+ include_directories : [
+ include_directories('..'),
+ ],
+ dependencies : librarydeps,
+ c_args : cargs,
+ install: true,
+)
+
+pkg = import('pkgconfig')
+
+pkg.generate(
+ libgnomesoftware,
+ description : 'GNOME Software is a software center for GNOME',
+ filebase : 'gnome-software',
+ name : 'gnome-software',
+ subdirs : 'gnome-software',
+ variables : 'plugindir=${libdir}/gs-plugins-' + gs_plugin_api_version,
+)
+
+executable(
+ 'gnome-software-cmd',
+ sources : [
+ 'gs-cmd.c',
+ ],
+ include_directories : [
+ include_directories('..'),
+ ],
+ dependencies : [
+ appstream_glib,
+ gio_unix,
+ glib,
+ gmodule,
+ goa,
+ gtk,
+ json_glib,
+ libm,
+ libsoup,
+ valgrind,
+ ],
+ link_with : [
+ libgnomesoftware
+ ],
+ c_args : cargs,
+ install : true,
+ install_dir : get_option('libexecdir')
+)
+
+if get_option('tests')
+ cargs += ['-DTESTDATADIR="' + join_paths(meson.current_source_dir(), '..', 'data') + '"']
+ e = executable(
+ 'gs-self-test',
+ compiled_schemas,
+ sources : [
+ 'gs-self-test.c'
+ ],
+ include_directories : [
+ include_directories('..'),
+ ],
+ dependencies : [
+ appstream_glib,
+ gio_unix,
+ glib,
+ gmodule,
+ goa,
+ gtk,
+ json_glib,
+ libm,
+ libsoup
+ ],
+ link_with : [
+ libgnomesoftware
+ ],
+ c_args : cargs
+ )
+ test('gs-self-test-lib', e, suite: ['lib'], env: test_env, timeout : 120)
+endif