summaryrefslogtreecommitdiffstats
path: root/src/gs-shell-search-provider.c
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/gs-shell-search-provider.c403
1 files changed, 403 insertions, 0 deletions
diff --git a/src/gs-shell-search-provider.c b/src/gs-shell-search-provider.c
new file mode 100644
index 0000000..715f70f
--- /dev/null
+++ b/src/gs-shell-search-provider.c
@@ -0,0 +1,403 @@
+/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*-
+ * vi:set noexpandtab tabstop=8 shiftwidth=8:
+ *
+ * gs-shell-search-provider.c - Implementation of a GNOME Shell
+ * search provider
+ *
+ * Copyright (C) 2013 Matthias Clasen
+ *
+ * SPDX-License-Identifier: GPL-2.0+
+ */
+
+#include <config.h>
+
+#include <gio/gio.h>
+#include <glib/gi18n.h>
+#include <string.h>
+
+#include "gs-shell-search-provider-generated.h"
+#include "gs-shell-search-provider.h"
+#include "gs-common.h"
+
+#define GS_SHELL_SEARCH_PROVIDER_MAX_RESULTS 20
+
+typedef struct {
+ GsShellSearchProvider *provider;
+ GDBusMethodInvocation *invocation;
+} PendingSearch;
+
+struct _GsShellSearchProvider {
+ GObject parent;
+
+ GsShellSearchProvider2 *skeleton;
+ GsPluginLoader *plugin_loader;
+ GCancellable *cancellable;
+
+ GHashTable *metas_cache;
+ GsAppList *search_results;
+};
+
+G_DEFINE_TYPE (GsShellSearchProvider, gs_shell_search_provider, G_TYPE_OBJECT)
+
+static void
+pending_search_free (PendingSearch *search)
+{
+ g_object_unref (search->invocation);
+ g_slice_free (PendingSearch, search);
+}
+
+static gint
+search_sort_by_kudo_cb (GsApp *app1, GsApp *app2, gpointer user_data)
+{
+ guint pa, pb;
+ pa = gs_app_get_kudos_percentage (app1);
+ pb = gs_app_get_kudos_percentage (app2);
+ if (pa < pb)
+ return 1;
+ else if (pa > pb)
+ return -1;
+ return 0;
+}
+
+static void
+search_done_cb (GObject *source,
+ GAsyncResult *res,
+ gpointer user_data)
+{
+ PendingSearch *search = user_data;
+ GsShellSearchProvider *self = search->provider;
+ guint i;
+ GVariantBuilder builder;
+ g_autoptr(GsAppList) list = NULL;
+
+ /* cache no longer valid */
+ gs_app_list_remove_all (self->search_results);
+
+ list = gs_plugin_loader_job_process_finish (self->plugin_loader, res, NULL);
+ if (list == NULL) {
+ g_dbus_method_invocation_return_value (search->invocation, g_variant_new ("(as)", NULL));
+ pending_search_free (search);
+ g_application_release (g_application_get_default ());
+ return;
+ }
+
+ /* sort by kudos, as there is no ratings data by default */
+ gs_app_list_sort (list, search_sort_by_kudo_cb, NULL);
+
+ g_variant_builder_init (&builder, G_VARIANT_TYPE ("as"));
+ for (i = 0; i < gs_app_list_length (list); i++) {
+ GsApp *app = gs_app_list_index (list, i);
+ if (gs_app_get_state (app) != AS_APP_STATE_AVAILABLE)
+ continue;
+ g_variant_builder_add (&builder, "s", gs_app_get_unique_id (app));
+
+ /* cache this in case we need the app in GetResultMetas */
+ gs_app_list_add (self->search_results, app);
+ }
+ g_dbus_method_invocation_return_value (search->invocation, g_variant_new ("(as)", &builder));
+
+ pending_search_free (search);
+ g_application_release (g_application_get_default ());
+}
+
+static gchar *
+gs_shell_search_provider_get_app_sort_key (GsApp *app)
+{
+ GString *key = g_string_sized_new (64);
+
+ /* sort available apps before installed ones */
+ switch (gs_app_get_state (app)) {
+ case AS_APP_STATE_AVAILABLE:
+ g_string_append (key, "9:");
+ break;
+ default:
+ g_string_append (key, "1:");
+ break;
+ }
+
+ /* sort apps before runtimes and extensions */
+ switch (gs_app_get_kind (app)) {
+ case AS_APP_KIND_DESKTOP:
+ g_string_append (key, "9:");
+ break;
+ default:
+ g_string_append (key, "1:");
+ break;
+ }
+
+ /* sort by the search key */
+ g_string_append_printf (key, "%05x:", gs_app_get_match_value (app));
+
+ /* tie-break with id */
+ g_string_append (key, gs_app_get_unique_id (app));
+
+ return g_string_free (key, FALSE);
+}
+
+static gboolean
+gs_shell_search_provider_sort_cb (GsApp *app1, GsApp *app2, gpointer user_data)
+{
+ g_autofree gchar *key1 = NULL;
+ g_autofree gchar *key2 = NULL;
+ key1 = gs_shell_search_provider_get_app_sort_key (app1);
+ key2 = gs_shell_search_provider_get_app_sort_key (app2);
+ return g_strcmp0 (key2, key1);
+}
+
+static void
+execute_search (GsShellSearchProvider *self,
+ GDBusMethodInvocation *invocation,
+ gchar **terms)
+{
+ PendingSearch *pending_search;
+ g_autofree gchar *value = NULL;
+ g_autoptr(GsPluginJob) plugin_job = NULL;
+
+ value = g_strjoinv (" ", terms);
+
+ g_cancellable_cancel (self->cancellable);
+ g_clear_object (&self->cancellable);
+
+ /* don't attempt searches for a single character */
+ if (g_strv_length (terms) == 1 &&
+ g_utf8_strlen (terms[0], -1) == 1) {
+ g_dbus_method_invocation_return_value (invocation, g_variant_new ("(as)", NULL));
+ return;
+ }
+
+ pending_search = g_slice_new (PendingSearch);
+ pending_search->provider = self;
+ pending_search->invocation = g_object_ref (invocation);
+
+ g_application_hold (g_application_get_default ());
+ self->cancellable = g_cancellable_new ();
+
+ plugin_job = gs_plugin_job_newv (GS_PLUGIN_ACTION_SEARCH,
+ "search", value,
+ "refine-flags", GS_PLUGIN_REFINE_FLAGS_REQUIRE_ICON |
+ GS_PLUGIN_REFINE_FLAGS_REQUIRE_ORIGIN_HOSTNAME,
+ "max-results", GS_SHELL_SEARCH_PROVIDER_MAX_RESULTS,
+ "dedupe-flags", GS_APP_LIST_FILTER_FLAG_PREFER_INSTALLED |
+ GS_APP_LIST_FILTER_FLAG_KEY_ID_PROVIDES,
+ NULL);
+ gs_plugin_job_set_sort_func (plugin_job, gs_shell_search_provider_sort_cb);
+ gs_plugin_job_set_sort_func_data (plugin_job, self);
+ gs_plugin_loader_job_process_async (self->plugin_loader, plugin_job,
+ self->cancellable,
+ search_done_cb,
+ pending_search);
+}
+
+static gboolean
+handle_get_initial_result_set (GsShellSearchProvider2 *skeleton,
+ GDBusMethodInvocation *invocation,
+ gchar **terms,
+ gpointer user_data)
+{
+ GsShellSearchProvider *self = user_data;
+
+ g_debug ("****** GetInitialResultSet");
+ execute_search (self, invocation, terms);
+ return TRUE;
+}
+
+static gboolean
+handle_get_subsearch_result_set (GsShellSearchProvider2 *skeleton,
+ GDBusMethodInvocation *invocation,
+ gchar **previous_results,
+ gchar **terms,
+ gpointer user_data)
+{
+ GsShellSearchProvider *self = user_data;
+
+ g_debug ("****** GetSubSearchResultSet");
+ execute_search (self, invocation, terms);
+ return TRUE;
+}
+
+static gboolean
+handle_get_result_metas (GsShellSearchProvider2 *skeleton,
+ GDBusMethodInvocation *invocation,
+ gchar **results,
+ gpointer user_data)
+{
+ GsShellSearchProvider *self = user_data;
+ GVariantBuilder meta;
+ GVariant *meta_variant;
+ GdkPixbuf *pixbuf;
+ gint i;
+ GVariantBuilder builder;
+
+ g_debug ("****** GetResultMetas");
+
+ for (i = 0; results[i]; i++) {
+ GsApp *app;
+ g_autofree gchar *description = NULL;
+
+ /* already built */
+ if (g_hash_table_lookup (self->metas_cache, results[i]) != NULL)
+ continue;
+
+ /* get previously found app */
+ app = gs_app_list_lookup (self->search_results, results[i]);
+ if (app == NULL) {
+ g_warning ("failed to refine find app %s in cache", results[i]);
+ continue;
+ }
+
+ g_variant_builder_init (&meta, G_VARIANT_TYPE ("a{sv}"));
+ g_variant_builder_add (&meta, "{sv}", "id", g_variant_new_string (gs_app_get_unique_id (app)));
+ g_variant_builder_add (&meta, "{sv}", "name", g_variant_new_string (gs_app_get_name (app)));
+ pixbuf = gs_app_get_pixbuf (app);
+ if (pixbuf != NULL)
+ g_variant_builder_add (&meta, "{sv}", "icon", g_icon_serialize (G_ICON (pixbuf)));
+
+ if (gs_utils_list_has_app_fuzzy (self->search_results, app) &&
+ gs_app_get_origin_hostname (app) != NULL) {
+ /* TRANSLATORS: this refers to where the app came from */
+ g_autofree gchar *source_text = g_strdup_printf (_("Source: %s"),
+ gs_app_get_origin_hostname (app));
+ description = g_strdup_printf ("%s %s",
+ gs_app_get_summary (app),
+ source_text);
+ } else {
+ description = g_strdup (gs_app_get_summary (app));
+ }
+ g_variant_builder_add (&meta, "{sv}", "description", g_variant_new_string (description));
+
+ meta_variant = g_variant_builder_end (&meta);
+ g_hash_table_insert (self->metas_cache,
+ g_strdup (gs_app_get_unique_id (app)),
+ g_variant_ref_sink (meta_variant));
+
+ }
+
+ g_variant_builder_init (&builder, G_VARIANT_TYPE ("aa{sv}"));
+ for (i = 0; results[i]; i++) {
+ meta_variant = (GVariant*)g_hash_table_lookup (self->metas_cache, results[i]);
+ if (meta_variant == NULL)
+ continue;
+ g_variant_builder_add_value (&builder, meta_variant);
+ }
+
+ g_dbus_method_invocation_return_value (invocation, g_variant_new ("(aa{sv})", &builder));
+
+ return TRUE;
+}
+
+static gboolean
+handle_activate_result (GsShellSearchProvider2 *skeleton,
+ GDBusMethodInvocation *invocation,
+ gchar *result,
+ gchar **terms,
+ guint32 timestamp,
+ gpointer user_data)
+{
+ GApplication *app = g_application_get_default ();
+ g_autofree gchar *string = NULL;
+
+ string = g_strjoinv (" ", terms);
+
+ g_action_group_activate_action (G_ACTION_GROUP (app), "details",
+ g_variant_new ("(ss)", result, string));
+
+ gs_shell_search_provider2_complete_activate_result (skeleton, invocation);
+ return TRUE;
+}
+
+static gboolean
+handle_launch_search (GsShellSearchProvider2 *skeleton,
+ GDBusMethodInvocation *invocation,
+ gchar **terms,
+ guint32 timestamp,
+ gpointer user_data)
+{
+ GApplication *app = g_application_get_default ();
+ g_autofree gchar *string = g_strjoinv (" ", terms);
+
+ g_action_group_activate_action (G_ACTION_GROUP (app), "search",
+ g_variant_new ("s", string));
+
+ gs_shell_search_provider2_complete_launch_search (skeleton, invocation);
+ return TRUE;
+}
+
+gboolean
+gs_shell_search_provider_register (GsShellSearchProvider *self,
+ GDBusConnection *connection,
+ GError **error)
+{
+ return g_dbus_interface_skeleton_export (G_DBUS_INTERFACE_SKELETON (self->skeleton),
+ connection,
+ "/org/gnome/Software/SearchProvider", error);
+}
+
+void
+gs_shell_search_provider_unregister (GsShellSearchProvider *self)
+{
+ g_dbus_interface_skeleton_unexport (G_DBUS_INTERFACE_SKELETON (self->skeleton));
+}
+
+static void
+search_provider_dispose (GObject *obj)
+{
+ GsShellSearchProvider *self = GS_SHELL_SEARCH_PROVIDER (obj);
+
+ g_cancellable_cancel (self->cancellable);
+ g_clear_object (&self->cancellable);
+
+ if (self->metas_cache != NULL) {
+ g_hash_table_destroy (self->metas_cache);
+ self->metas_cache = NULL;
+ }
+
+ g_clear_object (&self->search_results);
+ g_clear_object (&self->plugin_loader);
+ g_clear_object (&self->skeleton);
+
+ G_OBJECT_CLASS (gs_shell_search_provider_parent_class)->dispose (obj);
+}
+
+static void
+gs_shell_search_provider_init (GsShellSearchProvider *self)
+{
+ self->metas_cache = g_hash_table_new_full ((GHashFunc) as_utils_unique_id_hash,
+ (GEqualFunc) as_utils_unique_id_equal,
+ g_free,
+ (GDestroyNotify) g_variant_unref);
+
+ self->search_results = gs_app_list_new ();
+ self->skeleton = gs_shell_search_provider2_skeleton_new ();
+
+ g_signal_connect (self->skeleton, "handle-get-initial-result-set",
+ G_CALLBACK (handle_get_initial_result_set), self);
+ g_signal_connect (self->skeleton, "handle-get-subsearch-result-set",
+ G_CALLBACK (handle_get_subsearch_result_set), self);
+ g_signal_connect (self->skeleton, "handle-get-result-metas",
+ G_CALLBACK (handle_get_result_metas), self);
+ g_signal_connect (self->skeleton, "handle-activate-result",
+ G_CALLBACK (handle_activate_result), self);
+ g_signal_connect (self->skeleton, "handle-launch-search",
+ G_CALLBACK (handle_launch_search), self);
+}
+
+static void
+gs_shell_search_provider_class_init (GsShellSearchProviderClass *klass)
+{
+ GObjectClass *oclass = G_OBJECT_CLASS (klass);
+
+ oclass->dispose = search_provider_dispose;
+}
+
+GsShellSearchProvider *
+gs_shell_search_provider_new (void)
+{
+ return g_object_new (gs_shell_search_provider_get_type (), NULL);
+}
+
+void
+gs_shell_search_provider_setup (GsShellSearchProvider *provider,
+ GsPluginLoader *loader)
+{
+ provider->plugin_loader = g_object_ref (loader);
+}