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.list;
     17 
     18 import com.android.contacts.ContactPhotoManager;
     19 import com.android.contacts.R;
     20 import com.android.contacts.widget.IndexerListAdapter;
     21 import com.android.contacts.widget.TextWithHighlightingFactory;
     22 
     23 import android.content.Context;
     24 import android.content.CursorLoader;
     25 import android.database.Cursor;
     26 import android.net.Uri;
     27 import android.os.Bundle;
     28 import android.provider.ContactsContract;
     29 import android.provider.ContactsContract.ContactCounts;
     30 import android.provider.ContactsContract.Contacts;
     31 import android.provider.ContactsContract.Directory;
     32 import android.text.TextUtils;
     33 import android.util.Log;
     34 import android.view.LayoutInflater;
     35 import android.view.View;
     36 import android.view.ViewGroup;
     37 import android.widget.QuickContactBadge;
     38 import android.widget.SectionIndexer;
     39 import android.widget.TextView;
     40 
     41 import java.util.HashSet;
     42 
     43 /**
     44  * Common base class for various contact-related lists, e.g. contact list, phone number list
     45  * etc.
     46  */
     47 public abstract class ContactEntryListAdapter extends IndexerListAdapter {
     48 
     49     private static final String TAG = "ContactEntryListAdapter";
     50 
     51     /**
     52      * Indicates whether the {@link Directory#LOCAL_INVISIBLE} directory should
     53      * be included in the search.
     54      */
     55     private static final boolean LOCAL_INVISIBLE_DIRECTORY_ENABLED = false;
     56 
     57     /**
     58      * The animation is used here to allocate animated name text views.
     59      */
     60     private TextWithHighlightingFactory mTextWithHighlightingFactory;
     61     private int mDisplayOrder;
     62     private int mSortOrder;
     63     private boolean mNameHighlightingEnabled;
     64 
     65     private boolean mDisplayPhotos;
     66     private boolean mQuickContactEnabled;
     67 
     68     /**
     69      * indicates if contact queries include profile
     70      */
     71     private boolean mIncludeProfile;
     72 
     73     /**
     74      * indicates if query results includes a profile
     75      */
     76     private boolean mProfileExists;
     77 
     78     private ContactPhotoManager mPhotoLoader;
     79 
     80     private String mQueryString;
     81     private char[] mUpperCaseQueryString;
     82     private boolean mSearchMode;
     83     private int mDirectorySearchMode;
     84     private int mDirectoryResultLimit = Integer.MAX_VALUE;
     85 
     86     private boolean mLoading = true;
     87     private boolean mEmptyListEnabled = true;
     88 
     89     private boolean mSelectionVisible;
     90 
     91     private ContactListFilter mFilter;
     92     private String mContactsCount = "";
     93     private boolean mDarkTheme = false;
     94 
     95     public ContactEntryListAdapter(Context context) {
     96         super(context);
     97         addPartitions();
     98     }
     99 
    100     @Override
    101     protected View createPinnedSectionHeaderView(Context context, ViewGroup parent) {
    102         return new ContactListPinnedHeaderView(context, null);
    103     }
    104 
    105     @Override
    106     protected void setPinnedSectionTitle(View pinnedHeaderView, String title) {
    107         ((ContactListPinnedHeaderView)pinnedHeaderView).setSectionHeader(title);
    108     }
    109 
    110     @Override
    111     protected void setPinnedHeaderContactsCount(View header) {
    112         // Update the header with the contacts count only if a profile header exists
    113         // otherwise, the contacts count are shown in the empty profile header view
    114         if (mProfileExists) {
    115             ((ContactListPinnedHeaderView)header).setCountView(mContactsCount);
    116         } else {
    117             clearPinnedHeaderContactsCount(header);
    118         }
    119     }
    120 
    121     @Override
    122     protected void clearPinnedHeaderContactsCount(View header) {
    123         ((ContactListPinnedHeaderView)header).setCountView(null);
    124     }
    125 
    126     protected void addPartitions() {
    127         addPartition(createDefaultDirectoryPartition());
    128     }
    129 
    130     protected DirectoryPartition createDefaultDirectoryPartition() {
    131         DirectoryPartition partition = new DirectoryPartition(true, true);
    132         partition.setDirectoryId(Directory.DEFAULT);
    133         partition.setDirectoryType(getContext().getString(R.string.contactsList));
    134         partition.setPriorityDirectory(true);
    135         partition.setPhotoSupported(true);
    136         return partition;
    137     }
    138 
    139     private int getPartitionByDirectoryId(long id) {
    140         int count = getPartitionCount();
    141         for (int i = 0; i < count; i++) {
    142             Partition partition = getPartition(i);
    143             if (partition instanceof DirectoryPartition) {
    144                 if (((DirectoryPartition)partition).getDirectoryId() == id) {
    145                     return i;
    146                 }
    147             }
    148         }
    149         return -1;
    150     }
    151 
    152     public abstract String getContactDisplayName(int position);
    153     public abstract void configureLoader(CursorLoader loader, long directoryId);
    154 
    155     /**
    156      * Marks all partitions as "loading"
    157      */
    158     public void onDataReload() {
    159         boolean notify = false;
    160         int count = getPartitionCount();
    161         for (int i = 0; i < count; i++) {
    162             Partition partition = getPartition(i);
    163             if (partition instanceof DirectoryPartition) {
    164                 DirectoryPartition directoryPartition = (DirectoryPartition)partition;
    165                 if (!directoryPartition.isLoading()) {
    166                     notify = true;
    167                 }
    168                 directoryPartition.setStatus(DirectoryPartition.STATUS_NOT_LOADED);
    169             }
    170         }
    171         if (notify) {
    172             notifyDataSetChanged();
    173         }
    174     }
    175 
    176     @Override
    177     public void clearPartitions() {
    178         int count = getPartitionCount();
    179         for (int i = 0; i < count; i++) {
    180             Partition partition = getPartition(i);
    181             if (partition instanceof DirectoryPartition) {
    182                 DirectoryPartition directoryPartition = (DirectoryPartition)partition;
    183                 directoryPartition.setStatus(DirectoryPartition.STATUS_NOT_LOADED);
    184             }
    185         }
    186         super.clearPartitions();
    187     }
    188 
    189     public boolean isSearchMode() {
    190         return mSearchMode;
    191     }
    192 
    193     public void setSearchMode(boolean flag) {
    194         mSearchMode = flag;
    195     }
    196 
    197     public String getQueryString() {
    198         return mQueryString;
    199     }
    200 
    201     public void setQueryString(String queryString) {
    202         mQueryString = queryString;
    203         if (TextUtils.isEmpty(queryString)) {
    204             mUpperCaseQueryString = null;
    205         } else {
    206             mUpperCaseQueryString = queryString.toUpperCase().toCharArray();
    207         }
    208     }
    209 
    210     public char[] getUpperCaseQueryString() {
    211         return mUpperCaseQueryString;
    212     }
    213 
    214     public int getDirectorySearchMode() {
    215         return mDirectorySearchMode;
    216     }
    217 
    218     public void setDirectorySearchMode(int mode) {
    219         mDirectorySearchMode = mode;
    220     }
    221 
    222     public int getDirectoryResultLimit() {
    223         return mDirectoryResultLimit;
    224     }
    225 
    226     public void setDirectoryResultLimit(int limit) {
    227         this.mDirectoryResultLimit = limit;
    228     }
    229 
    230     public int getContactNameDisplayOrder() {
    231         return mDisplayOrder;
    232     }
    233 
    234     public void setContactNameDisplayOrder(int displayOrder) {
    235         mDisplayOrder = displayOrder;
    236     }
    237 
    238     public int getSortOrder() {
    239         return mSortOrder;
    240     }
    241 
    242     public void setSortOrder(int sortOrder) {
    243         mSortOrder = sortOrder;
    244     }
    245 
    246     public void setPhotoLoader(ContactPhotoManager photoLoader) {
    247         mPhotoLoader = photoLoader;
    248     }
    249 
    250     protected ContactPhotoManager getPhotoLoader() {
    251         return mPhotoLoader;
    252     }
    253 
    254     public boolean getDisplayPhotos() {
    255         return mDisplayPhotos;
    256     }
    257 
    258     public void setDisplayPhotos(boolean displayPhotos) {
    259         mDisplayPhotos = displayPhotos;
    260     }
    261 
    262     public boolean isEmptyListEnabled() {
    263         return mEmptyListEnabled;
    264     }
    265 
    266     public void setEmptyListEnabled(boolean flag) {
    267         mEmptyListEnabled = flag;
    268     }
    269 
    270     public boolean isSelectionVisible() {
    271         return mSelectionVisible;
    272     }
    273 
    274     public void setSelectionVisible(boolean flag) {
    275         this.mSelectionVisible = flag;
    276     }
    277 
    278     public boolean isQuickContactEnabled() {
    279         return mQuickContactEnabled;
    280     }
    281 
    282     public void setQuickContactEnabled(boolean quickContactEnabled) {
    283         mQuickContactEnabled = quickContactEnabled;
    284     }
    285 
    286     public boolean shouldIncludeProfile() {
    287         return mIncludeProfile;
    288     }
    289 
    290     public void setIncludeProfile(boolean includeProfile) {
    291         mIncludeProfile = includeProfile;
    292     }
    293 
    294     public void setProfileExists(boolean exists) {
    295         mProfileExists = exists;
    296         // Stick the "ME" header for the profile
    297         if (exists) {
    298             SectionIndexer indexer = getIndexer();
    299             if (indexer != null) {
    300                 ((ContactsSectionIndexer) indexer).setProfileHeader(
    301                         getContext().getString(R.string.user_profile_contacts_list_header));
    302             }
    303         }
    304     }
    305 
    306     public boolean hasProfile() {
    307         return mProfileExists;
    308     }
    309 
    310     public void setDarkTheme(boolean value) {
    311         mDarkTheme = value;
    312     }
    313 
    314     public void configureDirectoryLoader(DirectoryListLoader loader) {
    315         loader.setDirectorySearchMode(mDirectorySearchMode);
    316         loader.setLocalInvisibleDirectoryEnabled(LOCAL_INVISIBLE_DIRECTORY_ENABLED);
    317     }
    318 
    319     /**
    320      * Updates partitions according to the directory meta-data contained in the supplied
    321      * cursor.
    322      */
    323     public void changeDirectories(Cursor cursor) {
    324         if (cursor.getCount() == 0) {
    325             // Directory table must have at least local directory, without which this adapter will
    326             // enter very weird state.
    327             Log.e(TAG, "Directory search loader returned an empty cursor, which implies we have " +
    328                     "no directory entries.", new RuntimeException());
    329             return;
    330         }
    331         HashSet<Long> directoryIds = new HashSet<Long>();
    332 
    333         int idColumnIndex = cursor.getColumnIndex(Directory._ID);
    334         int directoryTypeColumnIndex = cursor.getColumnIndex(DirectoryListLoader.DIRECTORY_TYPE);
    335         int displayNameColumnIndex = cursor.getColumnIndex(Directory.DISPLAY_NAME);
    336         int photoSupportColumnIndex = cursor.getColumnIndex(Directory.PHOTO_SUPPORT);
    337 
    338         // TODO preserve the order of partition to match those of the cursor
    339         // Phase I: add new directories
    340         cursor.moveToPosition(-1);
    341         while (cursor.moveToNext()) {
    342             long id = cursor.getLong(idColumnIndex);
    343             directoryIds.add(id);
    344             if (getPartitionByDirectoryId(id) == -1) {
    345                 DirectoryPartition partition = new DirectoryPartition(false, true);
    346                 partition.setDirectoryId(id);
    347                 partition.setDirectoryType(cursor.getString(directoryTypeColumnIndex));
    348                 partition.setDisplayName(cursor.getString(displayNameColumnIndex));
    349                 int photoSupport = cursor.getInt(photoSupportColumnIndex);
    350                 partition.setPhotoSupported(photoSupport == Directory.PHOTO_SUPPORT_THUMBNAIL_ONLY
    351                         || photoSupport == Directory.PHOTO_SUPPORT_FULL);
    352                 addPartition(partition);
    353             }
    354         }
    355 
    356         // Phase II: remove deleted directories
    357         int count = getPartitionCount();
    358         for (int i = count; --i >= 0; ) {
    359             Partition partition = getPartition(i);
    360             if (partition instanceof DirectoryPartition) {
    361                 long id = ((DirectoryPartition)partition).getDirectoryId();
    362                 if (!directoryIds.contains(id)) {
    363                     removePartition(i);
    364                 }
    365             }
    366         }
    367 
    368         invalidate();
    369         notifyDataSetChanged();
    370     }
    371 
    372     @Override
    373     public void changeCursor(int partitionIndex, Cursor cursor) {
    374         if (partitionIndex >= getPartitionCount()) {
    375             // There is no partition for this data
    376             return;
    377         }
    378 
    379         Partition partition = getPartition(partitionIndex);
    380         if (partition instanceof DirectoryPartition) {
    381             ((DirectoryPartition)partition).setStatus(DirectoryPartition.STATUS_LOADED);
    382         }
    383 
    384         if (mDisplayPhotos && mPhotoLoader != null && isPhotoSupported(partitionIndex)) {
    385             mPhotoLoader.refreshCache();
    386         }
    387 
    388         super.changeCursor(partitionIndex, cursor);
    389 
    390         if (isSectionHeaderDisplayEnabled() && partitionIndex == getIndexedPartition()) {
    391             updateIndexer(cursor);
    392         }
    393     }
    394 
    395     public void changeCursor(Cursor cursor) {
    396         changeCursor(0, cursor);
    397     }
    398 
    399     /**
    400      * Updates the indexer, which is used to produce section headers.
    401      */
    402     private void updateIndexer(Cursor cursor) {
    403         if (cursor == null) {
    404             setIndexer(null);
    405             return;
    406         }
    407 
    408         Bundle bundle = cursor.getExtras();
    409         if (bundle.containsKey(ContactCounts.EXTRA_ADDRESS_BOOK_INDEX_TITLES)) {
    410             String sections[] =
    411                     bundle.getStringArray(ContactCounts.EXTRA_ADDRESS_BOOK_INDEX_TITLES);
    412             int counts[] = bundle.getIntArray(ContactCounts.EXTRA_ADDRESS_BOOK_INDEX_COUNTS);
    413             setIndexer(new ContactsSectionIndexer(sections, counts));
    414         } else {
    415             setIndexer(null);
    416         }
    417     }
    418 
    419     @Override
    420     public int getViewTypeCount() {
    421         // We need a separate view type for each item type, plus another one for
    422         // each type with header, plus one for "other".
    423         return getItemViewTypeCount() * 2 + 1;
    424     }
    425 
    426     @Override
    427     public int getItemViewType(int partitionIndex, int position) {
    428         int type = super.getItemViewType(partitionIndex, position);
    429         if (!isUserProfile(position)
    430                 && isSectionHeaderDisplayEnabled()
    431                 && partitionIndex == getIndexedPartition()) {
    432             Placement placement = getItemPlacementInSection(position);
    433             return placement.firstInSection ? type : getItemViewTypeCount() + type;
    434         } else {
    435             return type;
    436         }
    437     }
    438 
    439     @Override
    440     public boolean isEmpty() {
    441         // TODO
    442 //        if (contactsListActivity.mProviderStatus != ProviderStatus.STATUS_NORMAL) {
    443 //            return true;
    444 //        }
    445 
    446         if (!mEmptyListEnabled) {
    447             return false;
    448         } else if (isSearchMode()) {
    449             return TextUtils.isEmpty(getQueryString());
    450         } else if (mLoading) {
    451             // We don't want the empty state to show when loading.
    452             return false;
    453         } else {
    454             return super.isEmpty();
    455         }
    456     }
    457 
    458     public boolean isLoading() {
    459         int count = getPartitionCount();
    460         for (int i = 0; i < count; i++) {
    461             Partition partition = getPartition(i);
    462             if (partition instanceof DirectoryPartition
    463                     && ((DirectoryPartition) partition).isLoading()) {
    464                 return true;
    465             }
    466         }
    467         return false;
    468     }
    469 
    470     public boolean areAllPartitionsEmpty() {
    471         int count = getPartitionCount();
    472         for (int i = 0; i < count; i++) {
    473             if (!isPartitionEmpty(i)) {
    474                 return false;
    475             }
    476         }
    477         return true;
    478     }
    479 
    480     /**
    481      * Changes visibility parameters for the default directory partition.
    482      */
    483     public void configureDefaultPartition(boolean showIfEmpty, boolean hasHeader) {
    484         int defaultPartitionIndex = -1;
    485         int count = getPartitionCount();
    486         for (int i = 0; i < count; i++) {
    487             Partition partition = getPartition(i);
    488             if (partition instanceof DirectoryPartition &&
    489                     ((DirectoryPartition)partition).getDirectoryId() == Directory.DEFAULT) {
    490                 defaultPartitionIndex = i;
    491                 break;
    492             }
    493         }
    494         if (defaultPartitionIndex != -1) {
    495             setShowIfEmpty(defaultPartitionIndex, showIfEmpty);
    496             setHasHeader(defaultPartitionIndex, hasHeader);
    497         }
    498     }
    499 
    500     @Override
    501     protected View newHeaderView(Context context, int partition, Cursor cursor,
    502             ViewGroup parent) {
    503         LayoutInflater inflater = LayoutInflater.from(context);
    504         return inflater.inflate(R.layout.directory_header, parent, false);
    505     }
    506 
    507     @Override
    508     protected void bindHeaderView(View view, int partitionIndex, Cursor cursor) {
    509         Partition partition = getPartition(partitionIndex);
    510         if (!(partition instanceof DirectoryPartition)) {
    511             return;
    512         }
    513 
    514         DirectoryPartition directoryPartition = (DirectoryPartition)partition;
    515         long directoryId = directoryPartition.getDirectoryId();
    516         TextView labelTextView = (TextView)view.findViewById(R.id.label);
    517         TextView displayNameTextView = (TextView)view.findViewById(R.id.display_name);
    518         if (directoryId == Directory.DEFAULT || directoryId == Directory.LOCAL_INVISIBLE) {
    519             labelTextView.setText(R.string.local_search_label);
    520             displayNameTextView.setText(null);
    521         } else {
    522             labelTextView.setText(R.string.directory_search_label);
    523             String directoryName = directoryPartition.getDisplayName();
    524             String displayName = !TextUtils.isEmpty(directoryName)
    525                     ? directoryName
    526                     : directoryPartition.getDirectoryType();
    527             displayNameTextView.setText(displayName);
    528         }
    529 
    530         TextView countText = (TextView)view.findViewById(R.id.count);
    531         if (directoryPartition.isLoading()) {
    532             countText.setText(R.string.search_results_searching);
    533         } else {
    534             int count = cursor == null ? 0 : cursor.getCount();
    535             if (directoryId != Directory.DEFAULT && directoryId != Directory.LOCAL_INVISIBLE
    536                     && count >= getDirectoryResultLimit()) {
    537                 countText.setText(mContext.getString(
    538                         R.string.foundTooManyContacts, getDirectoryResultLimit()));
    539             } else {
    540                 countText.setText(getQuantityText(
    541                         count, R.string.listFoundAllContactsZero, R.plurals.searchFoundContacts));
    542             }
    543         }
    544     }
    545 
    546     /**
    547      * Checks whether the contact entry at the given position represents the user's profile.
    548      */
    549     protected boolean isUserProfile(int position) {
    550         // The profile only ever appears in the first position if it is present.  So if the position
    551         // is anything beyond 0, it can't be the profile.
    552         boolean isUserProfile = false;
    553         if (position == 0) {
    554             int partition = getPartitionForPosition(position);
    555             if (partition >= 0) {
    556                 // Save the old cursor position - the call to getItem() may modify the cursor
    557                 // position.
    558                 int offset = getCursor(partition).getPosition();
    559                 Cursor cursor = (Cursor) getItem(position);
    560                 if (cursor != null) {
    561                     int profileColumnIndex = cursor.getColumnIndex(Contacts.IS_USER_PROFILE);
    562                     if (profileColumnIndex != -1) {
    563                         isUserProfile = cursor.getInt(profileColumnIndex) == 1;
    564                     }
    565                     // Restore the old cursor position.
    566                     cursor.moveToPosition(offset);
    567                 }
    568             }
    569         }
    570         return isUserProfile;
    571     }
    572 
    573     // TODO: fix PluralRules to handle zero correctly and use Resources.getQuantityText directly
    574     public String getQuantityText(int count, int zeroResourceId, int pluralResourceId) {
    575         if (count == 0) {
    576             return getContext().getString(zeroResourceId);
    577         } else {
    578             String format = getContext().getResources()
    579                     .getQuantityText(pluralResourceId, count).toString();
    580             return String.format(format, count);
    581         }
    582     }
    583 
    584     public boolean isPhotoSupported(int partitionIndex) {
    585         Partition partition = getPartition(partitionIndex);
    586         if (partition instanceof DirectoryPartition) {
    587             return ((DirectoryPartition) partition).isPhotoSupported();
    588         }
    589         return true;
    590     }
    591 
    592     /**
    593      * Returns the currently selected filter.
    594      */
    595     public ContactListFilter getFilter() {
    596         return mFilter;
    597     }
    598 
    599     public void setFilter(ContactListFilter filter) {
    600         mFilter = filter;
    601     }
    602 
    603     // TODO: move sharable logic (bindXX() methods) to here with extra arguments
    604 
    605     protected void bindQuickContact(final ContactListItemView view, int partitionIndex,
    606             Cursor cursor, int photoIdColumn, int contactIdColumn, int lookUpKeyColumn) {
    607         long photoId = 0;
    608         if (!cursor.isNull(photoIdColumn)) {
    609             photoId = cursor.getLong(photoIdColumn);
    610         }
    611 
    612         QuickContactBadge quickContact = view.getQuickContact();
    613         quickContact.assignContactUri(
    614                 getContactUri(partitionIndex, cursor, contactIdColumn, lookUpKeyColumn));
    615         getPhotoLoader().loadPhoto(quickContact, photoId, false, mDarkTheme);
    616     }
    617 
    618     protected Uri getContactUri(int partitionIndex, Cursor cursor,
    619             int contactIdColumn, int lookUpKeyColumn) {
    620         long contactId = cursor.getLong(contactIdColumn);
    621         String lookupKey = cursor.getString(lookUpKeyColumn);
    622         Uri uri = Contacts.getLookupUri(contactId, lookupKey);
    623         long directoryId = ((DirectoryPartition)getPartition(partitionIndex)).getDirectoryId();
    624         if (directoryId != Directory.DEFAULT) {
    625             uri = uri.buildUpon().appendQueryParameter(
    626                     ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(directoryId)).build();
    627         }
    628         return uri;
    629     }
    630 
    631     public void setContactsCount(String count) {
    632         mContactsCount = count;
    633     }
    634 
    635     public String getContactsCount() {
    636         return mContactsCount;
    637     }
    638 }
    639