ContactsBinaryDictionary.java revision 2798c85c0f77fdf4f12eccfe241f84ddec3de994
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
15package com.android.inputmethod.latin;
16
17import android.content.ContentResolver;
18import android.content.Context;
19import android.database.ContentObserver;
20import android.database.Cursor;
21import android.os.SystemClock;
22import android.provider.BaseColumns;
23import android.provider.ContactsContract.Contacts;
24import android.text.TextUtils;
25import android.util.Log;
26
27import com.android.inputmethod.keyboard.Keyboard;
28
29import java.util.Locale;
30
31public 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            return true;
218        }
219        // Check all contacts since it's not possible to find out which names have changed.
220        // This is needed because it's possible to receive extraneous onChange events even when no
221        // name has changed.
222        Cursor cursor = mContext.getContentResolver().query(
223                Contacts.CONTENT_URI, PROJECTION, null, null, null);
224        if (cursor != null) {
225            try {
226                if (cursor.moveToFirst()) {
227                    while (!cursor.isAfterLast()) {
228                        String name = cursor.getString(INDEX_NAME);
229                        if (isValidName(name) && !isNameInDictionary(name)) {
230                            if (DEBUG) {
231                                Log.d(TAG, "Contact name missing: " + name + " (runtime = "
232                                        + (SystemClock.uptimeMillis() - startTime) + " ms)");
233                            }
234                            return true;
235                        }
236                        cursor.moveToNext();
237                    }
238                }
239            } finally {
240                cursor.close();
241            }
242        }
243        if (DEBUG) {
244            Log.d(TAG, "No contacts changed. (runtime = " + (SystemClock.uptimeMillis() - startTime)
245                    + " ms)");
246        }
247        return false;
248    }
249
250    private static boolean isValidName(String name) {
251        if (name != null && -1 == name.indexOf('@')) {
252            return true;
253        }
254        return false;
255    }
256
257    /**
258     * Checks if the words in a name are in the current binary dictionary.
259     */
260    private boolean isNameInDictionary(String name) {
261        int len = name.codePointCount(0, name.length());
262        String prevWord = null;
263        for (int i = 0; i < len; i++) {
264            if (Character.isLetter(name.codePointAt(i))) {
265                int end = getWordEndPosition(name, len, i);
266                String word = name.substring(i, end);
267                i = end - 1;
268                final int wordLen = word.codePointCount(0, word.length());
269                if (wordLen < MAX_WORD_LENGTH && wordLen > 1) {
270                    if (!TextUtils.isEmpty(prevWord) && mUseFirstLastBigrams) {
271                        if (!super.isValidBigramLocked(prevWord, word)) {
272                            return false;
273                        }
274                    } else {
275                        if (!super.isValidWordLocked(word)) {
276                            return false;
277                        }
278                    }
279                    prevWord = word;
280                }
281            }
282        }
283        return true;
284    }
285}
286