/* * Copyright (C) 2009 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License */ package com.android.providers.contacts; import android.provider.ContactsContract.FullNameStyle; import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupType; import com.android.providers.contacts.SearchIndexManager.IndexBuilder; import java.util.Arrays; import java.util.Comparator; import java.util.Iterator; /** * Given a full name, constructs all possible variants of the name. */ public abstract class NameLookupBuilder { private static final int MAX_NAME_TOKENS = 4; private final NameSplitter mSplitter; private String[][] mNicknameClusters = new String[MAX_NAME_TOKENS][]; private StringBuilder mStringBuilder = new StringBuilder(); private String[] mNames = new String[NameSplitter.MAX_TOKENS]; private static final int[] KOREAN_JAUM_CONVERT_MAP = { // JAUM in Hangul Compatibility Jamo area 0x3131 ~ 0x314E to // in Hangul Jamo area 0x1100 ~ 0x1112 0x1100, // 0x3131 HANGUL LETTER KIYEOK 0x1101, // 0x3132 HANGUL LETTER SSANGKIYEOK 0x00, // 0x3133 HANGUL LETTER KIYEOKSIOS (Ignored) 0x1102, // 0x3134 HANGUL LETTER NIEUN 0x00, // 0x3135 HANGUL LETTER NIEUNCIEUC (Ignored) 0x00, // 0x3136 HANGUL LETTER NIEUNHIEUH (Ignored) 0x1103, // 0x3137 HANGUL LETTER TIKEUT 0x1104, // 0x3138 HANGUL LETTER SSANGTIKEUT 0x1105, // 0x3139 HANGUL LETTER RIEUL 0x00, // 0x313A HANGUL LETTER RIEULKIYEOK (Ignored) 0x00, // 0x313B HANGUL LETTER RIEULMIEUM (Ignored) 0x00, // 0x313C HANGUL LETTER RIEULPIEUP (Ignored) 0x00, // 0x313D HANGUL LETTER RIEULSIOS (Ignored) 0x00, // 0x313E HANGUL LETTER RIEULTHIEUTH (Ignored) 0x00, // 0x313F HANGUL LETTER RIEULPHIEUPH (Ignored) 0x00, // 0x3140 HANGUL LETTER RIEULHIEUH (Ignored) 0x1106, // 0x3141 HANGUL LETTER MIEUM 0x1107, // 0x3142 HANGUL LETTER PIEUP 0x1108, // 0x3143 HANGUL LETTER SSANGPIEUP 0x00, // 0x3144 HANGUL LETTER PIEUPSIOS (Ignored) 0x1109, // 0x3145 HANGUL LETTER SIOS 0x110A, // 0x3146 HANGUL LETTER SSANGSIOS 0x110B, // 0x3147 HANGUL LETTER IEUNG 0x110C, // 0x3148 HANGUL LETTER CIEUC 0x110D, // 0x3149 HANGUL LETTER SSANGCIEUC 0x110E, // 0x314A HANGUL LETTER CHIEUCH 0x110F, // 0x314B HANGUL LETTER KHIEUKH 0x1110, // 0x314C HANGUL LETTER THIEUTH 0x1111, // 0x314D HANGUL LETTER PHIEUPH 0x1112 // 0x314E HANGUL LETTER HIEUH }; public NameLookupBuilder(NameSplitter splitter) { mSplitter = splitter; } /** * Inserts a name lookup record with the supplied column values. */ protected abstract void insertNameLookup(long rawContactId, long dataId, int lookupType, String string); /** * Returns common nickname cluster IDs for a given name. For example, it * will return the same value for "Robert", "Bob" and "Rob". Some names belong to multiple * clusters, e.g. Leo could be Leonard or Leopold. * * May return null. * * @param normalizedName A normalized first name, see {@link NameNormalizer#normalize}. */ protected abstract String[] getCommonNicknameClusters(String normalizedName); /** * Inserts name lookup records for the given structured name. */ public void insertNameLookup(long rawContactId, long dataId, String name, int fullNameStyle) { int tokenCount = mSplitter.tokenize(mNames, name); if (tokenCount == 0) { return; } for (int i = 0; i < tokenCount; i++) { mNames[i] = normalizeName(mNames[i]); } boolean tooManyTokens = tokenCount > MAX_NAME_TOKENS; if (tooManyTokens) { insertNameVariant(rawContactId, dataId, tokenCount, NameLookupType.NAME_EXACT, true); // Favor longer parts of the name Arrays.sort(mNames, 0, tokenCount, new Comparator() { public int compare(String s1, String s2) { return s2.length() - s1.length(); } }); // Insert a collation key for each extra word - useful for contact filtering // and suggestions String firstToken = mNames[0]; for (int i = MAX_NAME_TOKENS; i < tokenCount; i++) { mNames[0] = mNames[i]; insertCollationKey(rawContactId, dataId, MAX_NAME_TOKENS); } mNames[0] = firstToken; tokenCount = MAX_NAME_TOKENS; } // Phase I: insert all variants not involving nickname clusters for (int i = 0; i < tokenCount; i++) { mNicknameClusters[i] = getCommonNicknameClusters(mNames[i]); } insertNameVariants(rawContactId, dataId, 0, tokenCount, !tooManyTokens, true); insertNicknamePermutations(rawContactId, dataId, 0, tokenCount); } public void appendToSearchIndex(IndexBuilder builder, String name, int fullNameStyle) { int tokenCount = mSplitter.tokenize(mNames, name); if (tokenCount == 0) { return; } for (int i = 0; i < tokenCount; i++) { builder.appendName(mNames[i]); } appendNameShorthandLookup(builder, name, fullNameStyle); appendNameLookupForLocaleBasedName(builder, name, fullNameStyle); } /** * Insert more name indexes according to locale specifies. */ private void appendNameLookupForLocaleBasedName(IndexBuilder builder, String fullName, int fullNameStyle) { if (fullNameStyle == FullNameStyle.KOREAN) { NameSplitter.Name name = new NameSplitter.Name(); mSplitter.split(name, fullName, fullNameStyle); if (name.givenNames != null) { builder.appendName(name.givenNames); appendKoreanNameConsonantsLookup(builder, name.givenNames); } appendKoreanNameConsonantsLookup(builder, fullName); } } /** * Inserts Korean lead consonants records of name for the given structured name. */ private void appendKoreanNameConsonantsLookup(IndexBuilder builder, String name) { int position = 0; int consonantLength = 0; int character; final int stringLength = name.length(); mStringBuilder.setLength(0); do { character = name.codePointAt(position++); if ((character == 0x20) || (character == 0x2c) || (character == 0x2E)) { // Skip spaces, commas and periods. continue; } // Exclude characters that are not in Korean leading consonants area // and Korean characters area. if ((character < 0x1100) || (character > 0x1112 && character < 0x3131) || (character > 0x314E && character < 0xAC00) || (character > 0xD7A3)) { break; } // Decompose and take a only lead-consonant for composed Korean characters. if (character >= 0xAC00) { // Lead consonant = "Lead consonant base" + // (character - "Korean Character base") / // ("Lead consonant count" * "middle Vowel count") character = 0x1100 + (character - 0xAC00) / 588; } else if (character >= 0x3131) { // Hangul Compatibility Jamo area 0x3131 ~ 0x314E : // Convert to Hangul Jamo area 0x1100 ~ 0x1112 if (character - 0x3131 >= KOREAN_JAUM_CONVERT_MAP.length) { // This is not lead-consonant break; } character = KOREAN_JAUM_CONVERT_MAP[character - 0x3131]; if (character == 0) { // This is not lead-consonant break; } } mStringBuilder.appendCodePoint(character); consonantLength++; } while (position < stringLength); // At least, insert consonants when Korean characters are two or more. // Only one character cases are covered by NAME_COLLATION_KEY if (consonantLength > 1) { builder.appendName(mStringBuilder.toString()); } } protected String normalizeName(String name) { return NameNormalizer.normalize(name); } /** * Inserts all name variants based on permutations of tokens between * fromIndex and toIndex * * @param initiallyExact true if the name without permutations is the exact * original name * @param buildCollationKey true if a collation key makes sense for these * permutations (false if at least one of the tokens is a * nickname cluster key) */ private void insertNameVariants(long rawContactId, long dataId, int fromIndex, int toIndex, boolean initiallyExact, boolean buildCollationKey) { if (fromIndex == toIndex) { insertNameVariant(rawContactId, dataId, toIndex, initiallyExact ? NameLookupType.NAME_EXACT : NameLookupType.NAME_VARIANT, buildCollationKey); return; } // Swap the first token with each other token (including itself, which is a no-op) // and recursively insert all permutations for the remaining tokens String firstToken = mNames[fromIndex]; for (int i = fromIndex; i < toIndex; i++) { mNames[fromIndex] = mNames[i]; mNames[i] = firstToken; insertNameVariants(rawContactId, dataId, fromIndex + 1, toIndex, initiallyExact && i == fromIndex, buildCollationKey); mNames[i] = mNames[fromIndex]; mNames[fromIndex] = firstToken; } } /** * Inserts a single name variant and optionally its collation key counterpart. */ private void insertNameVariant(long rawContactId, long dataId, int tokenCount, int lookupType, boolean buildCollationKey) { mStringBuilder.setLength(0); for (int i = 0; i < tokenCount; i++) { if (i != 0) { mStringBuilder.append('.'); } mStringBuilder.append(mNames[i]); } insertNameLookup(rawContactId, dataId, lookupType, mStringBuilder.toString()); if (buildCollationKey) { insertCollationKey(rawContactId, dataId, tokenCount); } } /** * Inserts a collation key for the current contents of {@link #mNames}. */ private void insertCollationKey(long rawContactId, long dataId, int tokenCount) { mStringBuilder.setLength(0); for (int i = 0; i < tokenCount; i++) { mStringBuilder.append(mNames[i]); } insertNameLookup(rawContactId, dataId, NameLookupType.NAME_COLLATION_KEY, mStringBuilder.toString()); } /** * For all tokens that correspond to nickname clusters, substitutes each cluster key * and inserts all permutations with that key. */ private void insertNicknamePermutations(long rawContactId, long dataId, int fromIndex, int tokenCount) { for (int i = fromIndex; i < tokenCount; i++) { String[] clusters = mNicknameClusters[i]; if (clusters != null) { String token = mNames[i]; for (int j = 0; j < clusters.length; j++) { mNames[i] = clusters[j]; // Insert all permutations with this nickname cluster insertNameVariants(rawContactId, dataId, 0, tokenCount, false, false); // Repeat recursively for other nickname clusters insertNicknamePermutations(rawContactId, dataId, i + 1, tokenCount); } mNames[i] = token; } } } /** * Insert more name indexes according to locale specifies for those locales * for which we have alternative shorthand name methods (eg, Pinyin for * Chinese, Romaji for Japanese). */ public void appendNameShorthandLookup(IndexBuilder builder, String name, int fullNameStyle) { Iterator it = ContactLocaleUtils.getInstance().getNameLookupKeys(name, fullNameStyle); if (it != null) { while (it.hasNext()) { builder.appendName(it.next()); } } } }