[geary/wip/composer-folks: 1/22] Don't load all engine contacts on startup



commit 0b734f7ba2d2126f57de16463c6d0b8984a8d23e
Author: Michael Gratton <mike vee net>
Date:   Fri Jun 7 17:58:31 2019 +1000

    Don't load all engine contacts on startup
    
    Prepare for doing composer autocomplete via queries by not loading all
    contacts at startup in the engine. Update Geary.ContactStore API to that
    end, and convert Geary.Account.get_contact_store accessor method to a
    property.

 po/POTFILES.in                                     |   3 +-
 .../application/application-contact-store.vala     |   4 +-
 src/client/application/application-contact.vala    |  19 +--
 src/client/application/application-controller.vala |   6 +-
 src/client/composer/composer-widget.vala           |  22 +---
 src/client/composer/contact-list-store.vala        |  26 ++--
 src/engine/api/geary-account.vala                  |  12 +-
 src/engine/api/geary-contact-store.vala            |  62 +++------
 src/engine/common/common-contact-store-impl.vala   | 141 +++++++++++++++++++++
 src/engine/imap-db/imap-db-account.vala            |  55 +-------
 src/engine/imap-db/imap-db-contact.vala            |  70 ----------
 src/engine/imap-db/imap-db-database.vala           |   2 +-
 src/engine/imap-db/imap-db-folder.vala             |   7 +-
 .../imap-engine/imap-engine-contact-store.vala     |  29 -----
 .../imap-engine/imap-engine-generic-account.vala   |   6 +-
 src/engine/meson.build                             |   3 +-
 test/engine/api/geary-account-mock.vala            |  24 ++--
 17 files changed, 219 insertions(+), 272 deletions(-)
---
diff --git a/po/POTFILES.in b/po/POTFILES.in
index 533634ab..fd5d51c3 100644
--- a/po/POTFILES.in
+++ b/po/POTFILES.in
@@ -166,6 +166,7 @@ src/engine/app/email-store/app-copy-operation.vala
 src/engine/app/email-store/app-fetch-operation.vala
 src/engine/app/email-store/app-list-operation.vala
 src/engine/app/email-store/app-mark-operation.vala
+src/engine/common/common-contact-store-impl.vala
 src/engine/common/common-message-data.vala
 src/engine/db/db.vala
 src/engine/db/db-connection.vala
@@ -218,7 +219,6 @@ src/engine/imap/command/imap-status-command.vala
 src/engine/imap/command/imap-store-command.vala
 src/engine/imap-db/imap-db-account.vala
 src/engine/imap-db/imap-db-attachment.vala
-src/engine/imap-db/imap-db-contact.vala
 src/engine/imap-db/imap-db-database.vala
 src/engine/imap-db/imap-db-email-identifier.vala
 src/engine/imap-db/imap-db-folder.vala
@@ -239,7 +239,6 @@ src/engine/imap-engine/gmail/imap-engine-gmail-spam-trash-folder.vala
 src/engine/imap-engine/imap-engine-account-operation.vala
 src/engine/imap-engine/imap-engine-account-processor.vala
 src/engine/imap-engine/imap-engine-account-synchronizer.vala
-src/engine/imap-engine/imap-engine-contact-store.vala
 src/engine/imap-engine/imap-engine-email-prefetcher.vala
 src/engine/imap-engine/imap-engine-generic-account.vala
 src/engine/imap-engine/imap-engine-generic-folder.vala
diff --git a/src/client/application/application-contact-store.vala 
b/src/client/application/application-contact-store.vala
index 8603d8f8..7d029610 100644
--- a/src/client/application/application-contact-store.vala
+++ b/src/client/application/application-contact-store.vala
@@ -86,7 +86,9 @@ public class Application.ContactStore : Geary.BaseObject {
         }
         if (contact == null) {
             Geary.Contact? engine =
-                this.account.get_contact_store().get_by_rfc822(mailbox);
+                yield this.account.contact_store.get_by_rfc822(
+                    mailbox, cancellable
+                );
             contact = new Contact(this, individual, engine, mailbox);
             if (individual != null) {
                 this.contact_id_cache.set_entry(individual.id, contact);
diff --git a/src/client/application/application-contact.vala b/src/client/application/application-contact.vala
index 40249834..bda158aa 100644
--- a/src/client/application/application-contact.vala
+++ b/src/client/application/application-contact.vala
@@ -182,14 +182,17 @@ public class Application.Contact : Geary.BaseObject {
         throws GLib.Error {
         ContactStore? store = this.store;
         if (store != null && this.contact != null) {
-            Geary.ContactFlags flags = new Geary.ContactFlags();
-            flags.add(Geary.ContactFlags.ALWAYS_LOAD_REMOTE_IMAGES);
-
-            yield store.account.get_contact_store().mark_contacts_async(
-                Geary.Collection.single(this.contact),
-                enabled ? flags : null,
-                !enabled ? flags : null //,
-                // XXX cancellable
+            Geary.ContactFlags flags = (
+                this.contact.contact_flags ?? new Geary.ContactFlags()
+            );
+            if (enabled) {
+                flags.add(Geary.ContactFlags.ALWAYS_LOAD_REMOTE_IMAGES);
+            } else {
+                flags.remove(Geary.ContactFlags.ALWAYS_LOAD_REMOTE_IMAGES);
+            }
+
+            yield store.account.contact_store.update_contacts(
+                Geary.Collection.single(this.contact), cancellable
             );
         }
 
diff --git a/src/client/application/application-controller.vala 
b/src/client/application/application-controller.vala
index cc58b48c..205e3fec 100644
--- a/src/client/application/application-controller.vala
+++ b/src/client/application/application-controller.vala
@@ -616,20 +616,16 @@ public class Application.Controller : Geary.BaseObject {
         );
         account.report_problem.connect(on_report_problem);
         connect_account_async.begin(account, cancellable_open_account);
-
-        ContactListStore list_store = this.contact_list_store_cache.create(account.get_contact_store());
-        account.contacts_loaded.connect(list_store.set_sort_function);
     }
 
     private async void close_account(Geary.AccountInformation config) {
         AccountContext? context = this.accounts.get(config);
         if (context != null) {
             Geary.Account account = context.account;
-            Geary.ContactStore contact_store = account.get_contact_store();
+            Geary.ContactStore contact_store = account.contact_store;
             ContactListStore list_store =
                 this.contact_list_store_cache.get(contact_store);
 
-            account.contacts_loaded.disconnect(list_store.set_sort_function);
             this.contact_list_store_cache.unset(contact_store);
 
             if (this.current_account == account) {
diff --git a/src/client/composer/composer-widget.vala b/src/client/composer/composer-widget.vala
index 0a340a94..5f1823cd 100644
--- a/src/client/composer/composer-widget.vala
+++ b/src/client/composer/composer-widget.vala
@@ -240,8 +240,6 @@ public class ComposerWidget : Gtk.EventBox, Geary.BaseInterface {
 
     public Configuration config { get; set; }
 
-    private ContactListStore? contact_list_store = null;
-
     private string body_html = "";
 
     [GtkChild]
@@ -380,8 +378,6 @@ public class ComposerWidget : Gtk.EventBox, Geary.BaseInterface {
     // Is the composer closing (e.g. saving a draft or sending)?
     private bool is_closing = false;
 
-    private ContactListStoreCache contact_list_store_cache;
-
     private ComposerContainer container {
         get { return (ComposerContainer) parent; }
     }
@@ -406,7 +402,6 @@ public class ComposerWidget : Gtk.EventBox, Geary.BaseInterface {
                           Configuration config) {
         base_ref();
         this.account = account;
-        this.contact_list_store_cache = contact_list_store_cache;
         this.config = config;
         this.compose_type = compose_type;
         if (this.compose_type == ComposeType.NEW_MESSAGE)
@@ -653,22 +648,7 @@ public class ComposerWidget : Gtk.EventBox, Geary.BaseInterface {
      * Loads and sets contact auto-complete data for the current account.
      */
     private void load_entry_completions() {
-        Geary.ContactStore contacts = this.account.get_contact_store();
-        if (this.contact_list_store == null ||
-            this.contact_list_store.contact_store != contacts) {
-            ContactListStore? store = this.contact_list_store_cache.get(contacts);
-
-            if (store == null) {
-                error("Error loading contact_list_store from cache");
-            } else {
-                this.contact_list_store = store;
-
-                this.to_entry.completion = new ContactEntryCompletion(store);
-                this.cc_entry.completion = new ContactEntryCompletion(store);
-                this.bcc_entry.completion = new ContactEntryCompletion(store);
-                this.reply_to_entry.completion = new ContactEntryCompletion(store);
-            }
-        }
+        Geary.ContactStore contacts = this.account.contact_store;
     }
 
     /**
diff --git a/src/client/composer/contact-list-store.vala b/src/client/composer/contact-list-store.vala
index 3af3cf11..d1d83a2e 100644
--- a/src/client/composer/contact-list-store.vala
+++ b/src/client/composer/contact-list-store.vala
@@ -70,29 +70,29 @@ public class ContactListStore : Gtk.ListStore, Geary.BaseInterface {
         base_ref();
         set_column_types(Column.get_types());
         this.contact_store = contact_store;
-        contact_store.contacts_added.connect(on_contacts_added);
-        contact_store.contacts_updated.connect(on_contacts_updated);
+        //contact_store.contacts_added.connect(on_contacts_added);
+        //contact_store.contacts_updated.connect(on_contacts_updated);
     }
 
     ~ContactListStore() {
         base_unref();
-        this.contact_store.contacts_added.disconnect(on_contacts_added);
-        this.contact_store.contacts_updated.disconnect(on_contacts_updated);
+        //this.contact_store.contacts_added.disconnect(on_contacts_added);
+        //this.contact_store.contacts_updated.disconnect(on_contacts_updated);
     }
 
     /**
      * Loads contacts from the model's contact store.
      */
     public async void load() {
-        uint count = 0;
-        foreach (Geary.Contact contact in this.contact_store.contacts) {
-            add_contact(contact);
-            count++;
-            if (count % LOAD_BATCH_SIZE == 0) {
-                Idle.add(load.callback);
-                yield;
-            }
-        }
+        // uint count = 0;
+        // foreach (Geary.Contact contact in this.contact_store.contacts) {
+        //     add_contact(contact);
+        //     count++;
+        //     if (count % LOAD_BATCH_SIZE == 0) {
+        //         Idle.add(load.callback);
+        //         yield;
+        //     }
+        // }
     }
 
     public void set_sort_function() {
diff --git a/src/engine/api/geary-account.vala b/src/engine/api/geary-account.vala
index 41e74cfa..b0edd857 100644
--- a/src/engine/api/geary-account.vala
+++ b/src/engine/api/geary-account.vala
@@ -126,6 +126,11 @@ public abstract class Geary.Account : BaseObject {
      */
     public ClientService outgoing { get; private set; }
 
+    /**
+     * The contact information store for this account.
+     */
+    public Geary.ContactStore contact_store { get; protected set; }
+
     public Geary.ProgressMonitor search_upgrade_monitor { get; protected set; }
     public Geary.ProgressMonitor db_upgrade_monitor { get; protected set; }
     public Geary.ProgressMonitor db_vacuum_monitor { get; protected set; }
@@ -151,8 +156,6 @@ public abstract class Geary.Account : BaseObject {
      */
     public signal void report_problem(Geary.ProblemReport problem);
 
-    public signal void contacts_loaded();
-
     /**
      * Fired when folders become available or unavailable in the account.
      *
@@ -384,11 +387,6 @@ public abstract class Geary.Account : BaseObject {
      */
     public abstract Gee.Collection<Geary.Folder> list_folders() throws Error;
 
-    /**
-     * Gets a perpetually update-to-date collection of autocompletion contacts.
-     */
-    public abstract Geary.ContactStore get_contact_store();
-
     /**
      * Returns the folder representing the given special folder type.  If no such folder exists,
      * null is returned.
diff --git a/src/engine/api/geary-contact-store.vala b/src/engine/api/geary-contact-store.vala
index f7c522f1..43266274 100644
--- a/src/engine/api/geary-contact-store.vala
+++ b/src/engine/api/geary-contact-store.vala
@@ -1,50 +1,28 @@
-/* Copyright 2016 Software Freedom Conservancy Inc.
+/*
+ * Copyright 2016 Software Freedom Conservancy Inc.
+ * Copyright 2019 Michael Gratton <mike vee net>
  *
  * This software is licensed under the GNU Lesser General Public License
- * (version 2.1 or later).  See the COPYING file in this distribution.
+ * (version 2.1 or later). See the COPYING file in this distribution.
  */
 
-public abstract class Geary.ContactStore : BaseObject {
-    public Gee.Collection<Contact> contacts {
-        owned get { return contact_map.values; }
-    }
-
-    private Gee.Map<string, Contact> contact_map;
-
-    public signal void contacts_added(Gee.Collection<Contact> contacts);
-
-    public signal void contacts_updated(Gee.Collection<Contact> contacts);
-
-    protected ContactStore() {
-        contact_map = new Gee.HashMap<string, Contact>();
-    }
-
-    public void update_contacts(Gee.Collection<Contact> new_contacts) {
-        Gee.LinkedList<Contact> added = new Gee.LinkedList<Contact>();
-        Gee.LinkedList<Contact> updated = new Gee.LinkedList<Contact>();
-
-        foreach (Contact contact in new_contacts) {
-            Contact? old_contact = contact_map[contact.normalized_email];
-            if (old_contact == null) {
-                contact_map[contact.normalized_email] = contact;
-                added.add(contact);
-            } else if (old_contact.highest_importance < contact.highest_importance) {
-                old_contact.highest_importance = contact.highest_importance;
-                updated.add(contact);
-            }
-        }
-
-        if (!added.is_empty)
-            contacts_added(added);
+/**
+ * Interface for objects that provide contact information storage.
+ *
+ * Implementations of this class will typically be backed by a
+ * database. As such, to avoid IO overhead, batch calls together using
+ * collections of contacts wherever possible.
+ */
+public interface Geary.ContactStore : GLib.Object {
 
-        if (!updated.is_empty)
-            contacts_updated(updated);
-    }
+    /** Returns the contact matching the given email address, if any */
+    public abstract async Contact? get_by_rfc822(Geary.RFC822.MailboxAddress address,
+                                                 GLib.Cancellable? cancellable)
+        throws GLib.Error;
 
-    public abstract async void mark_contacts_async(Gee.Collection<Contact> contacts, ContactFlags? to_add,
-        ContactFlags? to_remove) throws Error;
+    /** Updates (or adds) a set of contacts in the underlying store */
+    public abstract async void update_contacts(Gee.Collection<Contact> updated,
+                                               GLib.Cancellable? cancellable)
+        throws GLib.Error;
 
-    public Contact? get_by_rfc822(Geary.RFC822.MailboxAddress address) {
-        return contact_map[address.address.normalize().casefold()];
-    }
 }
diff --git a/src/engine/common/common-contact-store-impl.vala 
b/src/engine/common/common-contact-store-impl.vala
new file mode 100644
index 00000000..e060bbc0
--- /dev/null
+++ b/src/engine/common/common-contact-store-impl.vala
@@ -0,0 +1,141 @@
+/*
+ * Copyright 2016 Software Freedom Conservancy Inc.
+ * Copyright 2019 Michael Gratton <mike vee net>
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later).  See the COPYING file in this distribution.
+ */
+
+/**
+ * An database-backed implementation of Geary.Contacts
+ */
+internal class Geary.ContactStoreImpl : BaseObject, Geary.ContactStore {
+
+
+    // Insert or update a contact in the ContactTable. If contact
+    // already exists, flags are merged and the importance is updated
+    // to the highest importance seen.
+    //
+    // Internal and static since it is used by ImapDB.Database during
+    // upgrades
+    internal static void do_update_contact(Db.Connection cx,
+                                           Contact contact,
+                                           GLib.Cancellable? cancellable)
+        throws GLib.Error {
+        Contact? existing = do_fetch_contact(
+            cx, contact.email, cancellable
+        );
+
+        if (contact == null) {
+            // Not found, so just insert it
+            Db.Statement stmt = cx.prepare(
+                "INSERT INTO ContactTable(normalized_email, email, real_name, flags, highest_importance) "
+                + "VALUES(?, ?, ?, ?, ?)");
+            stmt.bind_string(0, contact.normalized_email);
+            stmt.bind_string(1, contact.email);
+            stmt.bind_string(2, contact.real_name);
+            stmt.bind_string(3, (contact.contact_flags != null) ? contact.contact_flags.serialize() : null);
+            stmt.bind_int(4, contact.highest_importance);
+
+            stmt.exec(cancellable);
+        } else {
+            // Update existing contact
+
+            // Merge two flags sets together
+            ContactFlags? merged_flags = contact.contact_flags;
+            if (existing.contact_flags != null) {
+                if (merged_flags != null) {
+                    merged_flags.add_all(existing.contact_flags);
+                } else {
+                    merged_flags = existing.contact_flags;
+                }
+            }
+
+            // update remaining fields, careful not to overwrite
+            // non-null real_name with null (but using latest
+            // real_name if supplied) ... email is not updated (it's
+            // how existing was keyed), normalized_email is inserted at
+            // the same time as email, leaving only real_name, flags,
+            // and highest_importance
+            Db.Statement stmt = cx.prepare(
+                "UPDATE ContactTable SET real_name=?, flags=?, highest_importance=? WHERE email=?");
+            stmt.bind_string(
+                0, !String.is_empty(contact.real_name) ? contact.real_name : existing.real_name
+            );
+            stmt.bind_string(
+                1, (merged_flags != null) ? merged_flags.serialize() : null
+            );
+            stmt.bind_int(
+                2, int.max(contact.highest_importance, existing.highest_importance)
+            );
+            stmt.bind_string(
+                3, contact.email
+            );
+
+            stmt.exec(cancellable);
+        }
+    }
+
+    // Static since it is indirectly used by ImapDB.Database during
+    // upgrades
+    private static Contact? do_fetch_contact(Db.Connection cx,
+                                             string email,
+                                             GLib.Cancellable? cancellable)
+        throws GLib.Error {
+        Db.Statement stmt = cx.prepare(
+            "SELECT real_name, highest_importance, normalized_email, flags FROM ContactTable "
+            + "WHERE email=?");
+        stmt.bind_string(0, email);
+
+        Db.Result result = stmt.exec(cancellable);
+        if (result.finished)
+            return null;
+
+        return new Contact(
+            email,
+            result.string_at(0),
+            result.int_at(1),
+            result.string_at(2),
+            ContactFlags.deserialize(result.string_at(3))
+        );
+    }
+
+
+    private Geary.Db.Database backing;
+
+
+    internal ContactStoreImpl(Geary.Db.Database backing) {
+        base_ref();
+        this.backing = backing;
+    }
+
+    /** Returns the contact matching the given email address, if any */
+    public async Contact? get_by_rfc822(Geary.RFC822.MailboxAddress address,
+                                        GLib.Cancellable? cancellable)
+        throws GLib.Error {
+        Contact? contact = null;
+        yield this.backing.exec_transaction_async(
+            Db.TransactionType.RO,
+            (cx, cancellable) => {
+                contact = do_fetch_contact(cx, address.mailbox, cancellable);
+                return Db.TransactionOutcome.COMMIT;
+            },
+            cancellable);
+        return contact;
+    }
+
+    public async void update_contacts(Gee.Collection<Contact> updated,
+                                      GLib.Cancellable? cancellable)
+        throws GLib.Error {
+        yield this.backing.exec_transaction_async(
+            Db.TransactionType.RW,
+            (cx, cancellable) => {
+                foreach (Contact contact in updated) {
+                    do_update_contact(cx, contact, cancellable);
+                }
+                return Db.TransactionOutcome.COMMIT;
+            },
+            cancellable);
+    }
+
+}
diff --git a/src/engine/imap-db/imap-db-account.vala b/src/engine/imap-db/imap-db-account.vala
index c57fc837..87744343 100644
--- a/src/engine/imap-db/imap-db-account.vala
+++ b/src/engine/imap-db/imap-db-account.vala
@@ -74,7 +74,6 @@ private class Geary.ImapDB.Account : BaseObject {
     private static Gee.HashMap<string, string> search_op_is_values =
         new Gee.HashMap<string, string>();
 
-    public signal void contacts_loaded();
 
     /**
      * The root path for all remote IMAP folders.
@@ -90,7 +89,7 @@ private class Geary.ImapDB.Account : BaseObject {
     }
 
     // Only available when the Account is opened
-    public ImapEngine.ContactStore contact_store { get; private set; }
+    public ContactStore contact_store { get; private set; }
     public IntervalProgressMonitor search_index_monitor { get; private set;
         default = new IntervalProgressMonitor(ProgressType.SEARCH_INDEX, 0, 0); }
     public SimpleProgressMonitor upgrade_monitor { get; private set; default = new SimpleProgressMonitor(
@@ -257,7 +256,6 @@ private class Geary.ImapDB.Account : BaseObject {
 
     public Account(AccountInformation config) {
         this.account_information = config;
-        this.contact_store = new ImapEngine.ContactStore(this);
         this.name = config.id + ":db";
     }
 
@@ -354,7 +352,7 @@ private class Geary.ImapDB.Account : BaseObject {
             return false;
         });
 
-        initialize_contacts(cancellable);
+        this.contact_store = new ContactStoreImpl(db);
     }
 
     public async void close_async(Cancellable? cancellable) throws Error {
@@ -458,40 +456,6 @@ private class Geary.ImapDB.Account : BaseObject {
         }, cancellable);
     }
 
-    private void initialize_contacts(Cancellable? cancellable = null) throws Error {
-        check_open();
-
-        Gee.Collection<Contact> contacts = new Gee.LinkedList<Contact>();
-        Db.TransactionOutcome outcome = db.exec_transaction(Db.TransactionType.RO,
-            (context) => {
-            Db.Statement statement = context.prepare(
-                "SELECT email, real_name, highest_importance, normalized_email, flags " +
-                "FROM ContactTable");
-
-            Db.Result result = statement.exec(cancellable);
-            while (!result.finished) {
-                try {
-                    Contact contact = new Contact(result.nonnull_string_at(0), result.string_at(1),
-                        result.int_at(2), result.string_at(3), 
ContactFlags.deserialize(result.string_at(4)));
-                    contacts.add(contact);
-                } catch (Geary.DatabaseError err) {
-                    // We don't want to abandon loading all contacts just because there was a
-                    // problem with one.
-                    debug("Problem loading contact: %s", err.message);
-                }
-
-                result.next();
-            }
-
-            return Db.TransactionOutcome.DONE;
-        }, cancellable);
-
-        if (outcome == Db.TransactionOutcome.DONE) {
-            contact_store.update_contacts(contacts);
-            contacts_loaded();
-        }
-    }
-
     /**
      * Lists all children of a given folder.
      *
@@ -1392,21 +1356,6 @@ private class Geary.ImapDB.Account : BaseObject {
         return email;
     }
 
-    public async void update_contact_flags_async(Geary.Contact contact, Cancellable? cancellable)
-        throws Error{
-        check_open();
-
-        yield db.exec_transaction_async(Db.TransactionType.RW, (cx, cancellable) => {
-            Db.Statement update_stmt =
-                cx.prepare("UPDATE ContactTable SET flags=? WHERE email=?");
-            update_stmt.bind_string(0, contact.contact_flags.serialize());
-            update_stmt.bind_string(1, contact.email);
-            update_stmt.exec(cancellable);
-
-            return Db.TransactionOutcome.COMMIT;
-        }, cancellable);
-    }
-
     /**
      * Return a map of each passed-in email identifier to the set of folders
      * that contain it.  If an email id doesn't appear in the resulting map,
diff --git a/src/engine/imap-db/imap-db-database.vala b/src/engine/imap-db/imap-db-database.vala
index b963320f..02672f52 100644
--- a/src/engine/imap-db/imap-db-database.vala
+++ b/src/engine/imap-db/imap-db-database.vala
@@ -190,7 +190,7 @@ private class Geary.ImapDB.Database : Geary.Db.VersionedDatabase {
                     MessageAddresses message_addresses =
                     new MessageAddresses.from_result(account_owner_email, result);
                     foreach (Contact contact in message_addresses.contacts) {
-                        do_update_contact(cx, contact, null);
+                        ContactStoreImpl.do_update_contact(cx, contact, null);
                     }
                     result.next();
                 }
diff --git a/src/engine/imap-db/imap-db-folder.vala b/src/engine/imap-db/imap-db-folder.vala
index 8737af70..a28147d6 100644
--- a/src/engine/imap-db/imap-db-folder.vala
+++ b/src/engine/imap-db/imap-db-folder.vala
@@ -321,8 +321,11 @@ private class Geary.ImapDB.Folder : BaseObject, Geary.ReferenceSemantics {
                 return Db.TransactionOutcome.COMMIT;
             }, cancellable);
 
-            if (updated_contacts.size > 0)
-                contact_store.update_contacts(updated_contacts);
+            if (updated_contacts.size > 0) {
+                yield this.contact_store.update_contacts(
+                    updated_contacts, cancellable
+                );
+            }
 
             if (update_totals) {
                 // Update the email_unread properties.
diff --git a/src/engine/imap-engine/imap-engine-generic-account.vala 
b/src/engine/imap-engine/imap-engine-generic-account.vala
index 8386419a..2f74b8b9 100644
--- a/src/engine/imap-engine/imap-engine-generic-account.vala
+++ b/src/engine/imap-engine/imap-engine-generic-account.vala
@@ -83,7 +83,7 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.Account {
         base(config, imap, smtp);
 
         this.local = local;
-        this.local.contacts_loaded.connect(() => { contacts_loaded(); });
+        this.contact_store = local.contact_store;
 
         imap.min_pool_size = IMAP_MIN_POOL_SIZE;
         imap.notify["current-status"].connect(
@@ -485,10 +485,6 @@ private abstract class Geary.ImapEngine.GenericAccount : Geary.Account {
         return all_folders;
     }
 
-    public override Geary.ContactStore get_contact_store() {
-        return local.contact_store;
-    }
-
     public override async Geary.Folder get_required_special_folder_async(Geary.SpecialFolderType special,
         Cancellable? cancellable) throws Error {
         if (!(special in get_supported_special_folders())) {
diff --git a/src/engine/meson.build b/src/engine/meson.build
index a9f962c8..d85f0c63 100644
--- a/src/engine/meson.build
+++ b/src/engine/meson.build
@@ -68,6 +68,7 @@ geary_engine_vala_sources = files(
   'app/email-store/app-list-operation.vala',
   'app/email-store/app-mark-operation.vala',
 
+  'common/common-contact-store-impl.vala',
   'common/common-message-data.vala',
 
   'db/db.vala',
@@ -172,7 +173,6 @@ geary_engine_vala_sources = files(
 
   'imap-db/imap-db-account.vala',
   'imap-db/imap-db-attachment.vala',
-  'imap-db/imap-db-contact.vala',
   'imap-db/imap-db-database.vala',
   'imap-db/imap-db-email-identifier.vala',
   'imap-db/imap-db-folder.vala',
@@ -189,7 +189,6 @@ geary_engine_vala_sources = files(
   'imap-engine/imap-engine-account-operation.vala',
   'imap-engine/imap-engine-account-processor.vala',
   'imap-engine/imap-engine-account-synchronizer.vala',
-  'imap-engine/imap-engine-contact-store.vala',
   'imap-engine/imap-engine-email-prefetcher.vala',
   'imap-engine/imap-engine-generic-account.vala',
   'imap-engine/imap-engine-generic-folder.vala',
diff --git a/test/engine/api/geary-account-mock.vala b/test/engine/api/geary-account-mock.vala
index ce5304e3..7fb35b34 100644
--- a/test/engine/api/geary-account-mock.vala
+++ b/test/engine/api/geary-account-mock.vala
@@ -16,18 +16,24 @@ public class Geary.MockAccount : Account, MockObject {
 
     }
 
-    public class MockContactStore : ContactStore {
+    public class MockContactStore : GLib.Object, ContactStore  {
 
         internal MockContactStore() {
 
         }
 
-        public override async void
-            mark_contacts_async(Gee.Collection<Contact> contacts,
-                                ContactFlags? to_add,
-                                ContactFlags? to_remove) throws Error {
-                throw new EngineError.UNSUPPORTED("Mock method");
-            }
+        public async Contact? get_by_rfc822(Geary.RFC822.MailboxAddress address,
+                                            GLib.Cancellable? cancellable)
+        throws GLib.Error {
+            throw new EngineError.UNSUPPORTED("Mock method");
+        }
+
+        public async void update_contacts(Gee.Collection<Contact> contacts,
+                                          GLib.Cancellable? cancellable)
+            throws GLib.Error {
+            throw new EngineError.UNSUPPORTED("Mock method");
+        }
+
     }
 
 
@@ -171,10 +177,6 @@ public class Geary.MockAccount : Account, MockObject {
         );
     }
 
-    public override Geary.ContactStore get_contact_store() {
-        return new MockContactStore();
-    }
-
     public override Folder? get_special_folder(SpecialFolderType special)
         throws Error {
         return object_call<Folder?>(


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