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