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.text.TextUtils;
     33 import android.util.Log;
     34 import android.view.View;
     35 import android.view.ViewGroup;
     36 
     37 import com.android.contacts.common.R;
     38 
     39 import java.util.ArrayList;
     40 import java.util.List;
     41 
     42 /**
     43  * A cursor adapter for the {@link Phone#CONTENT_ITEM_TYPE} and
     44  * {@link SipAddress#CONTENT_ITEM_TYPE}.
     45  *
     46  * By default this adapter just handles phone numbers. When {@link #setUseCallableUri(boolean)} is
     47  * called with "true", this adapter starts handling SIP addresses too, by using {@link Callable}
     48  * API instead of {@link Phone}.
     49  */
     50 public class PhoneNumberListAdapter extends ContactEntryListAdapter {
     51     private static final String TAG = PhoneNumberListAdapter.class.getSimpleName();
     52 
     53     protected static class PhoneQuery {
     54         private static final String[] PROJECTION_PRIMARY = new String[] {
     55             Phone._ID,                          // 0
     56             Phone.TYPE,                         // 1
     57             Phone.LABEL,                        // 2
     58             Phone.NUMBER,                       // 3
     59             Phone.CONTACT_ID,                   // 4
     60             Phone.LOOKUP_KEY,                   // 5
     61             Phone.PHOTO_ID,                     // 6
     62             Phone.DISPLAY_NAME_PRIMARY,         // 7
     63         };
     64 
     65         private static final String[] PROJECTION_ALTERNATIVE = new String[] {
     66             Phone._ID,                          // 0
     67             Phone.TYPE,                         // 1
     68             Phone.LABEL,                        // 2
     69             Phone.NUMBER,                       // 3
     70             Phone.CONTACT_ID,                   // 4
     71             Phone.LOOKUP_KEY,                   // 5
     72             Phone.PHOTO_ID,                     // 6
     73             Phone.DISPLAY_NAME_ALTERNATIVE,     // 7
     74         };
     75 
     76         public static final int PHONE_ID           = 0;
     77         public static final int PHONE_TYPE         = 1;
     78         public static final int PHONE_LABEL        = 2;
     79         public static final int PHONE_NUMBER       = 3;
     80         public static final int PHONE_CONTACT_ID   = 4;
     81         public static final int PHONE_LOOKUP_KEY   = 5;
     82         public static final int PHONE_PHOTO_ID     = 6;
     83         public static final int PHONE_DISPLAY_NAME = 7;
     84     }
     85 
     86     private final CharSequence mUnknownNameText;
     87 
     88     private ContactListItemView.PhotoPosition mPhotoPosition;
     89 
     90     private boolean mUseCallableUri;
     91 
     92     public PhoneNumberListAdapter(Context context) {
     93         super(context);
     94         setDefaultFilterHeaderText(R.string.list_filter_phones);
     95         mUnknownNameText = context.getText(android.R.string.unknownName);
     96     }
     97 
     98     protected CharSequence getUnknownNameText() {
     99         return mUnknownNameText;
    100     }
    101 
    102     @Override
    103     public void configureLoader(CursorLoader loader, long directoryId) {
    104         if (directoryId != Directory.DEFAULT) {
    105             Log.w(TAG, "PhoneNumberListAdapter is not ready for non-default directory ID ("
    106                     + "directoryId: " + directoryId + ")");
    107         }
    108 
    109         final Builder builder;
    110         if (isSearchMode()) {
    111             final Uri baseUri =
    112                     mUseCallableUri ? Callable.CONTENT_FILTER_URI : Phone.CONTENT_FILTER_URI;
    113             builder = baseUri.buildUpon();
    114             final String query = getQueryString();
    115             if (TextUtils.isEmpty(query)) {
    116                 builder.appendPath("");
    117             } else {
    118                 builder.appendPath(query);      // Builder will encode the query
    119             }
    120             builder.appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY,
    121                     String.valueOf(directoryId));
    122         } else {
    123             final Uri baseUri = mUseCallableUri ? Callable.CONTENT_URI : Phone.CONTENT_URI;
    124             builder = baseUri.buildUpon().appendQueryParameter(
    125                     ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Directory.DEFAULT));
    126             if (isSectionHeaderDisplayEnabled()) {
    127                 builder.appendQueryParameter(ContactCounts.ADDRESS_BOOK_INDEX_EXTRAS, "true");
    128             }
    129             applyFilter(loader, builder, directoryId, getFilter());
    130         }
    131 
    132         // Remove duplicates when it is possible.
    133         builder.appendQueryParameter(ContactsContract.REMOVE_DUPLICATE_ENTRIES, "true");
    134         loader.setUri(builder.build());
    135 
    136         // TODO a projection that includes the search snippet
    137         if (getContactNameDisplayOrder() == ContactsContract.Preferences.DISPLAY_ORDER_PRIMARY) {
    138             loader.setProjection(PhoneQuery.PROJECTION_PRIMARY);
    139         } else {
    140             loader.setProjection(PhoneQuery.PROJECTION_ALTERNATIVE);
    141         }
    142 
    143         if (getSortOrder() == ContactsContract.Preferences.SORT_ORDER_PRIMARY) {
    144             loader.setSortOrder(Phone.SORT_KEY_PRIMARY);
    145         } else {
    146             loader.setSortOrder(Phone.SORT_KEY_ALTERNATIVE);
    147         }
    148     }
    149 
    150     /**
    151      * Configure {@code loader} and {@code uriBuilder} according to {@code directoryId} and {@code
    152      * filter}.
    153      */
    154     private void applyFilter(CursorLoader loader, Uri.Builder uriBuilder, long directoryId,
    155             ContactListFilter filter) {
    156         if (filter == null || directoryId != Directory.DEFAULT) {
    157             return;
    158         }
    159 
    160         final StringBuilder selection = new StringBuilder();
    161         final List<String> selectionArgs = new ArrayList<String>();
    162 
    163         switch (filter.filterType) {
    164             case ContactListFilter.FILTER_TYPE_CUSTOM: {
    165                 selection.append(Contacts.IN_VISIBLE_GROUP + "=1");
    166                 selection.append(" AND " + Contacts.HAS_PHONE_NUMBER + "=1");
    167                 break;
    168             }
    169             case ContactListFilter.FILTER_TYPE_ACCOUNT: {
    170                 filter.addAccountQueryParameterToUrl(uriBuilder);
    171                 break;
    172             }
    173             case ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS:
    174             case ContactListFilter.FILTER_TYPE_DEFAULT:
    175                 break; // No selection needed.
    176             case ContactListFilter.FILTER_TYPE_WITH_PHONE_NUMBERS_ONLY:
    177                 break; // This adapter is always "phone only", so no selection needed either.
    178             default:
    179                 Log.w(TAG, "Unsupported filter type came " +
    180                         "(type: " + filter.filterType + ", toString: " + filter + ")" +
    181                         " showing all contacts.");
    182                 // No selection.
    183                 break;
    184         }
    185         loader.setSelection(selection.toString());
    186         loader.setSelectionArgs(selectionArgs.toArray(new String[0]));
    187     }
    188 
    189     @Override
    190     public String getContactDisplayName(int position) {
    191         return ((Cursor) getItem(position)).getString(PhoneQuery.PHONE_DISPLAY_NAME);
    192     }
    193 
    194     /**
    195      * Builds a {@link Data#CONTENT_URI} for the given cursor position.
    196      *
    197      * @return Uri for the data. may be null if the cursor is not ready.
    198      */
    199     public Uri getDataUri(int position) {
    200         Cursor cursor = ((Cursor)getItem(position));
    201         if (cursor != null) {
    202             long id = cursor.getLong(PhoneQuery.PHONE_ID);
    203             return ContentUris.withAppendedId(Data.CONTENT_URI, id);
    204         } else {
    205             Log.w(TAG, "Cursor was null in getDataUri() call. Returning null instead.");
    206             return null;
    207         }
    208     }
    209 
    210     @Override
    211     protected View newView(Context context, int partition, Cursor cursor, int position,
    212             ViewGroup parent) {
    213         final ContactListItemView view = new ContactListItemView(context, null);
    214         view.setUnknownNameText(mUnknownNameText);
    215         view.setQuickContactEnabled(isQuickContactEnabled());
    216         view.setPhotoPosition(mPhotoPosition);
    217         return view;
    218     }
    219 
    220     @Override
    221     protected void bindView(View itemView, int partition, Cursor cursor, int position) {
    222         ContactListItemView view = (ContactListItemView)itemView;
    223 
    224         view.setHighlightedPrefix(isSearchMode() ? getUpperCaseQueryString() : null);
    225 
    226         // Look at elements before and after this position, checking if contact IDs are same.
    227         // If they have one same contact ID, it means they can be grouped.
    228         //
    229         // In one group, only the first entry will show its photo and its name, and the other
    230         // entries in the group show just their data (e.g. phone number, email address).
    231         cursor.moveToPosition(position);
    232         boolean isFirstEntry = true;
    233         boolean showBottomDivider = true;
    234         final long currentContactId = cursor.getLong(PhoneQuery.PHONE_CONTACT_ID);
    235         if (cursor.moveToPrevious() && !cursor.isBeforeFirst()) {
    236             final long previousContactId = cursor.getLong(PhoneQuery.PHONE_CONTACT_ID);
    237             if (currentContactId == previousContactId) {
    238                 isFirstEntry = false;
    239             }
    240         }
    241         cursor.moveToPosition(position);
    242         if (cursor.moveToNext() && !cursor.isAfterLast()) {
    243             final long nextContactId = cursor.getLong(PhoneQuery.PHONE_CONTACT_ID);
    244             if (currentContactId == nextContactId) {
    245                 // The following entry should be in the same group, which means we don't want a
    246                 // divider between them.
    247                 // TODO: we want a different divider than the divider between groups. Just hiding
    248                 // this divider won't be enough.
    249                 showBottomDivider = false;
    250             }
    251         }
    252         cursor.moveToPosition(position);
    253 
    254         bindSectionHeaderAndDivider(view, position);
    255         if (isFirstEntry) {
    256             bindName(view, cursor);
    257             if (isQuickContactEnabled()) {
    258                 // No need for photo uri here, because we can not have directory results. If we
    259                 // ever do, we need to add photo uri to the query
    260                 bindQuickContact(view, partition, cursor, PhoneQuery.PHONE_PHOTO_ID, -1,
    261                         PhoneQuery.PHONE_CONTACT_ID, PhoneQuery.PHONE_LOOKUP_KEY);
    262             } else {
    263                 bindPhoto(view, cursor);
    264             }
    265         } else {
    266             unbindName(view);
    267 
    268             view.removePhotoView(true, false);
    269         }
    270         bindPhoneNumber(view, cursor);
    271         view.setDividerVisible(showBottomDivider);
    272     }
    273 
    274     protected void bindPhoneNumber(ContactListItemView view, Cursor cursor) {
    275         CharSequence label = null;
    276         if (!cursor.isNull(PhoneQuery.PHONE_TYPE)) {
    277             final int type = cursor.getInt(PhoneQuery.PHONE_TYPE);
    278             final String customLabel = cursor.getString(PhoneQuery.PHONE_LABEL);
    279 
    280             // TODO cache
    281             label = Phone.getTypeLabel(getContext().getResources(), type, customLabel);
    282         }
    283         view.setLabel(label);
    284         view.showData(cursor, PhoneQuery.PHONE_NUMBER);
    285     }
    286 
    287     protected void bindSectionHeaderAndDivider(final ContactListItemView view, int position) {
    288         if (isSectionHeaderDisplayEnabled()) {
    289             Placement placement = getItemPlacementInSection(position);
    290             view.setSectionHeader(placement.firstInSection ? placement.sectionHeader : null);
    291             view.setDividerVisible(!placement.lastInSection);
    292         } else {
    293             view.setSectionHeader(null);
    294             view.setDividerVisible(true);
    295         }
    296     }
    297 
    298     protected void bindName(final ContactListItemView view, Cursor cursor) {
    299         view.showDisplayName(cursor, PhoneQuery.PHONE_DISPLAY_NAME, getContactNameDisplayOrder());
    300         // Note: we don't show phonetic names any more (see issue 5265330)
    301     }
    302 
    303     protected void unbindName(final ContactListItemView view) {
    304         view.hideDisplayName();
    305     }
    306 
    307     protected void bindPhoto(final ContactListItemView view, Cursor cursor) {
    308         long photoId = 0;
    309         if (!cursor.isNull(PhoneQuery.PHONE_PHOTO_ID)) {
    310             photoId = cursor.getLong(PhoneQuery.PHONE_PHOTO_ID);
    311         }
    312 
    313         getPhotoLoader().loadThumbnail(view.getPhotoView(), photoId, false);
    314     }
    315 
    316     public void setPhotoPosition(ContactListItemView.PhotoPosition photoPosition) {
    317         mPhotoPosition = photoPosition;
    318     }
    319 
    320     public ContactListItemView.PhotoPosition getPhotoPosition() {
    321         return mPhotoPosition;
    322     }
    323 
    324     public void setUseCallableUri(boolean useCallableUri) {
    325         mUseCallableUri = useCallableUri;
    326     }
    327 
    328     public boolean usesCallableUri() {
    329         return mUseCallableUri;
    330     }
    331 }
    332