gs-app.c 113 KB
Newer Older
1 2
/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
 *
3
 * Copyright (C) 2013-2016 Richard Hughes <richard@hughsie.com>
4
 * Copyright (C) 2013 Matthias Clasen <mclasen@redhat.com>
5
 * Copyright (C) 2014-2018 Kalev Lember <klember@redhat.com>
6
 *
7
 * SPDX-License-Identifier: GPL-2.0+
8 9
 */

10 11
/**
 * SECTION:gs-app
12 13 14
 * @title: GsApp
 * @include: gnome-software.h
 * @stability: Unstable
15 16 17
 * @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
Matthias Clasen's avatar
Matthias Clasen committed
18
 * so you can't have different GsApp's for different versions or architectures
19 20 21
 * 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
Matthias Clasen's avatar
Matthias Clasen committed
22
 * represent a system update and its individual components.
23
 *
Matthias Clasen's avatar
Matthias Clasen committed
24 25 26
 * 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
27 28
 * a #GsApp in different parts of gnome-software can be observed by connecting to
 * signals on the #GsApp.
29
 *
Matthias Clasen's avatar
Matthias Clasen committed
30
 * Information about other #GsApp objects can be stored in this object, for
31
 * instance in the gs_app_add_related() method or gs_app_get_history().
32 33
 */

34 35
#include "config.h"

36
#include <string.h>
37
#include <gtk/gtk.h>
38
#include <glib/gi18n.h>
39

40
#include "gs-app-collation.h"
41
#include "gs-app-private.h"
42
#include "gs-os-release.h"
43
#include "gs-plugin.h"
44
#include "gs-utils.h"
45

46
typedef struct
47
{
48 49
	GObject			 parent_instance;

50
	GMutex			 mutex;
51
	gchar			*id;
52
	gchar			*unique_id;
53
	gboolean		 unique_id_valid;
54
	gchar			*branch;
55
	gchar			*name;
56
	GsAppQuality		 name_quality;
57
	GPtrArray		*icons;
58
	GPtrArray		*sources;
59
	GPtrArray		*source_ids;
60
	gchar			*project_group;
61
	gchar			*developer_name;
62
	gchar			*agreement;
63
	gchar			*version;
64
	gchar			*version_ui;
65
	gchar			*summary;
66
	GsAppQuality		 summary_quality;
67
	gchar			*summary_missing;
68
	gchar			*description;
69
	GsAppQuality		 description_quality;
70
	GPtrArray		*screenshots;
71
	GPtrArray		*categories;
72
	GPtrArray		*key_colors;
73
	GHashTable		*urls;
74
	GHashTable		*launchables;
75 76
	gchar			*license;
	GsAppQuality		 license_quality;
77
	gchar			**menu_path;
78
	gchar			*origin;
79
	gchar			*origin_appstream;
80
	gchar			*origin_hostname;
81
	gchar			*update_version;
82
	gchar			*update_version_ui;
83
	gchar			*update_details;
84
	AsUrgencyKind		 update_urgency;
85
	GsAppPermissions         update_permissions;
86
	gchar			*management_plugin;
87
	guint			 match_value;
88
	guint			 priority;
89
	gint			 rating;
90
	GArray			*review_ratings;
91
	GPtrArray		*reviews; /* of AsReview */
Richard Hughes's avatar
Richard Hughes committed
92
	GPtrArray		*provides; /* of AsProvide */
93 94
	guint64			 size_installed;
	guint64			 size_download;
95
	AsAppKind		 kind;
96
	AsAppState		 state;
97
	AsAppState		 state_recover;
98
	AsAppScope		 scope;
99
	AsBundleKind		 bundle_kind;
100
	guint			 progress;
101
	gboolean		 allow_cancel;
102
	GHashTable		*metadata;
103 104 105
	GsAppList		*addons;
	GsAppList		*related;
	GsAppList		*history;
106
	guint64			 install_date;
107
	guint64			 kudos;
Kalev Lember's avatar
Kalev Lember committed
108
	gboolean		 to_be_installed;
109
	GsAppQuirk		 quirk;
110
	gboolean		 license_is_free;
111
	GsApp			*runtime;
Richard Hughes's avatar
Richard Hughes committed
112
	GFile			*local_file;
113
	AsContentRating		*content_rating;
114
	GdkPixbuf		*pixbuf;
Robert Ancell's avatar
Robert Ancell committed
115
	GsPrice			*price;
116
	GCancellable		*cancellable;
Joaquim Rocha's avatar
Joaquim Rocha committed
117
	GsPluginAction		 pending_action;
Matthias Clasen's avatar
Matthias Clasen committed
118
	GsAppPermissions         permissions;
119
} GsAppPrivate;
120 121 122 123 124 125 126

enum {
	PROP_0,
	PROP_ID,
	PROP_NAME,
	PROP_VERSION,
	PROP_SUMMARY,
127
	PROP_DESCRIPTION,
128 129 130
	PROP_RATING,
	PROP_KIND,
	PROP_STATE,
131
	PROP_PROGRESS,
132
	PROP_CAN_CANCEL_INSTALLATION,
133
	PROP_INSTALL_DATE,
134
	PROP_QUIRK,
Joaquim Rocha's avatar
Joaquim Rocha committed
135
	PROP_PENDING_ACTION,
136 137 138
	PROP_LAST
};

139
G_DEFINE_TYPE_WITH_PRIVATE (GsApp, gs_app, G_TYPE_OBJECT)
140

141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182
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;
}

183 184 185
static void
gs_app_kv_lpad (GString *str, const gchar *key, const gchar *value)
{
186
	gs_utils_append_key_value (str, 20, key, value);
187 188
}

189 190 191 192 193 194 195 196 197 198 199 200
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);
}

201 202 203 204 205 206 207 208 209 210 211 212
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);
}

213
static const gchar *
214
_as_app_quirk_flag_to_string (GsAppQuirk quirk)
215
{
216
	if (quirk == GS_APP_QUIRK_PROVENANCE)
217
		return "provenance";
218
	if (quirk == GS_APP_QUIRK_COMPULSORY)
219
		return "compulsory";
220
	if (quirk == GS_APP_QUIRK_HAS_SOURCE)
221
		return "has-source";
222 223 224
	if (quirk == GS_APP_QUIRK_IS_WILDCARD)
		return "is-wildcard";
	if (quirk == GS_APP_QUIRK_NEEDS_REBOOT)
225
		return "needs-reboot";
226
	if (quirk == GS_APP_QUIRK_NOT_REVIEWABLE)
227
		return "not-reviewable";
228
	if (quirk == GS_APP_QUIRK_HAS_SHORTCUT)
229
		return "has-shortcut";
230
	if (quirk == GS_APP_QUIRK_NOT_LAUNCHABLE)
231
		return "not-launchable";
232
	if (quirk == GS_APP_QUIRK_NEEDS_USER_ACTION)
233
		return "needs-user-action";
234
	if (quirk == GS_APP_QUIRK_IS_PROXY)
235
		return "is-proxy";
236
	if (quirk == GS_APP_QUIRK_REMOVABLE_HARDWARE)
237
		return "removable-hardware";
238 239 240
	return NULL;
}

241 242 243 244
/* mutex must be held */
static const gchar *
gs_app_get_unique_id_unlocked (GsApp *app)
{
245 246
	GsAppPrivate *priv = gs_app_get_instance_private (app);

247
	/* invalid */
248
	if (priv->id == NULL)
249 250 251
		return NULL;

	/* hmm, do what we can */
252 253 254 255 256 257 258 259 260
	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;
261
	}
262
	return priv->unique_id;
263 264
}

265 266 267 268 269
/**
 * gs_app_compare_priority:
 * @app1: a #GsApp
 * @app2: a #GsApp
 *
270
 * Compares two applications using their priority.
271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297
 *
 * 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;
}

298
/**
299 300
 * gs_app_quirk_to_string:
 * @quirk: a #GsAppQuirk
301 302 303 304 305 306
 *
 * Returns the quirk bitfield as a string.
 *
 * Returns: (transfer full): a string
 **/
static gchar *
307
gs_app_quirk_to_string (GsAppQuirk quirk)
308 309 310 311 312
{
	GString *str = g_string_new ("");
	guint64 i;

	/* nothing set */
313
	if (quirk == GS_APP_QUIRK_NONE) {
314 315 316 317 318
		g_string_append (str, "none");
		return g_string_free (str, FALSE);
	}

	/* get flags */
319
	for (i = 1; i < GS_APP_QUIRK_LAST; i *= 2) {
320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336
		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);
}

337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372
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);
}

373 374
/**
 * gs_app_to_string:
375 376 377 378 379 380 381
 * @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
382 383
 *
 * Since: 3.22
384 385 386 387
 **/
gchar *
gs_app_to_string (GsApp *app)
{
388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407
	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);
408
	GsAppPrivate *priv = gs_app_get_instance_private (app);
409
	AsImage *im;
410
	GList *keys;
411 412
	const gchar *tmp;
	guint i;
413

414 415
	g_return_if_fail (GS_IS_APP (app));
	g_return_if_fail (str != NULL);
416

417
	g_string_append_printf (str, " [%p]\n", app);
418 419 420
	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) {
421
		g_autofree gchar *qstr = gs_app_quirk_to_string (priv->quirk);
422 423
		gs_app_kv_lpad (str, "quirk", qstr);
	}
424 425 426 427 428
	if (priv->progress > 0)
		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)
429
		gs_app_kv_lpad (str, "unique-id", gs_app_get_unique_id (app));
430 431 432
	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) {
433
		gs_app_kv_lpad (str, "bundle-kind",
434
				as_bundle_kind_to_string (priv->bundle_kind));
435
	}
436
	if (priv->kudos > 0) {
437
		g_autofree gchar *kudo_str = NULL;
438
		kudo_str = gs_app_kudos_to_string (priv->kudos);
439 440
		gs_app_kv_lpad (str, "kudos", kudo_str);
	}
441
	gs_app_kv_printf (str, "kudo-percentage", "%u",
442
			  gs_app_get_kudos_percentage (app));
443 444 445 446 447 448
	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);
	for (i = 0; i < priv->icons->len; i++) {
		AsIcon *icon = g_ptr_array_index (priv->icons, i);
449
		gs_app_kv_lpad (str, "icon-kind",
450
				as_icon_kind_to_string (as_icon_get_kind (icon)));
451 452 453 454
		if (as_icon_get_pixbuf (icon) != NULL) {
			gs_app_kv_printf (str, "icon-pixbuf", "%p",
					  as_icon_get_pixbuf (icon));
		}
455
		if (as_icon_get_name (icon) != NULL)
456
			gs_app_kv_lpad (str, "icon-name",
457 458
					as_icon_get_name (icon));
		if (as_icon_get_prefix (icon) != NULL)
459
			gs_app_kv_lpad (str, "icon-prefix",
460 461
					as_icon_get_prefix (icon));
		if (as_icon_get_filename (icon) != NULL)
462
			gs_app_kv_lpad (str, "icon-filename",
463
					as_icon_get_filename (icon));
464
	}
465 466 467 468 469 470 471 472 473 474 475 476 477 478 479
	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) {
480
		gs_app_kv_printf (str, "update-urgency", "%u",
481
				  priv->update_urgency);
482
	}
483 484 485 486 487
	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++) {
Robert Ancell's avatar
Robert Ancell committed
488
		AsScreenshot *ss = g_ptr_array_index (priv->screenshots, i);
489
		g_autofree gchar *key = NULL;
490 491 492 493
		tmp = as_screenshot_get_caption (ss, NULL);
		im = as_screenshot_get_image (ss, 0, 0);
		if (im == NULL)
			continue;
494
		key = g_strdup_printf ("screenshot-%02u", i);
495 496 497
		gs_app_kv_printf (str, key, "%s [%s]",
				  as_image_get_url (im),
				  tmp != NULL ? tmp : "<none>");
498
	}
499
	for (i = 0; i < priv->sources->len; i++) {
500
		g_autofree gchar *key = NULL;
501
		tmp = g_ptr_array_index (priv->sources, i);
502
		key = g_strdup_printf ("source-%02u", i);
503
		gs_app_kv_lpad (str, key, tmp);
504
	}
505
	for (i = 0; i < priv->source_ids->len; i++) {
506
		g_autofree gchar *key = NULL;
507
		tmp = g_ptr_array_index (priv->source_ids, i);
508
		key = g_strdup_printf ("source-id-%02u", i);
509
		gs_app_kv_lpad (str, key, tmp);
510
	}
511 512
	if (priv->local_file != NULL) {
		g_autofree gchar *fn = g_file_get_path (priv->local_file);
Richard Hughes's avatar
Richard Hughes committed
513 514
		gs_app_kv_lpad (str, "local-filename", fn);
	}
515 516
	if (priv->content_rating != NULL) {
		guint age = as_content_rating_get_minimum_age (priv->content_rating);
517 518 519 520 521
		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",
522
				as_content_rating_get_kind (priv->content_rating));
523
	}
524
	tmp = g_hash_table_lookup (priv->urls, as_url_kind_to_string (AS_URL_KIND_HOMEPAGE));
525
	if (tmp != NULL)
526
		gs_app_kv_lpad (str, "url{homepage}", tmp);
527
	keys = g_hash_table_get_keys (priv->launchables);
528
	for (GList *l = keys; l != NULL; l = l->next) {
529 530 531 532 533 534
		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);
535 536
	if (priv->license != NULL) {
		gs_app_kv_lpad (str, "license", priv->license);
537 538 539
		gs_app_kv_lpad (str, "license-is-free",
				gs_app_get_license_is_free (app) ? "yes" : "no");
	}
540 541 542 543 544 545 546 547
	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);
548
		gs_app_kv_lpad (str, "menu-path", path);
549
	}
550 551 552 553
	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);
554 555
	if (priv->origin_appstream != NULL && priv->origin_appstream[0] != '\0')
		gs_app_kv_lpad (str, "origin-appstream", priv->origin_appstream);
556 557 558 559 560 561 562
	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++) {
			gint rat = g_array_index (priv->review_ratings, gint, i);
563
			gs_app_kv_printf (str, "review-rating", "[%u:%i]",
564
					  i, rat);
565 566
		}
	}
567 568 569 570 571
	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) {
572 573
		gs_app_kv_printf (str, "install-date", "%"
				  G_GUINT64_FORMAT "",
574
				  priv->install_date);
575
	}
576 577 578
	if (priv->size_installed != 0)
		gs_app_kv_size (str, "size-installed", priv->size_installed);
	if (priv->size_download != 0)
579
		gs_app_kv_size (str, "size-download", gs_app_get_size_download (app));
580
	if (priv->price != NULL)
Robert Ancell's avatar
Robert Ancell committed
581
		gs_app_kv_printf (str, "price", "%s %.2f",
582 583
				  gs_price_get_currency (priv->price),
				  gs_price_get_amount (priv->price));
584 585
	for (i = 0; i < gs_app_list_length (priv->related); i++) {
		GsApp *app_tmp = gs_app_list_index (priv->related, i);
586 587 588 589
		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);
590
	}
591 592
	for (i = 0; i < gs_app_list_length (priv->history); i++) {
		GsApp *app_tmp = gs_app_list_index (priv->history, i);
593 594
		gs_app_kv_lpad (str, "history", gs_app_get_unique_id (app_tmp));
	}
595 596
	for (i = 0; i < priv->categories->len; i++) {
		tmp = g_ptr_array_index (priv->categories, i);
597
		gs_app_kv_lpad (str, "category", tmp);
598
	}
599 600
	for (i = 0; i < priv->key_colors->len; i++) {
		GdkRGBA *color = g_ptr_array_index (priv->key_colors, i);
601
		g_autofree gchar *key = NULL;
602
		key = g_strdup_printf ("key-color-%02u", i);
603
		gs_app_kv_printf (str, key, "%.0f,%.0f,%.0f",
604 605 606
				  color->red * 255.f,
				  color->green * 255.f,
				  color->blue * 255.f);
607
	}
608
	keys = g_hash_table_get_keys (priv->metadata);
609
	for (GList *l = keys; l != NULL; l = l->next) {
610 611
		GVariant *val;
		const GVariantType *val_type;
612
		g_autofree gchar *key = NULL;
613 614
		g_autofree gchar *val_str = NULL;

615
		key = g_strdup_printf ("{%s}", (const gchar *) l->data);
616 617 618 619 620 621 622 623 624 625 626 627 628 629
		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);
630 631
	}
	g_list_free (keys);
632

633 634 635 636
	/* add subclassed info */
	if (klass->to_string != NULL)
		klass->to_string (app, str);

637
	/* print runtime data too */
638
	if (priv->runtime != NULL) {
639 640
		g_string_append (str, "\n\tRuntime:\n\t");
		gs_app_to_string_append (priv->runtime, str);
641
	}
642
	g_string_append_printf (str, "\n");
643
}
644

645 646 647 648 649 650 651 652 653 654 655
typedef struct {
	GsApp *app;
	gchar *property_name;
} AppNotifyData;

static gboolean
notify_idle_cb (gpointer data)
{
	AppNotifyData *notify_data = data;

	g_object_notify (G_OBJECT (notify_data->app),
656
			 notify_data->property_name);
657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673

	g_object_unref (notify_data->app);
	g_free (notify_data->property_name);
	g_free (notify_data);

	return G_SOURCE_REMOVE;
}

static void
gs_app_queue_notify (GsApp *app, const gchar *property_name)
{
	AppNotifyData *notify_data;

	notify_data = g_new (AppNotifyData, 1);
	notify_data->app = g_object_ref (app);
	notify_data->property_name = g_strdup (property_name);

674
	g_idle_add (notify_idle_cb, notify_data);
675 676
}

677 678
/**
 * gs_app_get_id:
679 680 681 682
 * @app: a #GsApp
 *
 * Gets the application ID.
 *
683
 * Returns: The whole ID, e.g. "gimp.desktop"
684 685
 *
 * Since: 3.22
686 687 688 689
 **/
const gchar *
gs_app_get_id (GsApp *app)
{
690
	GsAppPrivate *priv = gs_app_get_instance_private (app);
691
	g_return_val_if_fail (GS_IS_APP (app), NULL);
692
	return priv->id;
693 694 695 696
}

/**
 * gs_app_set_id:
697 698 699 700
 * @app: a #GsApp
 * @id: a application ID, e.g. "gimp.desktop"
 *
 * Sets the application ID.
701 702 703 704
 */
void
gs_app_set_id (GsApp *app, const gchar *id)
{
705
	GsAppPrivate *priv = gs_app_get_instance_private (app);
706
	g_autoptr(GMutexLocker) locker = NULL;
707
	g_return_if_fail (GS_IS_APP (app));
708
	locker = g_mutex_locker_new (&priv->mutex);
709 710
	if (_g_set_str (&priv->id, id))
		priv->unique_id_valid = FALSE;
711 712
}

713 714 715 716 717 718 719
/**
 * gs_app_get_scope:
 * @app: a #GsApp
 *
 * Gets the scope of the application.
 *
 * Returns: the #AsAppScope, e.g. %AS_APP_SCOPE_USER
720 721
 *
 * Since: 3.22
722 723 724 725
 **/
AsAppScope
gs_app_get_scope (GsApp *app)
{
726
	GsAppPrivate *priv = gs_app_get_instance_private (app);
727
	g_return_val_if_fail (GS_IS_APP (app), AS_APP_SCOPE_UNKNOWN);
728
	return priv->scope;
729 730 731 732 733 734 735 736
}

/**
 * gs_app_set_scope:
 * @app: a #GsApp
 * @scope: a #AsAppScope, e.g. AS_APP_SCOPE_SYSTEM
 *
 * This sets the scope of the application.
737 738
 *
 * Since: 3.22
739 740 741 742
 **/
void
gs_app_set_scope (GsApp *app, AsAppScope scope)
{
743 744
	GsAppPrivate *priv = gs_app_get_instance_private (app);

745
	g_return_if_fail (GS_IS_APP (app));
746 747

	/* same */
748
	if (scope == priv->scope)
749 750
		return;

751
	priv->scope = scope;
752 753

	/* no longer valid */
754
	priv->unique_id_valid = FALSE;
755 756
}

757 758 759 760 761 762 763
/**
 * gs_app_get_bundle_kind:
 * @app: a #GsApp
 *
 * Gets the bundle kind of the application.
 *
 * Returns: the #AsAppScope, e.g. %AS_BUNDLE_KIND_FLATPAK
764 765
 *
 * Since: 3.22
766 767 768 769
 **/
AsBundleKind
gs_app_get_bundle_kind (GsApp *app)
{
770
	GsAppPrivate *priv = gs_app_get_instance_private (app);
771
	g_return_val_if_fail (GS_IS_APP (app), AS_BUNDLE_KIND_UNKNOWN);
772
	return priv->bundle_kind;
773 774 775 776 777 778 779 780
}

/**
 * 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.
781 782
 *
 * Since: 3.22
783 784 785 786
 **/
void
gs_app_set_bundle_kind (GsApp *app, AsBundleKind bundle_kind)
{
787 788
	GsAppPrivate *priv = gs_app_get_instance_private (app);

789
	g_return_if_fail (GS_IS_APP (app));
790 791

	/* same */
792
	if (bundle_kind == priv->bundle_kind)
793 794
		return;

795
	priv->bundle_kind = bundle_kind;
796 797

	/* no longer valid */
798
	priv->unique_id_valid = FALSE;
799 800
}

801 802
/**
 * gs_app_get_state:
803 804 805 806 807
 * @app: a #GsApp
 *
 * Gets the state of the application.
 *
 * Returns: the #AsAppState, e.g. %AS_APP_STATE_INSTALLED
808 809
 *
 * Since: 3.22
810
 **/
811
AsAppState
812 813
gs_app_get_state (GsApp *app)
{
814
	GsAppPrivate *priv = gs_app_get_instance_private (app);
815
	g_return_val_if_fail (GS_IS_APP (app), AS_APP_STATE_UNKNOWN);
816
	return priv->state;
817 818
}

819 820
/**
 * gs_app_get_progress:
821 822 823 824 825
 * @app: a #GsApp
 *
 * Gets the percentage completion.
 *
 * Returns: the percentage completion, or 0 for unknown
826 827
 *
 * Since: 3.22
828
 **/
829 830 831
guint
gs_app_get_progress (GsApp *app)
{
832
	GsAppPrivate *priv = gs_app_get_instance_private (app);
833
	g_return_val_if_fail (GS_IS_APP (app), 0);
834
	return priv->progress;
835 836
}

837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854
/**
 * 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;
}

855 856
/**
 * gs_app_set_state_recover:
857
 * @app: a #GsApp
858 859 860
 *
 * Sets the application state to the last status value that was not
 * transient.
861 862
 *
 * Since: 3.22
863
 **/
864 865 866
void
gs_app_set_state_recover (GsApp *app)
{
867 868
	GsAppPrivate *priv = gs_app_get_instance_private (app);
	if (priv->state_recover == AS_APP_STATE_UNKNOWN)
869
		return;
870
	if (priv->state_recover == priv->state)
871
		return;
872 873

	g_debug ("recovering state on %s from %s to %s",
874 875 876
		 priv->id,
		 as_app_state_to_string (priv->state),
		 as_app_state_to_string (priv->state_recover));
877