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