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.dialer.dialpad; 18 19import static com.android.dialer.dialpad.SmartDialController.LOG_TAG; 20 21import android.os.AsyncTask; 22import android.provider.ContactsContract; 23import android.provider.ContactsContract.Contacts; 24import android.telephony.PhoneNumberUtils; 25import android.util.Log; 26 27import com.android.contacts.common.preference.ContactsPreferences; 28import com.android.contacts.common.util.StopWatch; 29import com.android.dialer.dialpad.SmartDialCache.ContactNumber; 30 31import com.google.common.base.Objects; 32import com.google.common.collect.Lists; 33 34import java.util.ArrayList; 35import java.util.Collections; 36import java.util.HashSet; 37import java.util.List; 38import java.util.Set; 39 40/** 41 * This task searches through the provided cache to return the top 3 contacts(ranked by confidence) 42 * that match the query, then passes it back to the {@link SmartDialLoaderCallback} through a 43 * callback function. 44 */ 45public class SmartDialLoaderTask extends AsyncTask<String, Integer, List<SmartDialEntry>> { 46 47 public interface SmartDialLoaderCallback { 48 void setSmartDialAdapterEntries(List<SmartDialEntry> list, String query); 49 } 50 51 static private final boolean DEBUG = false; 52 53 private static final int MAX_ENTRIES = 3; 54 55 private final SmartDialCache mContactsCache; 56 57 private final SmartDialLoaderCallback mCallback; 58 59 private final String mQuery; 60 61 /** 62 * See {@link ContactsPreferences#getDisplayOrder()}. 63 * {@link ContactsContract.Preferences#DISPLAY_ORDER_PRIMARY} (first name first) 64 * {@link ContactsContract.Preferences#DISPLAY_ORDER_ALTERNATIVE} (last name first) 65 */ 66 private final SmartDialNameMatcher mNameMatcher; 67 68 public SmartDialLoaderTask(SmartDialLoaderCallback callback, String query, 69 SmartDialCache cache) { 70 this.mCallback = callback; 71 this.mNameMatcher = new SmartDialNameMatcher(PhoneNumberUtils.normalizeNumber(query)); 72 this.mContactsCache = cache; 73 this.mQuery = query; 74 } 75 76 @Override 77 protected List<SmartDialEntry> doInBackground(String... params) { 78 return getContactMatches(); 79 } 80 81 @Override 82 protected void onPostExecute(List<SmartDialEntry> result) { 83 if (mCallback != null) { 84 mCallback.setSmartDialAdapterEntries(result, mQuery); 85 } 86 } 87 88 /** 89 * Loads all visible contacts with phone numbers and check if their display names match the 90 * query. Return at most {@link #MAX_ENTRIES} {@link SmartDialEntry}'s for the matching 91 * contacts. 92 */ 93 private ArrayList<SmartDialEntry> getContactMatches() { 94 95 final SmartDialTrie trie = mContactsCache.getContacts(); 96 final boolean matchNanp = mContactsCache.getUserInNanpRegion(); 97 98 if (DEBUG) { 99 Log.d(LOG_TAG, "Size of cache: " + trie.size()); 100 } 101 102 final StopWatch stopWatch = DEBUG ? StopWatch.start("Start Match") : null; 103 final ArrayList<ContactNumber> allMatches = trie.getAllWithPrefix(mNameMatcher.getQuery()); 104 if (DEBUG) { 105 stopWatch.lap("Find matches"); 106 } 107 // Sort matches in order of ascending contact affinity (lower is better) 108 Collections.sort(allMatches, new SmartDialCache.ContactAffinityComparator()); 109 if (DEBUG) { 110 stopWatch.lap("Sort"); 111 } 112 final Set<ContactMatch> duplicates = new HashSet<ContactMatch>(); 113 final ArrayList<SmartDialEntry> candidates = Lists.newArrayList(); 114 for (ContactNumber contact : allMatches) { 115 final ContactMatch contactMatch = new ContactMatch(contact.lookupKey, contact.id); 116 // Don't add multiple contact numbers from the same contact into suggestions if 117 // there are multiple matches. Instead, just keep the highest priority number 118 // instead. 119 if (duplicates.contains(contactMatch)) { 120 continue; 121 } 122 duplicates.add(contactMatch); 123 final boolean matches = mNameMatcher.matches(contact.displayName); 124 125 candidates.add(new SmartDialEntry( 126 contact.displayName, 127 Contacts.getLookupUri(contact.id, contact.lookupKey), 128 contact.phoneNumber, 129 mNameMatcher.getMatchPositions(), 130 SmartDialNameMatcher.matchesNumber(contact.phoneNumber, 131 mNameMatcher.getQuery(), matchNanp) 132 )); 133 if (candidates.size() >= MAX_ENTRIES) { 134 break; 135 } 136 } 137 if (DEBUG) { 138 stopWatch.stopAndLog(LOG_TAG + " Match Complete", 0); 139 } 140 return candidates; 141 } 142 143 private class ContactMatch { 144 public final String lookupKey; 145 public final long id; 146 147 public ContactMatch(String lookupKey, long id) { 148 this.lookupKey = lookupKey; 149 this.id = id; 150 } 151 152 @Override 153 public int hashCode() { 154 return Objects.hashCode(lookupKey, id); 155 } 156 157 @Override 158 public boolean equals(Object object) { 159 if (this == object) { 160 return true; 161 } 162 if (object instanceof ContactMatch) { 163 ContactMatch that = (ContactMatch) object; 164 return Objects.equal(this.lookupKey, that.lookupKey) 165 && Objects.equal(this.id, that.id); 166 } 167 return false; 168 } 169 } 170} 171