Home | History | Annotate | Download | only in chips
      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 {
     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