[gnome-software/mwleeds/hardcoded-pwa-list: 4/10] epiphany: Refine appstream-provided web apps




commit 3ab81a4113425ca4ec4f3b3ab938a2d59b357d44
Author: Phaedrus Leeds <mwleeds protonmail com>
Date:   Thu Mar 24 15:26:40 2022 -0700

    epiphany: Refine appstream-provided web apps
    
    This commit significantly reworks the epiphany plugin, so that rather
    than only handling web apps that have been installed via Epiphany, we
    handle web apps provided in AppStream data, and allow them to be
    installed, removed, etc.

 plugins/epiphany/gs-plugin-epiphany.c | 545 ++++++++++++++++++++++++++--------
 plugins/epiphany/gs-self-test.c       |   6 +-
 2 files changed, 431 insertions(+), 120 deletions(-)
---
diff --git a/plugins/epiphany/gs-plugin-epiphany.c b/plugins/epiphany/gs-plugin-epiphany.c
index 7f1634dda..385ebf813 100644
--- a/plugins/epiphany/gs-plugin-epiphany.c
+++ b/plugins/epiphany/gs-plugin-epiphany.c
@@ -23,8 +23,25 @@
  *
  * 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 not Flatpak'd Epiphany, for new enough
- * versions of Epiphany.
+ * 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
  */
@@ -39,7 +56,11 @@ struct _GsPluginEpiphany
        GDBusProxy *launcher_portal_proxy;  /* (owned) */
        GFileMonitor *monitor; /* (owned) */
        guint changed_id;
-       GMutex installed_apps_mutex; /* protects the plugin cache */
+       /* 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) */
 };
 
 G_DEFINE_TYPE (GsPluginEpiphany, gs_plugin_epiphany, GS_TYPE_PLUGIN)
@@ -93,6 +114,13 @@ gs_plugin_epiphany_changed_cb (GFileMonitor      *monitor,
 {
        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.
         */
@@ -121,6 +149,15 @@ gs_plugin_epiphany_setup_async (GsPlugin            *plugin,
 
        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.
@@ -299,6 +336,7 @@ gs_plugin_epiphany_dispose (GObject *object)
        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);
 }
@@ -313,6 +351,44 @@ gs_plugin_epiphany_finalize (GObject *object)
        G_OBJECT_CLASS (gs_plugin_epiphany_parent_class)->finalize (object);
 }
 
+static gboolean ensure_installed_apps_cache (GsPluginEpiphany  *self,
+                                            GCancellable      *cancellable,
+                                            GError           **error);
+
+/* Run in @worker */
+static void
+gs_epiphany_refine_app_state (GsPlugin *plugin,
+                             GsApp    *app)
+{
+       GsPluginEpiphany *self = GS_PLUGIN_EPIPHANY (plugin);
+
+       assert_in_worker (self);
+
+       if (gs_app_get_state (app) == GS_APP_STATE_UNKNOWN) {
+               g_autoptr(GsApp) cached_app = NULL;
+               g_autoptr(GError) local_error = NULL;
+               const char *appstream_source;
+
+               if (!ensure_installed_apps_cache (self, NULL, &local_error)) {
+                       g_warning ("Failed to refresh installed apps cache: %s", local_error->message);
+                       return;
+               }
+
+               /* 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);
+       }
+}
+
 void
 gs_plugin_adopt_app (GsPlugin *plugin,
                     GsApp    *app)
@@ -348,9 +424,14 @@ gs_plugin_epiphany_list_installed_apps_async (GsPlugin                       *pl
 
 /* Run in @worker */
 static void
-set_license_from_hostname (GsApp      *app,
-                          const char *hostname)
+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;
@@ -368,14 +449,25 @@ set_license_from_hostname (GsApp      *app,
        };
 
        g_return_if_fail (GS_IS_APP (app));
+       g_return_if_fail (uri != NULL);
+       g_return_if_fail (url != NULL);
 
-       if (hostname == NULL || *hostname == '\0')
-               return;
+       gs_app_set_origin (app, "gnome-web");
+       gs_app_set_origin_ui (app, _("GNOME Web"));
+
+       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.
         */
-       if (gs_app_get_license (app) == NULL) {
+       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,
@@ -384,6 +476,107 @@ set_license_from_hostname (GsApp      *app,
                        }
                }
        }
+
+       gs_app_set_size_download (app, GS_SIZE_TYPE_VALID, 0);
+
+       gs_app_set_permissions (app, GS_APP_PERMISSIONS_NETWORK);
+
+       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_get_icons (app) == NULL &&
+                   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 */
@@ -391,71 +584,62 @@ static GsApp *
 gs_epiphany_create_app (GsPluginEpiphany *self,
                        const char       *id)
 {
-       g_autoptr(GsApp) app_cached = NULL;
-       g_autoptr(GsApp) tmp_app = NULL;
+       g_autoptr(GsApp) app = NULL;
 
        assert_in_worker (self);
 
-       tmp_app = gs_app_new (id);
-       gs_app_set_management_plugin (tmp_app, GS_PLUGIN (self));
-       gs_app_set_origin (tmp_app, "gnome-web");
-       gs_app_set_origin_ui (tmp_app, _("GNOME Web"));
-       gs_app_set_kind (tmp_app, AS_COMPONENT_KIND_WEB_APP);
-       gs_app_set_scope (tmp_app, AS_COMPONENT_SCOPE_USER);
+       app = gs_plugin_cache_lookup (GS_PLUGIN (self), id);
+       if (app != NULL)
+               return g_steal_pointer (&app);
 
-       app_cached = gs_plugin_cache_lookup (GS_PLUGIN (self), id);
-       if (app_cached != NULL)
-               return g_steal_pointer (&app_cached);
+       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, tmp_app);
-       return g_steal_pointer (&tmp_app);
+       gs_plugin_cache_add (GS_PLUGIN (self), id, app);
+       return g_steal_pointer (&app);
 }
 
 /* Run in @worker */
-static void
-list_installed_apps_thread_cb (GTask        *task,
-                               gpointer      source_object,
-                               gpointer      task_data,
-                               GCancellable *cancellable)
+static gboolean
+ensure_installed_apps_cache (GsPluginEpiphany  *self,
+                            GCancellable      *cancellable,
+                            GError           **error)
 {
-       GsPluginEpiphany *self = GS_PLUGIN_EPIPHANY (source_object);
-       g_autoptr(GsAppList) list = gs_app_list_new ();
-       g_autoptr(GsAppList) installed_cache = gs_app_list_new ();
-       g_autoptr(GError) local_error = NULL;
        g_autoptr(GVariant) webapps_v = NULL;
        g_auto(GStrv) webapps = NULL;
-       g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&self->installed_apps_mutex);
        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,
                                                                    &webapps,
                                                                    cancellable,
-                                                                   &local_error)) {
-               gs_epiphany_error_convert (&local_error);
-               g_task_return_error (task, g_steal_pointer (&local_error));
-               return;
+                                                                   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 *desktop_path;
-               const gchar *name;
                const gchar *url = NULL;
-               g_autofree char *icon_path = NULL;
+               g_autofree char *url_hash = NULL;
+               g_autofree char *metainfo_app_id = NULL;
                const gchar *exec;
-               const gchar *host;
                int argc;
+               GsPluginRefineFlags refine_flags;
                g_auto(GStrv) argv = NULL;
-               guint64 install_date = 0;
-               goffset desktop_size = 0, icon_size = 0;
                g_autoptr(GsApp) app = NULL;
                g_autoptr(GDesktopAppInfo) desktop_info = NULL;
-               g_autoptr(GFileInfo) file_info = NULL;
-               g_autoptr(GFile) desktop_file = NULL;
                g_autoptr(GUri) uri = NULL;
 
                g_debug ("%s: Working on installed web app %s", G_STRFUNC, desktop_file_id);
@@ -467,8 +651,6 @@ list_installed_apps_thread_cb (GTask        *task,
                        continue;
                }
 
-               name = g_app_info_get_name (G_APP_INFO (desktop_info));
-
                /* 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
@@ -480,78 +662,36 @@ list_installed_apps_thread_cb (GTask        *task,
                        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", desktop_file_id);
+                       g_warning ("Failed to parse URL for web app %s: %s",
+                                  desktop_file_id, url ? url : "(null)");
                        continue;
                }
 
-               icon_path = g_desktop_app_info_get_string (desktop_info, "Icon");
-
-               desktop_path = g_desktop_app_info_get_filename (desktop_info);
-               g_assert (desktop_path);
-               desktop_file = g_file_new_for_path (desktop_path);
+               /* 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));
 
-               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);
-               }
-
-               app = gs_epiphany_create_app (self, desktop_file_id);
-               gs_app_set_state (app, GS_APP_STATE_INSTALLED);
-               gs_app_set_name (app, GS_APP_QUALITY_NORMAL, name);
-               gs_app_set_url (app, AS_URL_KIND_HOMEPAGE, url);
-               gs_app_set_permissions (app, GS_APP_PERMISSIONS_NETWORK);
-
-               /* Use the domain name as a fallback summary.
-                * FIXME: Fetch the summary from the site's webapp manifest.
+               /* 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).
                 */
-               host = g_uri_get_host (uri);
-               gs_app_set_summary (app, GS_APP_QUALITY_LOWEST, host ? host : url);
-
-               set_license_from_hostname (app, host);
+               url_hash = g_compute_checksum_for_string (G_CHECKSUM_SHA1, url, -1);
+               metainfo_app_id = g_strconcat ("org.gnome.Software.WebApp_", url_hash, ".desktop", NULL);
+               g_debug ("Creating GsApp for webapp with URL %s using app ID %s (desktop file id: %s)",
+                        url, metainfo_app_id, desktop_file_id);
 
-               if (icon_path) {
-                       g_autoptr(GFile) icon_file = g_file_new_for_path (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 size = 0;
-
-                       g_debug ("%s: finding size for icon %s", G_STRFUNC, 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);
+               /* App gets added to the plugin cache here */
+               app = gs_epiphany_create_app (self, metainfo_app_id);
 
-                       /* dir should be either scalable or e.g. 512x512 */
-                       if (g_strcmp0 (icon_dir_basename, "scalable") == 0) {
-                               /* Ensure scalable icons are preferred */
-                               size = 4096;
-                       } else if ((x = strchr (icon_dir_basename, 'x')) != NULL) {
-                               g_ascii_string_to_unsigned (x + 1, 10, 1, G_MAXINT, &size, NULL);
-                       }
-                       if (size > 0 && size <= 4096) {
-                               gs_icon_set_width (icon, size);
-                               gs_icon_set_height (icon, size);
-                       } else {
-                               g_warning ("Unexpectedly unable to determine size of icon %s", icon_path);
-                       }
+               gs_app_set_state (app, GS_APP_STATE_INSTALLED);
 
-                       gs_app_add_icon (app, icon);
-               }
-               if (install_date) {
-                       gs_app_set_install_date (app, install_date);
-               }
-               if (desktop_size > 0 || icon_size > 0) {
-                       gs_app_set_size_installed (app, GS_SIZE_TYPE_VALID, desktop_size + icon_size);
-               }
-               gs_app_list_add (list, app);
+               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
@@ -560,16 +700,48 @@ list_installed_apps_thread_cb (GTask        *task,
        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 *app_id = gs_app_get_id (app);
-               g_autoptr(GsApp) app_cached = NULL;
+               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, app_id))
+               if (g_strv_contains ((const char * const *)webapps, installed_app_id))
                        continue;
 
-               gs_app_set_state (app, GS_APP_STATE_UNKNOWN);
-               gs_plugin_cache_remove (GS_PLUGIN (self), app_id);
+               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_installed_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 ();
+       g_autoptr(GError) local_error = NULL;
+
+       if (!ensure_installed_apps_cache (self, cancellable, &local_error)) {
+               g_task_return_error (task, g_steal_pointer (&local_error));
+               return;
        }
 
+       gs_plugin_cache_lookup_by_state (GS_PLUGIN (self), list, GS_APP_STATE_INSTALLED);
        g_task_return_pointer (task, g_steal_pointer (&list), g_object_unref);
 }
 
@@ -582,6 +754,110 @@ gs_plugin_epiphany_list_installed_apps_finish (GsPlugin      *plugin,
        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;
+
+       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, G_PRIORITY_DEFAULT,
+                               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;
+       g_autoptr(GError) local_error = NULL;
+
+       assert_in_worker (self);
+
+       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)
@@ -638,7 +914,7 @@ gs_plugin_app_install (GsPlugin      *plugin,
        const char *url;
        const char *name;
        const char *token = NULL;
-       g_autofree char *installed_desktop_id = NULL;
+       g_autofree char *installed_app_id = NULL;
        g_autoptr(GVariant) token_v = NULL;
        g_autoptr(GVariant) icon_v = NULL;
        GVariantBuilder opt_builder;
@@ -698,14 +974,21 @@ gs_plugin_app_install (GsPlugin      *plugin,
        g_variant_get (token_v, "(&s)", &token);
        if (!gs_ephy_web_app_provider_call_install_sync (self->epiphany_proxy,
                                                         url, name, token,
-                                                        &installed_desktop_id,
+                                                        &installed_app_id,
                                                         cancellable,
                                                         error)) {
                gs_epiphany_error_convert (error);
                gs_app_set_state_recover (app);
                return FALSE;
        }
-       gs_app_set_launchable (app, AS_LAUNCHABLE_KIND_DESKTOP_ID, installed_desktop_id);
+
+       {
+               g_autoptr(GMutexLocker) locker = g_mutex_locker_new (&self->installed_apps_mutex);
+               g_hash_table_insert (self->url_id_map, g_strdup (url),
+                                    g_strdup (installed_app_id));
+       }
+
+       gs_app_set_launchable (app, AS_LAUNCHABLE_KIND_DESKTOP_ID, installed_app_id);
        gs_app_set_state (app, GS_APP_STATE_INSTALLED);
 
        return TRUE;
@@ -718,20 +1001,42 @@ gs_plugin_app_remove (GsPlugin      *plugin,
                      GError       **error)
 {
        GsPluginEpiphany *self = GS_PLUGIN_EPIPHANY (plugin);
+       const char *installed_app_id;
+       const char *url;
 
        if (!gs_app_has_management_plugin (app, plugin))
                return TRUE;
 
+       installed_app_id = gs_app_get_launchable (app, AS_LAUNCHABLE_KIND_DESKTOP_ID);
+       if (installed_app_id == NULL) {
+               g_set_error_literal (error,
+                                    GS_PLUGIN_ERROR,
+                                    GS_PLUGIN_ERROR_NOT_SUPPORTED,
+                                    "App can't be uninstalled without installed app ID");
+               gs_app_set_state_recover (app);
+               return FALSE;
+       }
+
        gs_app_set_state (app, GS_APP_STATE_REMOVING);
        if (!gs_ephy_web_app_provider_call_uninstall_sync (self->epiphany_proxy,
-                                                          gs_app_get_id (app),
+                                                          installed_app_id,
                                                           cancellable,
                                                           error)) {
                gs_epiphany_error_convert (error);
                gs_app_set_state_recover (app);
                return FALSE;
        }
-       gs_app_set_state (app, GS_APP_STATE_AVAILABLE);
+
+       url = gs_app_get_launchable (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
+        */
+       gs_app_set_state (app, GS_APP_STATE_UNKNOWN);
 
        return TRUE;
 }
@@ -761,6 +1066,8 @@ gs_plugin_epiphany_class_init (GsPluginEpiphanyClass *klass)
        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_installed_apps_async = gs_plugin_epiphany_list_installed_apps_async;
        plugin_class->list_installed_apps_finish = gs_plugin_epiphany_list_installed_apps_finish;
 }
diff --git a/plugins/epiphany/gs-self-test.c b/plugins/epiphany/gs-self-test.c
index 89f384359..8f8f3c9fc 100644
--- a/plugins/epiphany/gs-self-test.c
+++ b/plugins/epiphany/gs-self-test.c
@@ -150,6 +150,8 @@ gs_plugins_epiphany_installed_func (GsPluginLoader *plugin_loader)
        g_autoptr(GsAppList) list = NULL;
        GsApp *app;
        const char *app_id = "org.gnome.Epiphany.WebApp_e9d0e1e4b0a10856aa3b38d9eb4375de4070d043";
+       const char *metainfo_app_id = 
"org.gnome.Software.WebApp_e636aa5f2069f6e9c02deccc7b65f43da7985e32.desktop";
+       const char *launchable_app_id;
        g_autofree char *app_id_desktop = NULL;
        g_autofree char *desktop_path = NULL;
        g_autofree char *origin_ui = NULL;
@@ -167,7 +169,9 @@ gs_plugins_epiphany_installed_func (GsPluginLoader *plugin_loader)
 
        g_assert_cmpint (gs_app_list_length (list), ==, 1);
        app = gs_app_list_index (list, 0);
-       g_assert_cmpstr (gs_app_get_id (app), ==, app_id_desktop);
+       g_assert_cmpstr (gs_app_get_id (app), ==, metainfo_app_id);
+       launchable_app_id = gs_app_get_launchable (app, AS_LAUNCHABLE_KIND_DESKTOP_ID);
+       g_assert_cmpstr (launchable_app_id, ==, app_id_desktop);
        g_assert_cmpint (gs_app_get_kind (app), ==, AS_COMPONENT_KIND_WEB_APP);
        g_assert_cmpint (gs_app_get_scope (app), ==, AS_COMPONENT_SCOPE_USER);
        g_assert_cmpint (gs_app_get_state (app), ==, GS_APP_STATE_INSTALLED);


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]