1639 lines
55 KiB
C
1639 lines
55 KiB
C
/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
|
||
* vi:set noexpandtab tabstop=8 shiftwidth=8:
|
||
*
|
||
* Copyright (C) 2021-2022 Matthew Leeds <mwleeds@protonmail.com>
|
||
*
|
||
* SPDX-License-Identifier: GPL-2.0-or-later
|
||
*/
|
||
|
||
#include <config.h>
|
||
#include <glib/gi18n.h>
|
||
#include <gnome-software.h>
|
||
#include <fcntl.h>
|
||
#include <gio/gunixfdlist.h>
|
||
#include <glib/gstdio.h>
|
||
|
||
#include "gs-epiphany-generated.h"
|
||
#include "gs-plugin-epiphany.h"
|
||
#include "gs-plugin-private.h"
|
||
|
||
/*
|
||
* SECTION:
|
||
* This plugin uses Epiphany to install, launch, and uninstall web applications.
|
||
*
|
||
* If the org.gnome.Epiphany.WebAppProvider D-Bus interface is not present or
|
||
* the DynamicLauncher portal is not available then it self-disables. This
|
||
* should work with both Flatpak'd and traditionally packaged Epiphany, for new
|
||
* enough versions of Epiphany.
|
||
*
|
||
* It's worth noting that this plugin has to deal with two different app IDs
|
||
* for installed web apps:
|
||
*
|
||
* 1. The app ID used in the <id> element in the AppStream metainfo file, which
|
||
* looks like "org.gnome.Software.WebApp_527a2dd6729c3574227c145bbc447997f0048537.desktop"
|
||
* See https://gitlab.gnome.org/mwleeds/gnome-pwa-list/-/blob/6e8b17b018f99dbf00b1fa956ed75c4a0ccbf389/pwa-metainfo-generator.py#L84-89
|
||
* This app ID is used for gs_app_new() so that the appstream plugin
|
||
* refines the apps created here, and used for the plugin cache.
|
||
*
|
||
* 2. The app ID generated by Epiphany when installing a web app, which looks
|
||
* like "org.gnome.Epiphany.WebApp_e9d0e1e4b0a10856aa3b38d9eb4375de4070d043.desktop"
|
||
* though it can have a different prefix if Epiphany was built with, for
|
||
* example, a development profile. Throughout this plugin this type of app
|
||
* ID is handled with a variable called "installed_app_id". This app ID is
|
||
* used in method calls to the org.gnome.Epiphany.WebAppProvider interface,
|
||
* and used for gs_app_set_launchable() and g_desktop_app_info_new().
|
||
*
|
||
* Since: 43
|
||
*/
|
||
|
||
struct _GsPluginEpiphany
|
||
{
|
||
GsPlugin parent;
|
||
|
||
GsWorkerThread *worker; /* (owned) */
|
||
|
||
GsEphyWebAppProvider *epiphany_proxy; /* (owned) */
|
||
GDBusProxy *launcher_portal_proxy; /* (owned) */
|
||
GFileMonitor *monitor; /* (owned) */
|
||
guint changed_id;
|
||
/* protects installed_apps_cached, url_id_map, and the plugin cache */
|
||
GMutex installed_apps_mutex;
|
||
/* installed_apps_cached: whether the plugin cache has all installed apps */
|
||
gboolean installed_apps_cached;
|
||
GHashTable *url_id_map; /* (owned) (not nullable) (element-type utf8 utf8) */
|
||
|
||
/* default permissions, shared between all applications */
|
||
GsAppPermissions *permissions; /* (owned) (not nullable) */
|
||
};
|
||
|
||
G_DEFINE_TYPE (GsPluginEpiphany, gs_plugin_epiphany, GS_TYPE_PLUGIN)
|
||
|
||
#define assert_in_worker(self) \
|
||
g_assert (gs_worker_thread_is_in_worker_context (self->worker))
|
||
|
||
static void
|
||
gs_epiphany_error_convert (GError **perror)
|
||
{
|
||
GError *error = perror != NULL ? *perror : NULL;
|
||
|
||
/* not set */
|
||
if (error == NULL)
|
||
return;
|
||
|
||
/* parse remote epiphany-webapp-provider error */
|
||
if (g_dbus_error_is_remote_error (error)) {
|
||
g_autofree gchar *remote_error = g_dbus_error_get_remote_error (error);
|
||
|
||
g_dbus_error_strip_remote_error (error);
|
||
|
||
if (g_str_equal (remote_error, "org.freedesktop.DBus.Error.ServiceUnknown")) {
|
||
error->code = GS_PLUGIN_ERROR_NOT_SUPPORTED;
|
||
} else if (g_str_has_prefix (remote_error, "org.gnome.Epiphany.WebAppProvider.Error")) {
|
||
error->code = GS_PLUGIN_ERROR_FAILED;
|
||
} else {
|
||
g_warning ("Can’t reliably fixup remote error ‘%s’", remote_error);
|
||
error->code = GS_PLUGIN_ERROR_FAILED;
|
||
}
|
||
error->domain = GS_PLUGIN_ERROR;
|
||
return;
|
||
}
|
||
|
||
/* this is allowed for low-level errors */
|
||
if (gs_utils_error_convert_gio (perror))
|
||
return;
|
||
|
||
/* this is allowed for low-level errors */
|
||
if (gs_utils_error_convert_gdbus (perror))
|
||
return;
|
||
}
|
||
|
||
/* Run in the main thread. */
|
||
static void
|
||
gs_plugin_epiphany_changed_cb (GFileMonitor *monitor,
|
||
GFile *file,
|
||
GFile *other_file,
|
||
GFileMonitorEvent event_type,
|
||
gpointer user_data)
|
||
{
|
||
GsPluginEpiphany *self = GS_PLUGIN_EPIPHANY (user_data);
|
||
|
||
{
|
||
g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&self->installed_apps_mutex);
|
||
gs_plugin_cache_invalidate (GS_PLUGIN (self));
|
||
g_hash_table_remove_all (self->url_id_map);
|
||
self->installed_apps_cached = FALSE;
|
||
}
|
||
|
||
/* FIXME: With the current API this is the only way to reload the list
|
||
* of installed apps.
|
||
*/
|
||
gs_plugin_reload (GS_PLUGIN (self));
|
||
}
|
||
|
||
static void
|
||
epiphany_web_app_provider_proxy_created_cb (GObject *source_object,
|
||
GAsyncResult *result,
|
||
gpointer user_data);
|
||
|
||
static void
|
||
dynamic_launcher_portal_proxy_created_cb (GObject *source_object,
|
||
GAsyncResult *result,
|
||
gpointer user_data);
|
||
|
||
static void
|
||
gs_plugin_epiphany_setup_async (GsPlugin *plugin,
|
||
GCancellable *cancellable,
|
||
GAsyncReadyCallback callback,
|
||
gpointer user_data)
|
||
{
|
||
GsPluginEpiphany *self = GS_PLUGIN_EPIPHANY (plugin);
|
||
g_autoptr(GTask) task = NULL;
|
||
g_autoptr(GError) local_error = NULL;
|
||
g_autofree char *portal_apps_path = NULL;
|
||
g_autoptr(GFile) portal_apps_file = NULL;
|
||
GDBusConnection *connection;
|
||
|
||
task = g_task_new (plugin, cancellable, callback, user_data);
|
||
g_task_set_source_tag (task, gs_plugin_epiphany_setup_async);
|
||
|
||
g_debug ("%s", G_STRFUNC);
|
||
|
||
self->installed_apps_cached = FALSE;
|
||
|
||
/* This is a mapping from URL to app ID, where the app ID comes from
|
||
* Epiphany. This allows us to use that app ID rather than the
|
||
* AppStream app ID in certain contexts (see the comment at the top of
|
||
* this file).
|
||
*/
|
||
self->url_id_map = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
|
||
|
||
/* Watch for changes to the set of installed apps in the main thread.
|
||
* This will also trigger when other apps' dynamic launchers are
|
||
* installed or removed but that is expected to be infrequent.
|
||
*/
|
||
portal_apps_path = g_build_filename (g_get_user_data_dir (), "xdg-desktop-portal", "applications", NULL);
|
||
portal_apps_file = g_file_new_for_path (portal_apps_path);
|
||
/* Monitoring the directory works even if it doesn't exist yet */
|
||
self->monitor = g_file_monitor_directory (portal_apps_file, G_FILE_MONITOR_WATCH_MOVES,
|
||
cancellable, &local_error);
|
||
if (self->monitor == NULL) {
|
||
gs_epiphany_error_convert (&local_error);
|
||
g_task_return_error (task, g_steal_pointer (&local_error));
|
||
return;
|
||
}
|
||
|
||
self->changed_id = g_signal_connect (self->monitor, "changed",
|
||
G_CALLBACK (gs_plugin_epiphany_changed_cb), self);
|
||
|
||
connection = gs_plugin_get_session_bus_connection (GS_PLUGIN (self));
|
||
g_assert (connection != NULL);
|
||
|
||
gs_ephy_web_app_provider_proxy_new (connection,
|
||
G_DBUS_PROXY_FLAGS_NONE,
|
||
"org.gnome.Epiphany.WebAppProvider",
|
||
"/org/gnome/Epiphany/WebAppProvider",
|
||
cancellable,
|
||
epiphany_web_app_provider_proxy_created_cb,
|
||
g_steal_pointer (&task));
|
||
}
|
||
|
||
static void
|
||
epiphany_web_app_provider_proxy_created_cb (GObject *source_object,
|
||
GAsyncResult *result,
|
||
gpointer user_data)
|
||
{
|
||
g_autoptr(GTask) task = G_TASK (user_data);
|
||
g_autofree gchar *name_owner = NULL;
|
||
g_autoptr(GError) local_error = NULL;
|
||
GsPluginEpiphany *self = g_task_get_source_object (task);
|
||
GDBusConnection *connection;
|
||
GCancellable *cancellable;
|
||
|
||
/* Check that the proxy exists (and is owned; it should auto-start) so
|
||
* we can disable the plugin for systems which don’t have new enough
|
||
* Epiphany.
|
||
*/
|
||
self->epiphany_proxy = gs_ephy_web_app_provider_proxy_new_finish (result, &local_error);
|
||
|
||
if (self->epiphany_proxy == NULL) {
|
||
gs_epiphany_error_convert (&local_error);
|
||
g_task_return_error (task, g_steal_pointer (&local_error));
|
||
return;
|
||
}
|
||
|
||
name_owner = g_dbus_proxy_get_name_owner (G_DBUS_PROXY (self->epiphany_proxy));
|
||
if (name_owner == NULL) {
|
||
g_task_return_new_error (task, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_NOT_SUPPORTED,
|
||
"Couldn’t create Epiphany WebAppProvider proxy: couldn’t get name owner");
|
||
return;
|
||
}
|
||
|
||
connection = g_dbus_proxy_get_connection (G_DBUS_PROXY (self->epiphany_proxy));
|
||
cancellable = g_task_get_cancellable (task);
|
||
|
||
g_dbus_proxy_new (connection,
|
||
G_DBUS_PROXY_FLAGS_DO_NOT_CONNECT_SIGNALS,
|
||
NULL,
|
||
"org.freedesktop.portal.Desktop",
|
||
"/org/freedesktop/portal/desktop",
|
||
"org.freedesktop.portal.DynamicLauncher",
|
||
cancellable,
|
||
dynamic_launcher_portal_proxy_created_cb,
|
||
g_steal_pointer (&task));
|
||
}
|
||
|
||
static void
|
||
dynamic_launcher_portal_proxy_created_cb (GObject *source_object,
|
||
GAsyncResult *result,
|
||
gpointer user_data)
|
||
{
|
||
g_autoptr(GTask) task = G_TASK (user_data);
|
||
g_autoptr(GVariant) version = NULL;
|
||
g_autoptr(GError) local_error = NULL;
|
||
GsPluginEpiphany *self = g_task_get_source_object (task);
|
||
|
||
/* Check that the proxy exists (and is owned; it should auto-start) so
|
||
* we can disable the plugin for systems which don’t have new enough
|
||
* Epiphany.
|
||
*/
|
||
self->launcher_portal_proxy = g_dbus_proxy_new_finish (result, &local_error);
|
||
|
||
if (self->launcher_portal_proxy == NULL) {
|
||
gs_epiphany_error_convert (&local_error);
|
||
g_task_return_error (task, g_steal_pointer (&local_error));
|
||
return;
|
||
}
|
||
|
||
version = g_dbus_proxy_get_cached_property (self->launcher_portal_proxy, "version");
|
||
if (version == NULL) {
|
||
g_task_return_new_error (task, GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_NOT_SUPPORTED,
|
||
"Dynamic launcher portal not available");
|
||
return;
|
||
} else {
|
||
g_debug ("Found version %" G_GUINT32_FORMAT " of the dynamic launcher portal",
|
||
g_variant_get_uint32 (version));
|
||
}
|
||
|
||
/* Start up a worker thread to process all the plugin’s function calls. */
|
||
self->worker = gs_worker_thread_new ("gs-plugin-epiphany");
|
||
|
||
g_task_return_boolean (task, TRUE);
|
||
}
|
||
|
||
static gboolean
|
||
gs_plugin_epiphany_setup_finish (GsPlugin *plugin,
|
||
GAsyncResult *result,
|
||
GError **error)
|
||
{
|
||
return g_task_propagate_boolean (G_TASK (result), error);
|
||
}
|
||
|
||
static void shutdown_cb (GObject *source_object,
|
||
GAsyncResult *result,
|
||
gpointer user_data);
|
||
|
||
static void
|
||
gs_plugin_epiphany_shutdown_async (GsPlugin *plugin,
|
||
GCancellable *cancellable,
|
||
GAsyncReadyCallback callback,
|
||
gpointer user_data)
|
||
{
|
||
GsPluginEpiphany *self = GS_PLUGIN_EPIPHANY (plugin);
|
||
g_autoptr(GTask) task = NULL;
|
||
|
||
task = g_task_new (self, cancellable, callback, user_data);
|
||
g_task_set_source_tag (task, gs_plugin_epiphany_shutdown_async);
|
||
|
||
/* Stop the worker thread. */
|
||
gs_worker_thread_shutdown_async (self->worker, cancellable, shutdown_cb, g_steal_pointer (&task));
|
||
}
|
||
|
||
static void
|
||
shutdown_cb (GObject *source_object,
|
||
GAsyncResult *result,
|
||
gpointer user_data)
|
||
{
|
||
g_autoptr(GTask) task = G_TASK (user_data);
|
||
GsPluginEpiphany *self = g_task_get_source_object (task);
|
||
g_autoptr(GsWorkerThread) worker = NULL;
|
||
g_autoptr(GError) local_error = NULL;
|
||
|
||
worker = g_steal_pointer (&self->worker);
|
||
|
||
if (!gs_worker_thread_shutdown_finish (worker, result, &local_error)) {
|
||
g_task_return_error (task, g_steal_pointer (&local_error));
|
||
return;
|
||
}
|
||
|
||
g_task_return_boolean (task, TRUE);
|
||
}
|
||
|
||
static gboolean
|
||
gs_plugin_epiphany_shutdown_finish (GsPlugin *plugin,
|
||
GAsyncResult *result,
|
||
GError **error)
|
||
{
|
||
return g_task_propagate_boolean (G_TASK (result), error);
|
||
}
|
||
|
||
static void
|
||
gs_plugin_epiphany_init (GsPluginEpiphany *self)
|
||
{
|
||
/* Re-used permissions by all GsApp instances; do not modify it out
|
||
of this place. */
|
||
self->permissions = gs_app_permissions_new ();
|
||
gs_app_permissions_set_flags (self->permissions, GS_APP_PERMISSIONS_FLAGS_NETWORK);
|
||
gs_app_permissions_seal (self->permissions);
|
||
|
||
/* set name of MetaInfo file */
|
||
gs_plugin_set_appstream_id (GS_PLUGIN (self), "org.gnome.Software.Plugin.Epiphany");
|
||
|
||
/* need help from appstream */
|
||
gs_plugin_add_rule (GS_PLUGIN (self), GS_PLUGIN_RULE_RUN_AFTER, "appstream");
|
||
|
||
/* prioritize over packages */
|
||
gs_plugin_add_rule (GS_PLUGIN (self), GS_PLUGIN_RULE_BETTER_THAN, "packagekit");
|
||
}
|
||
|
||
static void
|
||
gs_plugin_epiphany_dispose (GObject *object)
|
||
{
|
||
GsPluginEpiphany *self = GS_PLUGIN_EPIPHANY (object);
|
||
|
||
if (self->changed_id > 0) {
|
||
g_signal_handler_disconnect (self->monitor, self->changed_id);
|
||
self->changed_id = 0;
|
||
}
|
||
|
||
g_clear_object (&self->epiphany_proxy);
|
||
g_clear_object (&self->launcher_portal_proxy);
|
||
g_clear_object (&self->monitor);
|
||
g_clear_object (&self->worker);
|
||
g_clear_pointer (&self->url_id_map, g_hash_table_unref);
|
||
|
||
G_OBJECT_CLASS (gs_plugin_epiphany_parent_class)->dispose (object);
|
||
}
|
||
|
||
static void
|
||
gs_plugin_epiphany_finalize (GObject *object)
|
||
{
|
||
GsPluginEpiphany *self = GS_PLUGIN_EPIPHANY (object);
|
||
|
||
g_mutex_clear (&self->installed_apps_mutex);
|
||
g_clear_object (&self->permissions);
|
||
|
||
G_OBJECT_CLASS (gs_plugin_epiphany_parent_class)->finalize (object);
|
||
}
|
||
|
||
static gboolean ensure_installed_apps_cache (GsPluginEpiphany *self,
|
||
gboolean interactive,
|
||
GCancellable *cancellable,
|
||
GError **error);
|
||
|
||
/* May be run in @worker or in main thread. The caller must have already done ensure_installed_apps_cache() */
|
||
static void
|
||
gs_epiphany_refine_app_state (GsPlugin *plugin,
|
||
GsApp *app)
|
||
{
|
||
if (gs_app_get_state (app) == GS_APP_STATE_UNKNOWN) {
|
||
g_autoptr(GsApp) cached_app = NULL;
|
||
const char *appstream_source;
|
||
|
||
/* If we have a cached app, set the state from there. Otherwise
|
||
* only set the state to available if the app came from
|
||
* appstream data, because there's no way to re-install an app
|
||
* in Software that was originally installed from Epiphany,
|
||
* unless we have appstream metainfo for it.
|
||
*/
|
||
cached_app = gs_plugin_cache_lookup (plugin, gs_app_get_id (app));
|
||
appstream_source = gs_app_get_metadata_item (app, "appstream::source-file");
|
||
if (cached_app)
|
||
gs_app_set_state (app, gs_app_get_state (cached_app));
|
||
else if (appstream_source)
|
||
gs_app_set_state (app, GS_APP_STATE_AVAILABLE);
|
||
else {
|
||
gs_app_set_state (app, GS_APP_STATE_UNAVAILABLE);
|
||
gs_app_set_url_missing (app,
|
||
"help:gnome-software/how-to-reinstall-a-web-app");
|
||
}
|
||
}
|
||
}
|
||
|
||
void
|
||
gs_plugin_adopt_app (GsPlugin *plugin,
|
||
GsApp *app)
|
||
{
|
||
if (gs_app_get_kind (app) == AS_COMPONENT_KIND_WEB_APP &&
|
||
gs_app_get_bundle_kind (app) != AS_BUNDLE_KIND_PACKAGE) {
|
||
gs_app_set_management_plugin (app, plugin);
|
||
}
|
||
}
|
||
|
||
static gint
|
||
get_priority_for_interactivity (gboolean interactive)
|
||
{
|
||
return interactive ? G_PRIORITY_DEFAULT : G_PRIORITY_LOW;
|
||
}
|
||
|
||
static void list_apps_thread_cb (GTask *task,
|
||
gpointer source_object,
|
||
gpointer task_data,
|
||
GCancellable *cancellable);
|
||
|
||
static void
|
||
gs_plugin_epiphany_list_apps_async (GsPlugin *plugin,
|
||
GsAppQuery *query,
|
||
GsPluginListAppsFlags flags,
|
||
GCancellable *cancellable,
|
||
GAsyncReadyCallback callback,
|
||
gpointer user_data)
|
||
{
|
||
GsPluginEpiphany *self = GS_PLUGIN_EPIPHANY (plugin);
|
||
g_autoptr(GTask) task = NULL;
|
||
gboolean interactive = (flags & GS_PLUGIN_LIST_APPS_FLAGS_INTERACTIVE);
|
||
|
||
task = gs_plugin_list_apps_data_new_task (plugin, query, flags,
|
||
cancellable, callback, user_data);
|
||
g_task_set_source_tag (task, gs_plugin_epiphany_list_apps_async);
|
||
|
||
/* Queue a job to get the apps. */
|
||
gs_worker_thread_queue (self->worker, get_priority_for_interactivity (interactive),
|
||
list_apps_thread_cb, g_steal_pointer (&task));
|
||
}
|
||
|
||
/* Run in @worker */
|
||
static void
|
||
refine_app (GsPluginEpiphany *self,
|
||
GsApp *app,
|
||
GsPluginRefineFlags flags,
|
||
GUri *uri,
|
||
const char *url)
|
||
{
|
||
const char *hostname;
|
||
const char *installed_app_id;
|
||
const struct {
|
||
const gchar *hostname;
|
||
const gchar *license_spdx;
|
||
} app_licenses[] = {
|
||
/* Keep in alphabetical order by hostname */
|
||
{ "app.diagrams.net", "Apache-2.0" },
|
||
{ "devdocs.io", "MPL-2.0" },
|
||
{ "discourse.flathub.org", "GPL-2.0-or-later" },
|
||
{ "discourse.gnome.org", "GPL-2.0-or-later" },
|
||
{ "excalidraw.com", "MIT" },
|
||
{ "pinafore.social", "AGPL-3.0-only" },
|
||
{ "snapdrop.net", "GPL-3.0-only" },
|
||
{ "stackedit.io", "Apache-2.0" },
|
||
{ "squoosh.app", "Apache-2.0" },
|
||
};
|
||
|
||
g_return_if_fail (GS_IS_APP (app));
|
||
g_return_if_fail (uri != NULL);
|
||
g_return_if_fail (url != NULL);
|
||
|
||
gs_app_set_origin (app, "gnome-web");
|
||
if (gs_app_get_name (app) == NULL)
|
||
gs_app_set_origin_ui (app, _("Web App"));
|
||
else
|
||
gs_app_set_origin_ui (app, gs_app_get_name (app));
|
||
gs_app_set_origin_hostname (app, g_uri_get_host (uri));
|
||
gs_app_set_metadata (app, "GnomeSoftware::PackagingFormat", _("Web App"));
|
||
gs_app_set_metadata (app, "GnomeSoftware::PackagingIcon", "web-browser-symbolic");
|
||
|
||
gs_app_set_scope (app, AS_COMPONENT_SCOPE_USER);
|
||
gs_app_set_launchable (app, AS_LAUNCHABLE_KIND_URL, url);
|
||
|
||
installed_app_id = g_hash_table_lookup (self->url_id_map, url);
|
||
if (installed_app_id) {
|
||
gs_app_set_launchable (app, AS_LAUNCHABLE_KIND_DESKTOP_ID, installed_app_id);
|
||
}
|
||
|
||
/* Hard-code the licenses as it's hard to get them programmatically. We
|
||
* can move them to an AppStream file if needed.
|
||
*/
|
||
hostname = g_uri_get_host (uri);
|
||
if (gs_app_get_license (app) == NULL && hostname != NULL) {
|
||
for (gsize i = 0; i < G_N_ELEMENTS (app_licenses); i++) {
|
||
if (g_str_equal (hostname, app_licenses[i].hostname)) {
|
||
gs_app_set_license (app, GS_APP_QUALITY_NORMAL,
|
||
app_licenses[i].license_spdx);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
gs_app_set_size_download (app, GS_SIZE_TYPE_VALID, 0);
|
||
|
||
/* Use the default permissions */
|
||
gs_app_set_permissions (app, self->permissions);
|
||
|
||
if (gs_app_get_url (app, AS_URL_KIND_HOMEPAGE) == NULL)
|
||
gs_app_set_url (app, AS_URL_KIND_HOMEPAGE, url);
|
||
|
||
/* Use the domain name (e.g. "discourse.gnome.org") as a fallback summary.
|
||
* FIXME: Fetch the summary from the site's webapp manifest.
|
||
*/
|
||
if (gs_app_get_summary (app) == NULL) {
|
||
if (hostname != NULL && *hostname != '\0')
|
||
gs_app_set_summary (app, GS_APP_QUALITY_LOWEST, hostname);
|
||
else
|
||
gs_app_set_summary (app, GS_APP_QUALITY_LOWEST, url);
|
||
}
|
||
|
||
if (installed_app_id == NULL)
|
||
return;
|
||
|
||
{
|
||
const gchar *name;
|
||
g_autofree char *icon_path = NULL;
|
||
goffset desktop_size = 0, icon_size = 0;
|
||
g_autoptr(GDesktopAppInfo) desktop_info = NULL;
|
||
g_autoptr(GFileInfo) file_info = NULL;
|
||
g_autoptr(GFile) icon_file = NULL;
|
||
|
||
desktop_info = g_desktop_app_info_new (installed_app_id);
|
||
|
||
if (desktop_info == NULL) {
|
||
g_warning ("Couldn't get GDesktopAppInfo for app %s", installed_app_id);
|
||
return;
|
||
}
|
||
|
||
name = g_app_info_get_name (G_APP_INFO (desktop_info));
|
||
gs_app_set_name (app, GS_APP_QUALITY_NORMAL, name);
|
||
|
||
if (flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_SIZE) {
|
||
g_autoptr(GFile) desktop_file = NULL;
|
||
const gchar *desktop_path;
|
||
guint64 install_date = 0;
|
||
|
||
desktop_path = g_desktop_app_info_get_filename (desktop_info);
|
||
g_assert (desktop_path);
|
||
desktop_file = g_file_new_for_path (desktop_path);
|
||
|
||
file_info = g_file_query_info (desktop_file,
|
||
G_FILE_ATTRIBUTE_TIME_CREATED "," G_FILE_ATTRIBUTE_STANDARD_SIZE,
|
||
0, NULL, NULL);
|
||
if (file_info) {
|
||
install_date = g_file_info_get_attribute_uint64 (file_info, G_FILE_ATTRIBUTE_TIME_CREATED);
|
||
desktop_size = g_file_info_get_size (file_info);
|
||
}
|
||
if (install_date) {
|
||
gs_app_set_install_date (app, install_date);
|
||
}
|
||
}
|
||
|
||
icon_path = g_desktop_app_info_get_string (desktop_info, "Icon");
|
||
if (flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_SIZE &&
|
||
icon_path) {
|
||
icon_file = g_file_new_for_path (icon_path);
|
||
|
||
g_clear_object (&file_info);
|
||
file_info = g_file_query_info (icon_file,
|
||
G_FILE_ATTRIBUTE_STANDARD_SIZE,
|
||
0, NULL, NULL);
|
||
if (file_info)
|
||
icon_size = g_file_info_get_size (file_info);
|
||
}
|
||
if (flags & GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON &&
|
||
!gs_app_has_icons (app) &&
|
||
icon_path) {
|
||
g_autoptr(GIcon) icon = g_file_icon_new (icon_file);
|
||
g_autofree char *icon_dir = g_path_get_dirname (icon_path);
|
||
g_autofree char *icon_dir_basename = g_path_get_basename (icon_dir);
|
||
const char *x;
|
||
guint64 width = 0;
|
||
|
||
/* dir should be either scalable or e.g. 512x512 */
|
||
if (g_strcmp0 (icon_dir_basename, "scalable") == 0) {
|
||
/* Ensure scalable icons are preferred */
|
||
width = 4096;
|
||
} else if ((x = strchr (icon_dir_basename, 'x')) != NULL) {
|
||
g_ascii_string_to_unsigned (x + 1, 10, 1, G_MAXINT, &width, NULL);
|
||
}
|
||
if (width > 0 && width <= 4096) {
|
||
gs_icon_set_width (icon, width);
|
||
gs_icon_set_height (icon, width);
|
||
} else {
|
||
g_warning ("Unexpectedly unable to determine width of icon %s", icon_path);
|
||
}
|
||
|
||
gs_app_add_icon (app, icon);
|
||
}
|
||
if (desktop_size > 0 || icon_size > 0) {
|
||
gs_app_set_size_installed (app, GS_SIZE_TYPE_VALID, desktop_size + icon_size);
|
||
}
|
||
}
|
||
}
|
||
|
||
/* Run in @worker */
|
||
static GsApp *
|
||
gs_epiphany_create_app (GsPluginEpiphany *self,
|
||
const char *id)
|
||
{
|
||
g_autoptr(GsApp) app = NULL;
|
||
|
||
assert_in_worker (self);
|
||
|
||
app = gs_plugin_cache_lookup (GS_PLUGIN (self), id);
|
||
if (app != NULL)
|
||
return g_steal_pointer (&app);
|
||
|
||
app = gs_app_new (id);
|
||
gs_app_set_management_plugin (app, GS_PLUGIN (self));
|
||
gs_app_set_kind (app, AS_COMPONENT_KIND_WEB_APP);
|
||
gs_app_set_metadata (app, "GnomeSoftware::Creator",
|
||
gs_plugin_get_name (GS_PLUGIN (self)));
|
||
|
||
gs_plugin_cache_add (GS_PLUGIN (self), id, app);
|
||
return g_steal_pointer (&app);
|
||
}
|
||
|
||
static gchar * /* (transfer full) */
|
||
generate_app_id_for_url (const gchar *url)
|
||
{
|
||
/* Generate the app ID used in the AppStream data using the
|
||
* same method as pwa-metainfo-generator.py in
|
||
* https://gitlab.gnome.org/mwleeds/gnome-pwa-list
|
||
* Using this app ID rather than the one provided by Epiphany
|
||
* makes it possible for the appstream plugin to refine the
|
||
* GsApp we create (see the comment at the top of this file).
|
||
*/
|
||
g_autofree gchar *url_hash = g_compute_checksum_for_string (G_CHECKSUM_SHA1, url, -1);
|
||
return g_strconcat ("org.gnome.Software.WebApp_", url_hash, ".desktop", NULL);
|
||
}
|
||
|
||
/* Run in @worker */
|
||
static gboolean
|
||
ensure_installed_apps_cache (GsPluginEpiphany *self,
|
||
gboolean interactive,
|
||
GCancellable *cancellable,
|
||
GError **error)
|
||
{
|
||
g_auto(GStrv) webapps = NULL;
|
||
guint n_webapps;
|
||
g_autoptr(GsAppList) installed_cache = gs_app_list_new ();
|
||
g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&self->installed_apps_mutex);
|
||
|
||
assert_in_worker (self);
|
||
|
||
if (self->installed_apps_cached)
|
||
return TRUE;
|
||
|
||
if (!gs_ephy_web_app_provider_call_get_installed_apps_sync (self->epiphany_proxy,
|
||
interactive ? G_DBUS_CALL_FLAGS_ALLOW_INTERACTIVE_AUTHORIZATION : G_DBUS_CALL_FLAGS_NONE,
|
||
-1 /* timeout */,
|
||
&webapps,
|
||
cancellable,
|
||
error)) {
|
||
gs_epiphany_error_convert (error);
|
||
return FALSE;
|
||
}
|
||
|
||
n_webapps = g_strv_length (webapps);
|
||
g_debug ("%s: epiphany-webapp-provider returned %u installed web apps", G_STRFUNC, n_webapps);
|
||
for (guint i = 0; i < n_webapps; i++) {
|
||
const gchar *desktop_file_id = webapps[i];
|
||
const gchar *url = NULL;
|
||
g_autofree char *metainfo_app_id = NULL;
|
||
const gchar *exec;
|
||
int argc;
|
||
GsPluginRefineFlags refine_flags;
|
||
g_auto(GStrv) argv = NULL;
|
||
g_autoptr(GsApp) app = NULL;
|
||
g_autoptr(GDesktopAppInfo) desktop_info = NULL;
|
||
g_autoptr(GUri) uri = NULL;
|
||
|
||
g_debug ("%s: Working on installed web app %s", G_STRFUNC, desktop_file_id);
|
||
|
||
desktop_info = g_desktop_app_info_new (desktop_file_id);
|
||
|
||
if (desktop_info == NULL) {
|
||
g_warning ("Epiphany returned a non-existent or invalid desktop ID %s", desktop_file_id);
|
||
continue;
|
||
}
|
||
|
||
/* This way of getting the URL is a bit hacky but it's what
|
||
* Epiphany does, specifically in
|
||
* ephy_web_application_for_profile_directory() which lives in
|
||
* https://gitlab.gnome.org/GNOME/epiphany/-/blob/master/lib/ephy-web-app-utils.c
|
||
*/
|
||
exec = g_app_info_get_commandline (G_APP_INFO (desktop_info));
|
||
if (g_shell_parse_argv (exec, &argc, &argv, NULL)) {
|
||
g_assert (argc > 0);
|
||
url = argv[argc - 1];
|
||
}
|
||
if (!url || !(uri = g_uri_parse (url, G_URI_FLAGS_NONE, NULL))) {
|
||
g_warning ("Failed to parse URL for web app %s: %s",
|
||
desktop_file_id, url ? url : "(null)");
|
||
continue;
|
||
}
|
||
|
||
/* Store the installed app id for use in refine_app() */
|
||
g_hash_table_insert (self->url_id_map, g_strdup (url),
|
||
g_strdup (desktop_file_id));
|
||
|
||
metainfo_app_id = generate_app_id_for_url (url);
|
||
g_debug ("Creating GsApp for webapp with URL %s using app ID %s (desktop file id: %s)",
|
||
url, metainfo_app_id, desktop_file_id);
|
||
|
||
/* App gets added to the plugin cache here */
|
||
app = gs_epiphany_create_app (self, metainfo_app_id);
|
||
|
||
gs_app_set_state (app, GS_APP_STATE_INSTALLED);
|
||
|
||
refine_flags = GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON |
|
||
GS_PLUGIN_REFINE_FLAGS_REQUIRE_SIZE |
|
||
GS_PLUGIN_REFINE_FLAGS_REQUIRE_ID;
|
||
refine_app (self, app, refine_flags, uri, url);
|
||
}
|
||
|
||
/* Update the state on any apps that were uninstalled outside
|
||
* gnome-software
|
||
*/
|
||
gs_plugin_cache_lookup_by_state (GS_PLUGIN (self), installed_cache, GS_APP_STATE_INSTALLED);
|
||
for (guint i = 0; i < gs_app_list_length (installed_cache); i++) {
|
||
GsApp *app = gs_app_list_index (installed_cache, i);
|
||
const char *installed_app_id;
|
||
const char *appstream_source;
|
||
|
||
installed_app_id = gs_app_get_launchable (app, AS_LAUNCHABLE_KIND_DESKTOP_ID);
|
||
if (installed_app_id == NULL) {
|
||
g_warning ("Installed app unexpectedly has no desktop id: %s", gs_app_get_id (app));
|
||
continue;
|
||
}
|
||
|
||
if (g_strv_contains ((const char * const *)webapps, installed_app_id))
|
||
continue;
|
||
|
||
gs_plugin_cache_remove (GS_PLUGIN (self), gs_app_get_id (app));
|
||
|
||
appstream_source = gs_app_get_metadata_item (app, "appstream::source-file");
|
||
if (appstream_source)
|
||
gs_app_set_state (app, GS_APP_STATE_AVAILABLE);
|
||
else
|
||
gs_app_set_state (app, GS_APP_STATE_UNKNOWN);
|
||
}
|
||
|
||
self->installed_apps_cached = TRUE;
|
||
return TRUE;
|
||
}
|
||
|
||
/* Run in @worker */
|
||
static void
|
||
list_apps_thread_cb (GTask *task,
|
||
gpointer source_object,
|
||
gpointer task_data,
|
||
GCancellable *cancellable)
|
||
{
|
||
GsPluginEpiphany *self = GS_PLUGIN_EPIPHANY (source_object);
|
||
g_autoptr(GsAppList) list = gs_app_list_new ();
|
||
GsPluginListAppsData *data = task_data;
|
||
GsAppQueryTristate is_installed = GS_APP_QUERY_TRISTATE_UNSET;
|
||
const gchar * const *keywords = NULL;
|
||
g_autoptr(GError) local_error = NULL;
|
||
|
||
assert_in_worker (self);
|
||
|
||
if (data->query != NULL) {
|
||
is_installed = gs_app_query_get_is_installed (data->query);
|
||
keywords = gs_app_query_get_keywords (data->query);
|
||
}
|
||
|
||
/* Currently only support a subset of query properties, and only one set at once.
|
||
* Also don’t currently support GS_APP_QUERY_TRISTATE_FALSE. */
|
||
if ((is_installed == GS_APP_QUERY_TRISTATE_UNSET &&
|
||
keywords == NULL) ||
|
||
is_installed == GS_APP_QUERY_TRISTATE_FALSE ||
|
||
gs_app_query_get_n_properties_set (data->query) != 1) {
|
||
g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED,
|
||
"Unsupported query");
|
||
return;
|
||
}
|
||
|
||
/* Ensure the cache is up to date. */
|
||
if (!ensure_installed_apps_cache (self, data->flags & GS_PLUGIN_LIST_APPS_FLAGS_INTERACTIVE, cancellable, &local_error)) {
|
||
g_task_return_error (task, g_steal_pointer (&local_error));
|
||
return;
|
||
}
|
||
|
||
if (is_installed == GS_APP_QUERY_TRISTATE_TRUE)
|
||
gs_plugin_cache_lookup_by_state (GS_PLUGIN (self), list, GS_APP_STATE_INSTALLED);
|
||
else if (keywords != NULL) {
|
||
for (gsize i = 0; keywords[i]; i++) {
|
||
GHashTableIter iter;
|
||
gpointer key, value;
|
||
g_hash_table_iter_init (&iter, self->url_id_map);
|
||
while (g_hash_table_iter_next (&iter, &key, &value)) {
|
||
const gchar *url = key;
|
||
const gchar *app_id = value;
|
||
if (g_strcmp0 (app_id, keywords[i]) == 0) {
|
||
g_autoptr(GsApp) app = NULL;
|
||
g_autofree gchar *metainfo_app_id = NULL;
|
||
metainfo_app_id = generate_app_id_for_url (url);
|
||
app = gs_plugin_cache_lookup (GS_PLUGIN (self), metainfo_app_id);
|
||
if (app != NULL)
|
||
gs_app_list_add (list, app);
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
g_task_return_pointer (task, g_steal_pointer (&list), g_object_unref);
|
||
}
|
||
|
||
static GsAppList *
|
||
gs_plugin_epiphany_list_apps_finish (GsPlugin *plugin,
|
||
GAsyncResult *result,
|
||
GError **error)
|
||
{
|
||
g_return_val_if_fail (g_task_get_source_tag (G_TASK (result)) == gs_plugin_epiphany_list_apps_async, FALSE);
|
||
return g_task_propagate_pointer (G_TASK (result), error);
|
||
}
|
||
|
||
static void
|
||
gs_epiphany_refine_app (GsPluginEpiphany *self,
|
||
GsApp *app,
|
||
GsPluginRefineFlags refine_flags,
|
||
const char *url)
|
||
{
|
||
g_autoptr(GUri) uri = NULL;
|
||
|
||
g_return_if_fail (url != NULL && *url != '\0');
|
||
|
||
if (!(uri = g_uri_parse (url, G_URI_FLAGS_NONE, NULL))) {
|
||
g_warning ("Failed to parse URL for web app %s: %s", gs_app_get_id (app), url);
|
||
return;
|
||
}
|
||
|
||
refine_app (self, app, refine_flags, uri, url);
|
||
}
|
||
|
||
static void refine_thread_cb (GTask *task,
|
||
gpointer source_object,
|
||
gpointer task_data,
|
||
GCancellable *cancellable);
|
||
|
||
static void
|
||
gs_plugin_epiphany_refine_async (GsPlugin *plugin,
|
||
GsAppList *list,
|
||
GsPluginRefineFlags flags,
|
||
GCancellable *cancellable,
|
||
GAsyncReadyCallback callback,
|
||
gpointer user_data)
|
||
{
|
||
GsPluginEpiphany *self = GS_PLUGIN_EPIPHANY (plugin);
|
||
g_autoptr(GTask) task = NULL;
|
||
gboolean interactive = gs_plugin_has_flags (GS_PLUGIN (self), GS_PLUGIN_FLAGS_INTERACTIVE);
|
||
|
||
task = gs_plugin_refine_data_new_task (plugin, list, flags, cancellable, callback, user_data);
|
||
g_task_set_source_tag (task, gs_plugin_epiphany_refine_async);
|
||
|
||
/* Queue a job for the refine. */
|
||
gs_worker_thread_queue (self->worker, get_priority_for_interactivity (interactive),
|
||
refine_thread_cb, g_steal_pointer (&task));
|
||
}
|
||
|
||
/* Run in @worker. */
|
||
static void
|
||
refine_thread_cb (GTask *task,
|
||
gpointer source_object,
|
||
gpointer task_data,
|
||
GCancellable *cancellable)
|
||
{
|
||
GsPluginEpiphany *self = GS_PLUGIN_EPIPHANY (source_object);
|
||
GsPluginRefineData *data = task_data;
|
||
GsPluginRefineFlags flags = data->flags;
|
||
GsAppList *list = data->list;
|
||
gboolean interactive = gs_plugin_has_flags (GS_PLUGIN (self), GS_PLUGIN_FLAGS_INTERACTIVE);
|
||
g_autoptr(GError) local_error = NULL;
|
||
|
||
assert_in_worker (self);
|
||
|
||
if (!ensure_installed_apps_cache (self, interactive, cancellable, &local_error)) {
|
||
g_task_return_error (task, g_steal_pointer (&local_error));
|
||
return;
|
||
}
|
||
|
||
for (guint i = 0; i < gs_app_list_length (list); i++) {
|
||
GsApp *app = gs_app_list_index (list, i);
|
||
const char *url;
|
||
|
||
/* not us */
|
||
if (gs_app_get_kind (app) != AS_COMPONENT_KIND_WEB_APP ||
|
||
gs_app_get_bundle_kind (app) == AS_BUNDLE_KIND_PACKAGE)
|
||
continue;
|
||
|
||
url = gs_app_get_launchable (app, AS_LAUNCHABLE_KIND_URL);
|
||
if (url == NULL || *url == '\0') {
|
||
/* A launchable URL is required by the AppStream spec */
|
||
g_warning ("Web app %s missing launchable url", gs_app_get_id (app));
|
||
continue;
|
||
}
|
||
|
||
g_debug ("epiphany: refining app %s", gs_app_get_id (app));
|
||
gs_epiphany_refine_app (self, app, flags, url);
|
||
gs_epiphany_refine_app_state (GS_PLUGIN (self), app);
|
||
|
||
/* Usually the way to refine wildcard apps is to create a new
|
||
* GsApp and add it to the results list, but in this case we
|
||
* need to use the app that was refined by the appstream plugin
|
||
* as it has all the metadata set already, and this is the only
|
||
* plugin for dealing with web apps, so it should be safe to
|
||
* adopt the wildcard app.
|
||
*/
|
||
if (gs_app_has_quirk (app, GS_APP_QUIRK_IS_WILDCARD)) {
|
||
gs_app_remove_quirk (app, GS_APP_QUIRK_IS_WILDCARD);
|
||
gs_app_set_management_plugin (app, GS_PLUGIN (self));
|
||
gs_plugin_cache_add (GS_PLUGIN (self), gs_app_get_id (app), app);
|
||
}
|
||
}
|
||
|
||
/* success */
|
||
g_task_return_boolean (task, TRUE);
|
||
}
|
||
|
||
static gboolean
|
||
gs_plugin_epiphany_refine_finish (GsPlugin *plugin,
|
||
GAsyncResult *result,
|
||
GError **error)
|
||
{
|
||
g_return_val_if_fail (g_task_get_source_tag (G_TASK (result)) == gs_plugin_epiphany_refine_async, FALSE);
|
||
return g_task_propagate_boolean (G_TASK (result), error);
|
||
}
|
||
|
||
static GVariant *
|
||
get_serialized_icon (GsApp *app,
|
||
GIcon *icon)
|
||
{
|
||
g_autofree char *icon_path = NULL;
|
||
g_autoptr(GInputStream) stream = NULL;
|
||
g_autoptr(GBytes) bytes = NULL;
|
||
g_autoptr(GIcon) bytes_icon = NULL;
|
||
g_autoptr(GVariant) icon_v = NULL;
|
||
guint icon_width;
|
||
|
||
/* Note: GsRemoteIcon will work on this GFileIcon code path.
|
||
* The icons plugin should have called
|
||
* gs_app_ensure_icons_downloaded() for us.
|
||
*/
|
||
if (!G_IS_FILE_ICON (icon))
|
||
return NULL;
|
||
|
||
icon_path = g_file_get_path (g_file_icon_get_file (G_FILE_ICON (icon)));
|
||
if (!g_str_has_suffix (icon_path, ".png") &&
|
||
!g_str_has_suffix (icon_path, ".svg") &&
|
||
!g_str_has_suffix (icon_path, ".jpeg") &&
|
||
!g_str_has_suffix (icon_path, ".jpg")) {
|
||
g_warning ("Icon for app %s has unsupported file extension: %s",
|
||
gs_app_get_id (app), icon_path);
|
||
return NULL;
|
||
}
|
||
|
||
/* Scale down to the portal's size limit if needed */
|
||
icon_width = gs_icon_get_width (icon);
|
||
if (icon_width > 512)
|
||
icon_width = 512;
|
||
|
||
/* Serialize the icon as a #GBytesIcon since that's
|
||
* what the dynamic launcher portal requires.
|
||
*/
|
||
stream = g_loadable_icon_load (G_LOADABLE_ICON (icon), icon_width, NULL, NULL, NULL);
|
||
|
||
/* Icons are usually smaller than 1 MiB. Set a 10 MiB
|
||
* limit so we can't use a huge amount of memory or hit
|
||
* the D-Bus message size limit
|
||
*/
|
||
if (stream)
|
||
bytes = g_input_stream_read_bytes (stream, 10485760 /* 10 MiB */, NULL, NULL);
|
||
if (bytes)
|
||
bytes_icon = g_bytes_icon_new (bytes);
|
||
if (bytes_icon)
|
||
icon_v = g_icon_serialize (bytes_icon);
|
||
|
||
return g_steal_pointer (&icon_v);
|
||
}
|
||
|
||
typedef struct {
|
||
/* Input data. */
|
||
guint n_apps;
|
||
GsPluginInstallAppsFlags flags;
|
||
GsPluginProgressCallback progress_callback;
|
||
gpointer progress_user_data;
|
||
|
||
/* In-progress data. */
|
||
guint n_pending_ops;
|
||
GError *saved_error; /* (owned) (nullable) */
|
||
|
||
/* For progress reporting */
|
||
guint n_tokens_requested;
|
||
guint n_tokens_received;
|
||
guint n_apps_installed;
|
||
} InstallAppsData;
|
||
|
||
static void
|
||
install_apps_data_free (InstallAppsData *data)
|
||
{
|
||
/* Error should have been propagated by now, and all pending ops completed. */
|
||
g_assert (data->saved_error == NULL);
|
||
g_assert (data->n_pending_ops == 0);
|
||
|
||
g_free (data);
|
||
}
|
||
|
||
G_DEFINE_AUTOPTR_CLEANUP_FUNC (InstallAppsData, install_apps_data_free)
|
||
|
||
typedef struct {
|
||
GTask *task; /* (owned) */
|
||
GsApp *app; /* (owned) */
|
||
gchar *name; /* (owned) (not nullable) */
|
||
gchar *url; /* (owned) (not nullable) */
|
||
} InstallSingleAppData;
|
||
|
||
static void
|
||
install_single_app_data_free (InstallSingleAppData *data)
|
||
{
|
||
g_clear_object (&data->app);
|
||
g_clear_object (&data->task);
|
||
g_free (data->name);
|
||
g_free (data->url);
|
||
g_free (data);
|
||
}
|
||
|
||
G_DEFINE_AUTOPTR_CLEANUP_FUNC (InstallSingleAppData, install_single_app_data_free)
|
||
|
||
static void install_request_token_cb (GObject *source_object,
|
||
GAsyncResult *result,
|
||
gpointer user_data);
|
||
static void install_install_cb (GObject *source_object,
|
||
GAsyncResult *result,
|
||
gpointer user_data);
|
||
static void finish_install_apps_op (GTask *task,
|
||
GError *error);
|
||
|
||
static void
|
||
gs_plugin_epiphany_install_apps_async (GsPlugin *plugin,
|
||
GsAppList *apps,
|
||
GsPluginInstallAppsFlags flags,
|
||
GsPluginProgressCallback progress_callback,
|
||
gpointer progress_user_data,
|
||
GsPluginAppNeedsUserActionCallback app_needs_user_action_callback,
|
||
gpointer app_needs_user_action_data,
|
||
GCancellable *cancellable,
|
||
GAsyncReadyCallback callback,
|
||
gpointer user_data)
|
||
{
|
||
GsPluginEpiphany *self = GS_PLUGIN_EPIPHANY (plugin);
|
||
g_autoptr(GTask) task = NULL;
|
||
InstallAppsData *data;
|
||
g_autoptr(InstallAppsData) data_owned = NULL;
|
||
gboolean interactive = (flags & GS_PLUGIN_INSTALL_APPS_FLAGS_INTERACTIVE);
|
||
g_autoptr(GError) local_error = NULL;
|
||
|
||
task = g_task_new (self, cancellable, callback, user_data);
|
||
g_task_set_source_tag (task, gs_plugin_epiphany_install_apps_async);
|
||
|
||
data = data_owned = g_new0 (InstallAppsData, 1);
|
||
data->flags = flags;
|
||
data->progress_callback = progress_callback;
|
||
data->progress_user_data = progress_user_data;
|
||
data->n_apps = gs_app_list_length (apps);
|
||
g_task_set_task_data (task, g_steal_pointer (&data_owned), (GDestroyNotify) install_apps_data_free);
|
||
|
||
/* Start a load of operations in parallel to install the apps.
|
||
*
|
||
* When all installs are finished for all apps, finish_install_apps_op()
|
||
* will return success/error for the overall #GTask. */
|
||
data->n_pending_ops = 1;
|
||
data->n_tokens_requested = 0;
|
||
data->n_tokens_received = 0;
|
||
|
||
for (guint i = 0; i < data->n_apps; i++) {
|
||
GsApp *app = gs_app_list_index (apps, i);
|
||
g_autoptr(InstallSingleAppData) app_data = NULL;
|
||
const char *url;
|
||
const char *name;
|
||
g_autoptr(GVariant) icon_v = NULL;
|
||
GVariantBuilder opt_builder;
|
||
const int icon_sizes[] = {512, 192, 128, 1};
|
||
const char *missing_element = NULL;
|
||
|
||
/* only process this app if was created by this plugin */
|
||
if (!gs_app_has_management_plugin (app, GS_PLUGIN (self)))
|
||
continue;
|
||
|
||
/* This is a required flag for Epiphany. */
|
||
if (flags & GS_PLUGIN_INSTALL_APPS_FLAGS_NO_APPLY)
|
||
continue;
|
||
|
||
url = gs_app_get_url (app, AS_URL_KIND_HOMEPAGE);
|
||
if (url == NULL || *url == '\0')
|
||
missing_element = "url";
|
||
|
||
name = gs_app_get_name (app);
|
||
if (name == NULL || *name == '\0')
|
||
missing_element = "name";
|
||
|
||
for (guint j = 0; j < G_N_ELEMENTS (icon_sizes); j++) {
|
||
GIcon *icon = gs_app_get_icon_for_size (app, icon_sizes[j], 1, NULL);
|
||
if (icon != NULL)
|
||
icon_v = get_serialized_icon (app, icon);
|
||
if (icon_v != NULL)
|
||
break;
|
||
}
|
||
if (icon_v == NULL)
|
||
missing_element = "icon";
|
||
|
||
if (missing_element != NULL) {
|
||
g_autoptr(GsPluginEvent) event = NULL;
|
||
|
||
g_set_error (&local_error,
|
||
GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_FAILED,
|
||
"Can't install web app %s without %s",
|
||
gs_app_get_id (app), missing_element);
|
||
|
||
event = gs_plugin_event_new ("error", local_error,
|
||
"app", app,
|
||
NULL);
|
||
if (interactive)
|
||
gs_plugin_event_add_flag (event, GS_PLUGIN_EVENT_FLAG_INTERACTIVE);
|
||
gs_plugin_event_add_flag (event, GS_PLUGIN_EVENT_FLAG_WARNING);
|
||
gs_plugin_report_event (GS_PLUGIN (self), event);
|
||
g_clear_error (&local_error);
|
||
|
||
continue;
|
||
}
|
||
|
||
app_data = g_new0 (InstallSingleAppData, 1);
|
||
app_data->task = g_object_ref (task);
|
||
app_data->app = g_object_ref (app);
|
||
app_data->name = g_strdup (name);
|
||
app_data->url = g_strdup (url);
|
||
|
||
gs_app_set_state (app, GS_APP_STATE_INSTALLING);
|
||
gs_app_set_progress (app, 0);
|
||
|
||
/* First get a token from xdg-desktop-portal so Epiphany can do the
|
||
* installation without user confirmation
|
||
*/
|
||
data->n_tokens_requested++;
|
||
data->n_pending_ops++;
|
||
g_variant_builder_init (&opt_builder, G_VARIANT_TYPE_VARDICT);
|
||
g_dbus_proxy_call (self->launcher_portal_proxy,
|
||
"RequestInstallToken",
|
||
g_variant_new ("(sva{sv})",
|
||
name, icon_v, &opt_builder),
|
||
interactive ? G_DBUS_CALL_FLAGS_ALLOW_INTERACTIVE_AUTHORIZATION : G_DBUS_CALL_FLAGS_NONE,
|
||
-1,
|
||
cancellable,
|
||
install_request_token_cb,
|
||
g_steal_pointer (&app_data));
|
||
}
|
||
|
||
finish_install_apps_op (task, NULL);
|
||
}
|
||
|
||
static void
|
||
install_report_progress (GsPluginEpiphany *self,
|
||
InstallAppsData *data)
|
||
{
|
||
guint max_points, current_points;
|
||
|
||
if (data->progress_callback == NULL)
|
||
return;
|
||
|
||
/* We assign two progress points to each app: one for when its token has
|
||
* been received, and one for when it’s been installed. */
|
||
max_points = data->n_tokens_requested * 2;
|
||
current_points = data->n_tokens_received + data->n_apps_installed;
|
||
g_assert (current_points <= max_points);
|
||
g_assert (max_points > 0);
|
||
|
||
data->progress_callback (GS_PLUGIN (self),
|
||
current_points * 100 / max_points,
|
||
data->progress_user_data);
|
||
}
|
||
|
||
static void
|
||
install_request_token_cb (GObject *source_object,
|
||
GAsyncResult *result,
|
||
gpointer user_data)
|
||
{
|
||
GDBusProxy *launcher_portal_proxy = G_DBUS_PROXY (source_object);
|
||
g_autoptr(InstallSingleAppData) app_data = g_steal_pointer (&user_data);
|
||
GTask *task = app_data->task;
|
||
GsPluginEpiphany *self = g_task_get_source_object (task);
|
||
InstallAppsData *data = g_task_get_task_data (task);
|
||
GCancellable *cancellable = g_task_get_cancellable (task);
|
||
gboolean interactive = (data->flags & GS_PLUGIN_INSTALL_APPS_FLAGS_INTERACTIVE);
|
||
const char *token = NULL;
|
||
g_autoptr(GVariant) token_v = NULL;
|
||
g_autoptr(GError) local_error = NULL;
|
||
|
||
gs_app_set_progress (app_data->app, 50);
|
||
data->n_tokens_received++;
|
||
install_report_progress (self, data);
|
||
|
||
token_v = g_dbus_proxy_call_finish (launcher_portal_proxy, result, &local_error);
|
||
if (token_v == NULL) {
|
||
g_autoptr(GsPluginEvent) event = NULL;
|
||
|
||
gs_app_set_state_recover (app_data->app);
|
||
gs_epiphany_error_convert (&local_error);
|
||
|
||
event = gs_plugin_event_new ("error", local_error,
|
||
"app", app_data->app,
|
||
NULL);
|
||
if (interactive)
|
||
gs_plugin_event_add_flag (event, GS_PLUGIN_EVENT_FLAG_INTERACTIVE);
|
||
gs_plugin_event_add_flag (event, GS_PLUGIN_EVENT_FLAG_WARNING);
|
||
gs_plugin_report_event (GS_PLUGIN (self), event);
|
||
g_clear_error (&local_error);
|
||
|
||
finish_install_apps_op (task, g_steal_pointer (&local_error));
|
||
return;
|
||
}
|
||
|
||
/* Then pass the token to Epiphany which will use xdg-desktop-portal to
|
||
* complete the installation
|
||
*/
|
||
g_variant_get (token_v, "(&s)", &token);
|
||
gs_ephy_web_app_provider_call_install (self->epiphany_proxy,
|
||
app_data->url, app_data->name, token,
|
||
interactive ? G_DBUS_CALL_FLAGS_ALLOW_INTERACTIVE_AUTHORIZATION : G_DBUS_CALL_FLAGS_NONE,
|
||
-1 /* timeout */,
|
||
cancellable,
|
||
install_install_cb,
|
||
app_data /* steals ownership */);
|
||
app_data = NULL;
|
||
}
|
||
|
||
static void
|
||
install_install_cb (GObject *source_object,
|
||
GAsyncResult *result,
|
||
gpointer user_data)
|
||
{
|
||
GsEphyWebAppProvider *epiphany_proxy = GS_EPHY_WEB_APP_PROVIDER (source_object);
|
||
g_autoptr(InstallSingleAppData) app_data = g_steal_pointer (&user_data);
|
||
GTask *task = app_data->task;
|
||
GsPluginEpiphany *self = g_task_get_source_object (task);
|
||
InstallAppsData *data = g_task_get_task_data (task);
|
||
gboolean interactive = (data->flags & GS_PLUGIN_INSTALL_APPS_FLAGS_INTERACTIVE);
|
||
g_autofree char *installed_app_id = NULL;
|
||
g_autoptr(GError) local_error = NULL;
|
||
|
||
gs_app_set_progress (app_data->app, 100);
|
||
data->n_apps_installed++;
|
||
install_report_progress (self, data);
|
||
|
||
if (!gs_ephy_web_app_provider_call_install_finish (epiphany_proxy,
|
||
&installed_app_id,
|
||
result,
|
||
&local_error)) {
|
||
g_autoptr(GsPluginEvent) event = NULL;
|
||
|
||
gs_app_set_state_recover (app_data->app);
|
||
gs_epiphany_error_convert (&local_error);
|
||
|
||
event = gs_plugin_event_new ("error", local_error,
|
||
"app", app_data->app,
|
||
NULL);
|
||
if (interactive)
|
||
gs_plugin_event_add_flag (event, GS_PLUGIN_EVENT_FLAG_INTERACTIVE);
|
||
gs_plugin_event_add_flag (event, GS_PLUGIN_EVENT_FLAG_WARNING);
|
||
gs_plugin_report_event (GS_PLUGIN (self), event);
|
||
g_clear_error (&local_error);
|
||
|
||
finish_install_apps_op (task, g_steal_pointer (&local_error));
|
||
return;
|
||
}
|
||
|
||
/* Install complete! Update internal and app state. */
|
||
{
|
||
g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&self->installed_apps_mutex);
|
||
g_hash_table_insert (self->url_id_map, g_strdup (app_data->url),
|
||
g_strdup (installed_app_id));
|
||
}
|
||
|
||
gs_app_set_launchable (app_data->app, AS_LAUNCHABLE_KIND_DESKTOP_ID, installed_app_id);
|
||
gs_app_set_state (app_data->app, GS_APP_STATE_INSTALLED);
|
||
|
||
finish_install_apps_op (task, NULL);
|
||
}
|
||
|
||
/* @error is (transfer full) if non-%NULL */
|
||
static void
|
||
finish_install_apps_op (GTask *task,
|
||
GError *error)
|
||
{
|
||
InstallAppsData *data = g_task_get_task_data (task);
|
||
g_autoptr(GError) error_owned = g_steal_pointer (&error);
|
||
|
||
if (error_owned != NULL && data->saved_error == NULL)
|
||
data->saved_error = g_steal_pointer (&error_owned);
|
||
else if (error_owned != NULL)
|
||
g_debug ("Additional error while installing apps: %s", error_owned->message);
|
||
|
||
g_assert (data->n_pending_ops > 0);
|
||
data->n_pending_ops--;
|
||
|
||
if (data->n_pending_ops > 0)
|
||
return;
|
||
|
||
/* Get the results of the parallel ops. */
|
||
if (data->saved_error != NULL)
|
||
g_task_return_error (task, g_steal_pointer (&data->saved_error));
|
||
else
|
||
g_task_return_boolean (task, TRUE);
|
||
}
|
||
|
||
static gboolean
|
||
gs_plugin_epiphany_install_apps_finish (GsPlugin *plugin,
|
||
GAsyncResult *result,
|
||
GError **error)
|
||
{
|
||
return g_task_propagate_boolean (G_TASK (result), error);
|
||
}
|
||
|
||
typedef struct {
|
||
/* Input data. */
|
||
GsPluginUninstallAppsFlags flags;
|
||
GsPluginProgressCallback progress_callback;
|
||
gpointer progress_user_data;
|
||
|
||
/* In-progress data. */
|
||
guint n_pending_ops;
|
||
GError *saved_error; /* (owned) (nullable) */
|
||
|
||
/* For progress reporting */
|
||
guint n_uninstalls_started;
|
||
guint n_apps_uninstalled;
|
||
} UninstallAppsData;
|
||
|
||
static void
|
||
uninstall_apps_data_free (UninstallAppsData *data)
|
||
{
|
||
/* Error should have been propagated by now, and all pending ops completed. */
|
||
g_assert (data->saved_error == NULL);
|
||
g_assert (data->n_pending_ops == 0);
|
||
|
||
g_free (data);
|
||
}
|
||
|
||
G_DEFINE_AUTOPTR_CLEANUP_FUNC (UninstallAppsData, uninstall_apps_data_free)
|
||
|
||
typedef struct {
|
||
GTask *task; /* (owned) */
|
||
GsApp *app; /* (owned) */
|
||
} UninstallSingleAppData;
|
||
|
||
static void
|
||
uninstall_single_app_data_free (UninstallSingleAppData *data)
|
||
{
|
||
g_clear_object (&data->app);
|
||
g_clear_object (&data->task);
|
||
g_free (data);
|
||
}
|
||
|
||
G_DEFINE_AUTOPTR_CLEANUP_FUNC (UninstallSingleAppData, uninstall_single_app_data_free)
|
||
|
||
static void uninstall_cb (GObject *source_object,
|
||
GAsyncResult *result,
|
||
gpointer user_data);
|
||
static void finish_uninstall_apps_op (GTask *task,
|
||
GError *error);
|
||
|
||
static void
|
||
gs_plugin_epiphany_uninstall_apps_async (GsPlugin *plugin,
|
||
GsAppList *apps,
|
||
GsPluginUninstallAppsFlags flags,
|
||
GsPluginProgressCallback progress_callback,
|
||
gpointer progress_user_data,
|
||
GsPluginAppNeedsUserActionCallback app_needs_user_action_callback,
|
||
gpointer app_needs_user_action_data,
|
||
GCancellable *cancellable,
|
||
GAsyncReadyCallback callback,
|
||
gpointer user_data)
|
||
{
|
||
GsPluginEpiphany *self = GS_PLUGIN_EPIPHANY (plugin);
|
||
g_autoptr(GTask) task = NULL;
|
||
UninstallAppsData *data;
|
||
g_autoptr(UninstallAppsData) data_owned = NULL;
|
||
gboolean interactive = (flags & GS_PLUGIN_UNINSTALL_APPS_FLAGS_INTERACTIVE);
|
||
g_autoptr(GError) local_error = NULL;
|
||
|
||
task = g_task_new (self, cancellable, callback, user_data);
|
||
g_task_set_source_tag (task, gs_plugin_epiphany_uninstall_apps_async);
|
||
|
||
data = data_owned = g_new0 (UninstallAppsData, 1);
|
||
data->flags = flags;
|
||
data->progress_callback = progress_callback;
|
||
data->progress_user_data = progress_user_data;
|
||
g_task_set_task_data (task, g_steal_pointer (&data_owned), (GDestroyNotify) uninstall_apps_data_free);
|
||
|
||
/* Start a load of operations in parallel to uninstall the apps.
|
||
*
|
||
* When all uninstalls are finished for all apps, finish_uninstall_apps_op()
|
||
* will return success/error for the overall #GTask. */
|
||
data->n_pending_ops = 1;
|
||
data->n_uninstalls_started = 0;
|
||
|
||
for (guint i = 0; i < gs_app_list_length (apps); i++) {
|
||
GsApp *app = gs_app_list_index (apps, i);
|
||
g_autoptr(UninstallSingleAppData) app_data = NULL;
|
||
const char *installed_app_id;
|
||
|
||
/* only process this app if was created by this plugin */
|
||
if (!gs_app_has_management_plugin (app, GS_PLUGIN (self)))
|
||
continue;
|
||
|
||
installed_app_id = gs_app_get_launchable (app, AS_LAUNCHABLE_KIND_DESKTOP_ID);
|
||
if (installed_app_id == NULL) {
|
||
g_autoptr(GsPluginEvent) event = NULL;
|
||
|
||
g_set_error (&local_error,
|
||
GS_PLUGIN_ERROR, GS_PLUGIN_ERROR_FAILED,
|
||
"Can’t uninstall web app %s without installed app ID",
|
||
gs_app_get_id (app));
|
||
|
||
event = gs_plugin_event_new ("error", local_error,
|
||
"app", app,
|
||
NULL);
|
||
if (interactive)
|
||
gs_plugin_event_add_flag (event, GS_PLUGIN_EVENT_FLAG_INTERACTIVE);
|
||
gs_plugin_event_add_flag (event, GS_PLUGIN_EVENT_FLAG_WARNING);
|
||
gs_plugin_report_event (GS_PLUGIN (self), event);
|
||
g_clear_error (&local_error);
|
||
|
||
continue;
|
||
}
|
||
|
||
app_data = g_new0 (UninstallSingleAppData, 1);
|
||
app_data->task = g_object_ref (task);
|
||
app_data->app = g_object_ref (app);
|
||
|
||
gs_app_set_state (app, GS_APP_STATE_REMOVING);
|
||
gs_app_set_progress (app, 0);
|
||
|
||
data->n_uninstalls_started++;
|
||
data->n_pending_ops++;
|
||
gs_ephy_web_app_provider_call_uninstall (self->epiphany_proxy,
|
||
installed_app_id,
|
||
interactive ? G_DBUS_CALL_FLAGS_ALLOW_INTERACTIVE_AUTHORIZATION : G_DBUS_CALL_FLAGS_NONE,
|
||
-1 /* timeout */,
|
||
cancellable,
|
||
uninstall_cb,
|
||
g_steal_pointer (&app_data));
|
||
}
|
||
|
||
finish_uninstall_apps_op (task, NULL);
|
||
}
|
||
|
||
static void
|
||
uninstall_cb (GObject *source_object,
|
||
GAsyncResult *result,
|
||
gpointer user_data)
|
||
{
|
||
GsEphyWebAppProvider *epiphany_proxy = GS_EPHY_WEB_APP_PROVIDER (source_object);
|
||
g_autoptr(UninstallSingleAppData) app_data = g_steal_pointer (&user_data);
|
||
GTask *task = app_data->task;
|
||
GsPluginEpiphany *self = g_task_get_source_object (task);
|
||
UninstallAppsData *data = g_task_get_task_data (task);
|
||
gboolean interactive = (data->flags & GS_PLUGIN_UNINSTALL_APPS_FLAGS_INTERACTIVE);
|
||
const char *url;
|
||
g_autoptr(GError) local_error = NULL;
|
||
|
||
gs_app_set_progress (app_data->app, 100);
|
||
data->n_apps_uninstalled++;
|
||
|
||
/* Progress report. */
|
||
if (data->progress_callback != NULL) {
|
||
g_assert (data->n_uninstalls_started > 0);
|
||
data->progress_callback (GS_PLUGIN (self),
|
||
data->n_apps_uninstalled * 100 / data->n_uninstalls_started,
|
||
data->progress_user_data);
|
||
}
|
||
|
||
if (!gs_ephy_web_app_provider_call_uninstall_finish (epiphany_proxy,
|
||
result,
|
||
&local_error)) {
|
||
g_autoptr(GsPluginEvent) event = NULL;
|
||
|
||
gs_app_set_state_recover (app_data->app);
|
||
gs_epiphany_error_convert (&local_error);
|
||
|
||
event = gs_plugin_event_new ("error", local_error,
|
||
"app", app_data->app,
|
||
NULL);
|
||
if (interactive)
|
||
gs_plugin_event_add_flag (event, GS_PLUGIN_EVENT_FLAG_INTERACTIVE);
|
||
gs_plugin_event_add_flag (event, GS_PLUGIN_EVENT_FLAG_WARNING);
|
||
gs_plugin_report_event (GS_PLUGIN (self), event);
|
||
g_clear_error (&local_error);
|
||
|
||
finish_uninstall_apps_op (task, NULL);
|
||
return;
|
||
}
|
||
|
||
url = gs_app_get_launchable (app_data->app, AS_LAUNCHABLE_KIND_URL);
|
||
if (url != NULL && *url != '\0') {
|
||
g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&self->installed_apps_mutex);
|
||
g_hash_table_remove (self->url_id_map, url);
|
||
}
|
||
|
||
/* The app is not necessarily available; it may have been installed
|
||
* directly in Epiphany. So refine it to check.
|
||
*/
|
||
gs_app_set_state (app_data->app, GS_APP_STATE_UNKNOWN);
|
||
gs_epiphany_refine_app (self, app_data->app,
|
||
GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN |
|
||
GS_PLUGIN_REFINE_FLAGS_REQUIRE_SETUP_ACTION,
|
||
url);
|
||
gs_epiphany_refine_app_state (GS_PLUGIN (self), app_data->app);
|
||
|
||
finish_uninstall_apps_op (task, NULL);
|
||
}
|
||
|
||
/* @error is (transfer full) if non-%NULL */
|
||
static void
|
||
finish_uninstall_apps_op (GTask *task,
|
||
GError *error)
|
||
{
|
||
UninstallAppsData *data = g_task_get_task_data (task);
|
||
g_autoptr(GError) error_owned = g_steal_pointer (&error);
|
||
|
||
if (error_owned != NULL && data->saved_error == NULL)
|
||
data->saved_error = g_steal_pointer (&error_owned);
|
||
else if (error_owned != NULL)
|
||
g_debug ("Additional error while uninstalling apps: %s", error_owned->message);
|
||
|
||
g_assert (data->n_pending_ops > 0);
|
||
data->n_pending_ops--;
|
||
|
||
if (data->n_pending_ops > 0)
|
||
return;
|
||
|
||
/* Get the results of the parallel ops. */
|
||
if (data->saved_error != NULL)
|
||
g_task_return_error (task, g_steal_pointer (&data->saved_error));
|
||
else
|
||
g_task_return_boolean (task, TRUE);
|
||
}
|
||
|
||
static gboolean
|
||
gs_plugin_epiphany_uninstall_apps_finish (GsPlugin *plugin,
|
||
GAsyncResult *result,
|
||
GError **error)
|
||
{
|
||
return g_task_propagate_boolean (G_TASK (result), error);
|
||
}
|
||
|
||
static void
|
||
gs_plugin_epiphany_launch_async (GsPlugin *plugin,
|
||
GsApp *app,
|
||
GsPluginLaunchFlags flags,
|
||
GCancellable *cancellable,
|
||
GAsyncReadyCallback callback,
|
||
gpointer user_data)
|
||
{
|
||
gs_plugin_app_launch_async (plugin, app, flags, cancellable, callback, user_data);
|
||
}
|
||
|
||
static gboolean
|
||
gs_plugin_epiphany_launch_finish (GsPlugin *plugin,
|
||
GAsyncResult *result,
|
||
GError **error)
|
||
{
|
||
return gs_plugin_app_launch_finish (plugin, result, error);
|
||
}
|
||
|
||
static void
|
||
gs_plugin_epiphany_class_init (GsPluginEpiphanyClass *klass)
|
||
{
|
||
GObjectClass *object_class = G_OBJECT_CLASS (klass);
|
||
GsPluginClass *plugin_class = GS_PLUGIN_CLASS (klass);
|
||
|
||
object_class->dispose = gs_plugin_epiphany_dispose;
|
||
object_class->finalize = gs_plugin_epiphany_finalize;
|
||
|
||
plugin_class->setup_async = gs_plugin_epiphany_setup_async;
|
||
plugin_class->setup_finish = gs_plugin_epiphany_setup_finish;
|
||
plugin_class->shutdown_async = gs_plugin_epiphany_shutdown_async;
|
||
plugin_class->shutdown_finish = gs_plugin_epiphany_shutdown_finish;
|
||
plugin_class->refine_async = gs_plugin_epiphany_refine_async;
|
||
plugin_class->refine_finish = gs_plugin_epiphany_refine_finish;
|
||
plugin_class->list_apps_async = gs_plugin_epiphany_list_apps_async;
|
||
plugin_class->list_apps_finish = gs_plugin_epiphany_list_apps_finish;
|
||
plugin_class->install_apps_async = gs_plugin_epiphany_install_apps_async;
|
||
plugin_class->install_apps_finish = gs_plugin_epiphany_install_apps_finish;
|
||
plugin_class->uninstall_apps_async = gs_plugin_epiphany_uninstall_apps_async;
|
||
plugin_class->uninstall_apps_finish = gs_plugin_epiphany_uninstall_apps_finish;
|
||
plugin_class->launch_async = gs_plugin_epiphany_launch_async;
|
||
plugin_class->launch_finish = gs_plugin_epiphany_launch_finish;
|
||
}
|
||
|
||
GType
|
||
gs_plugin_query_type (void)
|
||
{
|
||
return GS_TYPE_PLUGIN_EPIPHANY;
|
||
}
|