• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 (directoryId == null || 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         if (cursor != null) {
445             final Cursor resultCursor = removeUndesiredDestinations(cursor,
446                     desiredMimeType, lookupKey);
447             cursor.close();
448             return resultCursor;
449         }
450 
451         return cursor;
452     }
453 
454     /**
455      * @return a new cursor based on the given cursor with all duplicate destinations removed.
456      *
457      * It's only intended to use for the alternate list, so...
458      * - This method ignores all other fields and dedupe solely on the destination.  Normally,
459      * if a cursor contains multiple contacts and they have the same destination, we'd still want
460      * to show both.
461      * - This method creates a MatrixCursor, so all data will be kept in memory.  We wouldn't want
462      * to do this if the original cursor is large, but it's okay here because the alternate list
463      * won't be that big.
464      *
465      * @param desiredMimeType If this is non-<code>null</code>, only entries with this mime type
466      *            will be added to the cursor
467      * @param lookupKey The lookup key used for this contact if there isn't one in the cursor. This
468      *            should be the same one used in the query that returned the cursor
469      */
470     // Visible for testing
removeUndesiredDestinations(final Cursor original, final String desiredMimeType, final String lookupKey)471     static Cursor removeUndesiredDestinations(final Cursor original, final String desiredMimeType,
472             final String lookupKey) {
473         final MatrixCursor result = new MatrixCursor(
474                 original.getColumnNames(), original.getCount());
475         final HashSet<String> destinationsSeen = new HashSet<String>();
476 
477         String defaultDisplayName = null;
478         String defaultPhotoThumbnailUri = null;
479         int defaultDisplayNameSource = 0;
480 
481         // Find some nice defaults in case we need them
482         original.moveToPosition(-1);
483         while (original.moveToNext()) {
484             final String mimeType = original.getString(Query.MIME_TYPE);
485 
486             if (ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE.equals(
487                     mimeType)) {
488                 // Store this data
489                 defaultDisplayName = original.getString(Query.NAME);
490                 defaultPhotoThumbnailUri = original.getString(Query.PHOTO_THUMBNAIL_URI);
491                 defaultDisplayNameSource = original.getInt(Query.DISPLAY_NAME_SOURCE);
492                 break;
493             }
494         }
495 
496         original.moveToPosition(-1);
497         while (original.moveToNext()) {
498             if (desiredMimeType != null) {
499                 final String mimeType = original.getString(Query.MIME_TYPE);
500                 if (!desiredMimeType.equals(mimeType)) {
501                     continue;
502                 }
503             }
504             final String destination = original.getString(Query.DESTINATION);
505             if (destinationsSeen.contains(destination)) {
506                 continue;
507             }
508             destinationsSeen.add(destination);
509 
510             final Object[] row = new Object[] {
511                     original.getString(Query.NAME),
512                     original.getString(Query.DESTINATION),
513                     original.getInt(Query.DESTINATION_TYPE),
514                     original.getString(Query.DESTINATION_LABEL),
515                     original.getLong(Query.CONTACT_ID),
516                     original.getLong(Query.DATA_ID),
517                     original.getString(Query.PHOTO_THUMBNAIL_URI),
518                     original.getInt(Query.DISPLAY_NAME_SOURCE),
519                     original.getString(Query.LOOKUP_KEY),
520                     original.getString(Query.MIME_TYPE)
521             };
522 
523             if (row[Query.NAME] == null) {
524                 row[Query.NAME] = defaultDisplayName;
525             }
526             if (row[Query.PHOTO_THUMBNAIL_URI] == null) {
527                 row[Query.PHOTO_THUMBNAIL_URI] = defaultPhotoThumbnailUri;
528             }
529             if ((Integer) row[Query.DISPLAY_NAME_SOURCE] == 0) {
530                 row[Query.DISPLAY_NAME_SOURCE] = defaultDisplayNameSource;
531             }
532             if (row[Query.LOOKUP_KEY] == null) {
533                 row[Query.LOOKUP_KEY] = lookupKey;
534             }
535 
536             // Ensure we don't have two '?' like content://.../...?account_name=...?sz=...
537             final String photoThumbnailUri = (String) row[Query.PHOTO_THUMBNAIL_URI];
538             if (photoThumbnailUri != null) {
539                 if (sCorrectedPhotoUris.containsKey(photoThumbnailUri)) {
540                     row[Query.PHOTO_THUMBNAIL_URI] = sCorrectedPhotoUris.get(photoThumbnailUri);
541                 } else if (photoThumbnailUri.indexOf('?') != photoThumbnailUri.lastIndexOf('?')) {
542                     final String[] parts = photoThumbnailUri.split("\\?");
543                     final StringBuilder correctedUriBuilder = new StringBuilder();
544                     for (int i = 0; i < parts.length; i++) {
545                         if (i == 1) {
546                             correctedUriBuilder.append("?"); // We only want one of these
547                         } else if (i > 1) {
548                             correctedUriBuilder.append("&"); // And we want these elsewhere
549                         }
550                         correctedUriBuilder.append(parts[i]);
551                     }
552 
553                     final String correctedUri = correctedUriBuilder.toString();
554                     sCorrectedPhotoUris.put(photoThumbnailUri, correctedUri);
555                     row[Query.PHOTO_THUMBNAIL_URI] = correctedUri;
556                 }
557             }
558 
559             result.addRow(row);
560         }
561 
562         return result;
563     }
564 
565     @Override
getItemId(int position)566     public long getItemId(int position) {
567         Cursor c = getCursor();
568         if (c.moveToPosition(position)) {
569             c.getLong(Queries.Query.DATA_ID);
570         }
571         return -1;
572     }
573 
getRecipientEntry(int position)574     public RecipientEntry getRecipientEntry(int position) {
575         Cursor c = getCursor();
576         c.moveToPosition(position);
577         return RecipientEntry.constructTopLevelEntry(
578                 c.getString(Queries.Query.NAME),
579                 c.getInt(Queries.Query.DISPLAY_NAME_SOURCE),
580                 c.getString(Queries.Query.DESTINATION),
581                 c.getInt(Queries.Query.DESTINATION_TYPE),
582                 c.getString(Queries.Query.DESTINATION_LABEL),
583                 c.getLong(Queries.Query.CONTACT_ID),
584                 mDirectoryId,
585                 c.getLong(Queries.Query.DATA_ID),
586                 c.getString(Queries.Query.PHOTO_THUMBNAIL_URI),
587                 true,
588                 c.getString(Queries.Query.LOOKUP_KEY));
589     }
590 
591     @Override
getView(int position, View convertView, ViewGroup parent)592     public View getView(int position, View convertView, ViewGroup parent) {
593         Cursor cursor = getCursor();
594         cursor.moveToPosition(position);
595         if (convertView == null) {
596             convertView = mDropdownChipLayouter.newView(AdapterType.RECIPIENT_ALTERNATES);
597         }
598         if (cursor.getLong(Queries.Query.DATA_ID) == mCurrentId) {
599             mCheckedItemPosition = position;
600             if (mCheckedItemChangedListener != null) {
601                 mCheckedItemChangedListener.onCheckedItemChanged(mCheckedItemPosition);
602             }
603         }
604         bindView(convertView, convertView.getContext(), cursor);
605         return convertView;
606     }
607 
608     @Override
bindView(View view, Context context, Cursor cursor)609     public void bindView(View view, Context context, Cursor cursor) {
610         int position = cursor.getPosition();
611         RecipientEntry entry = getRecipientEntry(position);
612 
613         mDropdownChipLayouter.bindView(view, null, entry, position,
614                 AdapterType.RECIPIENT_ALTERNATES, null, mDeleteDrawable);
615     }
616 
617     @Override
newView(Context context, Cursor cursor, ViewGroup parent)618     public View newView(Context context, Cursor cursor, ViewGroup parent) {
619         return mDropdownChipLayouter.newView(AdapterType.RECIPIENT_ALTERNATES);
620     }
621 
622     /*package*/ static interface OnCheckedItemChangedListener {
onCheckedItemChanged(int position)623         public void onCheckedItemChanged(int position);
624     }
625 }
626