1 /* 2 * Copyright (C) 2009 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.providers.contacts; 18 19 import android.provider.ContactsContract.FullNameStyle; 20 21 import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupType; 22 import com.android.providers.contacts.SearchIndexManager.IndexBuilder; 23 24 import java.util.Arrays; 25 import java.util.Comparator; 26 import java.util.Iterator; 27 28 /** 29 * Given a full name, constructs all possible variants of the name. 30 */ 31 public abstract class NameLookupBuilder { 32 33 private static final int MAX_NAME_TOKENS = 4; 34 35 private final NameSplitter mSplitter; 36 private String[][] mNicknameClusters = new String[MAX_NAME_TOKENS][]; 37 private StringBuilder mStringBuilder = new StringBuilder(); 38 private String[] mNames = new String[NameSplitter.MAX_TOKENS]; 39 40 private static final int[] KOREAN_JAUM_CONVERT_MAP = { 41 // JAUM in Hangul Compatibility Jamo area 0x3131 ~ 0x314E to 42 // in Hangul Jamo area 0x1100 ~ 0x1112 43 0x1100, // 0x3131 HANGUL LETTER KIYEOK 44 0x1101, // 0x3132 HANGUL LETTER SSANGKIYEOK 45 0x00, // 0x3133 HANGUL LETTER KIYEOKSIOS (Ignored) 46 0x1102, // 0x3134 HANGUL LETTER NIEUN 47 0x00, // 0x3135 HANGUL LETTER NIEUNCIEUC (Ignored) 48 0x00, // 0x3136 HANGUL LETTER NIEUNHIEUH (Ignored) 49 0x1103, // 0x3137 HANGUL LETTER TIKEUT 50 0x1104, // 0x3138 HANGUL LETTER SSANGTIKEUT 51 0x1105, // 0x3139 HANGUL LETTER RIEUL 52 0x00, // 0x313A HANGUL LETTER RIEULKIYEOK (Ignored) 53 0x00, // 0x313B HANGUL LETTER RIEULMIEUM (Ignored) 54 0x00, // 0x313C HANGUL LETTER RIEULPIEUP (Ignored) 55 0x00, // 0x313D HANGUL LETTER RIEULSIOS (Ignored) 56 0x00, // 0x313E HANGUL LETTER RIEULTHIEUTH (Ignored) 57 0x00, // 0x313F HANGUL LETTER RIEULPHIEUPH (Ignored) 58 0x00, // 0x3140 HANGUL LETTER RIEULHIEUH (Ignored) 59 0x1106, // 0x3141 HANGUL LETTER MIEUM 60 0x1107, // 0x3142 HANGUL LETTER PIEUP 61 0x1108, // 0x3143 HANGUL LETTER SSANGPIEUP 62 0x00, // 0x3144 HANGUL LETTER PIEUPSIOS (Ignored) 63 0x1109, // 0x3145 HANGUL LETTER SIOS 64 0x110A, // 0x3146 HANGUL LETTER SSANGSIOS 65 0x110B, // 0x3147 HANGUL LETTER IEUNG 66 0x110C, // 0x3148 HANGUL LETTER CIEUC 67 0x110D, // 0x3149 HANGUL LETTER SSANGCIEUC 68 0x110E, // 0x314A HANGUL LETTER CHIEUCH 69 0x110F, // 0x314B HANGUL LETTER KHIEUKH 70 0x1110, // 0x314C HANGUL LETTER THIEUTH 71 0x1111, // 0x314D HANGUL LETTER PHIEUPH 72 0x1112 // 0x314E HANGUL LETTER HIEUH 73 }; 74 NameLookupBuilder(NameSplitter splitter)75 public NameLookupBuilder(NameSplitter splitter) { 76 mSplitter = splitter; 77 } 78 79 /** 80 * Inserts a name lookup record with the supplied column values. 81 */ insertNameLookup(long rawContactId, long dataId, int lookupType, String string)82 protected abstract void insertNameLookup(long rawContactId, long dataId, int lookupType, 83 String string); 84 85 /** 86 * Inserts name lookup records for the given structured name. 87 */ insertNameLookup(long rawContactId, long dataId, String name, int fullNameStyle)88 public void insertNameLookup(long rawContactId, long dataId, String name, int fullNameStyle) { 89 int tokenCount = mSplitter.tokenize(mNames, name); 90 if (tokenCount == 0) { 91 return; 92 } 93 94 for (int i = 0; i < tokenCount; i++) { 95 mNames[i] = normalizeName(mNames[i]); 96 } 97 98 boolean tooManyTokens = tokenCount > MAX_NAME_TOKENS; 99 if (tooManyTokens) { 100 insertNameVariant(rawContactId, dataId, tokenCount, NameLookupType.NAME_EXACT, true); 101 102 // Favor longer parts of the name 103 Arrays.sort(mNames, 0, tokenCount, new Comparator<String>() { 104 105 public int compare(String s1, String s2) { 106 return s2.length() - s1.length(); 107 } 108 }); 109 110 // Insert a collation key for each extra word - useful for contact filtering 111 // and suggestions 112 String firstToken = mNames[0]; 113 for (int i = MAX_NAME_TOKENS; i < tokenCount; i++) { 114 mNames[0] = mNames[i]; 115 insertCollationKey(rawContactId, dataId, MAX_NAME_TOKENS); 116 } 117 mNames[0] = firstToken; 118 119 tokenCount = MAX_NAME_TOKENS; 120 } 121 122 insertNameVariants(rawContactId, dataId, 0, tokenCount, !tooManyTokens, true); 123 } 124 appendToSearchIndex(IndexBuilder builder, String name, int fullNameStyle)125 public void appendToSearchIndex(IndexBuilder builder, String name, int fullNameStyle) { 126 int tokenCount = mSplitter.tokenize(mNames, name); 127 if (tokenCount == 0) { 128 return; 129 } 130 131 for (int i = 0; i < tokenCount; i++) { 132 builder.appendName(mNames[i]); 133 } 134 135 appendNameShorthandLookup(builder, name, fullNameStyle); 136 appendNameLookupForLocaleBasedName(builder, name, fullNameStyle); 137 } 138 139 /** 140 * Insert more name indexes according to locale specifies. 141 */ appendNameLookupForLocaleBasedName(IndexBuilder builder, String fullName, int fullNameStyle)142 private void appendNameLookupForLocaleBasedName(IndexBuilder builder, 143 String fullName, int fullNameStyle) { 144 if (fullNameStyle == FullNameStyle.KOREAN) { 145 NameSplitter.Name name = new NameSplitter.Name(); 146 mSplitter.split(name, fullName, fullNameStyle); 147 if (name.givenNames != null) { 148 builder.appendName(name.givenNames); 149 appendKoreanNameConsonantsLookup(builder, name.givenNames); 150 } 151 appendKoreanNameConsonantsLookup(builder, fullName); 152 } 153 } 154 155 /** 156 * Inserts Korean lead consonants records of name for the given structured name. 157 */ appendKoreanNameConsonantsLookup(IndexBuilder builder, String name)158 private void appendKoreanNameConsonantsLookup(IndexBuilder builder, String name) { 159 int position = 0; 160 int consonantLength = 0; 161 int character; 162 163 final int stringLength = name.length(); 164 mStringBuilder.setLength(0); 165 do { 166 character = name.codePointAt(position++); 167 if ((character == 0x20) || (character == 0x2c) || (character == 0x2E)) { 168 // Skip spaces, commas and periods. 169 continue; 170 } 171 // Exclude characters that are not in Korean leading consonants area 172 // and Korean characters area. 173 if ((character < 0x1100) || (character > 0x1112 && character < 0x3131) || 174 (character > 0x314E && character < 0xAC00) || 175 (character > 0xD7A3)) { 176 break; 177 } 178 // Decompose and take a only lead-consonant for composed Korean characters. 179 if (character >= 0xAC00) { 180 // Lead consonant = "Lead consonant base" + 181 // (character - "Korean Character base") / 182 // ("Lead consonant count" * "middle Vowel count") 183 character = 0x1100 + (character - 0xAC00) / 588; 184 } else if (character >= 0x3131) { 185 // Hangul Compatibility Jamo area 0x3131 ~ 0x314E : 186 // Convert to Hangul Jamo area 0x1100 ~ 0x1112 187 if (character - 0x3131 >= KOREAN_JAUM_CONVERT_MAP.length) { 188 // This is not lead-consonant 189 break; 190 } 191 character = KOREAN_JAUM_CONVERT_MAP[character - 0x3131]; 192 if (character == 0) { 193 // This is not lead-consonant 194 break; 195 } 196 } 197 mStringBuilder.appendCodePoint(character); 198 consonantLength++; 199 } while (position < stringLength); 200 201 // At least, insert consonants when Korean characters are two or more. 202 // Only one character cases are covered by NAME_COLLATION_KEY 203 if (consonantLength > 1) { 204 builder.appendName(mStringBuilder.toString()); 205 } 206 } 207 normalizeName(String name)208 protected String normalizeName(String name) { 209 return NameNormalizer.normalize(name); 210 } 211 212 /** 213 * Inserts all name variants based on permutations of tokens between 214 * fromIndex and toIndex 215 * 216 * @param initiallyExact true if the name without permutations is the exact 217 * original name 218 * @param buildCollationKey true if a collation key makes sense for these 219 * permutations (false if at least one of the tokens is a 220 * nickname cluster key) 221 */ insertNameVariants(long rawContactId, long dataId, int fromIndex, int toIndex, boolean initiallyExact, boolean buildCollationKey)222 private void insertNameVariants(long rawContactId, long dataId, int fromIndex, int toIndex, 223 boolean initiallyExact, boolean buildCollationKey) { 224 if (fromIndex == toIndex) { 225 insertNameVariant(rawContactId, dataId, toIndex, 226 initiallyExact ? NameLookupType.NAME_EXACT : NameLookupType.NAME_VARIANT, 227 buildCollationKey); 228 return; 229 } 230 231 // Swap the first token with each other token (including itself, which is a no-op) 232 // and recursively insert all permutations for the remaining tokens 233 String firstToken = mNames[fromIndex]; 234 for (int i = fromIndex; i < toIndex; i++) { 235 mNames[fromIndex] = mNames[i]; 236 mNames[i] = firstToken; 237 238 insertNameVariants(rawContactId, dataId, fromIndex + 1, toIndex, 239 initiallyExact && i == fromIndex, buildCollationKey); 240 241 mNames[i] = mNames[fromIndex]; 242 mNames[fromIndex] = firstToken; 243 } 244 } 245 246 /** 247 * Inserts a single name variant and optionally its collation key counterpart. 248 */ insertNameVariant(long rawContactId, long dataId, int tokenCount, int lookupType, boolean buildCollationKey)249 private void insertNameVariant(long rawContactId, long dataId, int tokenCount, 250 int lookupType, boolean buildCollationKey) { 251 mStringBuilder.setLength(0); 252 253 for (int i = 0; i < tokenCount; i++) { 254 if (i != 0) { 255 mStringBuilder.append('.'); 256 } 257 mStringBuilder.append(mNames[i]); 258 } 259 260 insertNameLookup(rawContactId, dataId, lookupType, mStringBuilder.toString()); 261 262 if (buildCollationKey) { 263 insertCollationKey(rawContactId, dataId, tokenCount); 264 } 265 } 266 267 /** 268 * Inserts a collation key for the current contents of {@link #mNames}. 269 */ insertCollationKey(long rawContactId, long dataId, int tokenCount)270 private void insertCollationKey(long rawContactId, long dataId, int tokenCount) { 271 mStringBuilder.setLength(0); 272 273 for (int i = 0; i < tokenCount; i++) { 274 mStringBuilder.append(mNames[i]); 275 } 276 277 insertNameLookup(rawContactId, dataId, NameLookupType.NAME_COLLATION_KEY, 278 mStringBuilder.toString()); 279 } 280 281 /** 282 * Insert more name indexes according to locale specifies for those locales 283 * for which we have alternative shorthand name methods (eg, Pinyin for 284 * Chinese, Romaji for Japanese). 285 */ appendNameShorthandLookup(IndexBuilder builder, String name, int fullNameStyle)286 public void appendNameShorthandLookup(IndexBuilder builder, String name, int fullNameStyle) { 287 Iterator<String> it = 288 ContactLocaleUtils.getInstance().getNameLookupKeys(name, fullNameStyle); 289 if (it != null) { 290 while (it.hasNext()) { 291 builder.appendName(it.next()); 292 } 293 } 294 } 295 } 296