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
17package com.android.inputmethod.latin;
18
19import android.Manifest;
20import android.content.Context;
21import android.net.Uri;
22import android.provider.ContactsContract;
23import android.provider.ContactsContract.Contacts;
24import android.util.Log;
25
26import com.android.inputmethod.annotations.ExternallyReferenced;
27import com.android.inputmethod.latin.ContactsManager.ContactsChangedListener;
28import com.android.inputmethod.latin.common.StringUtils;
29import com.android.inputmethod.latin.permissions.PermissionsUtil;
30import com.android.inputmethod.latin.personalization.AccountUtils;
31
32import java.io.File;
33import java.util.ArrayList;
34import java.util.List;
35import java.util.Locale;
36
37import javax.annotation.Nullable;
38
39public class ContactsBinaryDictionary extends ExpandableBinaryDictionary
40        implements ContactsChangedListener {
41    private static final String TAG = ContactsBinaryDictionary.class.getSimpleName();
42    private static final String NAME = "contacts";
43
44    private static final boolean DEBUG = false;
45    private static final boolean DEBUG_DUMP = false;
46
47    /**
48     * Whether to use "firstname lastname" in bigram predictions.
49     */
50    private final boolean mUseFirstLastBigrams;
51    private final ContactsManager mContactsManager;
52
53    protected ContactsBinaryDictionary(final Context context, final Locale locale,
54            final File dictFile, final String name) {
55        super(context, getDictName(name, locale, dictFile), locale, Dictionary.TYPE_CONTACTS,
56                dictFile);
57        mUseFirstLastBigrams = ContactsDictionaryUtils.useFirstLastBigramsForLocale(locale);
58        mContactsManager = new ContactsManager(context);
59        mContactsManager.registerForUpdates(this /* listener */);
60        reloadDictionaryIfRequired();
61    }
62
63    // Note: This method is called by {@link DictionaryFacilitator} using Java reflection.
64    @ExternallyReferenced
65    public static ContactsBinaryDictionary getDictionary(final Context context, final Locale locale,
66            final File dictFile, final String dictNamePrefix, @Nullable final String account) {
67        return new ContactsBinaryDictionary(context, locale, dictFile, dictNamePrefix + NAME);
68    }
69
70    @Override
71    public synchronized void close() {
72        mContactsManager.close();
73        super.close();
74    }
75
76    /**
77     * Typically called whenever the dictionary is created for the first time or
78     * recreated when we think that there are updates to the dictionary.
79     * This is called asynchronously.
80     */
81    @Override
82    public void loadInitialContentsLocked() {
83        loadDeviceAccountsEmailAddressesLocked();
84        loadDictionaryForUriLocked(ContactsContract.Profile.CONTENT_URI);
85        // TODO: Switch this URL to the newer ContactsContract too
86        loadDictionaryForUriLocked(Contacts.CONTENT_URI);
87    }
88
89    /**
90     * Loads device accounts to the dictionary.
91     */
92    private void loadDeviceAccountsEmailAddressesLocked() {
93        final List<String> accountVocabulary =
94                AccountUtils.getDeviceAccountsEmailAddresses(mContext);
95        if (accountVocabulary == null || accountVocabulary.isEmpty()) {
96            return;
97        }
98        for (String word : accountVocabulary) {
99            if (DEBUG) {
100                Log.d(TAG, "loadAccountVocabulary: " + word);
101            }
102            runGCIfRequiredLocked(true /* mindsBlockByGC */);
103            addUnigramLocked(word, ContactsDictionaryConstants.FREQUENCY_FOR_CONTACTS,
104                    false /* isNotAWord */, false /* isPossiblyOffensive */,
105                    BinaryDictionary.NOT_A_VALID_TIMESTAMP);
106        }
107    }
108
109    /**
110     * Loads data within content providers to the dictionary.
111     */
112    private void loadDictionaryForUriLocked(final Uri uri) {
113        if (!PermissionsUtil.checkAllPermissionsGranted(
114                mContext, Manifest.permission.READ_CONTACTS)) {
115            Log.i(TAG, "No permission to read contacts. Not loading the Dictionary.");
116        }
117
118        final ArrayList<String> validNames = mContactsManager.getValidNames(uri);
119        for (final String name : validNames) {
120            addNameLocked(name);
121        }
122        if (uri.equals(Contacts.CONTENT_URI)) {
123            // Since we were able to add content successfully, update the local
124            // state of the manager.
125            mContactsManager.updateLocalState(validNames);
126        }
127    }
128
129    /**
130     * Adds the words in a name (e.g., firstname/lastname) to the binary dictionary along with their
131     * bigrams depending on locale.
132     */
133    private void addNameLocked(final String name) {
134        int len = StringUtils.codePointCount(name);
135        NgramContext ngramContext = NgramContext.getEmptyPrevWordsContext(
136                BinaryDictionary.MAX_PREV_WORD_COUNT_FOR_N_GRAM);
137        // TODO: Better tokenization for non-Latin writing systems
138        for (int i = 0; i < len; i++) {
139            if (Character.isLetter(name.codePointAt(i))) {
140                int end = ContactsDictionaryUtils.getWordEndPosition(name, len, i);
141                String word = name.substring(i, end);
142                if (DEBUG_DUMP) {
143                    Log.d(TAG, "addName word = " + word);
144                }
145                i = end - 1;
146                // Don't add single letter words, possibly confuses
147                // capitalization of i.
148                final int wordLen = StringUtils.codePointCount(word);
149                if (wordLen <= MAX_WORD_LENGTH && wordLen > 1) {
150                    if (DEBUG) {
151                        Log.d(TAG, "addName " + name + ", " + word + ", "  + ngramContext);
152                    }
153                    runGCIfRequiredLocked(true /* mindsBlockByGC */);
154                    addUnigramLocked(word,
155                            ContactsDictionaryConstants.FREQUENCY_FOR_CONTACTS, false /* isNotAWord */,
156                            false /* isPossiblyOffensive */,
157                            BinaryDictionary.NOT_A_VALID_TIMESTAMP);
158                    if (ngramContext.isValid() && mUseFirstLastBigrams) {
159                        runGCIfRequiredLocked(true /* mindsBlockByGC */);
160                        addNgramEntryLocked(ngramContext,
161                                word,
162                                ContactsDictionaryConstants.FREQUENCY_FOR_CONTACTS_BIGRAM,
163                                BinaryDictionary.NOT_A_VALID_TIMESTAMP);
164                    }
165                    ngramContext = ngramContext.getNextNgramContext(
166                            new NgramContext.WordInfo(word));
167                }
168            }
169        }
170    }
171
172    @Override
173    public void onContactsChange() {
174        setNeedsToRecreate();
175    }
176}
177