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 android.Manifest;
20 import android.content.Context;
21 import android.net.Uri;
22 import android.provider.ContactsContract;
23 import android.provider.ContactsContract.Contacts;
24 import android.util.Log;
25 
26 import com.android.inputmethod.annotations.ExternallyReferenced;
27 import com.android.inputmethod.latin.ContactsManager.ContactsChangedListener;
28 import com.android.inputmethod.latin.common.StringUtils;
29 import com.android.inputmethod.latin.permissions.PermissionsUtil;
30 import com.android.inputmethod.latin.personalization.AccountUtils;
31 
32 import java.io.File;
33 import java.util.ArrayList;
34 import java.util.List;
35 import java.util.Locale;
36 
37 import javax.annotation.Nullable;
38 
39 public 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 
ContactsBinaryDictionary(final Context context, final Locale locale, final File dictFile, final String name)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
getDictionary(final Context context, final Locale locale, final File dictFile, final String dictNamePrefix, @Nullable final String account)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
close()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
loadInitialContentsLocked()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      */
loadDeviceAccountsEmailAddressesLocked()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      */
loadDictionaryForUriLocked(final Uri uri)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      */
addNameLocked(final String name)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
onContactsChange()173     public void onContactsChange() {
174         setNeedsToRecreate();
175     }
176 }
177