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