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