Home | History | Annotate | Download | only in latin
      1 /*
      2  * Copyright (C) 2012 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.inputmethod.latin;
     18 
     19 import com.android.inputmethod.latin.personalization.AccountUtils;
     20 
     21 import android.content.ContentResolver;
     22 import android.content.Context;
     23 import android.database.ContentObserver;
     24 import android.database.Cursor;
     25 import android.database.sqlite.SQLiteException;
     26 import android.net.Uri;
     27 import android.os.SystemClock;
     28 import android.provider.BaseColumns;
     29 import android.provider.ContactsContract;
     30 import android.provider.ContactsContract.Contacts;
     31 import android.text.TextUtils;
     32 import android.util.Log;
     33 
     34 import com.android.inputmethod.latin.utils.StringUtils;
     35 
     36 import java.util.List;
     37 import java.util.Locale;
     38 
     39 public class ContactsBinaryDictionary extends ExpandableBinaryDictionary {
     40 
     41     private static final String[] PROJECTION = {BaseColumns._ID, Contacts.DISPLAY_NAME};
     42     private static final String[] PROJECTION_ID_ONLY = {BaseColumns._ID};
     43 
     44     private static final String TAG = ContactsBinaryDictionary.class.getSimpleName();
     45     private static final String NAME = "contacts";
     46 
     47     private static boolean DEBUG = false;
     48 
     49     /**
     50      * Frequency for contacts information into the dictionary
     51      */
     52     private static final int FREQUENCY_FOR_CONTACTS = 40;
     53     private static final int FREQUENCY_FOR_CONTACTS_BIGRAM = 90;
     54 
     55     /** The maximum number of contacts that this dictionary supports. */
     56     private static final int MAX_CONTACT_COUNT = 10000;
     57 
     58     private static final int INDEX_NAME = 1;
     59 
     60     /** The number of contacts in the most recent dictionary rebuild. */
     61     static private int sContactCountAtLastRebuild = 0;
     62 
     63     /** The locale for this contacts dictionary. Controls name bigram predictions. */
     64     public final Locale mLocale;
     65 
     66     private ContentObserver mObserver;
     67 
     68     /**
     69      * Whether to use "firstname lastname" in bigram predictions.
     70      */
     71     private final boolean mUseFirstLastBigrams;
     72 
     73     public ContactsBinaryDictionary(final Context context, final Locale locale) {
     74         super(context, getFilenameWithLocale(NAME, locale.toString()), Dictionary.TYPE_CONTACTS,
     75                 false /* isUpdatable */);
     76         mLocale = locale;
     77         mUseFirstLastBigrams = useFirstLastBigramsForLocale(locale);
     78         registerObserver(context);
     79 
     80         // Load the current binary dictionary from internal storage. If no binary dictionary exists,
     81         // loadDictionary will start a new thread to generate one asynchronously.
     82         loadDictionary();
     83     }
     84 
     85     private synchronized void registerObserver(final Context context) {
     86         // Perform a managed query. The Activity will handle closing and requerying the cursor
     87         // when needed.
     88         if (mObserver != null) return;
     89         ContentResolver cres = context.getContentResolver();
     90         cres.registerContentObserver(Contacts.CONTENT_URI, true, mObserver =
     91                 new ContentObserver(null) {
     92                     @Override
     93                     public void onChange(boolean self) {
     94                         setRequiresReload(true);
     95                     }
     96                 });
     97     }
     98 
     99     public void reopen(final Context context) {
    100         registerObserver(context);
    101     }
    102 
    103     @Override
    104     public synchronized void close() {
    105         if (mObserver != null) {
    106             mContext.getContentResolver().unregisterContentObserver(mObserver);
    107             mObserver = null;
    108         }
    109         super.close();
    110     }
    111 
    112     @Override
    113     public void loadDictionaryAsync() {
    114         loadDeviceAccountsEmailAddresses();
    115         loadDictionaryAsyncForUri(ContactsContract.Profile.CONTENT_URI);
    116         // TODO: Switch this URL to the newer ContactsContract too
    117         loadDictionaryAsyncForUri(Contacts.CONTENT_URI);
    118     }
    119 
    120     private void loadDeviceAccountsEmailAddresses() {
    121         final List<String> accountVocabulary =
    122                 AccountUtils.getDeviceAccountsEmailAddresses(mContext);
    123         if (accountVocabulary == null || accountVocabulary.isEmpty()) {
    124             return;
    125         }
    126         for (String word : accountVocabulary) {
    127             if (DEBUG) {
    128                 Log.d(TAG, "loadAccountVocabulary: " + word);
    129             }
    130             super.addWord(word, null /* shortcut */, FREQUENCY_FOR_CONTACTS, 0 /* shortcutFreq */,
    131                     false /* isNotAWord */);
    132         }
    133     }
    134 
    135     private void loadDictionaryAsyncForUri(final Uri uri) {
    136         try {
    137             Cursor cursor = mContext.getContentResolver()
    138                     .query(uri, PROJECTION, null, null, null);
    139             if (cursor != null) {
    140                 try {
    141                     if (cursor.moveToFirst()) {
    142                         sContactCountAtLastRebuild = getContactCount();
    143                         addWords(cursor);
    144                     }
    145                 } finally {
    146                     cursor.close();
    147                 }
    148             }
    149         } catch (final SQLiteException e) {
    150             Log.e(TAG, "SQLiteException in the remote Contacts process.", e);
    151         } catch (final IllegalStateException e) {
    152             Log.e(TAG, "Contacts DB is having problems", e);
    153         }
    154     }
    155 
    156     private boolean useFirstLastBigramsForLocale(final Locale locale) {
    157         // TODO: Add firstname/lastname bigram rules for other languages.
    158         if (locale != null && locale.getLanguage().equals(Locale.ENGLISH.getLanguage())) {
    159             return true;
    160         }
    161         return false;
    162     }
    163 
    164     private void addWords(final Cursor cursor) {
    165         int count = 0;
    166         while (!cursor.isAfterLast() && count < MAX_CONTACT_COUNT) {
    167             String name = cursor.getString(INDEX_NAME);
    168             if (isValidName(name)) {
    169                 addName(name);
    170                 ++count;
    171             }
    172             cursor.moveToNext();
    173         }
    174     }
    175 
    176     private int getContactCount() {
    177         // TODO: consider switching to a rawQuery("select count(*)...") on the database if
    178         // performance is a bottleneck.
    179         try {
    180             final Cursor cursor = mContext.getContentResolver().query(
    181                     Contacts.CONTENT_URI, PROJECTION_ID_ONLY, null, null, null);
    182             if (cursor != null) {
    183                 try {
    184                     return cursor.getCount();
    185                 } finally {
    186                     cursor.close();
    187                 }
    188             }
    189         } catch (final SQLiteException e) {
    190             Log.e(TAG, "SQLiteException in the remote Contacts process.", e);
    191         }
    192         return 0;
    193     }
    194 
    195     /**
    196      * Adds the words in a name (e.g., firstname/lastname) to the binary dictionary along with their
    197      * bigrams depending on locale.
    198      */
    199     private void addName(final String name) {
    200         int len = StringUtils.codePointCount(name);
    201         String prevWord = null;
    202         // TODO: Better tokenization for non-Latin writing systems
    203         for (int i = 0; i < len; i++) {
    204             if (Character.isLetter(name.codePointAt(i))) {
    205                 int end = getWordEndPosition(name, len, i);
    206                 String word = name.substring(i, end);
    207                 i = end - 1;
    208                 // Don't add single letter words, possibly confuses
    209                 // capitalization of i.
    210                 final int wordLen = StringUtils.codePointCount(word);
    211                 if (wordLen < MAX_WORD_LENGTH && wordLen > 1) {
    212                     if (DEBUG) {
    213                         Log.d(TAG, "addName " + name + ", " + word + ", " + prevWord);
    214                     }
    215                     super.addWord(word, null /* shortcut */, FREQUENCY_FOR_CONTACTS,
    216                             0 /* shortcutFreq */, false /* isNotAWord */);
    217                     if (!TextUtils.isEmpty(prevWord)) {
    218                         if (mUseFirstLastBigrams) {
    219                             super.addBigram(prevWord, word, FREQUENCY_FOR_CONTACTS_BIGRAM,
    220                                     0 /* lastModifiedTime */);
    221                         }
    222                     }
    223                     prevWord = word;
    224                 }
    225             }
    226         }
    227     }
    228 
    229     /**
    230      * Returns the index of the last letter in the word, starting from position startIndex.
    231      */
    232     private static int getWordEndPosition(final String string, final int len,
    233             final int startIndex) {
    234         int end;
    235         int cp = 0;
    236         for (end = startIndex + 1; end < len; end += Character.charCount(cp)) {
    237             cp = string.codePointAt(end);
    238             if (!(cp == Constants.CODE_DASH || cp == Constants.CODE_SINGLE_QUOTE
    239                     || Character.isLetter(cp))) {
    240                 break;
    241             }
    242         }
    243         return end;
    244     }
    245 
    246     @Override
    247     protected boolean needsToReloadBeforeWriting() {
    248         return true;
    249     }
    250 
    251     @Override
    252     protected boolean hasContentChanged() {
    253         final long startTime = SystemClock.uptimeMillis();
    254         final int contactCount = getContactCount();
    255         if (contactCount > MAX_CONTACT_COUNT) {
    256             // If there are too many contacts then return false. In this rare case it is impossible
    257             // to include all of them anyways and the cost of rebuilding the dictionary is too high.
    258             // TODO: Sort and check only the MAX_CONTACT_COUNT most recent contacts?
    259             return false;
    260         }
    261         if (contactCount != sContactCountAtLastRebuild) {
    262             if (DEBUG) {
    263                 Log.d(TAG, "Contact count changed: " + sContactCountAtLastRebuild + " to "
    264                         + contactCount);
    265             }
    266             return true;
    267         }
    268         // Check all contacts since it's not possible to find out which names have changed.
    269         // This is needed because it's possible to receive extraneous onChange events even when no
    270         // name has changed.
    271         Cursor cursor = mContext.getContentResolver().query(
    272                 Contacts.CONTENT_URI, PROJECTION, null, null, null);
    273         if (cursor != null) {
    274             try {
    275                 if (cursor.moveToFirst()) {
    276                     while (!cursor.isAfterLast()) {
    277                         String name = cursor.getString(INDEX_NAME);
    278                         if (isValidName(name) && !isNameInDictionary(name)) {
    279                             if (DEBUG) {
    280                                 Log.d(TAG, "Contact name missing: " + name + " (runtime = "
    281                                         + (SystemClock.uptimeMillis() - startTime) + " ms)");
    282                             }
    283                             return true;
    284                         }
    285                         cursor.moveToNext();
    286                     }
    287                 }
    288             } finally {
    289                 cursor.close();
    290             }
    291         }
    292         if (DEBUG) {
    293             Log.d(TAG, "No contacts changed. (runtime = " + (SystemClock.uptimeMillis() - startTime)
    294                     + " ms)");
    295         }
    296         return false;
    297     }
    298 
    299     private static boolean isValidName(final String name) {
    300         if (name != null && -1 == name.indexOf(Constants.CODE_COMMERCIAL_AT)) {
    301             return true;
    302         }
    303         return false;
    304     }
    305 
    306     /**
    307      * Checks if the words in a name are in the current binary dictionary.
    308      */
    309     private boolean isNameInDictionary(final String name) {
    310         int len = StringUtils.codePointCount(name);
    311         String prevWord = null;
    312         for (int i = 0; i < len; i++) {
    313             if (Character.isLetter(name.codePointAt(i))) {
    314                 int end = getWordEndPosition(name, len, i);
    315                 String word = name.substring(i, end);
    316                 i = end - 1;
    317                 final int wordLen = StringUtils.codePointCount(word);
    318                 if (wordLen < MAX_WORD_LENGTH && wordLen > 1) {
    319                     if (!TextUtils.isEmpty(prevWord) && mUseFirstLastBigrams) {
    320                         if (!super.isValidBigramLocked(prevWord, word)) {
    321                             return false;
    322                         }
    323                     } else {
    324                         if (!super.isValidWordLocked(word)) {
    325                             return false;
    326                         }
    327                     }
    328                     prevWord = word;
    329                 }
    330             }
    331         }
    332         return true;
    333     }
    334 }
    335