summaryrefslogtreecommitdiffstats
path: root/lib/gs-app.c
diff options
context:
space:
mode:
Diffstat (limited to 'lib/gs-app.c')
-rw-r--r--lib/gs-app.c4680
1 files changed, 4680 insertions, 0 deletions
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;
+}