Home | History | Annotate | Download | only in list
      1 /*
      2  * Copyright (C) 2010 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 package com.android.contacts.common.list;
     17 
     18 import android.content.ContentUris;
     19 import android.content.Context;
     20 import android.content.CursorLoader;
     21 import android.database.Cursor;
     22 import android.net.Uri;
     23 import android.net.Uri.Builder;
     24 import android.provider.ContactsContract;
     25 import android.provider.ContactsContract.CommonDataKinds.Callable;
     26 import android.provider.ContactsContract.CommonDataKinds.Phone;
     27 import android.provider.ContactsContract.CommonDataKinds.SipAddress;
     28 import android.provider.ContactsContract.ContactCounts;
     29 import android.provider.ContactsContract.Contacts;
     30 import android.provider.ContactsContract.Data;
     31 import android.provider.ContactsContract.Directory;
     32 import android.util.Log;
     33 import android.view.View;
     34 import android.view.ViewGroup;
     35 
     36 import com.android.contacts.common.GeoUtil;
     37 import com.android.contacts.common.R;
     38 import com.android.contacts.common.extensions.ExtendedPhoneDirectoriesManager;
     39 import com.android.contacts.common.extensions.ExtensionsFactory;
     40 import com.android.contacts.common.util.Constants;
     41 
     42 import java.util.ArrayList;
     43 import java.util.List;
     44 
     45 /**
     46  * A cursor adapter for the {@link Phone#CONTENT_ITEM_TYPE} and
     47  * {@link SipAddress#CONTENT_ITEM_TYPE}.
     48  *
     49  * By default this adapter just handles phone numbers. When {@link #setUseCallableUri(boolean)} is
     50  * called with "true", this adapter starts handling SIP addresses too, by using {@link Callable}
     51  * API instead of {@link Phone}.
     52  */
     53 public class PhoneNumberListAdapter extends ContactEntryListAdapter {
     54 
     55     private static final String TAG = PhoneNumberListAdapter.class.getSimpleName();
     56 
     57     // A list of extended directories to add to the directories from the database
     58     private final List<DirectoryPartition> mExtendedDirectories;
     59 
     60     // Extended directories will have ID's that are higher than any of the id's from the database.
     61     // Thi sis so that we can identify them and set them up properly. If no extended directories
     62     // exist, this will be Long.MAX_VALUE
     63     private long mFirstExtendedDirectoryId = Long.MAX_VALUE;
     64 
     65     public static class PhoneQuery {
     66         public static final String[] PROJECTION_PRIMARY = new String[] {
     67             Phone._ID,                          // 0
     68             Phone.TYPE,                         // 1
     69             Phone.LABEL,                        // 2
     70             Phone.NUMBER,                       // 3
     71             Phone.CONTACT_ID,                   // 4
     72             Phone.LOOKUP_KEY,                   // 5
     73             Phone.PHOTO_ID,                     // 6
     74             Phone.DISPLAY_NAME_PRIMARY,         // 7
     75             Phone.PHOTO_THUMBNAIL_URI,          // 8
     76         };
     77 
     78         public static final String[] PROJECTION_ALTERNATIVE = new String[] {
     79             Phone._ID,                          // 0
     80             Phone.TYPE,                         // 1
     81             Phone.LABEL,                        // 2
     82             Phone.NUMBER,                       // 3
     83             Phone.CONTACT_ID,                   // 4
     84             Phone.LOOKUP_KEY,                   // 5
     85             Phone.PHOTO_ID,                     // 6
     86             Phone.DISPLAY_NAME_ALTERNATIVE,     // 7
     87             Phone.PHOTO_THUMBNAIL_URI,          // 8
     88         };
     89 
     90         public static final int PHONE_ID                = 0;
     91         public static final int PHONE_TYPE              = 1;
     92         public static final int PHONE_LABEL             = 2;
     93         public static final int PHONE_NUMBER            = 3;
     94         public static final int CONTACT_ID              = 4;
     95         public static final int LOOKUP_KEY              = 5;
     96         public static final int PHOTO_ID                = 6;
     97         public static final int DISPLAY_NAME            = 7;
     98         public static final int PHOTO_URI               = 8;
     99     }
    100 
    101     private final CharSequence mUnknownNameText;
    102 
    103     private ContactListItemView.PhotoPosition mPhotoPosition;
    104 
    105     private boolean mUseCallableUri;
    106 
    107     public PhoneNumberListAdapter(Context context) {
    108         super(context);
    109         setDefaultFilterHeaderText(R.string.list_filter_phones);
    110         mUnknownNameText = context.getText(android.R.string.unknownName);
    111 
    112         final ExtendedPhoneDirectoriesManager manager
    113                 = ExtensionsFactory.getExtendedPhoneDirectoriesManager();
    114         if (manager != null) {
    115             mExtendedDirectories = manager.getExtendedDirectories(mContext);
    116         } else {
    117             // Empty list to avoid sticky NPE's
    118             mExtendedDirectories = new ArrayList<DirectoryPartition>();
    119         }
    120     }
    121 
    122     protected CharSequence getUnknownNameText() {
    123         return mUnknownNameText;
    124     }
    125 
    126     @Override
    127     public void configureLoader(CursorLoader loader, long directoryId) {
    128         String query = getQueryString();
    129         if (query == null) {
    130             query = "";
    131         }
    132         if (isExtendedDirectory(directoryId)) {
    133             final DirectoryPartition directory = getExtendedDirectoryFromId(directoryId);
    134             final String contentUri = directory.getContentUri();
    135             if (contentUri == null) {
    136                 throw new IllegalStateException("Extended directory must have a content URL: "
    137                         + directory);
    138             }
    139             final Builder builder = Uri.parse(contentUri).buildUpon();
    140             builder.appendPath(query);
    141             builder.appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY,
    142                     String.valueOf(getDirectoryResultLimit(directory)));
    143             loader.setUri(builder.build());
    144             loader.setProjection(PhoneQuery.PROJECTION_PRIMARY);
    145         } else {
    146             final boolean isRemoteDirectoryQuery = isRemoteDirectory(directoryId);
    147             final Builder builder;
    148             if (isSearchMode()) {
    149                 final Uri baseUri;
    150                 if (isRemoteDirectoryQuery) {
    151                     baseUri = Phone.CONTENT_FILTER_URI;
    152                 } else if (mUseCallableUri) {
    153                     baseUri = Callable.CONTENT_FILTER_URI;
    154                 } else {
    155                     baseUri = Phone.CONTENT_FILTER_URI;
    156                 }
    157                 builder = baseUri.buildUpon();
    158                 builder.appendPath(query);      // Builder will encode the query
    159                 builder.appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY,
    160                         String.valueOf(directoryId));
    161                 if (isRemoteDirectoryQuery) {
    162                     builder.appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY,
    163                             String.valueOf(getDirectoryResultLimit(getDirectoryById(directoryId))));
    164                 }
    165             } else {
    166                 final Uri baseUri = mUseCallableUri ? Callable.CONTENT_URI : Phone.CONTENT_URI;
    167                 builder = baseUri.buildUpon().appendQueryParameter(
    168                         ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Directory.DEFAULT));
    169                 if (isSectionHeaderDisplayEnabled()) {
    170                     builder.appendQueryParameter(ContactCounts.ADDRESS_BOOK_INDEX_EXTRAS, "true");
    171                 }
    172                 applyFilter(loader, builder, directoryId, getFilter());
    173             }
    174 
    175             // Remove duplicates when it is possible.
    176             builder.appendQueryParameter(ContactsContract.REMOVE_DUPLICATE_ENTRIES, "true");
    177             loader.setUri(builder.build());
    178 
    179             // TODO a projection that includes the search snippet
    180             if (getContactNameDisplayOrder() ==
    181                     ContactsContract.Preferences.DISPLAY_ORDER_PRIMARY) {
    182                 loader.setProjection(PhoneQuery.PROJECTION_PRIMARY);
    183             } else {
    184                 loader.setProjection(PhoneQuery.PROJECTION_ALTERNATIVE);
    185             }
    186 
    187             if (getSortOrder() == ContactsContract.Preferences.SORT_ORDER_PRIMARY) {
    188                 loader.setSortOrder(Phone.SORT_KEY_PRIMARY);
    189             } else {
    190                 loader.setSortOrder(Phone.SORT_KEY_ALTERNATIVE);
    191             }
    192         }
    193     }
    194 
    195     protected boolean isExtendedDirectory(long directoryId) {
    196         return directoryId >= mFirstExtendedDirectoryId;
    197     }
    198 
    199     private DirectoryPartition getExtendedDirectoryFromId(long directoryId) {
    200         final int directoryIndex = (int) (directoryId - mFirstExtendedDirectoryId);
    201         return mExtendedDirectories.get(directoryIndex);
    202     }
    203 
    204     /**
    205      * Configure {@code loader} and {@code uriBuilder} according to {@code directoryId} and {@code
    206      * filter}.
    207      */
    208     private void applyFilter(CursorLoader loader, Uri.Builder uriBuilder, long directoryId,
    209             ContactListFilter filter) {
    210         if (filter == null || directoryId != Directory.DEFAULT) {
    211             return;
    212         }
    213 
    214         final StringBuilder selection = new StringBuilder();
    215         final List<String> selectionArgs = new ArrayList<String>();
    216 
    217         switch (filter.filterType) {
    218             case ContactListFilter.FILTER_TYPE_CUSTOM: {
    219                 selection.append(Contacts.IN_VISIBLE_GROUP + "=1");
    220                 selection.append(" AND " + Contacts.HAS_PHONE_NUMBER + "=1");
    221                 break;
    222             }
    223             case ContactListFilter.FILTER_TYPE_ACCOUNT: {
    224                 filter.addAccountQueryParameterToUrl(uriBuilder);
    225                 break;
    226             }
    227             case ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS:
    228             case ContactListFilter.FILTER_TYPE_DEFAULT:
    229                 break; // No selection needed.
    230             case ContactListFilter.FILTER_TYPE_WITH_PHONE_NUMBERS_ONLY:
    231                 break; // This adapter is always "phone only", so no selection needed either.
    232             default:
    233                 Log.w(TAG, "Unsupported filter type came " +
    234                         "(type: " + filter.filterType + ", toString: " + filter + ")" +
    235                         " showing all contacts.");
    236                 // No selection.
    237                 break;
    238         }
    239         loader.setSelection(selection.toString());
    240         loader.setSelectionArgs(selectionArgs.toArray(new String[0]));
    241     }
    242 
    243     @Override
    244     public String getContactDisplayName(int position) {
    245         return ((Cursor) getItem(position)).getString(PhoneQuery.DISPLAY_NAME);
    246     }
    247 
    248     public String getPhoneNumber(int position) {
    249         final Cursor item = (Cursor)getItem(position);
    250         return item != null ? item.getString(PhoneQuery.PHONE_NUMBER) : null;
    251     }
    252 
    253     /**
    254      * Builds a {@link Data#CONTENT_URI} for the given cursor position.
    255      *
    256      * @return Uri for the data. may be null if the cursor is not ready.
    257      */
    258     public Uri getDataUri(int position) {
    259         final int partitionIndex = getPartitionForPosition(position);
    260         final Cursor item = (Cursor)getItem(position);
    261         return item != null ? getDataUri(partitionIndex, item) : null;
    262     }
    263 
    264     public Uri getDataUri(int partitionIndex, Cursor cursor) {
    265         final long directoryId =
    266                 ((DirectoryPartition)getPartition(partitionIndex)).getDirectoryId();
    267         if (!isRemoteDirectory(directoryId)) {
    268             final long phoneId = cursor.getLong(PhoneQuery.PHONE_ID);
    269             return ContentUris.withAppendedId(Data.CONTENT_URI, phoneId);
    270         }
    271         return null;
    272     }
    273 
    274     @Override
    275     protected View newView(Context context, int partition, Cursor cursor, int position,
    276             ViewGroup parent) {
    277         final ContactListItemView view = new ContactListItemView(context, null);
    278         view.setUnknownNameText(mUnknownNameText);
    279         view.setQuickContactEnabled(isQuickContactEnabled());
    280         view.setPhotoPosition(mPhotoPosition);
    281         return view;
    282     }
    283 
    284     protected void setHighlight(ContactListItemView view, Cursor cursor) {
    285         view.setHighlightedPrefix(isSearchMode() ? getUpperCaseQueryString() : null);
    286     }
    287 
    288     // Override default, which would return number of phone numbers, so we
    289     // instead return number of contacts.
    290     @Override
    291     protected int getResultCount(Cursor cursor) {
    292         if (cursor == null) {
    293             return 0;
    294         }
    295         cursor.moveToPosition(-1);
    296         long curContactId = -1;
    297         int numContacts = 0;
    298         while(cursor.moveToNext()) {
    299             final long contactId = cursor.getLong(PhoneQuery.CONTACT_ID);
    300             if (contactId != curContactId) {
    301                 curContactId = contactId;
    302                 ++numContacts;
    303             }
    304         }
    305         return numContacts;
    306     }
    307 
    308     @Override
    309     protected void bindView(View itemView, int partition, Cursor cursor, int position) {
    310         ContactListItemView view = (ContactListItemView)itemView;
    311 
    312         setHighlight(view, cursor);
    313 
    314         // Look at elements before and after this position, checking if contact IDs are same.
    315         // If they have one same contact ID, it means they can be grouped.
    316         //
    317         // In one group, only the first entry will show its photo and its name, and the other
    318         // entries in the group show just their data (e.g. phone number, email address).
    319         cursor.moveToPosition(position);
    320         boolean isFirstEntry = true;
    321         boolean showBottomDivider = true;
    322         final long currentContactId = cursor.getLong(PhoneQuery.CONTACT_ID);
    323         if (cursor.moveToPrevious() && !cursor.isBeforeFirst()) {
    324             final long previousContactId = cursor.getLong(PhoneQuery.CONTACT_ID);
    325             if (currentContactId == previousContactId) {
    326                 isFirstEntry = false;
    327             }
    328         }
    329         cursor.moveToPosition(position);
    330         if (cursor.moveToNext() && !cursor.isAfterLast()) {
    331             final long nextContactId = cursor.getLong(PhoneQuery.CONTACT_ID);
    332             if (currentContactId == nextContactId) {
    333                 // The following entry should be in the same group, which means we don't want a
    334                 // divider between them.
    335                 // TODO: we want a different divider than the divider between groups. Just hiding
    336                 // this divider won't be enough.
    337                 showBottomDivider = false;
    338             }
    339         }
    340         cursor.moveToPosition(position);
    341 
    342         bindSectionHeaderAndDivider(view, position);
    343         if (isFirstEntry) {
    344             bindName(view, cursor);
    345             if (isQuickContactEnabled()) {
    346                 bindQuickContact(view, partition, cursor, PhoneQuery.PHOTO_ID,
    347                         PhoneQuery.PHOTO_URI, PhoneQuery.CONTACT_ID,
    348                         PhoneQuery.LOOKUP_KEY);
    349             } else {
    350                 if (getDisplayPhotos()) {
    351                     bindPhoto(view, partition, cursor);
    352                 }
    353             }
    354         } else {
    355             unbindName(view);
    356 
    357             view.removePhotoView(true, false);
    358         }
    359 
    360         final DirectoryPartition directory = (DirectoryPartition) getPartition(partition);
    361         bindPhoneNumber(view, cursor, directory.isDisplayNumber());
    362         view.setDividerVisible(showBottomDivider);
    363     }
    364 
    365     protected void bindPhoneNumber(ContactListItemView view, Cursor cursor, boolean displayNumber) {
    366         CharSequence label = null;
    367         if (displayNumber &&  !cursor.isNull(PhoneQuery.PHONE_TYPE)) {
    368             final int type = cursor.getInt(PhoneQuery.PHONE_TYPE);
    369             final String customLabel = cursor.getString(PhoneQuery.PHONE_LABEL);
    370 
    371             // TODO cache
    372             label = Phone.getTypeLabel(getContext().getResources(), type, customLabel);
    373         }
    374         view.setLabel(label);
    375         final String text;
    376         if (displayNumber) {
    377             text = cursor.getString(PhoneQuery.PHONE_NUMBER);
    378         } else {
    379             // Display phone label. If that's null, display geocoded location for the number
    380             final String phoneLabel = cursor.getString(PhoneQuery.PHONE_LABEL);
    381             if (phoneLabel != null) {
    382                 text = phoneLabel;
    383             } else {
    384                 final String phoneNumber = cursor.getString(PhoneQuery.PHONE_NUMBER);
    385                 text = GeoUtil.getGeocodedLocationFor(mContext, phoneNumber);
    386             }
    387         }
    388         view.setPhoneNumber(text);
    389     }
    390 
    391     protected void bindSectionHeaderAndDivider(final ContactListItemView view, int position) {
    392         if (isSectionHeaderDisplayEnabled()) {
    393             Placement placement = getItemPlacementInSection(position);
    394             view.setSectionHeader(placement.firstInSection ? placement.sectionHeader : null);
    395             view.setDividerVisible(!placement.lastInSection);
    396         } else {
    397             view.setSectionHeader(null);
    398             view.setDividerVisible(true);
    399         }
    400     }
    401 
    402     protected void bindName(final ContactListItemView view, Cursor cursor) {
    403         view.showDisplayName(cursor, PhoneQuery.DISPLAY_NAME, getContactNameDisplayOrder());
    404         // Note: we don't show phonetic names any more (see issue 5265330)
    405     }
    406 
    407     protected void unbindName(final ContactListItemView view) {
    408         view.hideDisplayName();
    409     }
    410 
    411     protected void bindPhoto(final ContactListItemView view, int partitionIndex, Cursor cursor) {
    412         if (!isPhotoSupported(partitionIndex)) {
    413             view.removePhotoView();
    414             return;
    415         }
    416 
    417         long photoId = 0;
    418         if (!cursor.isNull(PhoneQuery.PHOTO_ID)) {
    419             photoId = cursor.getLong(PhoneQuery.PHOTO_ID);
    420         }
    421 
    422         if (photoId != 0) {
    423             getPhotoLoader().loadThumbnail(view.getPhotoView(), photoId, false);
    424         } else {
    425             final String photoUriString = cursor.getString(PhoneQuery.PHOTO_URI);
    426             final Uri photoUri = photoUriString == null ? null : Uri.parse(photoUriString);
    427             getPhotoLoader().loadDirectoryPhoto(view.getPhotoView(), photoUri, false);
    428         }
    429     }
    430 
    431     public void setPhotoPosition(ContactListItemView.PhotoPosition photoPosition) {
    432         mPhotoPosition = photoPosition;
    433     }
    434 
    435     public ContactListItemView.PhotoPosition getPhotoPosition() {
    436         return mPhotoPosition;
    437     }
    438 
    439     public void setUseCallableUri(boolean useCallableUri) {
    440         mUseCallableUri = useCallableUri;
    441     }
    442 
    443     public boolean usesCallableUri() {
    444         return mUseCallableUri;
    445     }
    446 
    447     /**
    448      * Override base implementation to inject extended directories between local & remote
    449      * directories. This is done in the following steps:
    450      * 1. Call base implementation to add directories from the cursor.
    451      * 2. Iterate all base directories and establish the following information:
    452      *   a. The highest directory id so that we can assign unused id's to the extended directories.
    453      *   b. The index of the last non-remote directory. This is where we will insert extended
    454      *      directories.
    455      * 3. Iterate the extended directories and for each one, assign an ID and insert it in the
    456      *    proper location.
    457      */
    458     @Override
    459     public void changeDirectories(Cursor cursor) {
    460         super.changeDirectories(cursor);
    461         if (getDirectorySearchMode() == DirectoryListLoader.SEARCH_MODE_NONE) {
    462             return;
    463         }
    464         final int numExtendedDirectories = mExtendedDirectories.size();
    465         if (getPartitionCount() == cursor.getCount() + numExtendedDirectories) {
    466             // already added all directories;
    467             return;
    468         }
    469         //
    470         mFirstExtendedDirectoryId = Long.MAX_VALUE;
    471         if (numExtendedDirectories > 0) {
    472             // The Directory.LOCAL_INVISIBLE is not in the cursor but we can't reuse it's
    473             // "special" ID.
    474             long maxId = Directory.LOCAL_INVISIBLE;
    475             int insertIndex = 0;
    476             for (int i = 0, n = getPartitionCount(); i < n; i++) {
    477                 final DirectoryPartition partition = (DirectoryPartition) getPartition(i);
    478                 final long id = partition.getDirectoryId();
    479                 if (id > maxId) {
    480                     maxId = id;
    481                 }
    482                 if (!isRemoteDirectory(id)) {
    483                     // assuming remote directories come after local, we will end up with the index
    484                     // where we should insert extended directories. This also works if there are no
    485                     // remote directories at all.
    486                     insertIndex = i + 1;
    487                 }
    488             }
    489             // Extended directories ID's cannot collide with base directories
    490             mFirstExtendedDirectoryId = maxId + 1;
    491             for (int i = 0; i < numExtendedDirectories; i++) {
    492                 final long id = mFirstExtendedDirectoryId + i;
    493                 final DirectoryPartition directory = mExtendedDirectories.get(i);
    494                 if (getPartitionByDirectoryId(id) == -1) {
    495                     addPartition(insertIndex, directory);
    496                     directory.setDirectoryId(id);
    497                 }
    498             }
    499         }
    500     }
    501 
    502     protected Uri getContactUri(int partitionIndex, Cursor cursor,
    503             int contactIdColumn, int lookUpKeyColumn) {
    504         final DirectoryPartition directory = (DirectoryPartition) getPartition(partitionIndex);
    505         final long directoryId = directory.getDirectoryId();
    506         if (!isExtendedDirectory(directoryId)) {
    507             return super.getContactUri(partitionIndex, cursor, contactIdColumn, lookUpKeyColumn);
    508         }
    509         return Contacts.CONTENT_LOOKUP_URI.buildUpon()
    510                 .appendPath(Constants.LOOKUP_URI_ENCODED)
    511                 .appendQueryParameter(Directory.DISPLAY_NAME, directory.getLabel())
    512                 .appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY,
    513                         String.valueOf(directoryId))
    514                 .encodedFragment(cursor.getString(lookUpKeyColumn))
    515                 .build();
    516     }
    517 }
    518