1 /* 2 * Copyright (C) 2011 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 17 package com.android.ex.chips; 18 19 import android.accounts.Account; 20 import android.content.Context; 21 import android.database.Cursor; 22 import android.database.MatrixCursor; 23 import android.graphics.drawable.StateListDrawable; 24 import android.net.Uri; 25 import android.provider.ContactsContract; 26 import android.provider.ContactsContract.Contacts; 27 import android.text.TextUtils; 28 import android.text.util.Rfc822Token; 29 import android.text.util.Rfc822Tokenizer; 30 import android.util.Log; 31 import android.view.View; 32 import android.view.ViewGroup; 33 import android.widget.CursorAdapter; 34 35 import com.android.ex.chips.BaseRecipientAdapter.DirectoryListQuery; 36 import com.android.ex.chips.BaseRecipientAdapter.DirectorySearchParams; 37 import com.android.ex.chips.DropdownChipLayouter.AdapterType; 38 import com.android.ex.chips.Queries.Query; 39 40 import java.util.ArrayList; 41 import java.util.HashMap; 42 import java.util.HashSet; 43 import java.util.List; 44 import java.util.Map; 45 import java.util.Set; 46 47 /** 48 * RecipientAlternatesAdapter backs the RecipientEditTextView for managing contacts 49 * queried by email or by phone number. 50 */ 51 public class RecipientAlternatesAdapter extends CursorAdapter { 52 public static final int MAX_LOOKUPS = 50; 53 54 private final long mCurrentId; 55 56 private int mCheckedItemPosition = -1; 57 58 private OnCheckedItemChangedListener mCheckedItemChangedListener; 59 60 private static final String TAG = "RecipAlternates"; 61 62 public static final int QUERY_TYPE_EMAIL = 0; 63 public static final int QUERY_TYPE_PHONE = 1; 64 private final Long mDirectoryId; 65 private DropdownChipLayouter mDropdownChipLayouter; 66 private final StateListDrawable mDeleteDrawable; 67 68 private static final Map<String, String> sCorrectedPhotoUris = new HashMap<String, String>(); 69 70 public interface RecipientMatchCallback { 71 public void matchesFound(Map<String, RecipientEntry> results); 72 /** 73 * Called with all addresses that could not be resolved to valid recipients. 74 */ 75 public void matchesNotFound(Set<String> unfoundAddresses); 76 } 77 78 public static void getMatchingRecipients(Context context, BaseRecipientAdapter adapter, 79 ArrayList<String> inAddresses, Account account, RecipientMatchCallback callback, 80 ChipsUtil.PermissionsCheckListener permissionsCheckListener) { 81 getMatchingRecipients(context, adapter, inAddresses, QUERY_TYPE_EMAIL, account, callback, 82 permissionsCheckListener); 83 } 84 85 /** 86 * Get a HashMap of address to RecipientEntry that contains all contact 87 * information for a contact with the provided address, if one exists. This 88 * may block the UI, so run it in an async task. 89 * 90 * @param context Context. 91 * @param inAddresses Array of addresses on which to perform the lookup. 92 * @param callback RecipientMatchCallback called when a match or matches are found. 93 */ 94 public static void getMatchingRecipients(Context context, BaseRecipientAdapter adapter, 95 ArrayList<String> inAddresses, int addressType, Account account, 96 RecipientMatchCallback callback, 97 ChipsUtil.PermissionsCheckListener permissionsCheckListener) { 98 Queries.Query query; 99 if (addressType == QUERY_TYPE_EMAIL) { 100 query = Queries.EMAIL; 101 } else { 102 query = Queries.PHONE; 103 } 104 int addressesSize = Math.min(MAX_LOOKUPS, inAddresses.size()); 105 HashSet<String> addresses = new HashSet<String>(); 106 StringBuilder bindString = new StringBuilder(); 107 // Create the "?" string and set up arguments. 108 for (int i = 0; i < addressesSize; i++) { 109 Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(inAddresses.get(i).toLowerCase()); 110 addresses.add(tokens.length > 0 ? tokens[0].getAddress() : inAddresses.get(i)); 111 bindString.append("?"); 112 if (i < addressesSize - 1) { 113 bindString.append(","); 114 } 115 } 116 117 if (Log.isLoggable(TAG, Log.DEBUG)) { 118 Log.d(TAG, "Doing reverse lookup for " + addresses.toString()); 119 } 120 121 String[] addressArray = new String[addresses.size()]; 122 addresses.toArray(addressArray); 123 HashMap<String, RecipientEntry> recipientEntries = null; 124 Cursor c = null; 125 126 try { 127 if (ChipsUtil.hasPermissions(context, permissionsCheckListener)) { 128 c = context.getContentResolver().query( 129 query.getContentUri(), 130 query.getProjection(), 131 query.getProjection()[Queries.Query.DESTINATION] + " IN (" 132 + bindString.toString() + ")", addressArray, null); 133 } 134 recipientEntries = processContactEntries(c, null /* directoryId */); 135 callback.matchesFound(recipientEntries); 136 } finally { 137 if (c != null) { 138 c.close(); 139 } 140 } 141 142 final Set<String> matchesNotFound = new HashSet<String>(); 143 144 getMatchingRecipientsFromDirectoryQueries(context, recipientEntries, 145 addresses, account, matchesNotFound, query, callback, permissionsCheckListener); 146 147 getMatchingRecipientsFromExtensionMatcher(adapter, matchesNotFound, callback); 148 } 149 150 public static void getMatchingRecipientsFromDirectoryQueries(Context context, 151 Map<String, RecipientEntry> recipientEntries, Set<String> addresses, 152 Account account, Set<String> matchesNotFound, 153 RecipientMatchCallback callback, 154 ChipsUtil.PermissionsCheckListener permissionsCheckListener) { 155 getMatchingRecipientsFromDirectoryQueries( 156 context, recipientEntries, addresses, account, 157 matchesNotFound, Queries.EMAIL, callback, permissionsCheckListener); 158 } 159 160 private static void getMatchingRecipientsFromDirectoryQueries(Context context, 161 Map<String, RecipientEntry> recipientEntries, Set<String> addresses, 162 Account account, Set<String> matchesNotFound, Queries.Query query, 163 RecipientMatchCallback callback, 164 ChipsUtil.PermissionsCheckListener permissionsCheckListener) { 165 // See if any entries did not resolve; if so, we need to check other 166 // directories 167 168 if (recipientEntries.size() < addresses.size()) { 169 // Run a directory query for each unmatched recipient. 170 HashSet<String> unresolvedAddresses = new HashSet<String>(); 171 for (String address : addresses) { 172 if (!recipientEntries.containsKey(address)) { 173 unresolvedAddresses.add(address); 174 } 175 } 176 matchesNotFound.addAll(unresolvedAddresses); 177 178 final List<DirectorySearchParams> paramsList; 179 Cursor directoryCursor = null; 180 try { 181 if (ChipsUtil.hasPermissions(context, permissionsCheckListener)) { 182 directoryCursor = context.getContentResolver().query( 183 DirectoryListQuery.URI, DirectoryListQuery.PROJECTION, 184 null, null, null); 185 } 186 if (directoryCursor == null) { 187 return; 188 } 189 paramsList = BaseRecipientAdapter.setupOtherDirectories( 190 context, directoryCursor, account); 191 } finally { 192 if (directoryCursor != null) { 193 directoryCursor.close(); 194 } 195 } 196 197 if (paramsList != null) { 198 Cursor directoryContactsCursor = null; 199 for (String unresolvedAddress : unresolvedAddresses) { 200 for (int i = 0; i < paramsList.size(); i++) { 201 final long directoryId = paramsList.get(i).directoryId; 202 try { 203 directoryContactsCursor = doQuery(unresolvedAddress, 1 /* limit */, 204 directoryId, account, context, query, permissionsCheckListener); 205 if (directoryContactsCursor != null 206 && directoryContactsCursor.getCount() != 0) { 207 // We found the directory with at least one contact 208 final Map<String, RecipientEntry> entries = 209 processContactEntries(directoryContactsCursor, directoryId); 210 211 for (final String address : entries.keySet()) { 212 matchesNotFound.remove(address); 213 } 214 215 callback.matchesFound(entries); 216 break; 217 } 218 } finally { 219 if (directoryContactsCursor != null) { 220 directoryContactsCursor.close(); 221 directoryContactsCursor = null; 222 } 223 } 224 } 225 } 226 } 227 } 228 } 229 230 public static void getMatchingRecipientsFromExtensionMatcher(BaseRecipientAdapter adapter, 231 Set<String> matchesNotFound, RecipientMatchCallback callback) { 232 // If no matches found in contact provider or the directories, try the extension 233 // matcher. 234 // todo (aalbert): This whole method needs to be in the adapter? 235 if (adapter != null) { 236 final Map<String, RecipientEntry> entries = 237 adapter.getMatchingRecipients(matchesNotFound); 238 if (entries != null && entries.size() > 0) { 239 callback.matchesFound(entries); 240 for (final String address : entries.keySet()) { 241 matchesNotFound.remove(address); 242 } 243 } 244 } 245 callback.matchesNotFound(matchesNotFound); 246 } 247 248 private static HashMap<String, RecipientEntry> processContactEntries(Cursor c, 249 Long directoryId) { 250 HashMap<String, RecipientEntry> recipientEntries = new HashMap<String, RecipientEntry>(); 251 if (c != null && c.moveToFirst()) { 252 do { 253 String address = c.getString(Queries.Query.DESTINATION); 254 255 final RecipientEntry newRecipientEntry = RecipientEntry.constructTopLevelEntry( 256 c.getString(Queries.Query.NAME), 257 c.getInt(Queries.Query.DISPLAY_NAME_SOURCE), 258 c.getString(Queries.Query.DESTINATION), 259 c.getInt(Queries.Query.DESTINATION_TYPE), 260 c.getString(Queries.Query.DESTINATION_LABEL), 261 c.getLong(Queries.Query.CONTACT_ID), 262 directoryId, 263 c.getLong(Queries.Query.DATA_ID), 264 c.getString(Queries.Query.PHOTO_THUMBNAIL_URI), 265 true, 266 c.getString(Queries.Query.LOOKUP_KEY)); 267 268 /* 269 * In certain situations, we may have two results for one address, where one of the 270 * results is just the email address, and the other has a name and photo, so we want 271 * to use the better one. 272 */ 273 final RecipientEntry recipientEntry = 274 getBetterRecipient(recipientEntries.get(address), newRecipientEntry); 275 276 recipientEntries.put(address, recipientEntry); 277 if (Log.isLoggable(TAG, Log.DEBUG)) { 278 Log.d(TAG, "Received reverse look up information for " + address 279 + " RESULTS: " 280 + " NAME : " + c.getString(Queries.Query.NAME) 281 + " CONTACT ID : " + c.getLong(Queries.Query.CONTACT_ID) 282 + " ADDRESS :" + c.getString(Queries.Query.DESTINATION)); 283 } 284 } while (c.moveToNext()); 285 } 286 return recipientEntries; 287 } 288 289 /** 290 * Given two {@link RecipientEntry}s for the same email address, this will return the one that 291 * contains more complete information for display purposes. Defaults to <code>entry2</code> if 292 * no significant differences are found. 293 */ 294 static RecipientEntry getBetterRecipient(final RecipientEntry entry1, 295 final RecipientEntry entry2) { 296 // If only one has passed in, use it 297 if (entry2 == null) { 298 return entry1; 299 } 300 301 if (entry1 == null) { 302 return entry2; 303 } 304 305 // If only one has a display name, use it 306 if (!TextUtils.isEmpty(entry1.getDisplayName()) 307 && TextUtils.isEmpty(entry2.getDisplayName())) { 308 return entry1; 309 } 310 311 if (!TextUtils.isEmpty(entry2.getDisplayName()) 312 && TextUtils.isEmpty(entry1.getDisplayName())) { 313 return entry2; 314 } 315 316 // If only one has a display name that is not the same as the destination, use it 317 if (!TextUtils.equals(entry1.getDisplayName(), entry1.getDestination()) 318 && TextUtils.equals(entry2.getDisplayName(), entry2.getDestination())) { 319 return entry1; 320 } 321 322 if (!TextUtils.equals(entry2.getDisplayName(), entry2.getDestination()) 323 && TextUtils.equals(entry1.getDisplayName(), entry1.getDestination())) { 324 return entry2; 325 } 326 327 // If only one has a photo, use it 328 if ((entry1.getPhotoThumbnailUri() != null || entry1.getPhotoBytes() != null) 329 && (entry2.getPhotoThumbnailUri() == null && entry2.getPhotoBytes() == null)) { 330 return entry1; 331 } 332 333 if ((entry2.getPhotoThumbnailUri() != null || entry2.getPhotoBytes() != null) 334 && (entry1.getPhotoThumbnailUri() == null && entry1.getPhotoBytes() == null)) { 335 return entry2; 336 } 337 338 // Go with the second option as a default 339 return entry2; 340 } 341 342 private static Cursor doQuery(CharSequence constraint, int limit, Long directoryId, 343 Account account, Context context, Query query, 344 ChipsUtil.PermissionsCheckListener permissionsCheckListener) { 345 if (!ChipsUtil.hasPermissions(context, permissionsCheckListener)) { 346 if (Log.isLoggable(TAG, Log.DEBUG)) { 347 Log.d(TAG, "Not doing query because we don't have required permissions."); 348 } 349 return null; 350 } 351 final Uri.Builder builder = query 352 .getContentFilterUri() 353 .buildUpon() 354 .appendPath(constraint.toString()) 355 .appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY, 356 String.valueOf(limit + BaseRecipientAdapter.ALLOWANCE_FOR_DUPLICATES)); 357 if (directoryId != null) { 358 builder.appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY, 359 String.valueOf(directoryId)); 360 } 361 if (account != null) { 362 builder.appendQueryParameter(BaseRecipientAdapter.PRIMARY_ACCOUNT_NAME, account.name); 363 builder.appendQueryParameter(BaseRecipientAdapter.PRIMARY_ACCOUNT_TYPE, account.type); 364 } 365 return context.getContentResolver() 366 .query(builder.build(), query.getProjection(), null, null, null); 367 } 368 369 public RecipientAlternatesAdapter(Context context, long contactId, Long directoryId, 370 String lookupKey, long currentId, int queryMode, OnCheckedItemChangedListener listener, 371 DropdownChipLayouter dropdownChipLayouter, 372 ChipsUtil.PermissionsCheckListener permissionsCheckListener) { 373 this(context, contactId, directoryId, lookupKey, currentId, queryMode, listener, 374 dropdownChipLayouter, null, permissionsCheckListener); 375 } 376 377 public RecipientAlternatesAdapter(Context context, long contactId, Long directoryId, 378 String lookupKey, long currentId, int queryMode, OnCheckedItemChangedListener listener, 379 DropdownChipLayouter dropdownChipLayouter, StateListDrawable deleteDrawable, 380 ChipsUtil.PermissionsCheckListener permissionsCheckListener) { 381 super(context, 382 getCursorForConstruction(context, contactId, directoryId, lookupKey, queryMode, 383 permissionsCheckListener), 384 0); 385 mCurrentId = currentId; 386 mDirectoryId = directoryId; 387 mCheckedItemChangedListener = listener; 388 389 mDropdownChipLayouter = dropdownChipLayouter; 390 mDeleteDrawable = deleteDrawable; 391 } 392 393 private static Cursor getCursorForConstruction(Context context, long contactId, 394 Long directoryId, String lookupKey, int queryType, 395 ChipsUtil.PermissionsCheckListener permissionsCheckListener) { 396 final Uri uri; 397 final String desiredMimeType; 398 final String[] projection; 399 400 if (queryType == QUERY_TYPE_EMAIL) { 401 projection = Queries.EMAIL.getProjection(); 402 403 if (directoryId == null || lookupKey == null) { 404 uri = Queries.EMAIL.getContentUri(); 405 desiredMimeType = null; 406 } else { 407 uri = Contacts.getLookupUri(contactId, lookupKey) 408 .buildUpon() 409 .appendPath(Contacts.Entity.CONTENT_DIRECTORY) 410 .appendQueryParameter( 411 ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(directoryId)) 412 .build(); 413 desiredMimeType = ContactsContract.CommonDataKinds.Email.CONTENT_ITEM_TYPE; 414 } 415 } else { 416 projection = Queries.PHONE.getProjection(); 417 418 if (lookupKey == null) { 419 uri = Queries.PHONE.getContentUri(); 420 desiredMimeType = null; 421 } else { 422 uri = Contacts.getLookupUri(contactId, lookupKey) 423 .buildUpon() 424 .appendPath(Contacts.Entity.CONTENT_DIRECTORY) 425 .appendQueryParameter( 426 ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(directoryId)) 427 .build(); 428 desiredMimeType = ContactsContract.CommonDataKinds.Phone.CONTENT_ITEM_TYPE; 429 } 430 } 431 432 final String selection = new StringBuilder() 433 .append(projection[Queries.Query.CONTACT_ID]) 434 .append(" = ?") 435 .toString(); 436 final Cursor cursor; 437 if (ChipsUtil.hasPermissions(context, permissionsCheckListener)) { 438 cursor = context.getContentResolver().query( 439 uri, projection, selection, new String[] {String.valueOf(contactId)}, null); 440 } else { 441 cursor = new MatrixCursor(projection); 442 } 443 444 final Cursor resultCursor = removeUndesiredDestinations(cursor, desiredMimeType, lookupKey); 445 cursor.close(); 446 447 return resultCursor; 448 } 449 450 /** 451 * @return a new cursor based on the given cursor with all duplicate destinations removed. 452 * 453 * It's only intended to use for the alternate list, so... 454 * - This method ignores all other fields and dedupe solely on the destination. Normally, 455 * if a cursor contains multiple contacts and they have the same destination, we'd still want 456 * to show both. 457 * - This method creates a MatrixCursor, so all data will be kept in memory. We wouldn't want 458 * to do this if the original cursor is large, but it's okay here because the alternate list 459 * won't be that big. 460 * 461 * @param desiredMimeType If this is non-<code>null</code>, only entries with this mime type 462 * will be added to the cursor 463 * @param lookupKey The lookup key used for this contact if there isn't one in the cursor. This 464 * should be the same one used in the query that returned the cursor 465 */ 466 // Visible for testing 467 static Cursor removeUndesiredDestinations(final Cursor original, final String desiredMimeType, 468 final String lookupKey) { 469 final MatrixCursor result = new MatrixCursor( 470 original.getColumnNames(), original.getCount()); 471 final HashSet<String> destinationsSeen = new HashSet<String>(); 472 473 String defaultDisplayName = null; 474 String defaultPhotoThumbnailUri = null; 475 int defaultDisplayNameSource = 0; 476 477 // Find some nice defaults in case we need them 478 original.moveToPosition(-1); 479 while (original.moveToNext()) { 480 final String mimeType = original.getString(Query.MIME_TYPE); 481 482 if (ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE.equals( 483 mimeType)) { 484 // Store this data 485 defaultDisplayName = original.getString(Query.NAME); 486 defaultPhotoThumbnailUri = original.getString(Query.PHOTO_THUMBNAIL_URI); 487 defaultDisplayNameSource = original.getInt(Query.DISPLAY_NAME_SOURCE); 488 break; 489 } 490 } 491 492 original.moveToPosition(-1); 493 while (original.moveToNext()) { 494 if (desiredMimeType != null) { 495 final String mimeType = original.getString(Query.MIME_TYPE); 496 if (!desiredMimeType.equals(mimeType)) { 497 continue; 498 } 499 } 500 final String destination = original.getString(Query.DESTINATION); 501 if (destinationsSeen.contains(destination)) { 502 continue; 503 } 504 destinationsSeen.add(destination); 505 506 final Object[] row = new Object[] { 507 original.getString(Query.NAME), 508 original.getString(Query.DESTINATION), 509 original.getInt(Query.DESTINATION_TYPE), 510 original.getString(Query.DESTINATION_LABEL), 511 original.getLong(Query.CONTACT_ID), 512 original.getLong(Query.DATA_ID), 513 original.getString(Query.PHOTO_THUMBNAIL_URI), 514 original.getInt(Query.DISPLAY_NAME_SOURCE), 515 original.getString(Query.LOOKUP_KEY), 516 original.getString(Query.MIME_TYPE) 517 }; 518 519 if (row[Query.NAME] == null) { 520 row[Query.NAME] = defaultDisplayName; 521 } 522 if (row[Query.PHOTO_THUMBNAIL_URI] == null) { 523 row[Query.PHOTO_THUMBNAIL_URI] = defaultPhotoThumbnailUri; 524 } 525 if ((Integer) row[Query.DISPLAY_NAME_SOURCE] == 0) { 526 row[Query.DISPLAY_NAME_SOURCE] = defaultDisplayNameSource; 527 } 528 if (row[Query.LOOKUP_KEY] == null) { 529 row[Query.LOOKUP_KEY] = lookupKey; 530 } 531 532 // Ensure we don't have two '?' like content://.../...?account_name=...?sz=... 533 final String photoThumbnailUri = (String) row[Query.PHOTO_THUMBNAIL_URI]; 534 if (photoThumbnailUri != null) { 535 if (sCorrectedPhotoUris.containsKey(photoThumbnailUri)) { 536 row[Query.PHOTO_THUMBNAIL_URI] = sCorrectedPhotoUris.get(photoThumbnailUri); 537 } else if (photoThumbnailUri.indexOf('?') != photoThumbnailUri.lastIndexOf('?')) { 538 final String[] parts = photoThumbnailUri.split("\\?"); 539 final StringBuilder correctedUriBuilder = new StringBuilder(); 540 for (int i = 0; i < parts.length; i++) { 541 if (i == 1) { 542 correctedUriBuilder.append("?"); // We only want one of these 543 } else if (i > 1) { 544 correctedUriBuilder.append("&"); // And we want these elsewhere 545 } 546 correctedUriBuilder.append(parts[i]); 547 } 548 549 final String correctedUri = correctedUriBuilder.toString(); 550 sCorrectedPhotoUris.put(photoThumbnailUri, correctedUri); 551 row[Query.PHOTO_THUMBNAIL_URI] = correctedUri; 552 } 553 } 554 555 result.addRow(row); 556 } 557 558 return result; 559 } 560 561 @Override 562 public long getItemId(int position) { 563 Cursor c = getCursor(); 564 if (c.moveToPosition(position)) { 565 c.getLong(Queries.Query.DATA_ID); 566 } 567 return -1; 568 } 569 570 public RecipientEntry getRecipientEntry(int position) { 571 Cursor c = getCursor(); 572 c.moveToPosition(position); 573 return RecipientEntry.constructTopLevelEntry( 574 c.getString(Queries.Query.NAME), 575 c.getInt(Queries.Query.DISPLAY_NAME_SOURCE), 576 c.getString(Queries.Query.DESTINATION), 577 c.getInt(Queries.Query.DESTINATION_TYPE), 578 c.getString(Queries.Query.DESTINATION_LABEL), 579 c.getLong(Queries.Query.CONTACT_ID), 580 mDirectoryId, 581 c.getLong(Queries.Query.DATA_ID), 582 c.getString(Queries.Query.PHOTO_THUMBNAIL_URI), 583 true, 584 c.getString(Queries.Query.LOOKUP_KEY)); 585 } 586 587 @Override 588 public View getView(int position, View convertView, ViewGroup parent) { 589 Cursor cursor = getCursor(); 590 cursor.moveToPosition(position); 591 if (convertView == null) { 592 convertView = mDropdownChipLayouter.newView(AdapterType.RECIPIENT_ALTERNATES); 593 } 594 if (cursor.getLong(Queries.Query.DATA_ID) == mCurrentId) { 595 mCheckedItemPosition = position; 596 if (mCheckedItemChangedListener != null) { 597 mCheckedItemChangedListener.onCheckedItemChanged(mCheckedItemPosition); 598 } 599 } 600 bindView(convertView, convertView.getContext(), cursor); 601 return convertView; 602 } 603 604 @Override 605 public void bindView(View view, Context context, Cursor cursor) { 606 int position = cursor.getPosition(); 607 RecipientEntry entry = getRecipientEntry(position); 608 609 mDropdownChipLayouter.bindView(view, null, entry, position, 610 AdapterType.RECIPIENT_ALTERNATES, null, mDeleteDrawable); 611 } 612 613 @Override 614 public View newView(Context context, Cursor cursor, ViewGroup parent) { 615 return mDropdownChipLayouter.newView(AdapterType.RECIPIENT_ALTERNATES); 616 } 617 618 /*package*/ static interface OnCheckedItemChangedListener { 619 public void onCheckedItemChanged(int position); 620 } 621 } 622