From 4084fa5caeee09ef7993957c5e922dab14c57f3f Mon Sep 17 00:00:00 2001 From: Jatin Matani Date: Mon, 9 Feb 2015 12:22:47 -0800 Subject: [PATCH] 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 --- .../latin/ContactsBinaryDictionary.java | 234 +++--------------- .../latin/ContactsContentObserver.java | 110 ++++++++ .../latin/ContactsDictionaryConstants.java | 48 ++++ .../latin/ContactsDictionaryUtils.java | 55 ++++ .../inputmethod/latin/ContactsManager.java | 160 ++++++++++++ .../latin/DictionaryFacilitator.java | 2 + .../latin/DictionaryFacilitatorImpl.java | 5 + .../latin/utils/ExecutorUtils.java | 12 +- 8 files changed, 424 insertions(+), 202 deletions(-) create mode 100644 java/src/com/android/inputmethod/latin/ContactsContentObserver.java create mode 100644 java/src/com/android/inputmethod/latin/ContactsDictionaryConstants.java create mode 100644 java/src/com/android/inputmethod/latin/ContactsDictionaryUtils.java create mode 100644 java/src/com/android/inputmethod/latin/ContactsManager.java diff --git a/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java index 66a21ecef..ba0f9b807 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,47 +34,27 @@ import java.util.Locale; 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"; private static final boolean DEBUG = false; 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 @@ public class ContactsBinaryDictionary extends ExpandableBinaryDictionary { 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 @@ public class ContactsBinaryDictionary extends ExpandableBinaryDictionary { loadDictionaryForUriLocked(Contacts.CONTENT_URI); } + /** + * Loads device accounts to the dictionary. + */ private void loadDeviceAccountsEmailAddressesLocked() { final List accountVocabulary = AccountUtils.getDeviceAccountsEmailAddresses(mContext); @@ -139,80 +98,25 @@ public class ContactsBinaryDictionary extends ExpandableBinaryDictionary { 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 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 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 @@ public class ContactsBinaryDictionary extends ExpandableBinaryDictionary { // 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 @@ public class ContactsBinaryDictionary extends ExpandableBinaryDictionary { 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 @@ public class ContactsBinaryDictionary extends ExpandableBinaryDictionary { } } - /** - * 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 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 000000000..019d17d56 --- /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 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 000000000..8d8faca58 --- /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 000000000..b77388434 --- /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 000000000..dc5abd955 --- /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 getValidNames(final Uri uri) { + final ArrayList 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 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 a451b672d..c22dc287c 100644 --- a/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java +++ b/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java @@ -177,4 +177,6 @@ public interface DictionaryFacilitator { 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 4ed94058a..3d76751ce 100644 --- a/java/src/com/android/inputmethod/latin/DictionaryFacilitatorImpl.java +++ b/java/src/com/android/inputmethod/latin/DictionaryFacilitatorImpl.java @@ -804,4 +804,9 @@ public class DictionaryFacilitatorImpl implements DictionaryFacilitator { 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 e77f6fd40..50be16072 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 com.android.inputmethod.annotations.UsedForTesting; 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 sExecutorMap = + static final ConcurrentHashMap sExecutorMap = new ConcurrentHashMap<>(); private static class ThreadFactoryWithId implements ThreadFactory { @@ -46,13 +47,14 @@ public class ExecutorUtils { /** * 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 @@ public class ExecutorUtils { @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() {