• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
ContactsBinaryDictionary(final Context context, final Locale locale)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 
registerObserver(final Context context)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 
reopen(final Context context)99     public void reopen(final Context context) {
100         registerObserver(context);
101     }
102 
103     @Override
close()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
loadDictionaryAsync()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 
loadDeviceAccountsEmailAddresses()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 
loadDictionaryAsyncForUri(final Uri uri)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 
useFirstLastBigramsForLocale(final Locale locale)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 
addWords(final Cursor cursor)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 
getContactCount()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      */
addName(final String name)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      */
getWordEndPosition(final String string, final int len, final int startIndex)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
needsToReloadBeforeWriting()247     protected boolean needsToReloadBeforeWriting() {
248         return true;
249     }
250 
251     @Override
hasContentChanged()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 
isValidName(final String name)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      */
isNameInDictionary(final String name)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