From 661834dd3b45b477165effe126a9c243750ae8d6 Mon Sep 17 00:00:00 2001 From: Christine Chen Date: Thu, 30 May 2013 16:54:09 -0700 Subject: Add SmartDial database for the Dialer app. - Creates a database helper to create a smartdial database for the Dialer app. - Queries all rows in the Contact database and copies related columns to the smart dial database. - Create another prefix database to contain all prefixes of a contact. - During keypad input, the prefix databse is queried to find contact suggestions, and suggestions are ranked by the usage data and contact status (starred, primary contact, etc.) - Created unit test for the SmartDial database insertion and prefix computing functions. Change-Id: I4d7c3b3bcc52dd6efa4d6e69d3f1687c3abaeb69 --- src/com/android/dialer/dialpad/SmartDialTrie.java | 671 ---------------------- 1 file changed, 671 deletions(-) delete mode 100644 src/com/android/dialer/dialpad/SmartDialTrie.java (limited to 'src/com/android/dialer/dialpad/SmartDialTrie.java') diff --git a/src/com/android/dialer/dialpad/SmartDialTrie.java b/src/com/android/dialer/dialpad/SmartDialTrie.java deleted file mode 100644 index c62210b17..000000000 --- a/src/com/android/dialer/dialpad/SmartDialTrie.java +++ /dev/null @@ -1,671 +0,0 @@ -/* - * Copyright (C) 2013 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.dialer.dialpad; - -import android.text.TextUtils; - -import com.android.dialer.dialpad.SmartDialCache.ContactNumber; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Preconditions; -import com.google.common.collect.Lists; - -import java.util.ArrayList; -import java.util.HashSet; -import java.util.Set; - -/** - * Prefix trie where the only allowed characters are the characters '0' to '9'. Multiple contacts - * can occupy the same nodes. - * - *

Provides functions to get all contacts that lie on or below a node. - * This is useful for retrieving all contacts that start with that prefix.

- * - *

Also contains special logic to handle NANP numbers in the case that the user is from a region - * that uses NANP numbers.

- */ -public class SmartDialTrie { - @VisibleForTesting - static class ParseInfo { - byte[] indexes; - int nthFirstTokenPos; - int nthLastTokenPos; - } - - /** - * A country code and integer offset pair that represents the parsed country code in a - * phone number. The country code is a string containing the numeric country-code prefix in - * a phone number (e.g. 1 or 852). The offset is the integer position of where the country code - * ends in a phone number. - */ - public static class CountryCodeWithOffset { - public static final CountryCodeWithOffset NO_COUNTRY_CODE = new CountryCodeWithOffset(0, - ""); - - final String countryCode; - final int offset; - - public CountryCodeWithOffset(int offset, String countryCode) { - this.countryCode = countryCode; - this.offset = offset; - } - } - - final Node mRoot = new Node(); - private int mSize = 0; - private final SmartDialMap mMap; - private final boolean mFormatNanp; - - private static final int LAST_TOKENS_FOR_INITIALS = 2; - private static final int FIRST_TOKENS_FOR_INITIALS = 2; - - // Static set of all possible country codes in the world - public static Set sCountryCodes = null; - - public SmartDialTrie() { - // Use the latin letter to digit map by default if none provided - this(new LatinSmartDialMap(), false); - } - - /** - * Creates a new SmartDialTrie. - * - * @param formatNanp True if inserted numbers are to be treated as NANP numbers - * such that numbers are automatically broken up by country prefix and area code. - */ - @VisibleForTesting - public SmartDialTrie(boolean formatNanp) { - this(new LatinSmartDialMap(), formatNanp); - } - - /** - * Creates a new SmartDialTrie. - * - * @param charMap Mapping of characters to digits to use when inserting names into the trie. - * @param formatNanp True if inserted numbers are to be treated as NANP numbers - * such that numbers are automatically broken up by country prefix and area code. - */ - public SmartDialTrie(SmartDialMap map, boolean formatNanp) { - mMap = map; - mFormatNanp = formatNanp; - } - - /** - * Returns all contacts in the prefix tree that correspond to this prefix. - */ - public ArrayList getAllWithPrefix(CharSequence prefix) { - final ArrayList result = Lists.newArrayList(); - if (TextUtils.isEmpty(prefix)) { - return result; - } - Node current = mRoot; - for (int i = 0; i < prefix.length(); i++) { - char ch = prefix.charAt(i); - current = current.getChild(ch, false); - if (current == null) { - return result; - } - } - // return all contacts that correspond to this prefix - getAll(current, result); - return result; - } - - /** - * Returns all the contacts located at and under the provided node(including its children) - */ - private void getAll(Node root, ArrayList output) { - if (root == null) { - return; - } - if (root.getContents() != null) { - output.addAll(root.getContents()); - } - for (int i = 0; i < root.getChildrenSize(); i++) { - getAll(root.getChild(i, false), output); - } - } - - /** - * Adds the display name and phone number of a contact into the prefix trie. - * - * @param contact Desired contact to add - */ - public void put(ContactNumber contact) { - // Preconvert the display name into a byte array containing indexes to avoid having to - // remap each character over multiple passes - final ParseInfo info = parseToIndexes(contact.displayName, FIRST_TOKENS_FOR_INITIALS, - LAST_TOKENS_FOR_INITIALS); - putForPrefix(contact, mRoot, info, 0, true); - // We don't need to do the same for phone numbers since we only make one pass over them. - // Strip the calling code from the phone number here - if (!TextUtils.isEmpty(contact.phoneNumber)) { - // Handle country codes for numbers with a + prefix - final CountryCodeWithOffset code = getOffsetWithoutCountryCode(contact.phoneNumber); - if (code.offset != 0) { - putNumber(contact, contact.phoneNumber, code.offset); - } - if ((code.countryCode.equals("1") || code.offset == 0) && mFormatNanp) { - // Special case handling for NANP numbers (1-xxx-xxx-xxxx) - final String stripped = SmartDialNameMatcher.normalizeNumber( - contact.phoneNumber, code.offset, mMap); - if (!TextUtils.isEmpty(stripped)) { - int trunkPrefixOffset = 0; - if (stripped.charAt(0) == '1') { - // If the number starts with 1, we can assume its the trunk prefix. - trunkPrefixOffset = 1; - } - if (stripped.length() == (10 + trunkPrefixOffset)) { - // Valid NANP number - if (trunkPrefixOffset != 0) { - // Add the digits that follow the 1st digit (trunk prefix) - // If trunkPrefixOffset is 0, we will add the number as is anyway, so - // don't bother. - putNumber(contact, stripped, trunkPrefixOffset); - } - // Add the digits that follow the next 3 digits (area code) - putNumber(contact, stripped, 3 + trunkPrefixOffset); - } - } - } - putNumber(contact, contact.phoneNumber, 0); - } - mSize++; - } - - public static CountryCodeWithOffset getOffsetWithoutCountryCode(String number) { - if (!TextUtils.isEmpty(number)) { - if (number.charAt(0) == '+') { - // check for international code here - for (int i = 1; i <= 1 + 3; i++) { - if (number.length() <= i) break; - final String countryCode = number.substring(1, i); - if (isValidCountryCode(countryCode)) { - return new CountryCodeWithOffset(i, countryCode); - } - } - } - } - return CountryCodeWithOffset.NO_COUNTRY_CODE; - } - - /** - * Used by SmartDialNameMatcher to determine which character in the phone number to start - * the matching process from for a NANP formatted number. - * - * @param number Raw phone number - * @return An empty array if the provided number does not appear to be an NANP number, - * and an array containing integer offsets for the number (starting after the '1' prefix, - * and the area code prefix respectively. - */ - public static int[] getOffsetForNANPNumbers(String number, SmartDialMap map) { - int validDigits = 0; - boolean hasPrefix = false; - int firstOffset = 0; // Tracks the location of the first digit after the '1' prefix - int secondOffset = 0; // Tracks the location of the first digit after the area code - for (int i = 0; i < number.length(); i++) { - final char ch = number.charAt(i); - if (map.isValidDialpadNumericChar(ch)) { - if (validDigits == 0) { - // Check the first digit to see if it is 1 - if (ch == '1') { - if (hasPrefix) { - // Prefix has two '1's in a row. Invalid number, since area codes - // cannot start with 1, so just bail - break; - } - hasPrefix = true; - continue; - } - } - validDigits++; - if (validDigits == 1) { - // Found the first digit after the country code - firstOffset = i; - } else if (validDigits == 4) { - // Found the first digit after the area code - secondOffset = i; - } - } - - } - if (validDigits == 10) { - return hasPrefix ? new int[] {firstOffset, secondOffset} : new int[] {secondOffset}; - } - return new int[0]; - } - - /** - * Converts the given characters into a byte array of index and returns it together with offset - * information in a {@link ParseInfo} data structure. - * @param chars Characters to convert into indexes - * @param firstNTokens The first n tokens we want the offset for - * @param lastNTokens The last n tokens we want the offset for - */ - @VisibleForTesting - ParseInfo parseToIndexes(CharSequence chars, int firstNTokens, int lastNTokens) { - final int length = chars.length(); - final byte[] result = new byte[length]; - final ArrayList offSets = new ArrayList(); - char c; - int tokenCount = 0; - boolean atSeparator = true; - for (int i = 0; i < length; i++) { - c = mMap.normalizeCharacter(chars.charAt(i)); - if (mMap.isValidDialpadCharacter(c)) { - if (atSeparator) { - tokenCount++; - } - atSeparator = false; - result[i] = mMap.getDialpadIndex(c); - } else { - // Found the last character of the current token - if (!atSeparator) { - offSets.add(i); - } - result[i] = -1; - atSeparator = true; - } - } - - final ParseInfo info = new ParseInfo(); - info.indexes = result; - info.nthFirstTokenPos = offSets.size() >= firstNTokens ? offSets.get(firstNTokens - 1) : - length - 1; - info.nthLastTokenPos = offSets.size() >= lastNTokens ? offSets.get(offSets.size() - - lastNTokens) : 0; - return info; - } - - /** - * Puts a phone number and its associated contact into the prefix trie. - * - * @param contact - Contact to add to the trie - * @param phoneNumber - Phone number of the contact - * @param offSet - The nth character of the phone number to start from - */ - private void putNumber(ContactNumber contact, String phoneNumber, int offSet) { - Preconditions.checkArgument(offSet < phoneNumber.length()); - Node current = mRoot; - final int length = phoneNumber.length(); - char ch; - for (int i = offSet; i < length; i++) { - ch = phoneNumber.charAt(i); - if (mMap.isValidDialpadNumericChar(ch)) { - current = current.getChild(ch, true); - } - } - current.add(contact); - } - - /** - * Place an contact into the trie using at the provided node using the provided prefix. Uses as - * the input prefix a byte array instead of a CharSequence, as we will be traversing the array - * multiple times and it is more efficient to pre-convert the prefix into indexes before hand. - * Adds initial matches for the first token, and the last N tokens in the name. - * - * @param contact Contact to put - * @param root Root node to use as the starting point - * @param parseInfo ParseInfo data structure containing the converted byte array, as well as - * token offsets that determine which tokens should have entries added for initial - * search - * @param start - Starting index of the byte array - * @param isFullName If true, prefix will be treated as a full name and everytime a new name - * token is encountered, the rest of the name segment is added into the trie. - */ - private void putForPrefix(ContactNumber contact, Node root, ParseInfo info, int start, - boolean isFullName) { - final boolean addInitialMatches = (start >= info.nthLastTokenPos || - start <= info.nthFirstTokenPos); - final byte[] indexes = info.indexes; - Node current = root; - Node initialNode = root; - final int length = indexes.length; - boolean atSeparator = true; - byte index; - for (int i = start; i < length; i++) { - index = indexes[i]; - if (index > -1) { - if (atSeparator) { - atSeparator = false; - // encountered a new name token, so add this token into the tree starting from - // the root node - if (initialNode != this.mRoot) { - if (isFullName) { - putForPrefix(contact, this.mRoot, info, i, false); - } - if (addInitialMatches && - (i >= info.nthLastTokenPos || i <= info.nthFirstTokenPos) && - initialNode != root) { - putForPrefix(contact, initialNode, info, i, false); - } - } - // Set initial node to the node indexed by the first character of the current - // prefix - if (initialNode == root) { - initialNode = initialNode.getChild(index, true); - } - } - current = current.getChild(index, true); - } else { - atSeparator = true; - } - } - current.add(contact); - } - - /* Used only for testing to verify we insert the correct number of entries into the trie */ - @VisibleForTesting - int numEntries() { - final ArrayList result = Lists.newArrayList(); - getAll(mRoot, result); - return result.size(); - } - - - @VisibleForTesting - public int size() { - return mSize; - } - - @VisibleForTesting - /* package */ static class Node { - Node[] mChildren; - private ArrayList mContents; - - public Node() { - // don't allocate array or contents unless needed - } - - public int getChildrenSize() { - if (mChildren == null) { - return -1; - } - return mChildren.length; - } - - /** - * Returns a specific child of the current node. - * - * @param index Index of the child to return. - * @param createIfDoesNotExist Whether or not to create a node in that child slot if one - * does not already currently exist. - * @return The existing or newly created child, or {@literal null} if the child does not - * exist and createIfDoesNotExist is false. - */ - public Node getChild(int index, boolean createIfDoesNotExist) { - if (createIfDoesNotExist) { - if (mChildren == null) { - mChildren = new Node[10]; - } - if (mChildren[index] == null) { - mChildren[index] = new Node(); - } - } else { - if (mChildren == null) { - return null; - } - } - return mChildren[index]; - } - - /** - * Same as getChild(int index, boolean createIfDoesNotExist), but takes a character from '0' - * to '9' as an index. - */ - public Node getChild(char index, boolean createIfDoesNotExist) { - return getChild(index - '0', createIfDoesNotExist); - } - - public void add(ContactNumber contact) { - if (mContents == null) { - mContents = Lists.newArrayList(); - } - mContents.add(contact); - } - - public ArrayList getContents() { - return mContents; - } - } - - private static boolean isValidCountryCode(String countryCode) { - if (sCountryCodes == null) { - sCountryCodes = initCountryCodes(); - } - return sCountryCodes.contains(countryCode); - } - - private static Set initCountryCodes() { - final HashSet result = new HashSet(); - result.add("1"); - result.add("7"); - result.add("20"); - result.add("27"); - result.add("30"); - result.add("31"); - result.add("32"); - result.add("33"); - result.add("34"); - result.add("36"); - result.add("39"); - result.add("40"); - result.add("41"); - result.add("43"); - result.add("44"); - result.add("45"); - result.add("46"); - result.add("47"); - result.add("48"); - result.add("49"); - result.add("51"); - result.add("52"); - result.add("53"); - result.add("54"); - result.add("55"); - result.add("56"); - result.add("57"); - result.add("58"); - result.add("60"); - result.add("61"); - result.add("62"); - result.add("63"); - result.add("64"); - result.add("65"); - result.add("66"); - result.add("81"); - result.add("82"); - result.add("84"); - result.add("86"); - result.add("90"); - result.add("91"); - result.add("92"); - result.add("93"); - result.add("94"); - result.add("95"); - result.add("98"); - result.add("211"); - result.add("212"); - result.add("213"); - result.add("216"); - result.add("218"); - result.add("220"); - result.add("221"); - result.add("222"); - result.add("223"); - result.add("224"); - result.add("225"); - result.add("226"); - result.add("227"); - result.add("228"); - result.add("229"); - result.add("230"); - result.add("231"); - result.add("232"); - result.add("233"); - result.add("234"); - result.add("235"); - result.add("236"); - result.add("237"); - result.add("238"); - result.add("239"); - result.add("240"); - result.add("241"); - result.add("242"); - result.add("243"); - result.add("244"); - result.add("245"); - result.add("246"); - result.add("247"); - result.add("248"); - result.add("249"); - result.add("250"); - result.add("251"); - result.add("252"); - result.add("253"); - result.add("254"); - result.add("255"); - result.add("256"); - result.add("257"); - result.add("258"); - result.add("260"); - result.add("261"); - result.add("262"); - result.add("263"); - result.add("264"); - result.add("265"); - result.add("266"); - result.add("267"); - result.add("268"); - result.add("269"); - result.add("290"); - result.add("291"); - result.add("297"); - result.add("298"); - result.add("299"); - result.add("350"); - result.add("351"); - result.add("352"); - result.add("353"); - result.add("354"); - result.add("355"); - result.add("356"); - result.add("357"); - result.add("358"); - result.add("359"); - result.add("370"); - result.add("371"); - result.add("372"); - result.add("373"); - result.add("374"); - result.add("375"); - result.add("376"); - result.add("377"); - result.add("378"); - result.add("379"); - result.add("380"); - result.add("381"); - result.add("382"); - result.add("385"); - result.add("386"); - result.add("387"); - result.add("389"); - result.add("420"); - result.add("421"); - result.add("423"); - result.add("500"); - result.add("501"); - result.add("502"); - result.add("503"); - result.add("504"); - result.add("505"); - result.add("506"); - result.add("507"); - result.add("508"); - result.add("509"); - result.add("590"); - result.add("591"); - result.add("592"); - result.add("593"); - result.add("594"); - result.add("595"); - result.add("596"); - result.add("597"); - result.add("598"); - result.add("599"); - result.add("670"); - result.add("672"); - result.add("673"); - result.add("674"); - result.add("675"); - result.add("676"); - result.add("677"); - result.add("678"); - result.add("679"); - result.add("680"); - result.add("681"); - result.add("682"); - result.add("683"); - result.add("685"); - result.add("686"); - result.add("687"); - result.add("688"); - result.add("689"); - result.add("690"); - result.add("691"); - result.add("692"); - result.add("800"); - result.add("808"); - result.add("850"); - result.add("852"); - result.add("853"); - result.add("855"); - result.add("856"); - result.add("870"); - result.add("878"); - result.add("880"); - result.add("881"); - result.add("882"); - result.add("883"); - result.add("886"); - result.add("888"); - result.add("960"); - result.add("961"); - result.add("962"); - result.add("963"); - result.add("964"); - result.add("965"); - result.add("966"); - result.add("967"); - result.add("968"); - result.add("970"); - result.add("971"); - result.add("972"); - result.add("973"); - result.add("974"); - result.add("975"); - result.add("976"); - result.add("977"); - result.add("979"); - result.add("992"); - result.add("993"); - result.add("994"); - result.add("995"); - result.add("996"); - result.add("998"); - return result; - } -} -- cgit v1.2.3