| /* |
| * Copyright (C) 2010 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package com.android.contacts.list; |
| |
| import com.android.contacts.ContactPhotoManager; |
| import com.android.contacts.R; |
| import com.android.contacts.widget.IndexerListAdapter; |
| import com.android.contacts.widget.TextWithHighlightingFactory; |
| |
| import android.content.Context; |
| import android.content.CursorLoader; |
| import android.database.Cursor; |
| import android.net.Uri; |
| import android.os.Bundle; |
| import android.provider.ContactsContract; |
| import android.provider.ContactsContract.ContactCounts; |
| import android.provider.ContactsContract.Contacts; |
| import android.provider.ContactsContract.Directory; |
| import android.text.TextUtils; |
| import android.util.Log; |
| import android.view.LayoutInflater; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.widget.QuickContactBadge; |
| import android.widget.SectionIndexer; |
| import android.widget.TextView; |
| |
| import java.util.HashSet; |
| |
| /** |
| * Common base class for various contact-related lists, e.g. contact list, phone number list |
| * etc. |
| */ |
| public abstract class ContactEntryListAdapter extends IndexerListAdapter { |
| |
| private static final String TAG = "ContactEntryListAdapter"; |
| |
| /** |
| * Indicates whether the {@link Directory#LOCAL_INVISIBLE} directory should |
| * be included in the search. |
| */ |
| private static final boolean LOCAL_INVISIBLE_DIRECTORY_ENABLED = false; |
| |
| /** |
| * The animation is used here to allocate animated name text views. |
| */ |
| private TextWithHighlightingFactory mTextWithHighlightingFactory; |
| private int mDisplayOrder; |
| private int mSortOrder; |
| private boolean mNameHighlightingEnabled; |
| |
| private boolean mDisplayPhotos; |
| private boolean mQuickContactEnabled; |
| |
| /** |
| * indicates if contact queries include profile |
| */ |
| private boolean mIncludeProfile; |
| |
| /** |
| * indicates if query results includes a profile |
| */ |
| private boolean mProfileExists; |
| |
| private ContactPhotoManager mPhotoLoader; |
| |
| private String mQueryString; |
| private char[] mUpperCaseQueryString; |
| private boolean mSearchMode; |
| private int mDirectorySearchMode; |
| private int mDirectoryResultLimit = Integer.MAX_VALUE; |
| |
| private boolean mLoading = true; |
| private boolean mEmptyListEnabled = true; |
| |
| private boolean mSelectionVisible; |
| |
| private ContactListFilter mFilter; |
| private String mContactsCount = ""; |
| private boolean mDarkTheme = false; |
| |
| public ContactEntryListAdapter(Context context) { |
| super(context); |
| addPartitions(); |
| } |
| |
| @Override |
| protected View createPinnedSectionHeaderView(Context context, ViewGroup parent) { |
| return new ContactListPinnedHeaderView(context, null); |
| } |
| |
| @Override |
| protected void setPinnedSectionTitle(View pinnedHeaderView, String title) { |
| ((ContactListPinnedHeaderView)pinnedHeaderView).setSectionHeader(title); |
| } |
| |
| @Override |
| protected void setPinnedHeaderContactsCount(View header) { |
| // Update the header with the contacts count only if a profile header exists |
| // otherwise, the contacts count are shown in the empty profile header view |
| if (mProfileExists) { |
| ((ContactListPinnedHeaderView)header).setCountView(mContactsCount); |
| } else { |
| clearPinnedHeaderContactsCount(header); |
| } |
| } |
| |
| @Override |
| protected void clearPinnedHeaderContactsCount(View header) { |
| ((ContactListPinnedHeaderView)header).setCountView(null); |
| } |
| |
| protected void addPartitions() { |
| addPartition(createDefaultDirectoryPartition()); |
| } |
| |
| protected DirectoryPartition createDefaultDirectoryPartition() { |
| DirectoryPartition partition = new DirectoryPartition(true, true); |
| partition.setDirectoryId(Directory.DEFAULT); |
| partition.setDirectoryType(getContext().getString(R.string.contactsList)); |
| partition.setPriorityDirectory(true); |
| partition.setPhotoSupported(true); |
| return partition; |
| } |
| |
| private int getPartitionByDirectoryId(long id) { |
| int count = getPartitionCount(); |
| for (int i = 0; i < count; i++) { |
| Partition partition = getPartition(i); |
| if (partition instanceof DirectoryPartition) { |
| if (((DirectoryPartition)partition).getDirectoryId() == id) { |
| return i; |
| } |
| } |
| } |
| return -1; |
| } |
| |
| public abstract String getContactDisplayName(int position); |
| public abstract void configureLoader(CursorLoader loader, long directoryId); |
| |
| /** |
| * Marks all partitions as "loading" |
| */ |
| public void onDataReload() { |
| boolean notify = false; |
| int count = getPartitionCount(); |
| for (int i = 0; i < count; i++) { |
| Partition partition = getPartition(i); |
| if (partition instanceof DirectoryPartition) { |
| DirectoryPartition directoryPartition = (DirectoryPartition)partition; |
| if (!directoryPartition.isLoading()) { |
| notify = true; |
| } |
| directoryPartition.setStatus(DirectoryPartition.STATUS_NOT_LOADED); |
| } |
| } |
| if (notify) { |
| notifyDataSetChanged(); |
| } |
| } |
| |
| @Override |
| public void clearPartitions() { |
| int count = getPartitionCount(); |
| for (int i = 0; i < count; i++) { |
| Partition partition = getPartition(i); |
| if (partition instanceof DirectoryPartition) { |
| DirectoryPartition directoryPartition = (DirectoryPartition)partition; |
| directoryPartition.setStatus(DirectoryPartition.STATUS_NOT_LOADED); |
| } |
| } |
| super.clearPartitions(); |
| } |
| |
| public boolean isSearchMode() { |
| return mSearchMode; |
| } |
| |
| public void setSearchMode(boolean flag) { |
| mSearchMode = flag; |
| } |
| |
| public String getQueryString() { |
| return mQueryString; |
| } |
| |
| public void setQueryString(String queryString) { |
| mQueryString = queryString; |
| if (TextUtils.isEmpty(queryString)) { |
| mUpperCaseQueryString = null; |
| } else { |
| mUpperCaseQueryString = queryString.toUpperCase().toCharArray(); |
| } |
| } |
| |
| public char[] getUpperCaseQueryString() { |
| return mUpperCaseQueryString; |
| } |
| |
| public int getDirectorySearchMode() { |
| return mDirectorySearchMode; |
| } |
| |
| public void setDirectorySearchMode(int mode) { |
| mDirectorySearchMode = mode; |
| } |
| |
| public int getDirectoryResultLimit() { |
| return mDirectoryResultLimit; |
| } |
| |
| public void setDirectoryResultLimit(int limit) { |
| this.mDirectoryResultLimit = limit; |
| } |
| |
| public int getContactNameDisplayOrder() { |
| return mDisplayOrder; |
| } |
| |
| public void setContactNameDisplayOrder(int displayOrder) { |
| mDisplayOrder = displayOrder; |
| } |
| |
| public int getSortOrder() { |
| return mSortOrder; |
| } |
| |
| public void setSortOrder(int sortOrder) { |
| mSortOrder = sortOrder; |
| } |
| |
| public void setPhotoLoader(ContactPhotoManager photoLoader) { |
| mPhotoLoader = photoLoader; |
| } |
| |
| protected ContactPhotoManager getPhotoLoader() { |
| return mPhotoLoader; |
| } |
| |
| public boolean getDisplayPhotos() { |
| return mDisplayPhotos; |
| } |
| |
| public void setDisplayPhotos(boolean displayPhotos) { |
| mDisplayPhotos = displayPhotos; |
| } |
| |
| public boolean isEmptyListEnabled() { |
| return mEmptyListEnabled; |
| } |
| |
| public void setEmptyListEnabled(boolean flag) { |
| mEmptyListEnabled = flag; |
| } |
| |
| public boolean isSelectionVisible() { |
| return mSelectionVisible; |
| } |
| |
| public void setSelectionVisible(boolean flag) { |
| this.mSelectionVisible = flag; |
| } |
| |
| public boolean isQuickContactEnabled() { |
| return mQuickContactEnabled; |
| } |
| |
| public void setQuickContactEnabled(boolean quickContactEnabled) { |
| mQuickContactEnabled = quickContactEnabled; |
| } |
| |
| public boolean shouldIncludeProfile() { |
| return mIncludeProfile; |
| } |
| |
| public void setIncludeProfile(boolean includeProfile) { |
| mIncludeProfile = includeProfile; |
| } |
| |
| public void setProfileExists(boolean exists) { |
| mProfileExists = exists; |
| // Stick the "ME" header for the profile |
| if (exists) { |
| SectionIndexer indexer = getIndexer(); |
| if (indexer != null) { |
| ((ContactsSectionIndexer) indexer).setProfileHeader( |
| getContext().getString(R.string.user_profile_contacts_list_header)); |
| } |
| } |
| } |
| |
| public boolean hasProfile() { |
| return mProfileExists; |
| } |
| |
| public void setDarkTheme(boolean value) { |
| mDarkTheme = value; |
| } |
| |
| public void configureDirectoryLoader(DirectoryListLoader loader) { |
| loader.setDirectorySearchMode(mDirectorySearchMode); |
| loader.setLocalInvisibleDirectoryEnabled(LOCAL_INVISIBLE_DIRECTORY_ENABLED); |
| } |
| |
| /** |
| * Updates partitions according to the directory meta-data contained in the supplied |
| * cursor. |
| */ |
| public void changeDirectories(Cursor cursor) { |
| if (cursor.getCount() == 0) { |
| // Directory table must have at least local directory, without which this adapter will |
| // enter very weird state. |
| Log.e(TAG, "Directory search loader returned an empty cursor, which implies we have " + |
| "no directory entries.", new RuntimeException()); |
| return; |
| } |
| HashSet<Long> directoryIds = new HashSet<Long>(); |
| |
| int idColumnIndex = cursor.getColumnIndex(Directory._ID); |
| int directoryTypeColumnIndex = cursor.getColumnIndex(DirectoryListLoader.DIRECTORY_TYPE); |
| int displayNameColumnIndex = cursor.getColumnIndex(Directory.DISPLAY_NAME); |
| int photoSupportColumnIndex = cursor.getColumnIndex(Directory.PHOTO_SUPPORT); |
| |
| // TODO preserve the order of partition to match those of the cursor |
| // Phase I: add new directories |
| cursor.moveToPosition(-1); |
| while (cursor.moveToNext()) { |
| long id = cursor.getLong(idColumnIndex); |
| directoryIds.add(id); |
| if (getPartitionByDirectoryId(id) == -1) { |
| DirectoryPartition partition = new DirectoryPartition(false, true); |
| partition.setDirectoryId(id); |
| partition.setDirectoryType(cursor.getString(directoryTypeColumnIndex)); |
| partition.setDisplayName(cursor.getString(displayNameColumnIndex)); |
| int photoSupport = cursor.getInt(photoSupportColumnIndex); |
| partition.setPhotoSupported(photoSupport == Directory.PHOTO_SUPPORT_THUMBNAIL_ONLY |
| || photoSupport == Directory.PHOTO_SUPPORT_FULL); |
| addPartition(partition); |
| } |
| } |
| |
| // Phase II: remove deleted directories |
| int count = getPartitionCount(); |
| for (int i = count; --i >= 0; ) { |
| Partition partition = getPartition(i); |
| if (partition instanceof DirectoryPartition) { |
| long id = ((DirectoryPartition)partition).getDirectoryId(); |
| if (!directoryIds.contains(id)) { |
| removePartition(i); |
| } |
| } |
| } |
| |
| invalidate(); |
| notifyDataSetChanged(); |
| } |
| |
| @Override |
| public void changeCursor(int partitionIndex, Cursor cursor) { |
| if (partitionIndex >= getPartitionCount()) { |
| // There is no partition for this data |
| return; |
| } |
| |
| Partition partition = getPartition(partitionIndex); |
| if (partition instanceof DirectoryPartition) { |
| ((DirectoryPartition)partition).setStatus(DirectoryPartition.STATUS_LOADED); |
| } |
| |
| if (mDisplayPhotos && mPhotoLoader != null && isPhotoSupported(partitionIndex)) { |
| mPhotoLoader.refreshCache(); |
| } |
| |
| super.changeCursor(partitionIndex, cursor); |
| |
| if (isSectionHeaderDisplayEnabled() && partitionIndex == getIndexedPartition()) { |
| updateIndexer(cursor); |
| } |
| } |
| |
| public void changeCursor(Cursor cursor) { |
| changeCursor(0, cursor); |
| } |
| |
| /** |
| * Updates the indexer, which is used to produce section headers. |
| */ |
| private void updateIndexer(Cursor cursor) { |
| if (cursor == null) { |
| setIndexer(null); |
| return; |
| } |
| |
| Bundle bundle = cursor.getExtras(); |
| if (bundle.containsKey(ContactCounts.EXTRA_ADDRESS_BOOK_INDEX_TITLES)) { |
| String sections[] = |
| bundle.getStringArray(ContactCounts.EXTRA_ADDRESS_BOOK_INDEX_TITLES); |
| int counts[] = bundle.getIntArray(ContactCounts.EXTRA_ADDRESS_BOOK_INDEX_COUNTS); |
| setIndexer(new ContactsSectionIndexer(sections, counts)); |
| } else { |
| setIndexer(null); |
| } |
| } |
| |
| @Override |
| public int getViewTypeCount() { |
| // We need a separate view type for each item type, plus another one for |
| // each type with header, plus one for "other". |
| return getItemViewTypeCount() * 2 + 1; |
| } |
| |
| @Override |
| public int getItemViewType(int partitionIndex, int position) { |
| int type = super.getItemViewType(partitionIndex, position); |
| if (!isUserProfile(position) |
| && isSectionHeaderDisplayEnabled() |
| && partitionIndex == getIndexedPartition()) { |
| Placement placement = getItemPlacementInSection(position); |
| return placement.firstInSection ? type : getItemViewTypeCount() + type; |
| } else { |
| return type; |
| } |
| } |
| |
| @Override |
| public boolean isEmpty() { |
| // TODO |
| // if (contactsListActivity.mProviderStatus != ProviderStatus.STATUS_NORMAL) { |
| // return true; |
| // } |
| |
| if (!mEmptyListEnabled) { |
| return false; |
| } else if (isSearchMode()) { |
| return TextUtils.isEmpty(getQueryString()); |
| } else if (mLoading) { |
| // We don't want the empty state to show when loading. |
| return false; |
| } else { |
| return super.isEmpty(); |
| } |
| } |
| |
| public boolean isLoading() { |
| int count = getPartitionCount(); |
| for (int i = 0; i < count; i++) { |
| Partition partition = getPartition(i); |
| if (partition instanceof DirectoryPartition |
| && ((DirectoryPartition) partition).isLoading()) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| public boolean areAllPartitionsEmpty() { |
| int count = getPartitionCount(); |
| for (int i = 0; i < count; i++) { |
| if (!isPartitionEmpty(i)) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| /** |
| * Changes visibility parameters for the default directory partition. |
| */ |
| public void configureDefaultPartition(boolean showIfEmpty, boolean hasHeader) { |
| int defaultPartitionIndex = -1; |
| int count = getPartitionCount(); |
| for (int i = 0; i < count; i++) { |
| Partition partition = getPartition(i); |
| if (partition instanceof DirectoryPartition && |
| ((DirectoryPartition)partition).getDirectoryId() == Directory.DEFAULT) { |
| defaultPartitionIndex = i; |
| break; |
| } |
| } |
| if (defaultPartitionIndex != -1) { |
| setShowIfEmpty(defaultPartitionIndex, showIfEmpty); |
| setHasHeader(defaultPartitionIndex, hasHeader); |
| } |
| } |
| |
| @Override |
| protected View newHeaderView(Context context, int partition, Cursor cursor, |
| ViewGroup parent) { |
| LayoutInflater inflater = LayoutInflater.from(context); |
| return inflater.inflate(R.layout.directory_header, parent, false); |
| } |
| |
| @Override |
| protected void bindHeaderView(View view, int partitionIndex, Cursor cursor) { |
| Partition partition = getPartition(partitionIndex); |
| if (!(partition instanceof DirectoryPartition)) { |
| return; |
| } |
| |
| DirectoryPartition directoryPartition = (DirectoryPartition)partition; |
| long directoryId = directoryPartition.getDirectoryId(); |
| TextView labelTextView = (TextView)view.findViewById(R.id.label); |
| TextView displayNameTextView = (TextView)view.findViewById(R.id.display_name); |
| if (directoryId == Directory.DEFAULT || directoryId == Directory.LOCAL_INVISIBLE) { |
| labelTextView.setText(R.string.local_search_label); |
| displayNameTextView.setText(null); |
| } else { |
| labelTextView.setText(R.string.directory_search_label); |
| String directoryName = directoryPartition.getDisplayName(); |
| String displayName = !TextUtils.isEmpty(directoryName) |
| ? directoryName |
| : directoryPartition.getDirectoryType(); |
| displayNameTextView.setText(displayName); |
| } |
| |
| TextView countText = (TextView)view.findViewById(R.id.count); |
| if (directoryPartition.isLoading()) { |
| countText.setText(R.string.search_results_searching); |
| } else { |
| int count = cursor == null ? 0 : cursor.getCount(); |
| if (directoryId != Directory.DEFAULT && directoryId != Directory.LOCAL_INVISIBLE |
| && count >= getDirectoryResultLimit()) { |
| countText.setText(mContext.getString( |
| R.string.foundTooManyContacts, getDirectoryResultLimit())); |
| } else { |
| countText.setText(getQuantityText( |
| count, R.string.listFoundAllContactsZero, R.plurals.searchFoundContacts)); |
| } |
| } |
| } |
| |
| /** |
| * Checks whether the contact entry at the given position represents the user's profile. |
| */ |
| protected boolean isUserProfile(int position) { |
| // The profile only ever appears in the first position if it is present. So if the position |
| // is anything beyond 0, it can't be the profile. |
| boolean isUserProfile = false; |
| if (position == 0) { |
| int partition = getPartitionForPosition(position); |
| if (partition >= 0) { |
| // Save the old cursor position - the call to getItem() may modify the cursor |
| // position. |
| int offset = getCursor(partition).getPosition(); |
| Cursor cursor = (Cursor) getItem(position); |
| if (cursor != null) { |
| int profileColumnIndex = cursor.getColumnIndex(Contacts.IS_USER_PROFILE); |
| if (profileColumnIndex != -1) { |
| isUserProfile = cursor.getInt(profileColumnIndex) == 1; |
| } |
| // Restore the old cursor position. |
| cursor.moveToPosition(offset); |
| } |
| } |
| } |
| return isUserProfile; |
| } |
| |
| // TODO: fix PluralRules to handle zero correctly and use Resources.getQuantityText directly |
| public String getQuantityText(int count, int zeroResourceId, int pluralResourceId) { |
| if (count == 0) { |
| return getContext().getString(zeroResourceId); |
| } else { |
| String format = getContext().getResources() |
| .getQuantityText(pluralResourceId, count).toString(); |
| return String.format(format, count); |
| } |
| } |
| |
| public boolean isPhotoSupported(int partitionIndex) { |
| Partition partition = getPartition(partitionIndex); |
| if (partition instanceof DirectoryPartition) { |
| return ((DirectoryPartition) partition).isPhotoSupported(); |
| } |
| return true; |
| } |
| |
| /** |
| * Returns the currently selected filter. |
| */ |
| public ContactListFilter getFilter() { |
| return mFilter; |
| } |
| |
| public void setFilter(ContactListFilter filter) { |
| mFilter = filter; |
| } |
| |
| // TODO: move sharable logic (bindXX() methods) to here with extra arguments |
| |
| protected void bindQuickContact(final ContactListItemView view, int partitionIndex, |
| Cursor cursor, int photoIdColumn, int contactIdColumn, int lookUpKeyColumn) { |
| long photoId = 0; |
| if (!cursor.isNull(photoIdColumn)) { |
| photoId = cursor.getLong(photoIdColumn); |
| } |
| |
| QuickContactBadge quickContact = view.getQuickContact(); |
| quickContact.assignContactUri( |
| getContactUri(partitionIndex, cursor, contactIdColumn, lookUpKeyColumn)); |
| getPhotoLoader().loadPhoto(quickContact, photoId, false, mDarkTheme); |
| } |
| |
| protected Uri getContactUri(int partitionIndex, Cursor cursor, |
| int contactIdColumn, int lookUpKeyColumn) { |
| long contactId = cursor.getLong(contactIdColumn); |
| String lookupKey = cursor.getString(lookUpKeyColumn); |
| Uri uri = Contacts.getLookupUri(contactId, lookupKey); |
| long directoryId = ((DirectoryPartition)getPartition(partitionIndex)).getDirectoryId(); |
| if (directoryId != Directory.DEFAULT) { |
| uri = uri.buildUpon().appendQueryParameter( |
| ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(directoryId)).build(); |
| } |
| return uri; |
| } |
| |
| public void setContactsCount(String count) { |
| mContactsCount = count; |
| } |
| |
| public String getContactsCount() { |
| return mContactsCount; |
| } |
| } |