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