summaryrefslogtreecommitdiffstats
path: root/lib/gs-app-list.c
diff options
context:
space:
mode:
Diffstat (limited to 'lib/gs-app-list.c')
-rw-r--r--lib/gs-app-list.c1025
1 files changed, 1025 insertions, 0 deletions
diff --git a/lib/gs-app-list.c b/lib/gs-app-list.c
new file mode 100644
index 0000000..a5f6785
--- /dev/null
+++ b/lib/gs-app-list.c
@@ -0,0 +1,1025 @@
+/* -*- 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"
+#include "gs-enums.h"
+
+struct _GsAppList
+{
+ GObject parent_instance;
+ GPtrArray *array;
+ GMutex mutex;
+ guint size_peak;
+ GsAppListFlags flags;
+ GsAppState state;
+ guint progress; /* 0–100 inclusive, or %GS_APP_PROGRESS_UNKNOWN */
+ guint custom_progress; /* overrides the 'progress', if not %GS_APP_PROGRESS_UNKNOWN */
+};
+
+G_DEFINE_TYPE (GsAppList, gs_app_list, G_TYPE_OBJECT)
+
+enum {
+ PROP_STATE = 1,
+ PROP_PROGRESS,
+ PROP_LAST
+};
+
+enum {
+ SIGNAL_APP_STATE_CHANGED,
+ SIGNAL_LAST
+};
+
+static guint signals [SIGNAL_LAST] = { 0 };
+
+/**
+ * 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 #GsAppState, e.g. %GS_APP_STATE_INSTALLED
+ *
+ * Since: 3.30
+ **/
+GsAppState
+gs_app_list_get_state (GsAppList *list)
+{
+ g_return_val_if_fail (GS_IS_APP_LIST (list), GS_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);
+ if (list->custom_progress != GS_APP_PROGRESS_UNKNOWN)
+ return list->custom_progress;
+ return list->progress;
+}
+
+static gboolean
+app_list_notify_progress_idle_cb (gpointer user_data)
+{
+ GsAppList *list = user_data;
+
+ g_object_notify (G_OBJECT (list), "progress");
+ g_object_unref (list);
+
+ return G_SOURCE_REMOVE;
+}
+
+/**
+ * gs_app_list_override_progress:
+ * @list: a #GsAppList
+ * @progress: a progress to set, between 0 and 100 inclusive, or %GS_APP_PROGRESS_UNKNOWN
+ *
+ * Override the progress property to be this value, or use %GS_APP_PROGRESS_UNKNOWN,
+ * to unset the override. This can be used when only the overall progress is known,
+ * instead of a per-application progress.
+ *
+ * Since: 42
+ **/
+void
+gs_app_list_override_progress (GsAppList *list,
+ guint progress)
+{
+ g_return_if_fail (GS_IS_APP_LIST (list));
+
+ if (list->custom_progress != progress) {
+ list->custom_progress = progress;
+ g_idle_add (app_list_notify_progress_idle_cb, g_object_ref (list));
+ }
+}
+
+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) {
+ g_autoptr(GsAppList) list2 = gs_app_dup_addons (app);
+
+ for (guint i = 0; list2 != NULL && 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)
+{
+ GsAppState state = GS_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);
+ GsAppState state_tmp = gs_app_get_state (app_tmp);
+ if (state_tmp == GS_APP_STATE_INSTALLING ||
+ state_tmp == GS_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);
+
+ g_signal_emit (self, signals[SIGNAL_APP_STATE_CHANGED], 0, app);
+}
+
+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;
+}
+
+/**
+ * gs_app_list_set_size_peak:
+ * @list: A #GsAppList
+ * @size_peak: A value to set
+ *
+ * Sets the largest size the list has ever been.
+ *
+ * Since: 43
+ **/
+void
+gs_app_list_set_size_peak (GsAppList *list,
+ guint size_peak)
+{
+ g_return_if_fail (GS_IS_APP_LIST (list));
+ list->size_peak = 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_data_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;
+ }
+
+ 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;
+ }
+
+ /* does not exist */
+ id = gs_app_get_unique_id (app);
+ if (id == NULL) {
+ /* 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)
+{
+ /* check for duplicate */
+ if ((flag & GS_APP_LIST_ADD_FLAG_CHECK_FOR_DUPE) > 0 &&
+ !gs_app_list_check_for_duplicate (list, 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.
+ *
+ * Returns: %TRUE if the app was removed, %FALSE if it did not exist in the @list
+ * Since: 43
+ **/
+gboolean
+gs_app_list_remove (GsAppList *list, GsApp *app)
+{
+ g_autoptr(GMutexLocker) locker = NULL;
+ gboolean removed;
+
+ g_return_val_if_fail (GS_IS_APP_LIST (list), FALSE);
+ g_return_val_if_fail (GS_IS_APP (app), FALSE);
+
+ locker = g_mutex_locker_new (&list->mutex);
+ removed = g_ptr_array_remove (list->array, app);
+ if (removed) {
+ gs_app_list_maybe_unwatch_app (list, app);
+
+ /* recalculate global state */
+ gs_app_list_invalidate_state (list);
+ gs_app_list_invalidate_progress (list);
+ }
+
+ return removed;
+}
+
+/**
+ * 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);
+ const GsAppListSortHelper *helper = (GsAppListSortHelper *) user_data;
+ return helper->func (app1, app2, helper->user_data);
+}
+
+/**
+ * gs_app_list_sort:
+ * @list: A #GsAppList
+ * @func: A #GsAppListSortFunc
+ * @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);
+}
+
+/**
+ * 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)
+{
+ GRand *rand;
+ g_autoptr(GDateTime) date = NULL;
+ g_autoptr(GMutexLocker) locker = NULL;
+
+ g_return_if_fail (GS_IS_APP_LIST (list));
+
+ locker = g_mutex_locker_new (&list->mutex);
+
+ if (!gs_app_list_length (list))
+ return;
+
+ 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));
+
+ /* Fisher–Yates shuffle of the array.
+ * See https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle */
+ for (guint i = gs_app_list_length (list) - 1; i >= 1; i--) {
+ gpointer tmp;
+ guint j = g_rand_int_range (rand, 0, i + 1);
+
+ tmp = list->array->pdata[i];
+ list->array->pdata[i] = list->array->pdata[j];
+ list->array->pdata[j] = tmp;
+ }
+
+ 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 provided items */
+ if (flags & GS_APP_LIST_FILTER_FLAG_KEY_ID_PROVIDES) {
+ GPtrArray *provided = gs_app_get_provided (app);
+ g_ptr_array_add (keys, g_strdup (gs_app_get_id (app)));
+ for (guint i = 0; i < provided->len; i++) {
+ AsProvided *prov = g_ptr_array_index (provided, i);
+ GPtrArray *items;
+ if (as_provided_get_kind (prov) != AS_PROVIDED_KIND_ID)
+ continue;
+ items = as_provided_get_items (prov);
+ for (guint j = 0; j < items->len; j++)
+ g_ptr_array_add (keys, g_strdup (g_ptr_array_index (items, j)));
+ }
+ 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);
+ /* In case the same instance is in the 'list' multiple times */
+ g_hash_table_remove (kept_apps, app);
+ }
+ }
+}
+
+/**
+ * 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_enum (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_enum ("state", NULL, NULL,
+ GS_TYPE_APP_STATE,
+ GS_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);
+
+ /**
+ * GsAppList:app-state-changed:
+ * @app: a #GsApp
+ *
+ * Emitted when any of the internal #GsApp instances changes its state.
+ *
+ * Since: 3.40
+ */
+ signals [SIGNAL_APP_STATE_CHANGED] =
+ g_signal_new ("app-state-changed",
+ G_TYPE_FROM_CLASS (object_class), G_SIGNAL_RUN_LAST,
+ 0, NULL, NULL, g_cclosure_marshal_generic,
+ G_TYPE_NONE, 1, GS_TYPE_APP);
+}
+
+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);
+ list->custom_progress = GS_APP_PROGRESS_UNKNOWN;
+}
+
+/**
+ * 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);
+}