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 { matchesFound(Map<String, RecipientEntry> results)71 public void matchesFound(Map<String, RecipientEntry> results); 72 /** 73 * Called with all addresses that could not be resolved to valid recipients. 74 */ matchesNotFound(Set<String> unfoundAddresses)75 public void matchesNotFound(Set<String> unfoundAddresses); 76 } 77 getMatchingRecipients(Context context, BaseRecipientAdapter adapter, ArrayList<String> inAddresses, Account account, RecipientMatchCallback callback, ChipsUtil.PermissionsCheckListener permissionsCheckListener)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 */ getMatchingRecipients(Context context, BaseRecipientAdapter adapter, ArrayList<String> inAddresses, int addressType, Account account, RecipientMatchCallback callback, ChipsUtil.PermissionsCheckListener permissionsCheckListener)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 getMatchingRecipientsFromDirectoryQueries(Context context, Map<String, RecipientEntry> recipientEntries, Set<String> addresses, Account account, Set<String> matchesNotFound, RecipientMatchCallback callback, ChipsUtil.PermissionsCheckListener permissionsCheckListener)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 getMatchingRecipientsFromDirectoryQueries(Context context, Map<String, RecipientEntry> recipientEntries, Set<String> addresses, Account account, Set<String> matchesNotFound, Queries.Query query, RecipientMatchCallback callback, ChipsUtil.PermissionsCheckListener permissionsCheckListener)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 getMatchingRecipientsFromExtensionMatcher(BaseRecipientAdapter adapter, Set<String> matchesNotFound, RecipientMatchCallback callback)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 processContactEntries(Cursor c, Long directoryId)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 */ getBetterRecipient(final RecipientEntry entry1, final RecipientEntry entry2)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 doQuery(CharSequence constraint, int limit, Long directoryId, Account account, Context context, Query query, ChipsUtil.PermissionsCheckListener permissionsCheckListener)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 RecipientAlternatesAdapter(Context context, long contactId, Long directoryId, String lookupKey, long currentId, int queryMode, OnCheckedItemChangedListener listener, DropdownChipLayouter dropdownChipLayouter, ChipsUtil.PermissionsCheckListener permissionsCheckListener)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 RecipientAlternatesAdapter(Context context, long contactId, Long directoryId, String lookupKey, long currentId, int queryMode, OnCheckedItemChangedListener listener, DropdownChipLayouter dropdownChipLayouter, StateListDrawable deleteDrawable, ChipsUtil.PermissionsCheckListener permissionsCheckListener)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 getCursorForConstruction(Context context, long contactId, Long directoryId, String lookupKey, int queryType, ChipsUtil.PermissionsCheckListener permissionsCheckListener)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 removeUndesiredDestinations(final Cursor original, final String desiredMimeType, final String lookupKey)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 getItemId(int position)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 getRecipientEntry(int position)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 getView(int position, View convertView, ViewGroup parent)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 bindView(View view, Context context, Cursor cursor)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 newView(Context context, Cursor cursor, ViewGroup parent)614 public View newView(Context context, Cursor cursor, ViewGroup parent) { 615 return mDropdownChipLayouter.newView(AdapterType.RECIPIENT_ALTERNATES); 616 } 617 618 /*package*/ static interface OnCheckedItemChangedListener { onCheckedItemChanged(int position)619 public void onCheckedItemChanged(int position); 620 } 621 } 622