Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 4084fa5c authored by Jatin Matani's avatar Jatin Matani
Browse files

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
parent 5254c01d
Loading
Loading
Loading
Loading
+37 −197
Original line number Diff line number Diff line
@@ -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<String> accountVocabulary =
                AccountUtils.getDeviceAccountsEmailAddresses(mContext);
@@ -139,81 +98,26 @@ 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();
            }
        }
    }

    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;
        }
        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);
        final ArrayList<String> validNames = mContactsManager.getValidNames(uri);
        for (final String name : validNames) {
            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();
        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 0;
    }

    /**
     * Adds the words in a name (e.g., firstname/lastname) to the binary dictionary along with their
@@ -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<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();
    }
}
+110 −0
Original line number Diff line number Diff line
/*
 * 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);
    }
}
+48 −0
Original line number Diff line number Diff line
/*
 * 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;
}
+55 −0
Original line number Diff line number Diff line
/*
 * 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;
    }
}
+160 −0

File added.

Preview size limit exceeded, changes collapsed.

Loading