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