[gnome-games/wip/exalm/search-provider: 7/7] Add search provider



commit efc26823afb5302ef84fec749e2d84217f82f38a
Author: Alexander Mikhaylenko <alexm gnome org>
Date:   Tue Feb 11 01:20:32 2020 +0500

    Add search provider
    
    Since games are now cached in the database, it's possible to implement a
    search provider as a separate binary that will only peek into the database
    and cover cache.
    
    Searching by multiple search terms is a bit tricky in SQL as the query
    changes based on the number of search terms. Because of that, the query has
    to be generated on the fly before binding values to it. Additionally, the
    `ORDER BY` sorting doesn't completely match Games, so it can't be used.
    Instead, games are just retrieved unsorted and then sorted manually with
    `collate()`. This also means thatthe search provider has to have the
    correct locale set, so we have to link it with GTK and use Gtk.Application
    instead of GLib.Application.
    
    Stop cleaning up dbus directory for flatpak, as the search provider service
    is installed there.
    
    Fixes https://gitlab.gnome.org/GNOME/gnome-games/issues/45

 data/meson.build                               |  22 +++
 data/org.gnome.Games.SearchProvider.ini.in     |   5 +
 data/org.gnome.Games.SearchProvider.service.in |   3 +
 flatpak/org.gnome.Games.json                   |   1 -
 meson.build                                    |   3 +
 src/meson.build                                |  27 +++
 src/search-provider.vala                       | 245 +++++++++++++++++++++++++
 7 files changed, 305 insertions(+), 1 deletion(-)
---
diff --git a/data/meson.build b/data/meson.build
index 2bc4547a..580710d0 100644
--- a/data/meson.build
+++ b/data/meson.build
@@ -54,6 +54,17 @@ if appstream_util.found()
   )
 endif
 
+dbus_conf = configuration_data()
+dbus_conf.set('appid', application_id)
+dbus_conf.set('libexecdir', libexecdir)
+configure_file (
+  input: 'org.gnome.Games.SearchProvider.service.in',
+  output: '@0@.SearchProvider.service'.format(application_id),
+  configuration: dbus_conf,
+  install: true,
+  install_dir: servicedir
+)
+
 gsettings_conf = configuration_data ()
 gsettings_conf.set ('GETTEXT_PACKAGE', meson.project_name ())
 configure_file (
@@ -64,5 +75,16 @@ configure_file (
   install_dir: join_paths(datadir, 'glib-2.0', 'schemas'),
 )
 
+search_provider_conf = configuration_data()
+search_provider_conf.set('appid', application_id)
+search_provider_conf.set('profile', profile)
+configure_file(
+  configuration: search_provider_conf,
+  input: 'org.gnome.Games.SearchProvider.ini.in',
+  install_dir: join_paths(datadir, 'gnome-shell', 'search-providers'),
+  install: true,
+  output: '@0  SearchProvider ini'.format(application_id)
+)
+
 subdir ('icons')
 subdir ('options')
diff --git a/data/org.gnome.Games.SearchProvider.ini.in b/data/org.gnome.Games.SearchProvider.ini.in
new file mode 100644
index 00000000..67bcaef1
--- /dev/null
+++ b/data/org.gnome.Games.SearchProvider.ini.in
@@ -0,0 +1,5 @@
+[Shell Search Provider]
+DesktopId=@appid@.desktop
+BusName=@appid@.SearchProvider
+ObjectPath=/org/gnome/Games@profile@/SearchProvider
+Version=2
diff --git a/data/org.gnome.Games.SearchProvider.service.in b/data/org.gnome.Games.SearchProvider.service.in
new file mode 100644
index 00000000..aed82379
--- /dev/null
+++ b/data/org.gnome.Games.SearchProvider.service.in
@@ -0,0 +1,3 @@
+[D-BUS Service]
+Name=@appid@.SearchProvider
+Exec=@libexecdir@/gnome-games-search-provider
diff --git a/flatpak/org.gnome.Games.json b/flatpak/org.gnome.Games.json
index 09e20e3b..43ecec0b 100644
--- a/flatpak/org.gnome.Games.json
+++ b/flatpak/org.gnome.Games.json
@@ -56,7 +56,6 @@
         "*.la",
         "*.a",
         "/lib/girepository-1.0",
-        "/share/dbus-1",
         "/share/doc",
         "/share/gir-1.0"
     ],
diff --git a/meson.build b/meson.build
index 4ff525c9..a51bea91 100644
--- a/meson.build
+++ b/meson.build
@@ -52,9 +52,12 @@ srcdir = join_paths (meson.source_root (), 'src')
 podir = join_paths (meson.source_root (), 'po')
 
 prefix = get_option('prefix')
+bindir = join_paths (prefix, get_option ('bindir'))
 datadir = join_paths (prefix, get_option ('datadir'))
 libdir = join_paths (prefix, get_option ('libdir'))
+libexecdir = join_paths (prefix, get_option ('libexecdir'))
 localedir = join_paths (prefix, get_option ('localedir'))
+servicedir = join_paths (datadir, 'dbus-1', 'services')
 options_dir = join_paths (datadir, meson.project_name(), 'options')
 plugins_dir = join_paths (libdir, meson.project_name(), 'plugins')
 
diff --git a/src/meson.build b/src/meson.build
index b03c1860..6672c2bb 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -217,6 +217,33 @@ gnome_games_exec = executable (
   install: true
 )
 
+search_provider_c_args = [
+  '-DG_LOG_DOMAIN="GamesSearchProvider"',
+]
+
+search_provider_sources = [
+  'search-provider.vala',
+]
+
+search_provider_deps = [
+  config_dep,
+
+  gio_dep,
+  glib_dep,
+  gtk_dep,
+  sqlite_dep,
+]
+
+executable (
+  'gnome-games-search-provider',
+  search_provider_sources,
+  dependencies: search_provider_deps,
+  c_args: search_provider_c_args,
+  include_directories: confinc,
+  install: true,
+  install_dir: libexecdir
+)
+
 plugin_dependencies = [
   archive_dep,
   gio_dep,
diff --git a/src/search-provider.vala b/src/search-provider.vala
new file mode 100644
index 00000000..f3b376ba
--- /dev/null
+++ b/src/search-provider.vala
@@ -0,0 +1,245 @@
+// This file is part of GNOME Games. License: GPL-3.0+.
+
+[DBus (name = "org.gnome.Shell.SearchProvider2")]
+private class Games.SearchProvider : Object {
+       private const string DB_QUERY_BASE = "SELECT games.uid, title FROM games WHERE %s;";
+       private const string DB_QUERY_LIKE = "title LIKE ?";
+       private const string DB_QUERY_AND = " AND ";
+
+       private Application application;
+       private HashTable<string, string> games;
+
+       internal SearchProvider (Application app) {
+               application = app;
+       }
+
+       private bool filter_by_game (string[] terms, string title) {
+               if (terms.length == 0)
+                       return true;
+
+               foreach (var term in terms)
+                       if (!(term.casefold () in title.casefold ()))
+                               return false;
+
+               return true;
+       }
+
+       public async string[] get_initial_result_set (string[] terms) throws Error {
+               application.hold ();
+
+               var result = fetch_games (terms);
+
+               application.release ();
+
+               return result;
+       }
+
+       public async string[] get_subsearch_result_set (string[] previous_results, string[] terms) throws 
Error {
+               application.hold ();
+
+               string[] results = {};
+               foreach (var uid in previous_results) {
+                       var game = games[uid];
+
+                       if (filter_by_game (terms, game))
+                               results += uid;
+               }
+
+               application.release ();
+
+               return results;
+       }
+
+       private static int compare_cover_dirs (File file1, File file2) {
+               var name1 = file1.get_basename ();
+               var name2 = file2.get_basename ();
+
+               int size1 = int.parse (name1);
+               int size2 = int.parse (name2);
+
+               if (size1 < size2)
+                       return -1;
+
+               if (size1 > size2)
+                       return 1;
+
+               return strcmp (name1, name2);
+       }
+
+       private async File? find_game_cover (string uid) throws Error {
+               var cache_dir = Environment.get_user_cache_dir ();
+               var path = @"$cache_dir/gnome-games/covers/";
+               var covers_dir = File.new_for_path (path);
+
+               var enumerator = yield covers_dir.enumerate_children_async ("standard::*",
+                                                                           FileQueryInfoFlags.NONE);
+
+               var list = new List<File> ();
+
+               FileInfo info;
+               while ((info = enumerator.next_file (null)) != null) {
+                       if (info.get_file_type () != FileType.DIRECTORY)
+                               continue;
+
+                       list.prepend (enumerator.get_child (info));
+               }
+
+               list.sort (compare_cover_dirs);
+
+               foreach (var dir in list) {
+                       var child = dir.get_child (@"$uid.png");
+
+                       if (child.query_exists ())
+                               return child;
+               }
+
+               return null;
+       }
+
+       public async HashTable<string, Variant>[] get_result_metas (string[] results) throws Error {
+               application.hold ();
+
+               var result = new GenericArray<HashTable<string, Variant>> ();
+
+               foreach (var uid in results) {
+                       var title = games[uid];
+                       var cover = yield find_game_cover (uid);
+
+                       GLib.Icon icon;
+                       if (cover != null)
+                               icon = new FileIcon (cover);
+                       else
+                               icon = new ThemedIcon ("%s-symbolic".printf (Config.APPLICATION_ID));
+
+                       var metadata = new HashTable<string, Variant> (str_hash, str_equal);
+
+                       metadata.insert ("id", uid);
+                       metadata.insert ("name", title);
+                       metadata.insert ("icon", icon.to_string ());
+
+                       result.add (metadata);
+               }
+
+               application.release ();
+
+               return result.data;
+       }
+
+       public void activate_result (string uid, string[] terms, uint32 timestamp) throws Error {
+               run_with_args ({ "--uid", uid });
+       }
+
+       public void launch_search (string[] terms, uint32 timestamp) throws Error {
+               string[] args = {};
+
+               foreach (var term in terms) {
+                       args += "--search";
+                       args += term;
+               }
+
+               run_with_args (args);
+       }
+
+       private void run_with_args (string[] run_args) {
+               application.hold ();
+
+               try {
+                       string[] args = { "gnome-games" };
+
+                       foreach (var arg in run_args)
+                               args += arg;
+
+                       Process.spawn_async (null, args, null, SpawnFlags.SEARCH_PATH, null, null);
+               }
+               catch (Error error) {
+                       critical ("Couldn't launch search: %s", error.message);
+               }
+
+               application.release ();
+       }
+
+       private string get_query_for_n_terms (int n) {
+               string[] query_terms = {};
+
+               for (int i = 0; i < n; i++)
+                       query_terms += DB_QUERY_LIKE;
+
+               return DB_QUERY_BASE.printf (string.joinv (DB_QUERY_AND, query_terms));
+       }
+
+       private string[] fetch_games (string[] terms) {
+               var data_dir = Environment.get_user_data_dir ();
+               var path = @"$data_dir/gnome-games/database.sqlite3";
+
+               Sqlite.Database db;
+               var result = Sqlite.Database.open (path, out db);
+
+               if (result != Sqlite.OK) {
+                       critical ("Couldn’t open the database for %s", path);
+                       return {};
+               }
+
+               var query = get_query_for_n_terms (terms.length);
+
+               Sqlite.Statement statement;
+               result = db.prepare_v2 (query, query.length, out statement);
+
+               if (result != Sqlite.OK) {
+                       critical ("Preparation failed: %s", db.errmsg ());
+                       return {};
+               }
+
+               for (int i = 0; i < terms.length; i++) {
+                       result = statement.bind_text (i + 1, "%%%s%%".printf (terms[i]));
+
+                       if (result != Sqlite.OK) {
+                               critical ("Couldn't bind value: %s", db.errmsg ());
+                               return {};
+                       }
+               }
+
+               games = new HashTable<string, string> (str_hash, str_equal);
+               var results = new GenericArray<string> ();
+
+               while (statement.step () == Sqlite.ROW) {
+                       var uid = statement.column_text (0);
+                       var title = statement.column_text (1);
+
+                       games[uid] = title;
+                       results.add (uid);
+               }
+
+               results.sort_with_data ((a, b) => {
+                       return games[a].collate (games[b]);
+               });
+
+               return results.data;
+       }
+}
+
+// This has to be a Gtk.Application and not GLib.Application because
+// we sort games using string.collate() so the locale must be correct
+public class Games.SearchProviderApplication : Gtk.Application {
+       internal SearchProviderApplication () {
+               Object (application_id: Config.APPLICATION_ID + ".SearchProvider",
+                       flags: ApplicationFlags.IS_SERVICE,
+                       inactivity_timeout: 10000);
+       }
+
+       protected override bool dbus_register (DBusConnection connection, string object_path) {
+               try {
+                       var provider = new SearchProvider (this);
+                       connection.register_object (object_path, provider);
+               }
+               catch (IOError e) {
+                       warning ("Could not register search provider: %s", e.message);
+                       quit ();
+               }
+
+               return true;
+       }
+}
+
+int main () {
+       return new Games.SearchProviderApplication ().run ();
+}


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