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