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