ContactRecipientAdapter.java revision d3b009ae55651f1e60950342468e3c37fdeb0796
1/* 2 * Copyright (C) 2015 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 */ 16package com.android.messaging.ui.contact; 17 18import android.content.Context; 19import android.database.Cursor; 20import android.database.MergeCursor; 21import android.support.v4.util.Pair; 22import android.text.TextUtils; 23import android.text.util.Rfc822Token; 24import android.text.util.Rfc822Tokenizer; 25import android.widget.Filter; 26 27import com.android.ex.chips.BaseRecipientAdapter; 28import com.android.ex.chips.RecipientAlternatesAdapter; 29import com.android.ex.chips.RecipientAlternatesAdapter.RecipientMatchCallback; 30import com.android.ex.chips.RecipientEntry; 31import com.android.messaging.util.Assert; 32import com.android.messaging.util.Assert.DoesNotRunOnMainThread; 33import com.android.messaging.util.BugleGservices; 34import com.android.messaging.util.BugleGservicesKeys; 35import com.android.messaging.util.ContactRecipientEntryUtils; 36import com.android.messaging.util.ContactUtil; 37import com.android.messaging.util.PhoneUtils; 38 39import java.text.Collator; 40import java.util.ArrayList; 41import java.util.Collections; 42import java.util.Comparator; 43import java.util.HashMap; 44import java.util.HashSet; 45import java.util.List; 46import java.util.Locale; 47import java.util.Map; 48 49/** 50 * An extension on the base {@link BaseRecipientAdapter} that uses data layer from Bugle, 51 * such as the ContactRecipientPhotoManager that uses our own MediaResourceManager, and 52 * contact lookup that relies on ContactUtil. It provides data source and filtering ability 53 * for {@link ContactRecipientAutoCompleteView} 54 */ 55public final class ContactRecipientAdapter extends BaseRecipientAdapter { 56 public ContactRecipientAdapter(final Context context, 57 final ContactListItemView.HostInterface clivHost) { 58 this(context, Integer.MAX_VALUE, QUERY_TYPE_PHONE, clivHost); 59 } 60 61 public ContactRecipientAdapter(final Context context, final int preferredMaxResultCount, 62 final int queryMode, final ContactListItemView.HostInterface clivHost) { 63 super(context, preferredMaxResultCount, queryMode); 64 setPhotoManager(new ContactRecipientPhotoManager(context, clivHost)); 65 } 66 67 @Override 68 public boolean forceShowAddress() { 69 // We should always use the SingleRecipientAddressAdapter 70 // And never use the RecipientAlternatesAdapter 71 return true; 72 } 73 74 @Override 75 public Filter getFilter() { 76 return new ContactFilter(); 77 } 78 79 /** 80 * A Filter for a RecipientEditTextView that queries Bugle's ContactUtil for auto-complete 81 * results. 82 */ 83 public class ContactFilter extends Filter { 84 // Used to sort filtered contacts when it has combined results from email and phone. 85 private final RecipientEntryComparator mComparator = new RecipientEntryComparator(); 86 87 /** 88 * Returns a cursor containing the filtered results in contacts given the search text, 89 * and a boolean indicating whether the results are sorted. 90 * 91 * The queries are synchronously performed since this is not run on the main thread. 92 * 93 * Some locales (e.g. JPN) expect email addresses to be auto-completed for MMS. 94 * If this is the case, perform two queries on phone number followed by email and 95 * return the merged results. 96 */ 97 @DoesNotRunOnMainThread 98 private Pair<Cursor, Boolean> getFilteredResultsCursor(final Context context, 99 final String searchText) { 100 Assert.isNotMainThread(); 101 if (BugleGservices.get().getBoolean( 102 BugleGservicesKeys.ALWAYS_AUTOCOMPLETE_EMAIL_ADDRESS, 103 BugleGservicesKeys.ALWAYS_AUTOCOMPLETE_EMAIL_ADDRESS_DEFAULT)) { 104 return Pair.create((Cursor) new MergeCursor(new Cursor[] { 105 ContactUtil.filterPhones(getContext(), searchText) 106 .performSynchronousQuery(), 107 ContactUtil.filterEmails(getContext(), searchText) 108 .performSynchronousQuery() 109 }), false /* the merged cursor is not sorted */); 110 } else { 111 return Pair.create(ContactUtil.filterDestination(getContext(), searchText) 112 .performSynchronousQuery(), true); 113 } 114 } 115 116 @Override 117 protected FilterResults performFiltering(final CharSequence constraint) { 118 Assert.isNotMainThread(); 119 final FilterResults results = new FilterResults(); 120 121 // No query, return empty results. 122 if (TextUtils.isEmpty(constraint)) { 123 clearTempEntries(); 124 return results; 125 } 126 127 final String searchText = constraint.toString(); 128 129 // Query for auto-complete results, since performFiltering() is not done on the 130 // main thread, perform the cursor loader queries directly. 131 final Pair<Cursor, Boolean> filteredResults = getFilteredResultsCursor(getContext(), 132 searchText); 133 final Cursor cursor = filteredResults.first; 134 final boolean sorted = filteredResults.second; 135 if (cursor != null) { 136 try { 137 final List<RecipientEntry> entries = new ArrayList<RecipientEntry>(); 138 139 // First check if the constraint is a valid SMS destination. If so, add the 140 // destination as a suggestion item to the drop down. 141 if (PhoneUtils.isValidSmsMmsDestination(searchText)) { 142 entries.add(ContactRecipientEntryUtils 143 .constructSendToDestinationEntry(searchText)); 144 } 145 146 HashSet<Long> existingContactIds = new HashSet<Long>(); 147 while (cursor.moveToNext()) { 148 // Make sure there's only one first-level contact (i.e. contact for which 149 // we show the avatar picture and name) for every contact id. 150 final long contactId = cursor.getLong(ContactUtil.INDEX_CONTACT_ID); 151 final boolean isFirstLevel = !existingContactIds.contains(contactId); 152 if (isFirstLevel) { 153 existingContactIds.add(contactId); 154 } 155 entries.add(ContactUtil.createRecipientEntryForPhoneQuery(cursor, 156 isFirstLevel)); 157 } 158 159 if (!sorted) { 160 Collections.sort(entries, mComparator); 161 } 162 results.values = entries; 163 results.count = 1; 164 165 } finally { 166 cursor.close(); 167 } 168 } 169 return results; 170 } 171 172 @Override 173 protected void publishResults(final CharSequence constraint, final FilterResults results) { 174 mCurrentConstraint = constraint; 175 clearTempEntries(); 176 177 if (results.values != null) { 178 @SuppressWarnings("unchecked") 179 final List<RecipientEntry> entries = (List<RecipientEntry>) results.values; 180 updateEntries(entries); 181 } else { 182 updateEntries(Collections.<RecipientEntry>emptyList()); 183 } 184 } 185 186 private class RecipientEntryComparator implements Comparator<RecipientEntry> { 187 private final Collator mCollator; 188 189 public RecipientEntryComparator() { 190 mCollator = Collator.getInstance(Locale.getDefault()); 191 mCollator.setStrength(Collator.PRIMARY); 192 } 193 194 /** 195 * Compare two RecipientEntry's, first by locale-aware display name comparison, then by 196 * contact id comparison, finally by first-level-ness comparison. 197 */ 198 @Override 199 public int compare(RecipientEntry lhs, RecipientEntry rhs) { 200 // Send-to-destinations always appear before everything else. 201 final boolean sendToLhs = ContactRecipientEntryUtils 202 .isSendToDestinationContact(lhs); 203 final boolean sendToRhs = ContactRecipientEntryUtils 204 .isSendToDestinationContact(lhs); 205 if (sendToLhs != sendToRhs) { 206 if (sendToLhs) { 207 return -1; 208 } else if (sendToRhs) { 209 return 1; 210 } 211 } 212 213 final int displayNameCompare = mCollator.compare(lhs.getDisplayName(), 214 rhs.getDisplayName()); 215 if (displayNameCompare != 0) { 216 return displayNameCompare; 217 } 218 219 // Long.compare could accomplish the following three lines, but this is only 220 // available in API 19+ 221 final long lhsContactId = lhs.getContactId(); 222 final long rhsContactId = rhs.getContactId(); 223 final int contactCompare = lhsContactId < rhsContactId ? -1 : 224 (lhsContactId == rhsContactId ? 0 : 1); 225 if (contactCompare != 0) { 226 return contactCompare; 227 } 228 229 // These are the same contact. Make sure first-level contacts always 230 // appear at the front. 231 if (lhs.isFirstLevel()) { 232 return -1; 233 } else if (rhs.isFirstLevel()) { 234 return 1; 235 } else { 236 return 0; 237 } 238 } 239 } 240 } 241 242 /** 243 * Called when we need to substitute temporary recipient chips with better alternatives. 244 * For example, if a list of comma-delimited phone numbers are pasted into the edit box, 245 * we want to be able to look up in the ContactUtil for exact matches and get contact 246 * details such as name and photo thumbnail for the contact to display a better chip. 247 */ 248 @Override 249 public void getMatchingRecipients(final ArrayList<String> inAddresses, 250 final RecipientMatchCallback callback) { 251 final int addressesSize = Math.min( 252 RecipientAlternatesAdapter.MAX_LOOKUPS, inAddresses.size()); 253 final HashSet<String> addresses = new HashSet<String>(); 254 for (int i = 0; i < addressesSize; i++) { 255 final Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(inAddresses.get(i).toLowerCase()); 256 addresses.add(tokens.length > 0 ? tokens[0].getAddress() : inAddresses.get(i)); 257 } 258 259 final Map<String, RecipientEntry> recipientEntries = 260 new HashMap<String, RecipientEntry>(); 261 // query for each address 262 for (final String address : addresses) { 263 final Cursor cursor = ContactUtil.lookupDestination(getContext(), address) 264 .performSynchronousQuery(); 265 if (cursor != null) { 266 try { 267 if (cursor.moveToNext()) { 268 // There may be multiple matches to the same number, always take the 269 // first match. 270 // TODO: May need to consider if there's an existing conversation 271 // that matches this particular contact and prioritize that contact. 272 final RecipientEntry entry = 273 ContactUtil.createRecipientEntryForPhoneQuery(cursor, true); 274 recipientEntries.put(address, entry); 275 } 276 277 } finally { 278 cursor.close(); 279 } 280 } 281 } 282 283 // report matches 284 callback.matchesFound(recipientEntries); 285 } 286} 287