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