Refactor content provider code from ContactsDict

Break contacts binary dictionary into two parts
- one that talks to contacts content provider and maintains
  local state. Includes a manager class and a content observer
- other one that just manages the dict code.

Change-Id: Ie8f89ac9ce174c803ff3168ee0bee5cbe7721d5b
diff --git a/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java
index 66a21ec..ba0f9b8 100644
--- a/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java
@@ -16,23 +16,16 @@
 
 package com.android.inputmethod.latin;
 
-import android.content.ContentResolver;
 import android.content.Context;
-import android.database.ContentObserver;
-import android.database.Cursor;
-import android.database.sqlite.SQLiteException;
 import android.net.Uri;
-import android.os.SystemClock;
-import android.provider.BaseColumns;
 import android.provider.ContactsContract;
 import android.provider.ContactsContract.Contacts;
 import android.util.Log;
 
 import com.android.inputmethod.annotations.ExternallyReferenced;
-import com.android.inputmethod.latin.common.Constants;
+import com.android.inputmethod.latin.ContactsManager.ContactsChangedListener;
 import com.android.inputmethod.latin.common.StringUtils;
 import com.android.inputmethod.latin.personalization.AccountUtils;
-import com.android.inputmethod.latin.utils.ExecutorUtils;
 
 import java.io.File;
 import java.util.ArrayList;
@@ -41,11 +34,8 @@
 
 import javax.annotation.Nullable;
 
-public class ContactsBinaryDictionary extends ExpandableBinaryDictionary {
-
-    private static final String[] PROJECTION = {BaseColumns._ID, Contacts.DISPLAY_NAME};
-    private static final String[] PROJECTION_ID_ONLY = {BaseColumns._ID};
-
+public class ContactsBinaryDictionary extends ExpandableBinaryDictionary
+        implements ContactsChangedListener {
     private static final String TAG = ContactsBinaryDictionary.class.getSimpleName();
     private static final String NAME = "contacts";
 
@@ -53,35 +43,18 @@
     private static final boolean DEBUG_DUMP = false;
 
     /**
-     * Frequency for contacts information into the dictionary
-     */
-    private static final int FREQUENCY_FOR_CONTACTS = 40;
-    private static final int FREQUENCY_FOR_CONTACTS_BIGRAM = 90;
-
-    /** The maximum number of contacts that this dictionary supports. */
-    private static final int MAX_CONTACT_COUNT = 10000;
-
-    private static final int INDEX_NAME = 1;
-
-    /** The number of contacts in the most recent dictionary rebuild. */
-    private int mContactCountAtLastRebuild = 0;
-
-    /** The hash code of ArrayList of contacts names in the most recent dictionary rebuild. */
-    private int mHashCodeAtLastRebuild = 0;
-
-    private ContentObserver mObserver;
-
-    /**
      * Whether to use "firstname lastname" in bigram predictions.
      */
     private final boolean mUseFirstLastBigrams;
+    private final ContactsManager mContactsManager;
 
     protected ContactsBinaryDictionary(final Context context, final Locale locale,
             final File dictFile, final String name) {
         super(context, getDictName(name, locale, dictFile), locale, Dictionary.TYPE_CONTACTS,
                 dictFile);
-        mUseFirstLastBigrams = useFirstLastBigramsForLocale(locale);
-        registerObserver(context);
+        mUseFirstLastBigrams = ContactsDictionaryUtils.useFirstLastBigramsForLocale(locale);
+        mContactsManager = new ContactsManager(context);
+        mContactsManager.registerForUpdates(this /* listener */);
         reloadDictionaryIfRequired();
     }
 
@@ -92,34 +65,17 @@
         return new ContactsBinaryDictionary(context, locale, dictFile, dictNamePrefix + NAME);
     }
 
-    private synchronized void registerObserver(final Context context) {
-        if (mObserver != null) return;
-        ContentResolver cres = context.getContentResolver();
-        cres.registerContentObserver(Contacts.CONTENT_URI, true, mObserver =
-                new ContentObserver(null) {
-                    @Override
-                    public void onChange(boolean self) {
-                        ExecutorUtils.getExecutor("Check Contacts").execute(new Runnable() {
-                            @Override
-                            public void run() {
-                                if (haveContentsChanged()) {
-                                    setNeedsToRecreate();
-                                }
-                            }
-                        });
-                    }
-                });
-    }
-
     @Override
     public synchronized void close() {
-        if (mObserver != null) {
-            mContext.getContentResolver().unregisterContentObserver(mObserver);
-            mObserver = null;
-        }
+        mContactsManager.close();
         super.close();
     }
 
+    /**
+     * Typically called whenever the dictionary is created for the first time or
+     * recreated when we think that there are updates to the dictionary.
+     * This is called asynchronously.
+     */
     @Override
     public void loadInitialContentsLocked() {
         loadDeviceAccountsEmailAddressesLocked();
@@ -128,6 +84,9 @@
         loadDictionaryForUriLocked(Contacts.CONTENT_URI);
     }
 
+    /**
+     * Loads device accounts to the dictionary.
+     */
     private void loadDeviceAccountsEmailAddressesLocked() {
         final List<String> accountVocabulary =
                 AccountUtils.getDeviceAccountsEmailAddresses(mContext);
@@ -139,80 +98,25 @@
                 Log.d(TAG, "loadAccountVocabulary: " + word);
             }
             runGCIfRequiredLocked(true /* mindsBlockByGC */);
-            addUnigramLocked(word, FREQUENCY_FOR_CONTACTS,
+            addUnigramLocked(word, ContactsDictionaryConstants.FREQUENCY_FOR_CONTACTS,
                     false /* isNotAWord */, false /* isPossiblyOffensive */,
                     BinaryDictionary.NOT_A_VALID_TIMESTAMP);
         }
     }
 
+    /**
+     * Loads data within content providers to the dictionary.
+     */
     private void loadDictionaryForUriLocked(final Uri uri) {
-        Cursor cursor = null;
-        try {
-            cursor = mContext.getContentResolver().query(uri, PROJECTION, null, null, null);
-            if (null == cursor) {
-                return;
-            }
-            if (cursor.moveToFirst()) {
-                mContactCountAtLastRebuild = getContactCount();
-                addWordsLocked(cursor);
-            }
-        } catch (final SQLiteException e) {
-            Log.e(TAG, "SQLiteException in the remote Contacts process.", e);
-        } catch (final IllegalStateException e) {
-            Log.e(TAG, "Contacts DB is having problems", e);
-        } finally {
-            if (null != cursor) {
-                cursor.close();
-            }
+        final ArrayList<String> validNames = mContactsManager.getValidNames(uri);
+        for (final String name : validNames) {
+            addNameLocked(name);
         }
-    }
-
-    private static boolean useFirstLastBigramsForLocale(final Locale locale) {
-        // TODO: Add firstname/lastname bigram rules for other languages.
-        if (locale != null && locale.getLanguage().equals(Locale.ENGLISH.getLanguage())) {
-            return true;
+        if (uri.equals(Contacts.CONTENT_URI)) {
+            // Since we were able to add content successfully, update the local
+            // state of the manager.
+            mContactsManager.updateLocalState(validNames);
         }
-        return false;
-    }
-
-    private void addWordsLocked(final Cursor cursor) {
-        int count = 0;
-        final ArrayList<String> names = new ArrayList<>();
-        while (!cursor.isAfterLast() && count < MAX_CONTACT_COUNT) {
-            String name = cursor.getString(INDEX_NAME);
-            if (isValidName(name)) {
-                names.add(name);
-                addNameLocked(name);
-                ++count;
-            } else {
-                if (DEBUG_DUMP) {
-                    Log.d(TAG, "Invalid name: " + name);
-                }
-            }
-            cursor.moveToNext();
-        }
-        mHashCodeAtLastRebuild = names.hashCode();
-    }
-
-    private int getContactCount() {
-        // TODO: consider switching to a rawQuery("select count(*)...") on the database if
-        // performance is a bottleneck.
-        Cursor cursor = null;
-        try {
-            cursor = mContext.getContentResolver().query(Contacts.CONTENT_URI, PROJECTION_ID_ONLY,
-                    null, null, null);
-            if (null == cursor) {
-                return 0;
-            }
-            return cursor.getCount();
-        } catch (final SQLiteException e) {
-            Log.e(TAG, "SQLiteException in the remote Contacts process.", e);
-        } finally {
-            if (null != cursor) {
-                cursor.close();
-            }
-        }
-        return 0;
     }
 
     /**
@@ -225,7 +129,7 @@
         // TODO: Better tokenization for non-Latin writing systems
         for (int i = 0; i < len; i++) {
             if (Character.isLetter(name.codePointAt(i))) {
-                int end = getWordEndPosition(name, len, i);
+                int end = ContactsDictionaryUtils.getWordEndPosition(name, len, i);
                 String word = name.substring(i, end);
                 if (DEBUG_DUMP) {
                     Log.d(TAG, "addName word = " + word);
@@ -239,12 +143,15 @@
                         Log.d(TAG, "addName " + name + ", " + word + ", "  + ngramContext);
                     }
                     runGCIfRequiredLocked(true /* mindsBlockByGC */);
-                    addUnigramLocked(word, FREQUENCY_FOR_CONTACTS, false /* isNotAWord */,
+                    addUnigramLocked(word,
+                            ContactsDictionaryConstants.FREQUENCY_FOR_CONTACTS, false /* isNotAWord */,
                             false /* isPossiblyOffensive */,
                             BinaryDictionary.NOT_A_VALID_TIMESTAMP);
-                    if (!ngramContext.isValid() && mUseFirstLastBigrams) {
+                    if (ngramContext.isValid() && mUseFirstLastBigrams) {
                         runGCIfRequiredLocked(true /* mindsBlockByGC */);
-                        addNgramEntryLocked(ngramContext, word, FREQUENCY_FOR_CONTACTS_BIGRAM,
+                        addNgramEntryLocked(ngramContext,
+                                word,
+                                ContactsDictionaryConstants.FREQUENCY_FOR_CONTACTS_BIGRAM,
                                 BinaryDictionary.NOT_A_VALID_TIMESTAMP);
                     }
                     ngramContext = ngramContext.getNextNgramContext(
@@ -254,75 +161,8 @@
         }
     }
 
-    /**
-     * Returns the index of the last letter in the word, starting from position startIndex.
-     */
-    private static int getWordEndPosition(final String string, final int len,
-            final int startIndex) {
-        int end;
-        int cp = 0;
-        for (end = startIndex + 1; end < len; end += Character.charCount(cp)) {
-            cp = string.codePointAt(end);
-            if (!(cp == Constants.CODE_DASH || cp == Constants.CODE_SINGLE_QUOTE
-                    || Character.isLetter(cp))) {
-                break;
-            }
-        }
-        return end;
-    }
-
-    boolean haveContentsChanged() {
-        final long startTime = SystemClock.uptimeMillis();
-        final int contactCount = getContactCount();
-        if (contactCount > MAX_CONTACT_COUNT) {
-            // If there are too many contacts then return false. In this rare case it is impossible
-            // to include all of them anyways and the cost of rebuilding the dictionary is too high.
-            // TODO: Sort and check only the MAX_CONTACT_COUNT most recent contacts?
-            return false;
-        }
-        if (contactCount != mContactCountAtLastRebuild) {
-            if (DEBUG) {
-                Log.d(TAG, "Contact count changed: " + mContactCountAtLastRebuild + " to "
-                        + contactCount);
-            }
-            return true;
-        }
-        // Check all contacts since it's not possible to find out which names have changed.
-        // This is needed because it's possible to receive extraneous onChange events even when no
-        // name has changed.
-        final Cursor cursor = mContext.getContentResolver().query(Contacts.CONTENT_URI, PROJECTION,
-                null, null, null);
-        if (null == cursor) {
-            return false;
-        }
-        final ArrayList<String> names = new ArrayList<>();
-        try {
-            if (cursor.moveToFirst()) {
-                while (!cursor.isAfterLast()) {
-                    String name = cursor.getString(INDEX_NAME);
-                    if (isValidName(name)) {
-                        names.add(name);
-                    }
-                    cursor.moveToNext();
-                }
-            }
-            if (names.hashCode() != mHashCodeAtLastRebuild) {
-                return true;
-            }
-        } finally {
-            cursor.close();
-        }
-        if (DEBUG) {
-            Log.d(TAG, "No contacts changed. (runtime = " + (SystemClock.uptimeMillis() - startTime)
-                    + " ms)");
-        }
-        return false;
-    }
-
-    private static boolean isValidName(final String name) {
-        if (name != null && -1 == name.indexOf(Constants.CODE_COMMERCIAL_AT)) {
-            return true;
-        }
-        return false;
+    @Override
+    public void onContactsChange() {
+        setNeedsToRecreate();
     }
 }
diff --git a/java/src/com/android/inputmethod/latin/ContactsContentObserver.java b/java/src/com/android/inputmethod/latin/ContactsContentObserver.java
new file mode 100644
index 0000000..019d17d
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/ContactsContentObserver.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.inputmethod.latin;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.ContentObserver;
+import android.os.SystemClock;
+import android.provider.ContactsContract.Contacts;
+import android.util.Log;
+
+import com.android.inputmethod.annotations.UsedForTesting;
+import com.android.inputmethod.latin.ContactsManager.ContactsChangedListener;
+import com.android.inputmethod.latin.utils.ExecutorUtils;
+
+import java.util.ArrayList;
+import java.util.concurrent.ExecutorService;
+
+/**
+ * A content observer that listens to updates to content provider {@link Contacts.CONTENT_URI}.
+ */
+// TODO:add test
+public class ContactsContentObserver {
+    private static final String TAG = ContactsContentObserver.class.getSimpleName();
+    private static final boolean DEBUG = false;
+
+    private ContentObserver mObserver;
+
+    private final Context mContext;
+    private final ContactsManager mManager;
+
+    public ContactsContentObserver(final ContactsManager manager, final Context context) {
+        mManager = manager;
+        mContext = context;
+    }
+
+    public void registerObserver(final ContactsChangedListener listener) {
+        if (DEBUG) {
+            Log.d(TAG, "Registered Contacts Content Observer");
+        }
+        mObserver = new ContentObserver(null /* handler */) {
+            @Override
+            public void onChange(boolean self) {
+                getBgExecutor().execute(new Runnable() {
+                    @Override
+                    public void run() {
+                        if (haveContentsChanged()) {
+                            if (DEBUG) {
+                                Log.d(TAG, "Contacts have changed; notifying listeners");
+                            }
+                            listener.onContactsChange();
+                        }
+                    }
+                });
+            }
+        };
+        final ContentResolver contentResolver = mContext.getContentResolver();
+        contentResolver.registerContentObserver(Contacts.CONTENT_URI, true, mObserver);
+    }
+
+    @UsedForTesting
+    private ExecutorService getBgExecutor() {
+        return ExecutorUtils.getExecutor("Check Contacts");
+    }
+
+    private boolean haveContentsChanged() {
+        final long startTime = SystemClock.uptimeMillis();
+        final int contactCount = mManager.getContactCount();
+        if (contactCount > ContactsDictionaryConstants.MAX_CONTACT_COUNT) {
+            // If there are too many contacts then return false. In this rare case it is impossible
+            // to include all of them anyways and the cost of rebuilding the dictionary is too high.
+            // TODO: Sort and check only the MAX_CONTACT_COUNT most recent contacts?
+            return false;
+        }
+        if (contactCount != mManager.getContactCountAtLastRebuild()) {
+            if (DEBUG) {
+                Log.d(TAG, "Contact count changed: " + mManager.getContactCountAtLastRebuild()
+                        + " to " + contactCount);
+            }
+            return true;
+        }
+        final ArrayList<String> names = mManager.getValidNames(Contacts.CONTENT_URI);
+        if (names.hashCode() != mManager.getHashCodeAtLastRebuild()) {
+            return true;
+        }
+        if (DEBUG) {
+            Log.d(TAG, "No contacts changed. (runtime = " + (SystemClock.uptimeMillis() - startTime)
+                    + " ms)");
+        }
+        return false;
+    }
+
+    public void unregister() {
+        mContext.getContentResolver().unregisterContentObserver(mObserver);
+    }
+}
diff --git a/java/src/com/android/inputmethod/latin/ContactsDictionaryConstants.java b/java/src/com/android/inputmethod/latin/ContactsDictionaryConstants.java
new file mode 100644
index 0000000..8d8faca
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/ContactsDictionaryConstants.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.inputmethod.latin;
+
+import android.provider.BaseColumns;
+import android.provider.ContactsContract.Contacts;
+
+/**
+ * Constants related to Contacts Content Provider.
+ */
+public class ContactsDictionaryConstants {
+    /**
+     * Projections for {@link Contacts.CONTENT_URI}
+     */
+    public static final String[] PROJECTION = { BaseColumns._ID, Contacts.DISPLAY_NAME };
+    public static final String[] PROJECTION_ID_ONLY = { BaseColumns._ID };
+
+    /**
+     * Frequency for contacts information into the dictionary
+     */
+    public static final int FREQUENCY_FOR_CONTACTS = 40;
+    public static final int FREQUENCY_FOR_CONTACTS_BIGRAM = 90;
+
+    /**
+     *  The maximum number of contacts that this dictionary supports.
+     */
+    public static final int MAX_CONTACT_COUNT = 10000;
+
+    /**
+     * Index of the column for 'name' in content providers:
+     * Contacts & ContactsContract.Profile.
+     */
+    public static final int NAME_INDEX = 1;
+}
diff --git a/java/src/com/android/inputmethod/latin/ContactsDictionaryUtils.java b/java/src/com/android/inputmethod/latin/ContactsDictionaryUtils.java
new file mode 100644
index 0000000..b773884
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/ContactsDictionaryUtils.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.inputmethod.latin;
+
+import com.android.inputmethod.latin.common.Constants;
+
+import java.util.Locale;
+
+/**
+ * Utility methods related contacts dictionary.
+ */
+public class ContactsDictionaryUtils {
+
+    /**
+     * Returns the index of the last letter in the word, starting from position startIndex.
+     */
+    public static int getWordEndPosition(final String string, final int len,
+            final int startIndex) {
+        int end;
+        int cp = 0;
+        for (end = startIndex + 1; end < len; end += Character.charCount(cp)) {
+            cp = string.codePointAt(end);
+            if (cp != Constants.CODE_DASH && cp != Constants.CODE_SINGLE_QUOTE
+                   && !Character.isLetter(cp)) {
+                break;
+            }
+        }
+        return end;
+    }
+
+    /**
+     * Returns true if the locale supports using first name and last name as bigrams.
+     */
+    public static boolean useFirstLastBigramsForLocale(final Locale locale) {
+        // TODO: Add firstname/lastname bigram rules for other languages.
+        if (locale != null && locale.getLanguage().equals(Locale.ENGLISH.getLanguage())) {
+            return true;
+        }
+        return false;
+    }
+}
diff --git a/java/src/com/android/inputmethod/latin/ContactsManager.java b/java/src/com/android/inputmethod/latin/ContactsManager.java
new file mode 100644
index 0000000..dc5abd9
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/ContactsManager.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.inputmethod.latin;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteException;
+import android.net.Uri;
+import android.provider.ContactsContract.Contacts;
+import android.util.Log;
+
+import com.android.inputmethod.latin.common.Constants;
+
+import java.util.ArrayList;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Manages all interactions with Contacts DB.
+ *
+ * The manager provides an API for listening to meaning full updates by keeping a
+ * measure of the current state of the content provider.
+ */
+// TODO:Add test
+public class ContactsManager {
+    private static final String TAG = ContactsManager.class.getSimpleName();
+    private static final boolean DEBUG = false;
+
+    /**
+     * Interface to implement for classes interested in getting notified for updates
+     * to Contacts content provider.
+     */
+    public static interface ContactsChangedListener {
+        public void onContactsChange();
+    }
+
+    /**
+     * The number of contacts observed in the most recent instance of
+     * contacts content provider.
+     */
+    private AtomicInteger mContactCountAtLastRebuild = new AtomicInteger(0);
+
+    /**
+     * The hash code of list of valid contacts names in the most recent dictionary
+     * rebuild.
+     */
+    private AtomicInteger mHashCodeAtLastRebuild = new AtomicInteger(0);
+
+    private final Context mContext;
+    private final ContactsContentObserver mObserver;
+
+    public ContactsManager(final Context context) {
+        mContext = context;
+        mObserver = new ContactsContentObserver(this /* ContactsManager */, context);
+    }
+
+    // TODO: This was synchronized in previous version. Why?
+    public void registerForUpdates(final ContactsChangedListener listener) {
+        mObserver.registerObserver(listener);
+    }
+
+    public int getContactCountAtLastRebuild() {
+        return mContactCountAtLastRebuild.get();
+    }
+
+    public int getHashCodeAtLastRebuild() {
+        return mHashCodeAtLastRebuild.get();
+    }
+
+    /**
+     * Returns all the valid names in the Contacts DB. Callers should also
+     * call {@link #updateLocalState(ArrayList)} after they are done with result
+     * so that the manager can cache local state for determining updates.
+     */
+    public ArrayList<String> getValidNames(final Uri uri) {
+        final ArrayList<String> names = new ArrayList<>();
+        // Check all contacts since it's not possible to find out which names have changed.
+        // This is needed because it's possible to receive extraneous onChange events even when no
+        // name has changed.
+        final Cursor cursor = mContext.getContentResolver().query(uri,
+                ContactsDictionaryConstants.PROJECTION, null, null, null);
+        if (cursor != null) {
+            try {
+                if (cursor.moveToFirst()) {
+                    while (!cursor.isAfterLast()) {
+                        final String name = cursor.getString(
+                                ContactsDictionaryConstants.NAME_INDEX);
+                        if (isValidName(name)) {
+                            names.add(name);
+                        }
+                        cursor.moveToNext();
+                    }
+                }
+            } finally {
+                cursor.close();
+            }
+        }
+        return names;
+    }
+
+    /**
+     * Returns the number of contacts in contacts content provider.
+     */
+    public int getContactCount() {
+        // TODO: consider switching to a rawQuery("select count(*)...") on the database if
+        // performance is a bottleneck.
+        Cursor cursor = null;
+        try {
+            cursor = mContext.getContentResolver().query(Contacts.CONTENT_URI,
+                    ContactsDictionaryConstants.PROJECTION_ID_ONLY, null, null, null);
+            if (null == cursor) {
+                return 0;
+            }
+            return cursor.getCount();
+        } catch (final SQLiteException e) {
+            Log.e(TAG, "SQLiteException in the remote Contacts process.", e);
+        } finally {
+            if (null != cursor) {
+                cursor.close();
+            }
+        }
+        return 0;
+    }
+
+    private static boolean isValidName(final String name) {
+        if (name != null && -1 == name.indexOf(Constants.CODE_COMMERCIAL_AT)) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Updates the local state of the manager. This should be called when the callers
+     * are done with all the updates of the content provider successfully.
+     */
+    public void updateLocalState(final ArrayList<String> names) {
+        mContactCountAtLastRebuild.set(getContactCount());
+        mHashCodeAtLastRebuild.set(names.hashCode());
+    }
+
+    /**
+     * Performs any necessary cleanup.
+     */
+    public void close() {
+        mObserver.unregister();
+    }
+}
diff --git a/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java b/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java
index a451b67..c22dc28 100644
--- a/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java
+++ b/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java
@@ -177,4 +177,6 @@
             NgramContext ngramContext,
             int increment,
             int timeStampInSeconds);
+
+    void clearLanguageModel(String filePath);
 }
diff --git a/java/src/com/android/inputmethod/latin/DictionaryFacilitatorImpl.java b/java/src/com/android/inputmethod/latin/DictionaryFacilitatorImpl.java
index 4ed9405..3d76751 100644
--- a/java/src/com/android/inputmethod/latin/DictionaryFacilitatorImpl.java
+++ b/java/src/com/android/inputmethod/latin/DictionaryFacilitatorImpl.java
@@ -804,4 +804,9 @@
             int timeStampInSeconds) {
         // Do nothing.
     }
+
+    @Override
+    public void clearLanguageModel(String filePath) {
+        // Do nothing.
+    }
 }
diff --git a/java/src/com/android/inputmethod/latin/utils/ExecutorUtils.java b/java/src/com/android/inputmethod/latin/utils/ExecutorUtils.java
index e77f6fd..50be160 100644
--- a/java/src/com/android/inputmethod/latin/utils/ExecutorUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/ExecutorUtils.java
@@ -21,13 +21,14 @@
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
 import java.util.concurrent.ThreadFactory;
 
 /**
  * Utilities to manage executors.
  */
 public class ExecutorUtils {
-    static final ConcurrentHashMap<String, ExecutorService> sExecutorMap =
+    static final ConcurrentHashMap<String, ScheduledExecutorService> sExecutorMap =
             new ConcurrentHashMap<>();
 
     private static class ThreadFactoryWithId implements ThreadFactory {
@@ -46,13 +47,14 @@
     /**
      * Gets the executor for the given id.
      */
-    public static ExecutorService getExecutor(final String id) {
-        ExecutorService executor = sExecutorMap.get(id);
+    public static ScheduledExecutorService getExecutor(final String id) {
+        ScheduledExecutorService executor = sExecutorMap.get(id);
         if (executor == null) {
             synchronized (sExecutorMap) {
                 executor = sExecutorMap.get(id);
                 if (executor == null) {
-                    executor = Executors.newSingleThreadExecutor(new ThreadFactoryWithId(id));
+                    executor = Executors.newSingleThreadScheduledExecutor(
+                            new ThreadFactoryWithId(id));
                     sExecutorMap.put(id, executor);
                 }
             }
@@ -66,7 +68,7 @@
     @UsedForTesting
     public static void shutdownAllExecutors() {
         synchronized (sExecutorMap) {
-            for (final ExecutorService executor : sExecutorMap.values()) {
+            for (final ScheduledExecutorService executor : sExecutorMap.values()) {
                 executor.execute(new Runnable() {
                     @Override
                     public void run() {