/*
* 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.common.list;
import android.content.Context;
import android.content.CursorLoader;
import android.content.res.Resources;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.provider.ContactsContract;
import android.provider.ContactsContract.CommonDataKinds.Phone;
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 com.android.contacts.common.ContactPhotoManager;
import com.android.contacts.common.ContactPhotoManager.DefaultImageRequest;
import com.android.contacts.common.ContactsUtils;
import com.android.contacts.common.R;
import com.android.contacts.common.compat.DirectoryCompat;
import com.android.contacts.common.util.SearchUtil;
import com.android.dialer.compat.CompatUtils;
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 {
/**
* Indicates whether the {@link Directory#LOCAL_INVISIBLE} directory should be included in the
* search.
*/
public static final boolean LOCAL_INVISIBLE_DIRECTORY_ENABLED = false;
private static final String TAG = "ContactEntryListAdapter";
private int mDisplayOrder;
private int mSortOrder;
private boolean mDisplayPhotos;
private boolean mCircularPhotos = true;
private boolean mQuickContactEnabled;
private boolean mAdjustSelectionBoundsEnabled;
/** The root view of the fragment that this adapter is associated with. */
private View mFragmentRootView;
private ContactPhotoManager mPhotoLoader;
private String mQueryString;
private String mUpperCaseQueryString;
private boolean mSearchMode;
private int mDirectorySearchMode;
private int mDirectoryResultLimit = Integer.MAX_VALUE;
private boolean mEmptyListEnabled = true;
private boolean mSelectionVisible;
private ContactListFilter mFilter;
private boolean mDarkTheme = false;
/** Resource used to provide header-text for default filter. */
private CharSequence mDefaultFilterHeaderText;
public ContactEntryListAdapter(Context context) {
super(context);
setDefaultFilterHeaderText(R.string.local_search_label);
addPartitions();
}
/**
* @param fragmentRootView Root view of the fragment. This is used to restrict the scope of image
* loading requests that get cancelled on cursor changes.
*/
protected void setFragmentRootView(View fragmentRootView) {
mFragmentRootView = fragmentRootView;
}
protected void setDefaultFilterHeaderText(int resourceId) {
mDefaultFilterHeaderText = getContext().getResources().getText(resourceId);
}
@Override
protected ContactListItemView newView(
Context context, int partition, Cursor cursor, int position, ViewGroup parent) {
final ContactListItemView view = new ContactListItemView(context, null);
view.setIsSectionHeaderEnabled(isSectionHeaderDisplayEnabled());
view.setAdjustSelectionBoundsEnabled(isAdjustSelectionBoundsEnabled());
return view;
}
@Override
protected void bindView(View itemView, int partition, Cursor cursor, int position) {
final ContactListItemView view = (ContactListItemView) itemView;
view.setIsSectionHeaderEnabled(isSectionHeaderDisplayEnabled());
bindWorkProfileIcon(view, partition);
}
@Override
protected View createPinnedSectionHeaderView(Context context, ViewGroup parent) {
return new ContactListPinnedHeaderView(context, null, parent);
}
@Override
protected void setPinnedSectionTitle(View pinnedHeaderView, String title) {
((ContactListPinnedHeaderView) pinnedHeaderView).setSectionHeaderTitle(title);
}
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);
partition.setLabel(mDefaultFilterHeaderText.toString());
return partition;
}
/**
* Remove all directories after the default directory. This is typically used when contacts list
* screens are asked to exit the search mode and thus need to remove all remote directory results
* for the search.
*
*
This code assumes that the default directory and directories before that should not be
* deleted (e.g. Join screen has "suggested contacts" directory before the default director, and
* we should not remove the directory).
*/
public void removeDirectoriesAfterDefault() {
final int partitionCount = getPartitionCount();
for (int i = partitionCount - 1; i >= 0; i--) {
final Partition partition = getPartition(i);
if ((partition instanceof DirectoryPartition)
&& ((DirectoryPartition) partition).getDirectoryId() == Directory.DEFAULT) {
break;
} else {
removePartition(i);
}
}
}
protected 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;
}
protected DirectoryPartition getDirectoryById(long id) {
int count = getPartitionCount();
for (int i = 0; i < count; i++) {
Partition partition = getPartition(i);
if (partition instanceof DirectoryPartition) {
final DirectoryPartition directoryPartition = (DirectoryPartition) partition;
if (directoryPartition.getDirectoryId() == id) {
return directoryPartition;
}
}
}
return null;
}
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 = SearchUtil.cleanStartAndEndOfSearchQuery(queryString.toUpperCase());
}
}
public String 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 getDirectoryResultLimit(DirectoryPartition directoryPartition) {
final int limit = directoryPartition.getResultLimit();
return limit == DirectoryPartition.RESULT_LIMIT_DEFAULT ? 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;
}
protected ContactPhotoManager getPhotoLoader() {
return mPhotoLoader;
}
public void setPhotoLoader(ContactPhotoManager photoLoader) {
mPhotoLoader = photoLoader;
}
public boolean getDisplayPhotos() {
return mDisplayPhotos;
}
public void setDisplayPhotos(boolean displayPhotos) {
mDisplayPhotos = displayPhotos;
}
public boolean getCircularPhotos() {
return mCircularPhotos;
}
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 isAdjustSelectionBoundsEnabled() {
return mAdjustSelectionBoundsEnabled;
}
public void setAdjustSelectionBoundsEnabled(boolean enabled) {
mAdjustSelectionBoundsEnabled = enabled;
}
public void setProfileExists(boolean exists) {
// Stick the "ME" header for the profile
if (exists) {
setSectionHeader(R.string.user_profile_contacts_list_header, /* # of ME */ 1);
}
}
private void setSectionHeader(int resId, int numberOfItems) {
SectionIndexer indexer = getIndexer();
if (indexer != null) {
((ContactsSectionIndexer) indexer)
.setProfileAndFavoritesHeader(getContext().getString(resId), numberOfItems);
}
}
public void setDarkTheme(boolean value) {
mDarkTheme = value;
}
/** 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 directoryIds = new HashSet();
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);
if (DirectoryCompat.isRemoteDirectoryId(id)) {
if (DirectoryCompat.isEnterpriseDirectoryId(id)) {
partition.setLabel(mContext.getString(R.string.directory_search_label_work));
} else {
partition.setLabel(mContext.getString(R.string.directory_search_label));
}
} else {
if (DirectoryCompat.isEnterpriseDirectoryId(id)) {
partition.setLabel(mContext.getString(R.string.list_filter_phones_work));
} else {
partition.setLabel(mDefaultFilterHeaderText.toString());
}
}
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);
}
// When the cursor changes, cancel any pending asynchronous photo loads.
mPhotoLoader.cancelPendingRequests(mFragmentRootView);
}
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 || cursor.isClosed()) {
setIndexer(null);
return;
}
Bundle bundle = cursor.getExtras();
if (bundle.containsKey(Contacts.EXTRA_ADDRESS_BOOK_INDEX_TITLES)
&& bundle.containsKey(Contacts.EXTRA_ADDRESS_BOOK_INDEX_COUNTS)) {
String[] sections = bundle.getStringArray(Contacts.EXTRA_ADDRESS_BOOK_INDEX_TITLES);
int[] counts = bundle.getIntArray(Contacts.EXTRA_ADDRESS_BOOK_INDEX_COUNTS);
if (getExtraStartingSection()) {
// Insert an additional unnamed section at the top of the list.
String[] allSections = new String[sections.length + 1];
int[] allCounts = new int[counts.length + 1];
for (int i = 0; i < sections.length; i++) {
allSections[i + 1] = sections[i];
allCounts[i + 1] = counts[i];
}
allCounts[0] = 1;
allSections[0] = "";
setIndexer(new ContactsSectionIndexer(allSections, allCounts));
} else {
setIndexer(new ContactsSectionIndexer(sections, counts));
}
} else {
setIndexer(null);
}
}
protected boolean getExtraStartingSection() {
return false;
}
@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 {
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;
}
/** 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);
View view = inflater.inflate(R.layout.directory_header, parent, false);
if (!getPinnedPartitionHeadersEnabled()) {
// If the headers are unpinned, there is no need for their background
// color to be non-transparent. Setting this transparent reduces maintenance for
// non-pinned headers. We don't need to bother synchronizing the activity's
// background color with the header background color.
view.setBackground(null);
}
return view;
}
protected void bindWorkProfileIcon(final ContactListItemView view, int partitionId) {
final Partition partition = getPartition(partitionId);
if (partition instanceof DirectoryPartition) {
final DirectoryPartition directoryPartition = (DirectoryPartition) partition;
final long directoryId = directoryPartition.getDirectoryId();
final long userType = ContactsUtils.determineUserType(directoryId, null);
view.setWorkProfileIconEnabled(userType == ContactsUtils.USER_TYPE_WORK);
}
}
@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);
labelTextView.setText(directoryPartition.getLabel());
if (!DirectoryCompat.isRemoteDirectoryId(directoryId)) {
displayNameTextView.setText(null);
} else {
String directoryName = directoryPartition.getDisplayName();
String displayName =
!TextUtils.isEmpty(directoryName) ? directoryName : directoryPartition.getDirectoryType();
displayNameTextView.setText(displayName);
}
final Resources res = getContext().getResources();
final int headerPaddingTop =
partitionIndex == 1 && getPartition(0).isEmpty()
? 0
: res.getDimensionPixelOffset(R.dimen.directory_header_extra_top_padding);
// There should be no extra padding at the top of the first directory header
view.setPaddingRelative(
view.getPaddingStart(), headerPaddingTop, view.getPaddingEnd(), view.getPaddingBottom());
}
/** 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;
}
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
/**
* Loads the photo for the quick contact view and assigns the contact uri.
*
* @param photoIdColumn Index of the photo id column
* @param photoUriColumn Index of the photo uri column. Optional: Can be -1
* @param contactIdColumn Index of the contact id column
* @param lookUpKeyColumn Index of the lookup key column
* @param displayNameColumn Index of the display name column
*/
protected void bindQuickContact(
final ContactListItemView view,
int partitionIndex,
Cursor cursor,
int photoIdColumn,
int photoUriColumn,
int contactIdColumn,
int lookUpKeyColumn,
int displayNameColumn) {
long photoId = 0;
if (!cursor.isNull(photoIdColumn)) {
photoId = cursor.getLong(photoIdColumn);
}
QuickContactBadge quickContact = view.getQuickContact();
quickContact.assignContactUri(
getContactUri(partitionIndex, cursor, contactIdColumn, lookUpKeyColumn));
if (CompatUtils.hasPrioritizedMimeType()) {
// The Contacts app never uses the QuickContactBadge. Therefore, it is safe to assume
// that only Dialer will use this QuickContact badge. This means prioritizing the phone
// mimetype here is reasonable.
quickContact.setPrioritizedMimeType(Phone.CONTENT_ITEM_TYPE);
}
if (photoId != 0 || photoUriColumn == -1) {
getPhotoLoader().loadThumbnail(quickContact, photoId, mDarkTheme, mCircularPhotos, null);
} else {
final String photoUriString = cursor.getString(photoUriColumn);
final Uri photoUri = photoUriString == null ? null : Uri.parse(photoUriString);
DefaultImageRequest request = null;
if (photoUri == null) {
request = getDefaultImageRequestFromCursor(cursor, displayNameColumn, lookUpKeyColumn);
}
getPhotoLoader().loadPhoto(quickContact, photoUri, -1, mDarkTheme, mCircularPhotos, request);
}
}
@Override
public boolean hasStableIds() {
// Whenever bindViewId() is called, the values passed into setId() are stable or
// stable-ish. For example, when one contact is modified we don't expect a second
// contact's Contact._ID values to change.
return true;
}
protected void bindViewId(final ContactListItemView view, Cursor cursor, int idColumn) {
// Set a semi-stable id, so that talkback won't get confused when the list gets
// refreshed. There is little harm in inserting the same ID twice.
long contactId = cursor.getLong(idColumn);
view.setId((int) (contactId % Integer.MAX_VALUE));
}
protected Uri getContactUri(
int partitionIndex, Cursor cursor, int contactIdColumn, int lookUpKeyColumn) {
long contactId = cursor.getLong(contactIdColumn);
String lookupKey = cursor.getString(lookUpKeyColumn);
long directoryId = ((DirectoryPartition) getPartition(partitionIndex)).getDirectoryId();
Uri uri = Contacts.getLookupUri(contactId, lookupKey);
if (uri != null && directoryId != Directory.DEFAULT) {
uri =
uri.buildUpon()
.appendQueryParameter(
ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(directoryId))
.build();
}
return uri;
}
/**
* Retrieves the lookup key and display name from a cursor, and returns a {@link
* DefaultImageRequest} containing these contact details
*
* @param cursor Contacts cursor positioned at the current row to retrieve contact details for
* @param displayNameColumn Column index of the display name
* @param lookupKeyColumn Column index of the lookup key
* @return {@link DefaultImageRequest} with the displayName and identifier fields set to the
* display name and lookup key of the contact.
*/
public DefaultImageRequest getDefaultImageRequestFromCursor(
Cursor cursor, int displayNameColumn, int lookupKeyColumn) {
final String displayName = cursor.getString(displayNameColumn);
final String lookupKey = cursor.getString(lookupKeyColumn);
return new DefaultImageRequest(displayName, lookupKey, mCircularPhotos);
}
}