From 58d0b2a7cdd4b988f527f03a7cb4ba2a4b7cd145 Mon Sep 17 00:00:00 2001 From: calderwoodra Date: Wed, 21 Mar 2018 16:57:10 -0700 Subject: Delete old search, old contacts, p13n logger, filtered numbers add number search. These components are safe to delete because: - New Contacts has been in prod for several releases. - New Search has been in in prod for 2 releases. - p13n logger was based on old search and is no longer being implemented in Dialer. - Filtered Number Settings contact search since we no longer support M. Bug: 37208802,73902692 Test: tap PiperOrigin-RevId: 189992017 Change-Id: I2720a252ababd164b5d0fb1011753a3c96a704d1 --- .../contacts/common/list/AutoScrollListView.java | 125 -- .../common/list/ContactEntryListAdapter.java | 772 ---------- .../common/list/ContactEntryListFragment.java | 860 ----------- .../contacts/common/list/ContactListAdapter.java | 232 --- .../contacts/common/list/ContactListItemView.java | 1508 -------------------- .../common/list/ContactListPinnedHeaderView.java | 70 - .../common/list/ContactsSectionIndexer.java | 119 -- .../common/list/DefaultContactListAdapter.java | 216 --- .../contacts/common/list/DirectoryListLoader.java | 210 --- .../contacts/common/list/DirectoryPartition.java | 179 --- .../contacts/common/list/IndexerListAdapter.java | 214 --- .../common/list/PhoneNumberListAdapter.java | 656 --------- .../common/list/PhoneNumberPickerFragment.java | 465 ------ .../common/list/PinnedHeaderListAdapter.java | 159 --- .../contacts/common/list/PinnedHeaderListView.java | 563 -------- 15 files changed, 6348 deletions(-) delete mode 100644 java/com/android/contacts/common/list/AutoScrollListView.java delete mode 100644 java/com/android/contacts/common/list/ContactEntryListAdapter.java delete mode 100644 java/com/android/contacts/common/list/ContactEntryListFragment.java delete mode 100644 java/com/android/contacts/common/list/ContactListAdapter.java delete mode 100644 java/com/android/contacts/common/list/ContactListItemView.java delete mode 100644 java/com/android/contacts/common/list/ContactListPinnedHeaderView.java delete mode 100644 java/com/android/contacts/common/list/ContactsSectionIndexer.java delete mode 100644 java/com/android/contacts/common/list/DefaultContactListAdapter.java delete mode 100644 java/com/android/contacts/common/list/DirectoryListLoader.java delete mode 100644 java/com/android/contacts/common/list/DirectoryPartition.java delete mode 100644 java/com/android/contacts/common/list/IndexerListAdapter.java delete mode 100644 java/com/android/contacts/common/list/PhoneNumberListAdapter.java delete mode 100644 java/com/android/contacts/common/list/PhoneNumberPickerFragment.java delete mode 100644 java/com/android/contacts/common/list/PinnedHeaderListAdapter.java delete mode 100644 java/com/android/contacts/common/list/PinnedHeaderListView.java (limited to 'java/com/android/contacts/common/list') diff --git a/java/com/android/contacts/common/list/AutoScrollListView.java b/java/com/android/contacts/common/list/AutoScrollListView.java deleted file mode 100644 index 8d6455f7f..000000000 --- a/java/com/android/contacts/common/list/AutoScrollListView.java +++ /dev/null @@ -1,125 +0,0 @@ -/* - * 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.os.Build; -import android.util.AttributeSet; -import android.widget.ListView; - -/** - * A ListView that can be asked to scroll (smoothly or otherwise) to a specific position. This class - * takes advantage of similar functionality that exists in {@link ListView} and enhances it. - */ -public class AutoScrollListView extends ListView { - - /** Position the element at about 1/3 of the list height */ - private static final float PREFERRED_SELECTION_OFFSET_FROM_TOP = 0.33f; - - private int mRequestedScrollPosition = -1; - private boolean mSmoothScrollRequested; - - public AutoScrollListView(Context context) { - super(context); - } - - public AutoScrollListView(Context context, AttributeSet attrs) { - super(context, attrs); - } - - public AutoScrollListView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - } - - /** - * Brings the specified position to view by optionally performing a jump-scroll maneuver: first it - * jumps to some position near the one requested and then does a smooth scroll to the requested - * position. This creates an impression of full smooth scrolling without actually traversing the - * entire list. If smooth scrolling is not requested, instantly positions the requested item at a - * preferred offset. - */ - public void requestPositionToScreen(int position, boolean smoothScroll) { - mRequestedScrollPosition = position; - mSmoothScrollRequested = smoothScroll; - requestLayout(); - } - - @Override - protected void layoutChildren() { - super.layoutChildren(); - if (mRequestedScrollPosition == -1) { - return; - } - - final int position = mRequestedScrollPosition; - mRequestedScrollPosition = -1; - - int firstPosition = getFirstVisiblePosition() + 1; - int lastPosition = getLastVisiblePosition(); - if (position >= firstPosition && position <= lastPosition) { - return; // Already on screen - } - - final int offset = (int) (getHeight() * PREFERRED_SELECTION_OFFSET_FROM_TOP); - if (!mSmoothScrollRequested) { - setSelectionFromTop(position, offset); - - // Since we have changed the scrolling position, we need to redo child layout - // Calling "requestLayout" in the middle of a layout pass has no effect, - // so we call layoutChildren explicitly - super.layoutChildren(); - - } else { - // We will first position the list a couple of screens before or after - // the new selection and then scroll smoothly to it. - int twoScreens = (lastPosition - firstPosition) * 2; - int preliminaryPosition; - if (position < firstPosition) { - preliminaryPosition = position + twoScreens; - if (preliminaryPosition >= getCount()) { - preliminaryPosition = getCount() - 1; - } - if (preliminaryPosition < firstPosition) { - setSelection(preliminaryPosition); - super.layoutChildren(); - } - } else { - preliminaryPosition = position - twoScreens; - if (preliminaryPosition < 0) { - preliminaryPosition = 0; - } - if (preliminaryPosition > lastPosition) { - setSelection(preliminaryPosition); - super.layoutChildren(); - } - } - - smoothScrollToPositionFromTop(position, offset); - } - } - - @Override - protected void onLayout(boolean changed, int l, int t, int r, int b) { - super.onLayout(changed, l, t, r, b); - - // Workaround for a bug and a bug. - if (android.os.Build.VERSION.SDK_INT == Build.VERSION_CODES.N - || android.os.Build.VERSION.SDK_INT == Build.VERSION_CODES.N_MR1) { - layoutChildren(); - } - } -} diff --git a/java/com/android/contacts/common/list/ContactEntryListAdapter.java b/java/com/android/contacts/common/list/ContactEntryListAdapter.java deleted file mode 100644 index 413a1deff..000000000 --- a/java/com/android/contacts/common/list/ContactEntryListAdapter.java +++ /dev/null @@ -1,772 +0,0 @@ -/* - * 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.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.ContactsUtils; -import com.android.contacts.common.R; -import com.android.contacts.common.util.SearchUtil; -import com.android.dialer.common.LogUtil; -import com.android.dialer.common.cp2.DirectoryCompat; -import com.android.dialer.compat.CompatUtils; -import com.android.dialer.configprovider.ConfigProviderBindings; -import com.android.dialer.contactphoto.ContactPhotoManager; -import com.android.dialer.contactphoto.ContactPhotoManager.DefaultImageRequest; -import com.android.dialer.logging.InteractionEvent; -import com.android.dialer.logging.Logger; -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 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; - - public static final int SUGGESTIONS_LOADER_ID = 0; - - /** 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() { - if (ConfigProviderBindings.get(getContext()).getBoolean("p13n_ranker_should_enable", false)) { - addPartition(createSuggestionsDirectoryPartition()); - } - addPartition(createDefaultDirectoryPartition()); - } - - protected DirectoryPartition createSuggestionsDirectoryPartition() { - DirectoryPartition partition = new DirectoryPartition(true, true); - partition.setDirectoryId(SUGGESTIONS_LOADER_ID); - partition.setDirectoryType(getContext().getString(R.string.contact_suggestions)); - partition.setPriorityDirectory(true); - partition.setPhotoSupported(true); - partition.setLabel(getContext().getString(R.string.local_suggestions_search_label)); - return partition; - } - - 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()); - } - - // Enable default partition header if in search mode (including zero-suggest). - if (mQueryString != null) { - setDefaultPartitionHeader(true); - } - } - - 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. - LogUtil.i( - "ContactEntryListAdapter.changeDirectories", - "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; - } - - /** Configures visibility parameters for the directory partitions. */ - public void configurePartitionsVisibility(boolean isInSearchMode) { - for (int i = 0; i < getPartitionCount(); i++) { - setShowIfEmpty(i, false); - setHasHeader(i, isInSearchMode); - } - } - - // Sets header for the default partition. - private void setDefaultPartitionHeader(boolean setHeader) { - // Iterate in reverse here to ensure the first DEFAULT directory has header. - // Both "Suggestions" and "All Contacts" directories have DEFAULT id. - int defaultPartitionIndex = -1; - for (int i = getPartitionCount() - 1; i >= 0; i--) { - Partition partition = getPartition(i); - if (partition instanceof DirectoryPartition - && ((DirectoryPartition) partition).getDirectoryId() == Directory.DEFAULT) { - defaultPartitionIndex = i; - } - } - setHasHeader(defaultPartitionIndex, setHeader); - } - - @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); - } - Logger.get(mContext) - .logQuickContactOnTouch( - quickContact, InteractionEvent.Type.OPEN_QUICK_CONTACT_FROM_SEARCH, true); - - 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); - } -} diff --git a/java/com/android/contacts/common/list/ContactEntryListFragment.java b/java/com/android/contacts/common/list/ContactEntryListFragment.java deleted file mode 100644 index c807ea815..000000000 --- a/java/com/android/contacts/common/list/ContactEntryListFragment.java +++ /dev/null @@ -1,860 +0,0 @@ -/* - * 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.app.Fragment; -import android.app.LoaderManager; -import android.app.LoaderManager.LoaderCallbacks; -import android.content.Context; -import android.content.CursorLoader; -import android.content.Loader; -import android.database.Cursor; -import android.os.Bundle; -import android.os.Handler; -import android.os.Message; -import android.os.Parcelable; -import android.provider.ContactsContract.Directory; -import android.support.annotation.Nullable; -import android.text.TextUtils; -import android.view.LayoutInflater; -import android.view.MotionEvent; -import android.view.View; -import android.view.View.OnFocusChangeListener; -import android.view.View.OnTouchListener; -import android.view.ViewGroup; -import android.view.inputmethod.InputMethodManager; -import android.widget.AbsListView; -import android.widget.AbsListView.OnScrollListener; -import android.widget.AdapterView; -import android.widget.AdapterView.OnItemClickListener; -import android.widget.ListView; -import com.android.common.widget.CompositeCursorAdapter.Partition; -import com.android.contacts.common.preference.ContactsPreferences; -import com.android.contacts.common.util.ContactListViewUtils; -import com.android.dialer.common.LogUtil; -import com.android.dialer.contactphoto.ContactPhotoManager; -import com.android.dialer.performancereport.PerformanceReport; -import java.lang.ref.WeakReference; -import java.util.Locale; - -/** Common base class for various contact-related list fragments. */ -public abstract class ContactEntryListFragment extends Fragment - implements OnItemClickListener, - OnScrollListener, - OnFocusChangeListener, - OnTouchListener, - LoaderCallbacks { - private static final String KEY_LIST_STATE = "liststate"; - private static final String KEY_SECTION_HEADER_DISPLAY_ENABLED = "sectionHeaderDisplayEnabled"; - private static final String KEY_PHOTO_LOADER_ENABLED = "photoLoaderEnabled"; - private static final String KEY_QUICK_CONTACT_ENABLED = "quickContactEnabled"; - private static final String KEY_ADJUST_SELECTION_BOUNDS_ENABLED = "adjustSelectionBoundsEnabled"; - private static final String KEY_INCLUDE_PROFILE = "includeProfile"; - private static final String KEY_SEARCH_MODE = "searchMode"; - private static final String KEY_VISIBLE_SCROLLBAR_ENABLED = "visibleScrollbarEnabled"; - private static final String KEY_SCROLLBAR_POSITION = "scrollbarPosition"; - private static final String KEY_QUERY_STRING = "queryString"; - private static final String KEY_DIRECTORY_SEARCH_MODE = "directorySearchMode"; - private static final String KEY_SELECTION_VISIBLE = "selectionVisible"; - private static final String KEY_DARK_THEME = "darkTheme"; - private static final String KEY_LEGACY_COMPATIBILITY = "legacyCompatibility"; - private static final String KEY_DIRECTORY_RESULT_LIMIT = "directoryResultLimit"; - - private static final String DIRECTORY_ID_ARG_KEY = "directoryId"; - - private static final int DIRECTORY_LOADER_ID = -1; - - private static final int DIRECTORY_SEARCH_DELAY_MILLIS = 300; - private static final int DIRECTORY_SEARCH_MESSAGE = 1; - - private static final int DEFAULT_DIRECTORY_RESULT_LIMIT = 20; - private static final int STATUS_NOT_LOADED = 0; - private static final int STATUS_LOADING = 1; - private static final int STATUS_LOADED = 2; - protected boolean mUserProfileExists; - private boolean mSectionHeaderDisplayEnabled; - private boolean mPhotoLoaderEnabled; - private boolean mQuickContactEnabled = true; - private boolean mAdjustSelectionBoundsEnabled = true; - private boolean mIncludeProfile; - private boolean mSearchMode; - private boolean mVisibleScrollbarEnabled; - private boolean mShowEmptyListForEmptyQuery; - private int mVerticalScrollbarPosition = getDefaultVerticalScrollbarPosition(); - private String mQueryString; - private int mDirectorySearchMode = DirectoryListLoader.SEARCH_MODE_NONE; - private boolean mSelectionVisible; - private boolean mLegacyCompatibility; - private boolean mEnabled = true; - private T mAdapter; - private View mView; - private ListView mListView; - /** Used to save the scrolling state of the list when the fragment is not recreated. */ - private int mListViewTopIndex; - - private int mListViewTopOffset; - /** Used for keeping track of the scroll state of the list. */ - private Parcelable mListState; - - private int mDisplayOrder; - private int mSortOrder; - private int mDirectoryResultLimit = DEFAULT_DIRECTORY_RESULT_LIMIT; - private ContactPhotoManager mPhotoManager; - private ContactsPreferences mContactsPrefs; - private boolean mForceLoad; - private boolean mDarkTheme; - private int mDirectoryListStatus = STATUS_NOT_LOADED; - - /** - * Indicates whether we are doing the initial complete load of data (false) or a refresh caused by - * a change notification (true) - */ - private boolean mLoadPriorityDirectoriesOnly; - - private Context mContext; - - private LoaderManager mLoaderManager; - - private Handler mDelayedDirectorySearchHandler; - - private static class DelayedDirectorySearchHandler extends Handler { - private final WeakReference> contactEntryListFragmentRef; - - private DelayedDirectorySearchHandler(ContactEntryListFragment contactEntryListFragment) { - this.contactEntryListFragmentRef = new WeakReference<>(contactEntryListFragment); - } - - @Override - public void handleMessage(Message msg) { - ContactEntryListFragment contactEntryListFragment = contactEntryListFragmentRef.get(); - if (contactEntryListFragment == null) { - return; - } - if (msg.what == DIRECTORY_SEARCH_MESSAGE) { - contactEntryListFragment.loadDirectoryPartition(msg.arg1, (DirectoryPartition) msg.obj); - } - } - } - - private ContactsPreferences.ChangeListener mPreferencesChangeListener = - new ContactsPreferences.ChangeListener() { - @Override - public void onChange() { - loadPreferences(); - reloadData(); - } - }; - - protected ContactEntryListFragment() { - mDelayedDirectorySearchHandler = new DelayedDirectorySearchHandler(this); - } - - protected abstract View inflateView(LayoutInflater inflater, ViewGroup container); - - protected abstract T createListAdapter(); - - /** - * @param position Please note that the position is already adjusted for header views, so "0" - * means the first list item below header views. - */ - protected abstract void onItemClick(int position, long id); - - @Override - public void onAttach(Context context) { - super.onAttach(context); - setContext(context); - setLoaderManager(super.getLoaderManager()); - } - - @Override - public Context getContext() { - return mContext; - } - - /** Sets a context for the fragment in the unit test environment. */ - public void setContext(Context context) { - mContext = context; - configurePhotoLoader(); - } - - public void setEnabled(boolean enabled) { - if (mEnabled != enabled) { - mEnabled = enabled; - if (mAdapter != null) { - if (mEnabled) { - reloadData(); - } else { - mAdapter.clearPartitions(); - } - } - } - } - - @Override - public LoaderManager getLoaderManager() { - return mLoaderManager; - } - - /** Overrides a loader manager for use in unit tests. */ - public void setLoaderManager(LoaderManager loaderManager) { - mLoaderManager = loaderManager; - } - - public T getAdapter() { - return mAdapter; - } - - @Override - public View getView() { - return mView; - } - - public ListView getListView() { - return mListView; - } - - @Override - public void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - outState.putBoolean(KEY_SECTION_HEADER_DISPLAY_ENABLED, mSectionHeaderDisplayEnabled); - outState.putBoolean(KEY_PHOTO_LOADER_ENABLED, mPhotoLoaderEnabled); - outState.putBoolean(KEY_QUICK_CONTACT_ENABLED, mQuickContactEnabled); - outState.putBoolean(KEY_ADJUST_SELECTION_BOUNDS_ENABLED, mAdjustSelectionBoundsEnabled); - outState.putBoolean(KEY_INCLUDE_PROFILE, mIncludeProfile); - outState.putBoolean(KEY_SEARCH_MODE, mSearchMode); - outState.putBoolean(KEY_VISIBLE_SCROLLBAR_ENABLED, mVisibleScrollbarEnabled); - outState.putInt(KEY_SCROLLBAR_POSITION, mVerticalScrollbarPosition); - outState.putInt(KEY_DIRECTORY_SEARCH_MODE, mDirectorySearchMode); - outState.putBoolean(KEY_SELECTION_VISIBLE, mSelectionVisible); - outState.putBoolean(KEY_LEGACY_COMPATIBILITY, mLegacyCompatibility); - outState.putString(KEY_QUERY_STRING, mQueryString); - outState.putInt(KEY_DIRECTORY_RESULT_LIMIT, mDirectoryResultLimit); - outState.putBoolean(KEY_DARK_THEME, mDarkTheme); - - if (mListView != null) { - outState.putParcelable(KEY_LIST_STATE, mListView.onSaveInstanceState()); - } - } - - @Override - public void onCreate(Bundle savedState) { - super.onCreate(savedState); - restoreSavedState(savedState); - mAdapter = createListAdapter(); - mContactsPrefs = new ContactsPreferences(mContext); - } - - public void restoreSavedState(Bundle savedState) { - if (savedState == null) { - return; - } - - mSectionHeaderDisplayEnabled = savedState.getBoolean(KEY_SECTION_HEADER_DISPLAY_ENABLED); - mPhotoLoaderEnabled = savedState.getBoolean(KEY_PHOTO_LOADER_ENABLED); - mQuickContactEnabled = savedState.getBoolean(KEY_QUICK_CONTACT_ENABLED); - mAdjustSelectionBoundsEnabled = savedState.getBoolean(KEY_ADJUST_SELECTION_BOUNDS_ENABLED); - mIncludeProfile = savedState.getBoolean(KEY_INCLUDE_PROFILE); - mSearchMode = savedState.getBoolean(KEY_SEARCH_MODE); - mVisibleScrollbarEnabled = savedState.getBoolean(KEY_VISIBLE_SCROLLBAR_ENABLED); - mVerticalScrollbarPosition = savedState.getInt(KEY_SCROLLBAR_POSITION); - mDirectorySearchMode = savedState.getInt(KEY_DIRECTORY_SEARCH_MODE); - mSelectionVisible = savedState.getBoolean(KEY_SELECTION_VISIBLE); - mLegacyCompatibility = savedState.getBoolean(KEY_LEGACY_COMPATIBILITY); - mQueryString = savedState.getString(KEY_QUERY_STRING); - mDirectoryResultLimit = savedState.getInt(KEY_DIRECTORY_RESULT_LIMIT); - mDarkTheme = savedState.getBoolean(KEY_DARK_THEME); - - // Retrieve list state. This will be applied in onLoadFinished - mListState = savedState.getParcelable(KEY_LIST_STATE); - } - - @Override - public void onStart() { - super.onStart(); - - mContactsPrefs.registerChangeListener(mPreferencesChangeListener); - - mForceLoad = loadPreferences(); - - mDirectoryListStatus = STATUS_NOT_LOADED; - mLoadPriorityDirectoriesOnly = true; - - startLoading(); - } - - protected void startLoading() { - if (mAdapter == null) { - // The method was called before the fragment was started - return; - } - - configureAdapter(); - int partitionCount = mAdapter.getPartitionCount(); - for (int i = 0; i < partitionCount; i++) { - Partition partition = mAdapter.getPartition(i); - if (partition instanceof DirectoryPartition) { - DirectoryPartition directoryPartition = (DirectoryPartition) partition; - if (directoryPartition.getStatus() == DirectoryPartition.STATUS_NOT_LOADED) { - if (directoryPartition.isPriorityDirectory() || !mLoadPriorityDirectoriesOnly) { - startLoadingDirectoryPartition(i); - } - } - } else { - getLoaderManager().initLoader(i, null, this); - } - } - - // Next time this method is called, we should start loading non-priority directories - mLoadPriorityDirectoriesOnly = false; - } - - @Override - public Loader onCreateLoader(int id, Bundle args) { - if (id == DIRECTORY_LOADER_ID) { - DirectoryListLoader loader = new DirectoryListLoader(mContext); - loader.setDirectorySearchMode(mAdapter.getDirectorySearchMode()); - loader.setLocalInvisibleDirectoryEnabled( - ContactEntryListAdapter.LOCAL_INVISIBLE_DIRECTORY_ENABLED); - return loader; - } else { - CursorLoader loader = createCursorLoader(mContext); - long directoryId = - args != null && args.containsKey(DIRECTORY_ID_ARG_KEY) - ? args.getLong(DIRECTORY_ID_ARG_KEY) - : Directory.DEFAULT; - mAdapter.configureLoader(loader, directoryId); - return loader; - } - } - - public CursorLoader createCursorLoader(Context context) { - return new CursorLoader(context, null, null, null, null, null) { - @Override - protected Cursor onLoadInBackground() { - try { - return super.onLoadInBackground(); - } catch (RuntimeException e) { - // We don't even know what the projection should be, so no point trying to - // return an empty MatrixCursor with the correct projection here. - LogUtil.w( - "ContactEntryListFragment.onLoadInBackground", - "RuntimeException while trying to query ContactsProvider."); - return null; - } - } - }; - } - - private void startLoadingDirectoryPartition(int partitionIndex) { - DirectoryPartition partition = (DirectoryPartition) mAdapter.getPartition(partitionIndex); - partition.setStatus(DirectoryPartition.STATUS_LOADING); - long directoryId = partition.getDirectoryId(); - if (mForceLoad) { - if (directoryId == Directory.DEFAULT) { - loadDirectoryPartition(partitionIndex, partition); - } else { - loadDirectoryPartitionDelayed(partitionIndex, partition); - } - } else { - Bundle args = new Bundle(); - args.putLong(DIRECTORY_ID_ARG_KEY, directoryId); - getLoaderManager().initLoader(partitionIndex, args, this); - } - } - - /** - * Queues up a delayed request to search the specified directory. Since directory search will - * likely introduce a lot of network traffic, we want to wait for a pause in the user's typing - * before sending a directory request. - */ - private void loadDirectoryPartitionDelayed(int partitionIndex, DirectoryPartition partition) { - mDelayedDirectorySearchHandler.removeMessages(DIRECTORY_SEARCH_MESSAGE, partition); - Message msg = - mDelayedDirectorySearchHandler.obtainMessage( - DIRECTORY_SEARCH_MESSAGE, partitionIndex, 0, partition); - mDelayedDirectorySearchHandler.sendMessageDelayed(msg, DIRECTORY_SEARCH_DELAY_MILLIS); - } - - /** Loads the directory partition. */ - protected void loadDirectoryPartition(int partitionIndex, DirectoryPartition partition) { - Bundle args = new Bundle(); - args.putLong(DIRECTORY_ID_ARG_KEY, partition.getDirectoryId()); - getLoaderManager().restartLoader(partitionIndex, args, this); - } - - /** Cancels all queued directory loading requests. */ - private void removePendingDirectorySearchRequests() { - mDelayedDirectorySearchHandler.removeMessages(DIRECTORY_SEARCH_MESSAGE); - } - - @Override - public void onLoadFinished(Loader loader, Cursor data) { - if (!mEnabled) { - return; - } - - int loaderId = loader.getId(); - if (loaderId == DIRECTORY_LOADER_ID) { - mDirectoryListStatus = STATUS_LOADED; - mAdapter.changeDirectories(data); - startLoading(); - } else { - onPartitionLoaded(loaderId, data); - if (isSearchMode()) { - int directorySearchMode = getDirectorySearchMode(); - if (directorySearchMode != DirectoryListLoader.SEARCH_MODE_NONE) { - if (mDirectoryListStatus == STATUS_NOT_LOADED) { - mDirectoryListStatus = STATUS_LOADING; - getLoaderManager().initLoader(DIRECTORY_LOADER_ID, null, this); - } else { - startLoading(); - } - } - } else { - mDirectoryListStatus = STATUS_NOT_LOADED; - getLoaderManager().destroyLoader(DIRECTORY_LOADER_ID); - } - } - } - - @Override - public void onLoaderReset(Loader loader) {} - - protected void onPartitionLoaded(int partitionIndex, Cursor data) { - if (partitionIndex >= mAdapter.getPartitionCount()) { - // When we get unsolicited data, ignore it. This could happen - // when we are switching from search mode to the default mode. - return; - } - - // Return for non-"Suggestions" if on the zero-suggest screen. - if (TextUtils.isEmpty(mQueryString) && partitionIndex > 0) { - return; - } - - mAdapter.changeCursor(partitionIndex, data); - setProfileHeader(); - - if (!isLoading()) { - completeRestoreInstanceState(); - } - } - - public boolean isLoading() { - //noinspection SimplifiableIfStatement - if (mAdapter != null && mAdapter.isLoading()) { - return true; - } - - return isLoadingDirectoryList(); - - } - - public boolean isLoadingDirectoryList() { - return isSearchMode() - && getDirectorySearchMode() != DirectoryListLoader.SEARCH_MODE_NONE - && (mDirectoryListStatus == STATUS_NOT_LOADED || mDirectoryListStatus == STATUS_LOADING); - } - - @Override - public void onStop() { - super.onStop(); - mContactsPrefs.unregisterChangeListener(); - mAdapter.clearPartitions(); - } - - protected void reloadData() { - removePendingDirectorySearchRequests(); - mAdapter.onDataReload(); - mLoadPriorityDirectoriesOnly = true; - mForceLoad = true; - startLoading(); - } - - /** - * Shows a view at the top of the list with a pseudo local profile prompting the user to add a - * local profile. Default implementation does nothing. - */ - protected void setProfileHeader() { - mUserProfileExists = false; - } - - /** Provides logic that dismisses this fragment. The default implementation does nothing. */ - protected void finish() {} - - public boolean isSectionHeaderDisplayEnabled() { - return mSectionHeaderDisplayEnabled; - } - - public void setSectionHeaderDisplayEnabled(boolean flag) { - if (mSectionHeaderDisplayEnabled != flag) { - mSectionHeaderDisplayEnabled = flag; - if (mAdapter != null) { - mAdapter.setSectionHeaderDisplayEnabled(flag); - } - configureVerticalScrollbar(); - } - } - - public boolean isVisibleScrollbarEnabled() { - return mVisibleScrollbarEnabled; - } - - public void setVisibleScrollbarEnabled(boolean flag) { - if (mVisibleScrollbarEnabled != flag) { - mVisibleScrollbarEnabled = flag; - configureVerticalScrollbar(); - } - } - - private void configureVerticalScrollbar() { - boolean hasScrollbar = isVisibleScrollbarEnabled() && isSectionHeaderDisplayEnabled(); - - if (mListView != null) { - mListView.setFastScrollEnabled(hasScrollbar); - mListView.setVerticalScrollbarPosition(mVerticalScrollbarPosition); - mListView.setScrollBarStyle(ListView.SCROLLBARS_OUTSIDE_OVERLAY); - } - } - - public boolean isPhotoLoaderEnabled() { - return mPhotoLoaderEnabled; - } - - public void setPhotoLoaderEnabled(boolean flag) { - mPhotoLoaderEnabled = flag; - configurePhotoLoader(); - } - - public void setQuickContactEnabled(boolean flag) { - this.mQuickContactEnabled = flag; - } - - public void setAdjustSelectionBoundsEnabled(boolean flag) { - mAdjustSelectionBoundsEnabled = flag; - } - - public final boolean isSearchMode() { - return mSearchMode; - } - - /** - * Enter/exit search mode. This is method is tightly related to the current query, and should only - * be called by {@link #setQueryString}. - * - *

Also note this method doesn't call {@link #reloadData()}; {@link #setQueryString} does it. - */ - protected void setSearchMode(boolean flag) { - if (mSearchMode != flag) { - mSearchMode = flag; - - if (!flag) { - mDirectoryListStatus = STATUS_NOT_LOADED; - getLoaderManager().destroyLoader(DIRECTORY_LOADER_ID); - } - - if (mAdapter != null) { - mAdapter.setSearchMode(flag); - - mAdapter.clearPartitions(); - if (!flag) { - // If we are switching from search to regular display, remove all directory - // partitions after default one, assuming they are remote directories which - // should be cleaned up on exiting the search mode. - mAdapter.removeDirectoriesAfterDefault(); - } - mAdapter.configurePartitionsVisibility(flag); - } - - if (mListView != null) { - mListView.setFastScrollEnabled(!flag); - } - } - } - - @Nullable - public final String getQueryString() { - return mQueryString; - } - - public void setQueryString(String queryString) { - if (!TextUtils.equals(mQueryString, queryString)) { - if (mShowEmptyListForEmptyQuery && mAdapter != null && mListView != null) { - if (TextUtils.isEmpty(mQueryString)) { - // Restore the adapter if the query used to be empty. - mListView.setAdapter(mAdapter); - } else if (TextUtils.isEmpty(queryString)) { - // Instantly clear the list view if the new query is empty. - mListView.setAdapter(null); - } - } - - mQueryString = queryString; - setSearchMode(!TextUtils.isEmpty(mQueryString) || mShowEmptyListForEmptyQuery); - - if (mAdapter != null) { - mAdapter.setQueryString(queryString); - reloadData(); - } - } - } - - public void setShowEmptyListForNullQuery(boolean show) { - mShowEmptyListForEmptyQuery = show; - } - - public boolean getShowEmptyListForNullQuery() { - return mShowEmptyListForEmptyQuery; - } - - public int getDirectoryLoaderId() { - return DIRECTORY_LOADER_ID; - } - - public int getDirectorySearchMode() { - return mDirectorySearchMode; - } - - public void setDirectorySearchMode(int mode) { - mDirectorySearchMode = mode; - } - - protected int getContactNameDisplayOrder() { - return mDisplayOrder; - } - - protected void setContactNameDisplayOrder(int displayOrder) { - mDisplayOrder = displayOrder; - if (mAdapter != null) { - mAdapter.setContactNameDisplayOrder(displayOrder); - } - } - - public int getSortOrder() { - return mSortOrder; - } - - public void setSortOrder(int sortOrder) { - mSortOrder = sortOrder; - if (mAdapter != null) { - mAdapter.setSortOrder(sortOrder); - } - } - - public void setDirectoryResultLimit(int limit) { - mDirectoryResultLimit = limit; - } - - protected boolean loadPreferences() { - boolean changed = false; - if (getContactNameDisplayOrder() != mContactsPrefs.getDisplayOrder()) { - setContactNameDisplayOrder(mContactsPrefs.getDisplayOrder()); - changed = true; - } - - if (getSortOrder() != mContactsPrefs.getSortOrder()) { - setSortOrder(mContactsPrefs.getSortOrder()); - changed = true; - } - - return changed; - } - - @Override - public View onCreateView( - LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - onCreateView(inflater, container); - - boolean searchMode = isSearchMode(); - mAdapter.setSearchMode(searchMode); - mAdapter.configurePartitionsVisibility(searchMode); - mAdapter.setPhotoLoader(mPhotoManager); - mListView.setAdapter(mAdapter); - return mView; - } - - protected void onCreateView(LayoutInflater inflater, ViewGroup container) { - mView = inflateView(inflater, container); - - mListView = mView.findViewById(android.R.id.list); - if (mListView == null) { - throw new RuntimeException( - "Your content must have a ListView whose id attribute is " + "'android.R.id.list'"); - } - - View emptyView = mView.findViewById(android.R.id.empty); - if (emptyView != null) { - mListView.setEmptyView(emptyView); - } - - mListView.setOnItemClickListener(this); - mListView.setOnFocusChangeListener(this); - mListView.setOnTouchListener(this); - mListView.setFastScrollEnabled(!isSearchMode()); - - // Tell list view to not show dividers. We'll do it ourself so that we can *not* show - // them when an A-Z headers is visible. - mListView.setDividerHeight(0); - - // We manually save/restore the listview state - mListView.setSaveEnabled(false); - - configureVerticalScrollbar(); - configurePhotoLoader(); - - getAdapter().setFragmentRootView(getView()); - - ContactListViewUtils.applyCardPaddingToView(getResources(), mListView, mView); - } - - @Override - public void onHiddenChanged(boolean hidden) { - super.onHiddenChanged(hidden); - if (getActivity() != null && getView() != null && !hidden) { - // If the padding was last applied when in a hidden state, it may have been applied - // incorrectly. Therefore we need to reapply it. - ContactListViewUtils.applyCardPaddingToView(getResources(), mListView, getView()); - } - } - - protected void configurePhotoLoader() { - if (isPhotoLoaderEnabled() && mContext != null) { - if (mPhotoManager == null) { - mPhotoManager = ContactPhotoManager.getInstance(mContext); - } - if (mListView != null) { - mListView.setOnScrollListener(this); - } - if (mAdapter != null) { - mAdapter.setPhotoLoader(mPhotoManager); - } - } - } - - protected void configureAdapter() { - if (mAdapter == null) { - return; - } - - mAdapter.setQuickContactEnabled(mQuickContactEnabled); - mAdapter.setAdjustSelectionBoundsEnabled(mAdjustSelectionBoundsEnabled); - mAdapter.setQueryString(mQueryString); - mAdapter.setDirectorySearchMode(mDirectorySearchMode); - mAdapter.setPinnedPartitionHeadersEnabled(false); - mAdapter.setContactNameDisplayOrder(mDisplayOrder); - mAdapter.setSortOrder(mSortOrder); - mAdapter.setSectionHeaderDisplayEnabled(mSectionHeaderDisplayEnabled); - mAdapter.setSelectionVisible(mSelectionVisible); - mAdapter.setDirectoryResultLimit(mDirectoryResultLimit); - mAdapter.setDarkTheme(mDarkTheme); - } - - @Override - public void onScroll( - AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {} - - @Override - public void onScrollStateChanged(AbsListView view, int scrollState) { - PerformanceReport.recordScrollStateChange(scrollState); - if (scrollState == OnScrollListener.SCROLL_STATE_FLING) { - mPhotoManager.pause(); - } else if (isPhotoLoaderEnabled()) { - mPhotoManager.resume(); - } - } - - @Override - public void onItemClick(AdapterView parent, View view, int position, long id) { - hideSoftKeyboard(); - - int adjPosition = position - mListView.getHeaderViewsCount(); - if (adjPosition >= 0) { - onItemClick(adjPosition, id); - } - } - - private void hideSoftKeyboard() { - // Hide soft keyboard, if visible - InputMethodManager inputMethodManager = - (InputMethodManager) mContext.getSystemService(Context.INPUT_METHOD_SERVICE); - inputMethodManager.hideSoftInputFromWindow(mListView.getWindowToken(), 0); - } - - /** Dismisses the soft keyboard when the list takes focus. */ - @Override - public void onFocusChange(View view, boolean hasFocus) { - if (view == mListView && hasFocus) { - hideSoftKeyboard(); - } - } - - /** Dismisses the soft keyboard when the list is touched. */ - @Override - public boolean onTouch(View view, MotionEvent event) { - if (view == mListView) { - hideSoftKeyboard(); - } - return false; - } - - @Override - public void onPause() { - // Save the scrolling state of the list view - mListViewTopIndex = mListView.getFirstVisiblePosition(); - View v = mListView.getChildAt(0); - mListViewTopOffset = (v == null) ? 0 : (v.getTop() - mListView.getPaddingTop()); - - super.onPause(); - removePendingDirectorySearchRequests(); - } - - @Override - public void onResume() { - super.onResume(); - // Restore the selection of the list view. See a bug. - // This has to be done manually because if the list view has its emptyView set, - // the scrolling state will be reset when clearPartitions() is called on the adapter. - mListView.setSelectionFromTop(mListViewTopIndex, mListViewTopOffset); - } - - /** Restore the list state after the adapter is populated. */ - protected void completeRestoreInstanceState() { - if (mListState != null) { - mListView.onRestoreInstanceState(mListState); - mListState = null; - } - } - - public void setDarkTheme(boolean value) { - mDarkTheme = value; - if (mAdapter != null) { - mAdapter.setDarkTheme(value); - } - } - - private int getDefaultVerticalScrollbarPosition() { - final Locale locale = Locale.getDefault(); - final int layoutDirection = TextUtils.getLayoutDirectionFromLocale(locale); - switch (layoutDirection) { - case View.LAYOUT_DIRECTION_RTL: - return View.SCROLLBAR_POSITION_LEFT; - case View.LAYOUT_DIRECTION_LTR: - default: - return View.SCROLLBAR_POSITION_RIGHT; - } - } -} diff --git a/java/com/android/contacts/common/list/ContactListAdapter.java b/java/com/android/contacts/common/list/ContactListAdapter.java deleted file mode 100644 index 721609d1d..000000000 --- a/java/com/android/contacts/common/list/ContactListAdapter.java +++ /dev/null @@ -1,232 +0,0 @@ -/* - * 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.database.Cursor; -import android.net.Uri; -import android.provider.ContactsContract; -import android.provider.ContactsContract.Contacts; -import android.provider.ContactsContract.Directory; -import android.provider.ContactsContract.SearchSnippets; -import android.view.ViewGroup; -import com.android.contacts.common.R; -import com.android.contacts.common.preference.ContactsPreferences; -import com.android.dialer.contactphoto.ContactPhotoManager.DefaultImageRequest; - -/** - * A cursor adapter for the {@link ContactsContract.Contacts#CONTENT_TYPE} content type. Also - * includes support for including the {@link ContactsContract.Profile} record in the list. - */ -public abstract class ContactListAdapter extends ContactEntryListAdapter { - - private CharSequence mUnknownNameText; - - public ContactListAdapter(Context context) { - super(context); - - mUnknownNameText = context.getText(R.string.missing_name); - } - - protected static Uri buildSectionIndexerUri(Uri uri) { - return uri.buildUpon().appendQueryParameter(Contacts.EXTRA_ADDRESS_BOOK_INDEX, "true").build(); - } - - public Uri getContactUri(int partitionIndex, Cursor cursor) { - long contactId = cursor.getLong(ContactQuery.CONTACT_ID); - String lookupKey = cursor.getString(ContactQuery.CONTACT_LOOKUP_KEY); - Uri uri = Contacts.getLookupUri(contactId, lookupKey); - long directoryId = ((DirectoryPartition) getPartition(partitionIndex)).getDirectoryId(); - if (uri != null && directoryId != Directory.DEFAULT) { - uri = - uri.buildUpon() - .appendQueryParameter( - ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(directoryId)) - .build(); - } - return uri; - } - - @Override - protected ContactListItemView newView( - Context context, int partition, Cursor cursor, int position, ViewGroup parent) { - ContactListItemView view = super.newView(context, partition, cursor, position, parent); - view.setUnknownNameText(mUnknownNameText); - view.setQuickContactEnabled(isQuickContactEnabled()); - view.setAdjustSelectionBoundsEnabled(isAdjustSelectionBoundsEnabled()); - view.setActivatedStateSupported(isSelectionVisible()); - return view; - } - - protected void bindSectionHeaderAndDivider( - ContactListItemView view, int position, Cursor cursor) { - view.setIsSectionHeaderEnabled(isSectionHeaderDisplayEnabled()); - if (isSectionHeaderDisplayEnabled()) { - Placement placement = getItemPlacementInSection(position); - view.setSectionHeader(placement.sectionHeader); - } else { - view.setSectionHeader(null); - } - } - - protected void bindPhoto(final ContactListItemView view, int partitionIndex, Cursor cursor) { - if (!isPhotoSupported(partitionIndex)) { - view.removePhotoView(); - return; - } - - // Set the photo, if available - long photoId = 0; - if (!cursor.isNull(ContactQuery.CONTACT_PHOTO_ID)) { - photoId = cursor.getLong(ContactQuery.CONTACT_PHOTO_ID); - } - - if (photoId != 0) { - getPhotoLoader() - .loadThumbnail(view.getPhotoView(), photoId, false, getCircularPhotos(), null); - } else { - final String photoUriString = cursor.getString(ContactQuery.CONTACT_PHOTO_URI); - final Uri photoUri = photoUriString == null ? null : Uri.parse(photoUriString); - DefaultImageRequest request = null; - if (photoUri == null) { - request = - getDefaultImageRequestFromCursor( - cursor, ContactQuery.CONTACT_DISPLAY_NAME, ContactQuery.CONTACT_LOOKUP_KEY); - } - getPhotoLoader() - .loadDirectoryPhoto(view.getPhotoView(), photoUri, false, getCircularPhotos(), request); - } - } - - protected void bindNameAndViewId(final ContactListItemView view, Cursor cursor) { - view.showDisplayName(cursor, ContactQuery.CONTACT_DISPLAY_NAME); - // Note: we don't show phonetic any more (See issue 5265330) - - bindViewId(view, cursor, ContactQuery.CONTACT_ID); - } - - protected void bindPresenceAndStatusMessage(final ContactListItemView view, Cursor cursor) { - view.showPresenceAndStatusMessage( - cursor, ContactQuery.CONTACT_PRESENCE_STATUS, ContactQuery.CONTACT_CONTACT_STATUS); - } - - protected void bindSearchSnippet(final ContactListItemView view, Cursor cursor) { - view.showSnippet(cursor, ContactQuery.CONTACT_SNIPPET); - } - - @Override - public void changeCursor(int partitionIndex, Cursor cursor) { - super.changeCursor(partitionIndex, cursor); - - if (cursor == null || !cursor.moveToFirst()) { - return; - } - - // hasProfile tells whether the first row is a profile - final boolean hasProfile = cursor.getInt(ContactQuery.CONTACT_IS_USER_PROFILE) == 1; - - // Add ME profile on top of favorites - cursor.moveToFirst(); - setProfileExists(hasProfile); - } - - /** @return Projection useful for children. */ - protected final String[] getProjection(boolean forSearch) { - final int sortOrder = getContactNameDisplayOrder(); - if (forSearch) { - if (sortOrder == ContactsPreferences.DISPLAY_ORDER_PRIMARY) { - return ContactQuery.FILTER_PROJECTION_PRIMARY; - } else { - return ContactQuery.FILTER_PROJECTION_ALTERNATIVE; - } - } else { - if (sortOrder == ContactsPreferences.DISPLAY_ORDER_PRIMARY) { - return ContactQuery.CONTACT_PROJECTION_PRIMARY; - } else { - return ContactQuery.CONTACT_PROJECTION_ALTERNATIVE; - } - } - } - - protected static class ContactQuery { - - public static final int CONTACT_ID = 0; - public static final int CONTACT_DISPLAY_NAME = 1; - public static final int CONTACT_PRESENCE_STATUS = 2; - public static final int CONTACT_CONTACT_STATUS = 3; - public static final int CONTACT_PHOTO_ID = 4; - public static final int CONTACT_PHOTO_URI = 5; - public static final int CONTACT_LOOKUP_KEY = 6; - public static final int CONTACT_IS_USER_PROFILE = 7; - public static final int CONTACT_PHONETIC_NAME = 8; - public static final int CONTACT_STARRED = 9; - public static final int CONTACT_SNIPPET = 10; - private static final String[] CONTACT_PROJECTION_PRIMARY = - new String[] { - Contacts._ID, // 0 - Contacts.DISPLAY_NAME_PRIMARY, // 1 - Contacts.CONTACT_PRESENCE, // 2 - Contacts.CONTACT_STATUS, // 3 - Contacts.PHOTO_ID, // 4 - Contacts.PHOTO_THUMBNAIL_URI, // 5 - Contacts.LOOKUP_KEY, // 6 - Contacts.IS_USER_PROFILE, // 7 - Contacts.PHONETIC_NAME, // 8 - Contacts.STARRED, // 9 - }; - private static final String[] CONTACT_PROJECTION_ALTERNATIVE = - new String[] { - Contacts._ID, // 0 - Contacts.DISPLAY_NAME_ALTERNATIVE, // 1 - Contacts.CONTACT_PRESENCE, // 2 - Contacts.CONTACT_STATUS, // 3 - Contacts.PHOTO_ID, // 4 - Contacts.PHOTO_THUMBNAIL_URI, // 5 - Contacts.LOOKUP_KEY, // 6 - Contacts.IS_USER_PROFILE, // 7 - Contacts.PHONETIC_NAME, // 8 - Contacts.STARRED, // 9 - }; - private static final String[] FILTER_PROJECTION_PRIMARY = - new String[] { - Contacts._ID, // 0 - Contacts.DISPLAY_NAME_PRIMARY, // 1 - Contacts.CONTACT_PRESENCE, // 2 - Contacts.CONTACT_STATUS, // 3 - Contacts.PHOTO_ID, // 4 - Contacts.PHOTO_THUMBNAIL_URI, // 5 - Contacts.LOOKUP_KEY, // 6 - Contacts.IS_USER_PROFILE, // 7 - Contacts.PHONETIC_NAME, // 8 - Contacts.STARRED, // 9 - SearchSnippets.SNIPPET, // 10 - }; - private static final String[] FILTER_PROJECTION_ALTERNATIVE = - new String[] { - Contacts._ID, // 0 - Contacts.DISPLAY_NAME_ALTERNATIVE, // 1 - Contacts.CONTACT_PRESENCE, // 2 - Contacts.CONTACT_STATUS, // 3 - Contacts.PHOTO_ID, // 4 - Contacts.PHOTO_THUMBNAIL_URI, // 5 - Contacts.LOOKUP_KEY, // 6 - Contacts.IS_USER_PROFILE, // 7 - Contacts.PHONETIC_NAME, // 8 - Contacts.STARRED, // 9 - SearchSnippets.SNIPPET, // 10 - }; - } -} diff --git a/java/com/android/contacts/common/list/ContactListItemView.java b/java/com/android/contacts/common/list/ContactListItemView.java deleted file mode 100644 index 409aa1b9e..000000000 --- a/java/com/android/contacts/common/list/ContactListItemView.java +++ /dev/null @@ -1,1508 +0,0 @@ -/* - * 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.res.ColorStateList; -import android.content.res.TypedArray; -import android.database.Cursor; -import android.graphics.Canvas; -import android.graphics.Color; -import android.graphics.Rect; -import android.graphics.Typeface; -import android.graphics.drawable.Drawable; -import android.os.Bundle; -import android.provider.ContactsContract; -import android.provider.ContactsContract.Contacts; -import android.provider.ContactsContract.SearchSnippets; -import android.support.annotation.IntDef; -import android.support.annotation.NonNull; -import android.support.v4.content.ContextCompat; -import android.telephony.PhoneNumberUtils; -import android.text.Spannable; -import android.text.SpannableString; -import android.text.TextUtils; -import android.text.TextUtils.TruncateAt; -import android.util.AttributeSet; -import android.util.TypedValue; -import android.view.Gravity; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewGroup; -import android.widget.AbsListView.SelectionBoundsAdjuster; -import android.widget.ImageView; -import android.widget.ImageView.ScaleType; -import android.widget.QuickContactBadge; -import android.widget.TextView; -import com.android.contacts.common.ContactPresenceIconUtil; -import com.android.contacts.common.ContactStatusUtil; -import com.android.contacts.common.R; -import com.android.contacts.common.format.TextHighlighter; -import com.android.contacts.common.list.PhoneNumberListAdapter.Listener; -import com.android.contacts.common.util.ContactDisplayUtils; -import com.android.contacts.common.util.SearchUtil; -import com.android.dialer.callintent.CallIntentBuilder; -import com.android.dialer.util.ViewUtil; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -/** - * A custom view for an item in the contact list. The view contains the contact's photo, a set of - * text views (for name, status, etc...) and icons for presence and call. The view uses no XML file - * for layout and all the measurements and layouts are done in the onMeasure and onLayout methods. - * - *

The layout puts the contact's photo on the right side of the view, the call icon (if present) - * to the left of the photo, the text lines are aligned to the left and the presence icon (if - * present) is set to the left of the status line. - * - *

The layout also supports a header (used as a header of a group of contacts) that is above the - * contact's data and a divider between contact view. - */ -public class ContactListItemView extends ViewGroup implements SelectionBoundsAdjuster { - - /** IntDef for indices of ViewPager tabs. */ - @Retention(RetentionPolicy.SOURCE) - @IntDef({NONE, VIDEO, DUO, CALL_AND_SHARE}) - public @interface CallToAction {} - - public static final int NONE = 0; - public static final int VIDEO = 1; - public static final int DUO = 2; - public static final int CALL_AND_SHARE = 3; - - private final PhotoPosition mPhotoPosition = getDefaultPhotoPosition(); - private static final Pattern SPLIT_PATTERN = - Pattern.compile("([\\w-\\.]+)@((?:[\\w]+\\.)+)([a-zA-Z]{2,4})|[\\w]+"); - static final char SNIPPET_START_MATCH = '['; - static final char SNIPPET_END_MATCH = ']'; - /** A helper used to highlight a prefix in a text field. */ - private final TextHighlighter mTextHighlighter; - // Style values for layout and appearance - // The initialized values are defaults if none is provided through xml. - private int mPreferredHeight = 0; - private int mGapBetweenImageAndText = 0; - private int mGapBetweenLabelAndData = 0; - private int mPresenceIconMargin = 4; - private int mPresenceIconSize = 16; - private int mTextIndent = 0; - private int mTextOffsetTop; - private int mNameTextViewTextSize; - private int mHeaderWidth; - private Drawable mActivatedBackgroundDrawable; - private int mCallToActionSize = 48; - private int mCallToActionMargin = 16; - // Set in onLayout. Represent left and right position of the View on the screen. - private int mLeftOffset; - private int mRightOffset; - /** Used with {@link #mLabelView}, specifying the width ratio between label and data. */ - private int mLabelViewWidthWeight = 3; - /** Used with {@link #mDataView}, specifying the width ratio between label and data. */ - private int mDataViewWidthWeight = 5; - - private ArrayList mNameHighlightSequence; - private ArrayList mNumberHighlightSequence; - // Highlighting prefix for names. - private String mHighlightedPrefix; - /** Indicates whether the view should leave room for the "video call" icon. */ - private boolean mSupportVideoCall; - - // Header layout data - private TextView mHeaderTextView; - private boolean mIsSectionHeaderEnabled; - // The views inside the contact view - private boolean mQuickContactEnabled = true; - private QuickContactBadge mQuickContact; - private ImageView mPhotoView; - private TextView mNameTextView; - private TextView mLabelView; - private TextView mDataView; - private TextView mSnippetView; - private TextView mStatusView; - private ImageView mPresenceIcon; - @NonNull private final ImageView mCallToActionView; - private ImageView mWorkProfileIcon; - private ColorStateList mSecondaryTextColor; - private int mDefaultPhotoViewSize = 0; - /** - * Can be effective even when {@link #mPhotoView} is null, as we want to have horizontal padding - * to align other data in this View. - */ - private int mPhotoViewWidth; - /** - * Can be effective even when {@link #mPhotoView} is null, as we want to have vertical padding. - */ - private int mPhotoViewHeight; - /** - * Only effective when {@link #mPhotoView} is null. When true all the Views on the right side of - * the photo should have horizontal padding on those left assuming there is a photo. - */ - private boolean mKeepHorizontalPaddingForPhotoView; - /** Only effective when {@link #mPhotoView} is null. */ - private boolean mKeepVerticalPaddingForPhotoView; - /** - * True when {@link #mPhotoViewWidth} and {@link #mPhotoViewHeight} are ready for being used. - * False indicates those values should be updated before being used in position calculation. - */ - private boolean mPhotoViewWidthAndHeightAreReady = false; - - private int mNameTextViewHeight; - private int mNameTextViewTextColor = Color.BLACK; - private int mPhoneticNameTextViewHeight; - private int mLabelViewHeight; - private int mDataViewHeight; - private int mSnippetTextViewHeight; - private int mStatusTextViewHeight; - private int mCheckBoxWidth; - // Holds Math.max(mLabelTextViewHeight, mDataViewHeight), assuming Label and Data share the - // same row. - private int mLabelAndDataViewMaxHeight; - private boolean mActivatedStateSupported; - private boolean mAdjustSelectionBoundsEnabled = true; - private Rect mBoundsWithoutHeader = new Rect(); - private CharSequence mUnknownNameText; - - private String mPhoneNumber; - private int mPosition = -1; - private @CallToAction int mCallToAction = NONE; - - public ContactListItemView(Context context, AttributeSet attrs, boolean supportVideoCallIcon) { - this(context, attrs); - - mSupportVideoCall = supportVideoCallIcon; - } - - public ContactListItemView(Context context, AttributeSet attrs) { - super(context, attrs); - - TypedArray a; - - if (R.styleable.ContactListItemView != null) { - // Read all style values - a = getContext().obtainStyledAttributes(attrs, R.styleable.ContactListItemView); - mPreferredHeight = - a.getDimensionPixelSize( - R.styleable.ContactListItemView_list_item_height, mPreferredHeight); - mActivatedBackgroundDrawable = - a.getDrawable(R.styleable.ContactListItemView_activated_background); - mGapBetweenImageAndText = - a.getDimensionPixelOffset( - R.styleable.ContactListItemView_list_item_gap_between_image_and_text, - mGapBetweenImageAndText); - mGapBetweenLabelAndData = - a.getDimensionPixelOffset( - R.styleable.ContactListItemView_list_item_gap_between_label_and_data, - mGapBetweenLabelAndData); - mPresenceIconMargin = - a.getDimensionPixelOffset( - R.styleable.ContactListItemView_list_item_presence_icon_margin, mPresenceIconMargin); - mPresenceIconSize = - a.getDimensionPixelOffset( - R.styleable.ContactListItemView_list_item_presence_icon_size, mPresenceIconSize); - mDefaultPhotoViewSize = - a.getDimensionPixelOffset( - R.styleable.ContactListItemView_list_item_photo_size, mDefaultPhotoViewSize); - mTextIndent = - a.getDimensionPixelOffset( - R.styleable.ContactListItemView_list_item_text_indent, mTextIndent); - mTextOffsetTop = - a.getDimensionPixelOffset( - R.styleable.ContactListItemView_list_item_text_offset_top, mTextOffsetTop); - mDataViewWidthWeight = - a.getInteger( - R.styleable.ContactListItemView_list_item_data_width_weight, mDataViewWidthWeight); - mLabelViewWidthWeight = - a.getInteger( - R.styleable.ContactListItemView_list_item_label_width_weight, mLabelViewWidthWeight); - mNameTextViewTextColor = - a.getColor( - R.styleable.ContactListItemView_list_item_name_text_color, mNameTextViewTextColor); - mNameTextViewTextSize = - (int) - a.getDimension( - R.styleable.ContactListItemView_list_item_name_text_size, - (int) getResources().getDimension(R.dimen.contact_browser_list_item_text_size)); - mCallToActionSize = - a.getDimensionPixelOffset( - R.styleable.ContactListItemView_list_item_video_call_icon_size, mCallToActionSize); - mCallToActionMargin = - a.getDimensionPixelOffset( - R.styleable.ContactListItemView_list_item_video_call_icon_margin, - mCallToActionMargin); - - setPaddingRelative( - a.getDimensionPixelOffset(R.styleable.ContactListItemView_list_item_padding_left, 0), - a.getDimensionPixelOffset(R.styleable.ContactListItemView_list_item_padding_top, 0), - a.getDimensionPixelOffset(R.styleable.ContactListItemView_list_item_padding_right, 0), - a.getDimensionPixelOffset(R.styleable.ContactListItemView_list_item_padding_bottom, 0)); - - a.recycle(); - } - - mTextHighlighter = new TextHighlighter(Typeface.BOLD); - - if (R.styleable.Theme != null) { - a = getContext().obtainStyledAttributes(R.styleable.Theme); - mSecondaryTextColor = a.getColorStateList(R.styleable.Theme_android_textColorSecondary); - a.recycle(); - } - - mHeaderWidth = getResources().getDimensionPixelSize(R.dimen.contact_list_section_header_width); - - if (mActivatedBackgroundDrawable != null) { - mActivatedBackgroundDrawable.setCallback(this); - } - - mNameHighlightSequence = new ArrayList<>(); - mNumberHighlightSequence = new ArrayList<>(); - - mCallToActionView = new ImageView(getContext()); - mCallToActionView.setId(R.id.call_to_action); - mCallToActionView.setLayoutParams(new LayoutParams(mCallToActionSize, mCallToActionSize)); - mCallToActionView.setScaleType(ScaleType.CENTER); - mCallToActionView.setImageTintList( - ContextCompat.getColorStateList(getContext(), R.color.search_video_call_icon_tint)); - addView(mCallToActionView); - - setLayoutDirection(View.LAYOUT_DIRECTION_LOCALE); - } - - public static PhotoPosition getDefaultPhotoPosition() { - int layoutDirection = TextUtils.getLayoutDirectionFromLocale(Locale.getDefault()); - return layoutDirection == View.LAYOUT_DIRECTION_RTL ? PhotoPosition.RIGHT : PhotoPosition.LEFT; - } - - /** - * Helper method for splitting a string into tokens. The lists passed in are populated with the - * tokens and offsets into the content of each token. The tokenization function parses e-mail - * addresses as a single token; otherwise it splits on any non-alphanumeric character. - * - * @param content Content to split. - * @return List of token strings. - */ - private static List split(String content) { - final Matcher matcher = SPLIT_PATTERN.matcher(content); - final ArrayList tokens = new ArrayList<>(); - while (matcher.find()) { - tokens.add(matcher.group()); - } - return tokens; - } - - public void setUnknownNameText(CharSequence unknownNameText) { - mUnknownNameText = unknownNameText; - } - - public void setQuickContactEnabled(boolean flag) { - mQuickContactEnabled = flag; - } - - /** - * Sets whether the call to action is shown. For the {@link CallToAction} to be shown, it must be - * supported as well. - * - * @param action {@link CallToAction} you want to display (if it's supported). - * @param listener Listener to notify when the call to action is clicked. - * @param position The position in the adapter of the call to action. - */ - public void setCallToAction(@CallToAction int action, Listener listener, int position) { - mCallToAction = action; - mPosition = position; - - Drawable drawable; - int description; - OnClickListener onClickListener; - if (action == CALL_AND_SHARE) { - drawable = ContextCompat.getDrawable(getContext(), R.drawable.ic_phone_attach); - drawable.setAutoMirrored(true); - description = R.string.description_search_call_and_share; - onClickListener = v -> listener.onCallAndShareIconClicked(position); - } else if (action == VIDEO && mSupportVideoCall) { - drawable = - ContextCompat.getDrawable(getContext(), R.drawable.quantum_ic_videocam_vd_theme_24); - drawable.setAutoMirrored(true); - description = R.string.description_search_video_call; - onClickListener = v -> listener.onVideoCallIconClicked(position); - } else if (action == DUO) { - CallIntentBuilder.increaseLightbringerCallButtonAppearInSearchCount(); - drawable = - ContextCompat.getDrawable(getContext(), R.drawable.quantum_ic_videocam_vd_theme_24); - drawable.setAutoMirrored(true); - description = R.string.description_search_video_call; - onClickListener = v -> listener.onDuoVideoIconClicked(position); - } else { - mCallToActionView.setVisibility(View.GONE); - mCallToActionView.setOnClickListener(null); - return; - } - - mCallToActionView.setContentDescription(getContext().getString(description)); - mCallToActionView.setOnClickListener(onClickListener); - mCallToActionView.setImageDrawable(drawable); - mCallToActionView.setVisibility(View.VISIBLE); - } - - public @CallToAction int getCallToAction() { - return mCallToAction; - } - - public int getPosition() { - return mPosition; - } - - /** - * Sets whether the view supports a video calling icon. This is independent of whether the view is - * actually showing an icon. Support for the video calling icon ensures that the layout leaves - * space for the video icon, should it be shown. - * - * @param supportVideoCall {@code true} if the video call icon is supported, {@code false} - * otherwise. - */ - public void setSupportVideoCallIcon(boolean supportVideoCall) { - mSupportVideoCall = supportVideoCall; - } - - @Override - protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { - // We will match parent's width and wrap content vertically, but make sure - // height is no less than listPreferredItemHeight. - final int specWidth = resolveSize(0, widthMeasureSpec); - final int preferredHeight = mPreferredHeight; - - mNameTextViewHeight = 0; - mPhoneticNameTextViewHeight = 0; - mLabelViewHeight = 0; - mDataViewHeight = 0; - mLabelAndDataViewMaxHeight = 0; - mSnippetTextViewHeight = 0; - mStatusTextViewHeight = 0; - mCheckBoxWidth = 0; - - ensurePhotoViewSize(); - - // Width each TextView is able to use. - int effectiveWidth; - // All the other Views will honor the photo, so available width for them may be shrunk. - if (mPhotoViewWidth > 0 || mKeepHorizontalPaddingForPhotoView) { - effectiveWidth = - specWidth - - getPaddingLeft() - - getPaddingRight() - - (mPhotoViewWidth + mGapBetweenImageAndText); - } else { - effectiveWidth = specWidth - getPaddingLeft() - getPaddingRight(); - } - - if (mIsSectionHeaderEnabled) { - effectiveWidth -= mHeaderWidth + mGapBetweenImageAndText; - } - - effectiveWidth -= (mCallToActionSize + mCallToActionMargin); - - // Go over all visible text views and measure actual width of each of them. - // Also calculate their heights to get the total height for this entire view. - - if (isVisible(mNameTextView)) { - // Calculate width for name text - this parallels similar measurement in onLayout. - int nameTextWidth = effectiveWidth; - if (mPhotoPosition != PhotoPosition.LEFT) { - nameTextWidth -= mTextIndent; - } - mNameTextView.measure( - MeasureSpec.makeMeasureSpec(nameTextWidth, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); - mNameTextViewHeight = mNameTextView.getMeasuredHeight(); - } - - // If both data (phone number/email address) and label (type like "MOBILE") are quite long, - // we should ellipsize both using appropriate ratio. - final int dataWidth; - final int labelWidth; - if (isVisible(mDataView)) { - if (isVisible(mLabelView)) { - final int totalWidth = effectiveWidth - mGapBetweenLabelAndData; - dataWidth = - ((totalWidth * mDataViewWidthWeight) / (mDataViewWidthWeight + mLabelViewWidthWeight)); - labelWidth = - ((totalWidth * mLabelViewWidthWeight) / (mDataViewWidthWeight + mLabelViewWidthWeight)); - } else { - dataWidth = effectiveWidth; - labelWidth = 0; - } - } else { - dataWidth = 0; - if (isVisible(mLabelView)) { - labelWidth = effectiveWidth; - } else { - labelWidth = 0; - } - } - - if (isVisible(mDataView)) { - mDataView.measure( - MeasureSpec.makeMeasureSpec(dataWidth, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); - mDataViewHeight = mDataView.getMeasuredHeight(); - } - - if (isVisible(mLabelView)) { - mLabelView.measure( - MeasureSpec.makeMeasureSpec(labelWidth, MeasureSpec.AT_MOST), - MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); - mLabelViewHeight = mLabelView.getMeasuredHeight(); - } - mLabelAndDataViewMaxHeight = Math.max(mLabelViewHeight, mDataViewHeight); - - if (isVisible(mSnippetView)) { - mSnippetView.measure( - MeasureSpec.makeMeasureSpec(effectiveWidth, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); - mSnippetTextViewHeight = mSnippetView.getMeasuredHeight(); - } - - // Status view height is the biggest of the text view and the presence icon - if (isVisible(mPresenceIcon)) { - mPresenceIcon.measure( - MeasureSpec.makeMeasureSpec(mPresenceIconSize, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(mPresenceIconSize, MeasureSpec.EXACTLY)); - mStatusTextViewHeight = mPresenceIcon.getMeasuredHeight(); - } - - mCallToActionView.measure( - MeasureSpec.makeMeasureSpec(mCallToActionSize, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(mCallToActionSize, MeasureSpec.EXACTLY)); - - if (isVisible(mWorkProfileIcon)) { - mWorkProfileIcon.measure( - MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), - MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); - mNameTextViewHeight = Math.max(mNameTextViewHeight, mWorkProfileIcon.getMeasuredHeight()); - } - - if (isVisible(mStatusView)) { - // Presence and status are in a same row, so status will be affected by icon size. - final int statusWidth; - if (isVisible(mPresenceIcon)) { - statusWidth = (effectiveWidth - mPresenceIcon.getMeasuredWidth() - mPresenceIconMargin); - } else { - statusWidth = effectiveWidth; - } - mStatusView.measure( - MeasureSpec.makeMeasureSpec(statusWidth, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); - mStatusTextViewHeight = Math.max(mStatusTextViewHeight, mStatusView.getMeasuredHeight()); - } - - // Calculate height including padding. - int height = - (mNameTextViewHeight - + mPhoneticNameTextViewHeight - + mLabelAndDataViewMaxHeight - + mSnippetTextViewHeight - + mStatusTextViewHeight); - - // Make sure the height is at least as high as the photo - height = Math.max(height, mPhotoViewHeight + getPaddingBottom() + getPaddingTop()); - - // Make sure height is at least the preferred height - height = Math.max(height, preferredHeight); - - // Measure the header if it is visible. - if (mHeaderTextView != null && mHeaderTextView.getVisibility() == VISIBLE) { - mHeaderTextView.measure( - MeasureSpec.makeMeasureSpec(mHeaderWidth, MeasureSpec.EXACTLY), - MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED)); - } - - setMeasuredDimension(specWidth, height); - } - - @Override - protected void onLayout(boolean changed, int left, int top, int right, int bottom) { - final int height = bottom - top; - final int width = right - left; - - // Determine the vertical bounds by laying out the header first. - int topBound = 0; - int leftBound = getPaddingLeft(); - int rightBound = width - getPaddingRight(); - - final boolean isLayoutRtl = ViewUtil.isViewLayoutRtl(this); - - // Put the section header on the left side of the contact view. - if (mIsSectionHeaderEnabled) { - // Align the text view all the way left, to be consistent with Contacts. - if (isLayoutRtl) { - rightBound = width; - } else { - leftBound = 0; - } - if (mHeaderTextView != null) { - int headerHeight = mHeaderTextView.getMeasuredHeight(); - int headerTopBound = (height + topBound - headerHeight) / 2 + mTextOffsetTop; - - mHeaderTextView.layout( - isLayoutRtl ? rightBound - mHeaderWidth : leftBound, - headerTopBound, - isLayoutRtl ? rightBound : leftBound + mHeaderWidth, - headerTopBound + headerHeight); - } - if (isLayoutRtl) { - rightBound -= mHeaderWidth; - } else { - leftBound += mHeaderWidth; - } - } - - mBoundsWithoutHeader.set(left + leftBound, topBound, left + rightBound, height); - mLeftOffset = left + leftBound; - mRightOffset = left + rightBound; - if (mIsSectionHeaderEnabled) { - if (isLayoutRtl) { - rightBound -= mGapBetweenImageAndText; - } else { - leftBound += mGapBetweenImageAndText; - } - } - - if (mActivatedStateSupported && isActivated()) { - mActivatedBackgroundDrawable.setBounds(mBoundsWithoutHeader); - } - - final View photoView = mQuickContact != null ? mQuickContact : mPhotoView; - if (mPhotoPosition == PhotoPosition.LEFT) { - // Photo is the left most view. All the other Views should on the right of the photo. - if (photoView != null) { - // Center the photo vertically - final int photoTop = topBound + (height - topBound - mPhotoViewHeight) / 2; - photoView.layout( - leftBound, photoTop, leftBound + mPhotoViewWidth, photoTop + mPhotoViewHeight); - leftBound += mPhotoViewWidth + mGapBetweenImageAndText; - } else if (mKeepHorizontalPaddingForPhotoView) { - // Draw nothing but keep the padding. - leftBound += mPhotoViewWidth + mGapBetweenImageAndText; - } - } else { - // Photo is the right most view. Right bound should be adjusted that way. - if (photoView != null) { - // Center the photo vertically - final int photoTop = topBound + (height - topBound - mPhotoViewHeight) / 2; - photoView.layout( - rightBound - mPhotoViewWidth, photoTop, rightBound, photoTop + mPhotoViewHeight); - rightBound -= (mPhotoViewWidth + mGapBetweenImageAndText); - } else if (mKeepHorizontalPaddingForPhotoView) { - // Draw nothing but keep the padding. - rightBound -= (mPhotoViewWidth + mGapBetweenImageAndText); - } - - // Add indent between left-most padding and texts. - leftBound += mTextIndent; - } - - // Place the call to action at the end of the list (e.g. take into account RTL mode). - // Center the icon vertically - final int callToActionTop = topBound + (height - topBound - mCallToActionSize) / 2; - - if (!isLayoutRtl) { - // When photo is on left, icon is placed on the right edge. - mCallToActionView.layout( - rightBound - mCallToActionSize, - callToActionTop, - rightBound, - callToActionTop + mCallToActionSize); - } else { - // When photo is on right, icon is placed on the left edge. - mCallToActionView.layout( - leftBound, - callToActionTop, - leftBound + mCallToActionSize, - callToActionTop + mCallToActionSize); - } - - if (mPhotoPosition == PhotoPosition.LEFT) { - rightBound -= (mCallToActionSize + mCallToActionMargin); - } else { - leftBound += mCallToActionSize + mCallToActionMargin; - } - - // Center text vertically, then apply the top offset. - final int totalTextHeight = - mNameTextViewHeight - + mPhoneticNameTextViewHeight - + mLabelAndDataViewMaxHeight - + mSnippetTextViewHeight - + mStatusTextViewHeight; - int textTopBound = (height + topBound - totalTextHeight) / 2 + mTextOffsetTop; - - // Work Profile icon align top - int workProfileIconWidth = 0; - if (isVisible(mWorkProfileIcon)) { - workProfileIconWidth = mWorkProfileIcon.getMeasuredWidth(); - final int distanceFromEnd = mCheckBoxWidth > 0 ? mCheckBoxWidth + mGapBetweenImageAndText : 0; - if (mPhotoPosition == PhotoPosition.LEFT) { - // When photo is on left, label is placed on the right edge of the list item. - mWorkProfileIcon.layout( - rightBound - workProfileIconWidth - distanceFromEnd, - textTopBound, - rightBound - distanceFromEnd, - textTopBound + mNameTextViewHeight); - } else { - // When photo is on right, label is placed on the left of data view. - mWorkProfileIcon.layout( - leftBound + distanceFromEnd, - textTopBound, - leftBound + workProfileIconWidth + distanceFromEnd, - textTopBound + mNameTextViewHeight); - } - } - - // Layout all text view and presence icon - // Put name TextView first - if (isVisible(mNameTextView)) { - final int distanceFromEnd = - workProfileIconWidth - + (mCheckBoxWidth > 0 ? mCheckBoxWidth + mGapBetweenImageAndText : 0); - if (mPhotoPosition == PhotoPosition.LEFT) { - mNameTextView.layout( - leftBound, - textTopBound, - rightBound - distanceFromEnd, - textTopBound + mNameTextViewHeight); - } else { - mNameTextView.layout( - leftBound + distanceFromEnd, - textTopBound, - rightBound, - textTopBound + mNameTextViewHeight); - } - } - - if (isVisible(mNameTextView) || isVisible(mWorkProfileIcon)) { - textTopBound += mNameTextViewHeight; - } - - // Presence and status - if (isLayoutRtl) { - int statusRightBound = rightBound; - if (isVisible(mPresenceIcon)) { - int iconWidth = mPresenceIcon.getMeasuredWidth(); - mPresenceIcon.layout( - rightBound - iconWidth, textTopBound, rightBound, textTopBound + mStatusTextViewHeight); - statusRightBound -= (iconWidth + mPresenceIconMargin); - } - - if (isVisible(mStatusView)) { - mStatusView.layout( - leftBound, textTopBound, statusRightBound, textTopBound + mStatusTextViewHeight); - } - } else { - int statusLeftBound = leftBound; - if (isVisible(mPresenceIcon)) { - int iconWidth = mPresenceIcon.getMeasuredWidth(); - mPresenceIcon.layout( - leftBound, textTopBound, leftBound + iconWidth, textTopBound + mStatusTextViewHeight); - statusLeftBound += (iconWidth + mPresenceIconMargin); - } - - if (isVisible(mStatusView)) { - mStatusView.layout( - statusLeftBound, textTopBound, rightBound, textTopBound + mStatusTextViewHeight); - } - } - - if (isVisible(mStatusView) || isVisible(mPresenceIcon)) { - textTopBound += mStatusTextViewHeight; - } - - // Rest of text views - int dataLeftBound = leftBound; - - // Label and Data align bottom. - if (isVisible(mLabelView)) { - if (!isLayoutRtl) { - mLabelView.layout( - dataLeftBound, - textTopBound + mLabelAndDataViewMaxHeight - mLabelViewHeight, - rightBound, - textTopBound + mLabelAndDataViewMaxHeight); - dataLeftBound += mLabelView.getMeasuredWidth() + mGapBetweenLabelAndData; - } else { - dataLeftBound = leftBound + mLabelView.getMeasuredWidth(); - mLabelView.layout( - rightBound - mLabelView.getMeasuredWidth(), - textTopBound + mLabelAndDataViewMaxHeight - mLabelViewHeight, - rightBound, - textTopBound + mLabelAndDataViewMaxHeight); - rightBound -= (mLabelView.getMeasuredWidth() + mGapBetweenLabelAndData); - } - } - - if (isVisible(mDataView)) { - if (!isLayoutRtl) { - mDataView.layout( - dataLeftBound, - textTopBound + mLabelAndDataViewMaxHeight - mDataViewHeight, - rightBound, - textTopBound + mLabelAndDataViewMaxHeight); - } else { - mDataView.layout( - rightBound - mDataView.getMeasuredWidth(), - textTopBound + mLabelAndDataViewMaxHeight - mDataViewHeight, - rightBound, - textTopBound + mLabelAndDataViewMaxHeight); - } - } - if (isVisible(mLabelView) || isVisible(mDataView)) { - textTopBound += mLabelAndDataViewMaxHeight; - } - - if (isVisible(mSnippetView)) { - mSnippetView.layout( - leftBound, textTopBound, rightBound, textTopBound + mSnippetTextViewHeight); - } - } - - @Override - public void adjustListItemSelectionBounds(Rect bounds) { - if (mAdjustSelectionBoundsEnabled) { - bounds.top += mBoundsWithoutHeader.top; - bounds.bottom = bounds.top + mBoundsWithoutHeader.height(); - bounds.left = mBoundsWithoutHeader.left; - bounds.right = mBoundsWithoutHeader.right; - } - } - - protected boolean isVisible(View view) { - return view != null && view.getVisibility() == View.VISIBLE; - } - - /** Extracts width and height from the style */ - private void ensurePhotoViewSize() { - if (!mPhotoViewWidthAndHeightAreReady) { - mPhotoViewWidth = mPhotoViewHeight = getDefaultPhotoViewSize(); - if (!mQuickContactEnabled && mPhotoView == null) { - if (!mKeepHorizontalPaddingForPhotoView) { - mPhotoViewWidth = 0; - } - if (!mKeepVerticalPaddingForPhotoView) { - mPhotoViewHeight = 0; - } - } - - mPhotoViewWidthAndHeightAreReady = true; - } - } - - protected int getDefaultPhotoViewSize() { - return mDefaultPhotoViewSize; - } - - /** - * Gets a LayoutParam that corresponds to the default photo size. - * - * @return A new LayoutParam. - */ - private LayoutParams getDefaultPhotoLayoutParams() { - LayoutParams params = generateDefaultLayoutParams(); - params.width = getDefaultPhotoViewSize(); - params.height = params.width; - return params; - } - - @Override - protected void drawableStateChanged() { - super.drawableStateChanged(); - if (mActivatedStateSupported) { - mActivatedBackgroundDrawable.setState(getDrawableState()); - } - } - - @Override - protected boolean verifyDrawable(Drawable who) { - return who == mActivatedBackgroundDrawable || super.verifyDrawable(who); - } - - @Override - public void jumpDrawablesToCurrentState() { - super.jumpDrawablesToCurrentState(); - if (mActivatedStateSupported) { - mActivatedBackgroundDrawable.jumpToCurrentState(); - } - } - - @Override - public void dispatchDraw(Canvas canvas) { - if (mActivatedStateSupported && isActivated()) { - mActivatedBackgroundDrawable.draw(canvas); - } - - super.dispatchDraw(canvas); - } - - /** Sets section header or makes it invisible if the title is null. */ - public void setSectionHeader(String title) { - if (!TextUtils.isEmpty(title)) { - if (mHeaderTextView == null) { - mHeaderTextView = new TextView(getContext()); - mHeaderTextView.setTextAppearance(R.style.SectionHeaderStyle); - mHeaderTextView.setGravity(Gravity.CENTER_VERTICAL | Gravity.CENTER_HORIZONTAL); - addView(mHeaderTextView); - } - setMarqueeText(mHeaderTextView, title); - mHeaderTextView.setVisibility(View.VISIBLE); - mHeaderTextView.setAllCaps(true); - } else if (mHeaderTextView != null) { - mHeaderTextView.setVisibility(View.GONE); - } - } - - public void setIsSectionHeaderEnabled(boolean isSectionHeaderEnabled) { - mIsSectionHeaderEnabled = isSectionHeaderEnabled; - } - - /** Returns the quick contact badge, creating it if necessary. */ - public QuickContactBadge getQuickContact() { - if (!mQuickContactEnabled) { - throw new IllegalStateException("QuickContact is disabled for this view"); - } - if (mQuickContact == null) { - mQuickContact = new QuickContactBadge(getContext()); - mQuickContact.setOverlay(null); - mQuickContact.setLayoutParams(getDefaultPhotoLayoutParams()); - if (mNameTextView != null) { - mQuickContact.setContentDescription( - getContext() - .getString(R.string.description_quick_contact_for, mNameTextView.getText())); - } - - addView(mQuickContact); - mPhotoViewWidthAndHeightAreReady = false; - } - return mQuickContact; - } - - /** Returns the photo view, creating it if necessary. */ - public ImageView getPhotoView() { - if (mPhotoView == null) { - mPhotoView = new ImageView(getContext()); - mPhotoView.setLayoutParams(getDefaultPhotoLayoutParams()); - // Quick contact style used above will set a background - remove it - mPhotoView.setBackground(null); - addView(mPhotoView); - mPhotoViewWidthAndHeightAreReady = false; - } - return mPhotoView; - } - - /** Removes the photo view. */ - public void removePhotoView() { - removePhotoView(false, true); - } - - /** - * Removes the photo view. - * - * @param keepHorizontalPadding True means data on the right side will have padding on left, - * pretending there is still a photo view. - * @param keepVerticalPadding True means the View will have some height enough for accommodating a - * photo view. - */ - public void removePhotoView(boolean keepHorizontalPadding, boolean keepVerticalPadding) { - mPhotoViewWidthAndHeightAreReady = false; - mKeepHorizontalPaddingForPhotoView = keepHorizontalPadding; - mKeepVerticalPaddingForPhotoView = keepVerticalPadding; - if (mPhotoView != null) { - removeView(mPhotoView); - mPhotoView = null; - } - if (mQuickContact != null) { - removeView(mQuickContact); - mQuickContact = null; - } - } - - /** - * Sets a word prefix that will be highlighted if encountered in fields like name and search - * snippet. This will disable the mask highlighting for names. - * - *

NOTE: must be all upper-case - */ - public void setHighlightedPrefix(String upperCasePrefix) { - mHighlightedPrefix = upperCasePrefix; - } - - /** Clears previously set highlight sequences for the view. */ - public void clearHighlightSequences() { - mNameHighlightSequence.clear(); - mNumberHighlightSequence.clear(); - mHighlightedPrefix = null; - } - - /** - * Adds a highlight sequence to the name highlighter. - * - * @param start The start position of the highlight sequence. - * @param end The end position of the highlight sequence. - */ - public void addNameHighlightSequence(int start, int end) { - mNameHighlightSequence.add(new HighlightSequence(start, end)); - } - - /** - * Adds a highlight sequence to the number highlighter. - * - * @param start The start position of the highlight sequence. - * @param end The end position of the highlight sequence. - */ - public void addNumberHighlightSequence(int start, int end) { - mNumberHighlightSequence.add(new HighlightSequence(start, end)); - } - - /** Returns the text view for the contact name, creating it if necessary. */ - public TextView getNameTextView() { - if (mNameTextView == null) { - mNameTextView = new TextView(getContext()); - mNameTextView.setSingleLine(true); - mNameTextView.setEllipsize(getTextEllipsis()); - mNameTextView.setTextColor(mNameTextViewTextColor); - mNameTextView.setTextSize(TypedValue.COMPLEX_UNIT_PX, mNameTextViewTextSize); - // Manually call setActivated() since this view may be added after the first - // setActivated() call toward this whole item view. - mNameTextView.setActivated(isActivated()); - mNameTextView.setGravity(Gravity.CENTER_VERTICAL); - mNameTextView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START); - mNameTextView.setId(R.id.cliv_name_textview); - mNameTextView.setElegantTextHeight(false); - addView(mNameTextView); - } - return mNameTextView; - } - - /** Adds or updates a text view for the data label. */ - public void setLabel(CharSequence text) { - if (TextUtils.isEmpty(text)) { - if (mLabelView != null) { - mLabelView.setVisibility(View.GONE); - } - } else { - getLabelView(); - setMarqueeText(mLabelView, text); - mLabelView.setVisibility(VISIBLE); - } - } - - /** Returns the text view for the data label, creating it if necessary. */ - public TextView getLabelView() { - if (mLabelView == null) { - mLabelView = new TextView(getContext()); - mLabelView.setLayoutParams( - new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)); - - mLabelView.setSingleLine(true); - mLabelView.setEllipsize(getTextEllipsis()); - mLabelView.setTextAppearance(R.style.TextAppearanceSmall); - if (mPhotoPosition == PhotoPosition.LEFT) { - mLabelView.setAllCaps(true); - } else { - mLabelView.setTypeface(mLabelView.getTypeface(), Typeface.BOLD); - } - mLabelView.setActivated(isActivated()); - mLabelView.setId(R.id.cliv_label_textview); - addView(mLabelView); - } - return mLabelView; - } - - /** - * Sets phone number for a list item. This takes care of number highlighting if the highlight mask - * exists. - */ - public void setPhoneNumber(String text) { - mPhoneNumber = text; - if (text == null) { - if (mDataView != null) { - mDataView.setVisibility(View.GONE); - } - } else { - getDataView(); - - // TODO: Format number using PhoneNumberUtils.formatNumber before assigning it to - // mDataView. Make sure that determination of the highlight sequences are done only - // after number formatting. - - // Sets phone number texts for display after highlighting it, if applicable. - // CharSequence textToSet = text; - final SpannableString textToSet = new SpannableString(text); - - if (mNumberHighlightSequence.size() != 0) { - final HighlightSequence highlightSequence = mNumberHighlightSequence.get(0); - mTextHighlighter.applyMaskingHighlight( - textToSet, highlightSequence.start, highlightSequence.end); - } - - setMarqueeText(mDataView, textToSet); - mDataView.setVisibility(VISIBLE); - - // We have a phone number as "mDataView" so make it always LTR and VIEW_START - mDataView.setTextDirection(View.TEXT_DIRECTION_LTR); - mDataView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START); - } - } - - public String getPhoneNumber() { - return mPhoneNumber; - } - - private void setMarqueeText(TextView textView, CharSequence text) { - if (getTextEllipsis() == TruncateAt.MARQUEE) { - // To show MARQUEE correctly (with END effect during non-active state), we need - // to build Spanned with MARQUEE in addition to TextView's ellipsize setting. - final SpannableString spannable = new SpannableString(text); - spannable.setSpan( - TruncateAt.MARQUEE, 0, spannable.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - textView.setText(spannable); - } else { - textView.setText(text); - } - } - - /** Returns the text view for the data text, creating it if necessary. */ - public TextView getDataView() { - if (mDataView == null) { - mDataView = new TextView(getContext()); - mDataView.setSingleLine(true); - mDataView.setEllipsize(getTextEllipsis()); - mDataView.setTextAppearance(R.style.TextAppearanceSmall); - mDataView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START); - mDataView.setActivated(isActivated()); - mDataView.setId(R.id.cliv_data_view); - mDataView.setElegantTextHeight(false); - addView(mDataView); - } - return mDataView; - } - - /** Adds or updates a text view for the search snippet. */ - public void setSnippet(String text) { - if (TextUtils.isEmpty(text)) { - if (mSnippetView != null) { - mSnippetView.setVisibility(View.GONE); - } - } else { - mTextHighlighter.setPrefixText(getSnippetView(), text, mHighlightedPrefix); - mSnippetView.setVisibility(VISIBLE); - if (ContactDisplayUtils.isPossiblePhoneNumber(text)) { - // Give the text-to-speech engine a hint that it's a phone number - mSnippetView.setContentDescription(PhoneNumberUtils.createTtsSpannable(text)); - } else { - mSnippetView.setContentDescription(null); - } - } - } - - /** Returns the text view for the search snippet, creating it if necessary. */ - public TextView getSnippetView() { - if (mSnippetView == null) { - mSnippetView = new TextView(getContext()); - mSnippetView.setSingleLine(true); - mSnippetView.setEllipsize(getTextEllipsis()); - mSnippetView.setTextAppearance(android.R.style.TextAppearance_Small); - mSnippetView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START); - mSnippetView.setActivated(isActivated()); - addView(mSnippetView); - } - return mSnippetView; - } - - /** Returns the text view for the status, creating it if necessary. */ - public TextView getStatusView() { - if (mStatusView == null) { - mStatusView = new TextView(getContext()); - mStatusView.setSingleLine(true); - mStatusView.setEllipsize(getTextEllipsis()); - mStatusView.setTextAppearance(android.R.style.TextAppearance_Small); - mStatusView.setTextColor(mSecondaryTextColor); - mStatusView.setActivated(isActivated()); - mStatusView.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START); - addView(mStatusView); - } - return mStatusView; - } - - /** Adds or updates a text view for the status. */ - public void setStatus(CharSequence text) { - if (TextUtils.isEmpty(text)) { - if (mStatusView != null) { - mStatusView.setVisibility(View.GONE); - } - } else { - getStatusView(); - setMarqueeText(mStatusView, text); - mStatusView.setVisibility(VISIBLE); - } - } - - /** Adds or updates the presence icon view. */ - public void setPresence(Drawable icon) { - if (icon != null) { - if (mPresenceIcon == null) { - mPresenceIcon = new ImageView(getContext()); - addView(mPresenceIcon); - } - mPresenceIcon.setImageDrawable(icon); - mPresenceIcon.setScaleType(ScaleType.CENTER); - mPresenceIcon.setVisibility(View.VISIBLE); - } else { - if (mPresenceIcon != null) { - mPresenceIcon.setVisibility(View.GONE); - } - } - } - - /** - * Set to display work profile icon or not - * - * @param enabled set to display work profile icon or not - */ - public void setWorkProfileIconEnabled(boolean enabled) { - if (mWorkProfileIcon != null) { - mWorkProfileIcon.setVisibility(enabled ? View.VISIBLE : View.GONE); - } else if (enabled) { - mWorkProfileIcon = new ImageView(getContext()); - addView(mWorkProfileIcon); - mWorkProfileIcon.setImageResource(R.drawable.ic_work_profile); - mWorkProfileIcon.setScaleType(ScaleType.CENTER_INSIDE); - mWorkProfileIcon.setVisibility(View.VISIBLE); - } - } - - private TruncateAt getTextEllipsis() { - return TruncateAt.MARQUEE; - } - - public void showDisplayName(Cursor cursor, int nameColumnIndex) { - CharSequence name = cursor.getString(nameColumnIndex); - setDisplayName(name); - - // Since the quick contact content description is derived from the display name and there is - // no guarantee that when the quick contact is initialized the display name is already set, - // do it here too. - if (mQuickContact != null) { - mQuickContact.setContentDescription( - getContext().getString(R.string.description_quick_contact_for, mNameTextView.getText())); - } - } - - public void setDisplayName(CharSequence name) { - if (!TextUtils.isEmpty(name)) { - // Chooses the available highlighting method for highlighting. - if (mHighlightedPrefix != null) { - name = mTextHighlighter.applyPrefixHighlight(name, mHighlightedPrefix); - } else if (mNameHighlightSequence.size() != 0) { - final SpannableString spannableName = new SpannableString(name); - for (HighlightSequence highlightSequence : mNameHighlightSequence) { - mTextHighlighter.applyMaskingHighlight( - spannableName, highlightSequence.start, highlightSequence.end); - } - name = spannableName; - } - } else { - name = mUnknownNameText; - } - setMarqueeText(getNameTextView(), name); - - if (ContactDisplayUtils.isPossiblePhoneNumber(name)) { - // Give the text-to-speech engine a hint that it's a phone number - mNameTextView.setTextDirection(View.TEXT_DIRECTION_LTR); - mNameTextView.setContentDescription(PhoneNumberUtils.createTtsSpannable(name.toString())); - } else { - // Remove span tags of highlighting for talkback to avoid reading highlighting and rest - // of the name into two separate parts. - mNameTextView.setContentDescription(name.toString()); - } - } - - public void hideDisplayName() { - if (mNameTextView != null) { - removeView(mNameTextView); - mNameTextView = null; - } - } - - /** Sets the proper icon (star or presence or nothing) and/or status message. */ - public void showPresenceAndStatusMessage( - Cursor cursor, int presenceColumnIndex, int contactStatusColumnIndex) { - Drawable icon = null; - int presence = 0; - if (!cursor.isNull(presenceColumnIndex)) { - presence = cursor.getInt(presenceColumnIndex); - icon = ContactPresenceIconUtil.getPresenceIcon(getContext(), presence); - } - setPresence(icon); - - String statusMessage = null; - if (contactStatusColumnIndex != 0 && !cursor.isNull(contactStatusColumnIndex)) { - statusMessage = cursor.getString(contactStatusColumnIndex); - } - // If there is no status message from the contact, but there was a presence value, then use - // the default status message string - if (statusMessage == null && presence != 0) { - statusMessage = ContactStatusUtil.getStatusString(getContext(), presence); - } - setStatus(statusMessage); - } - - /** Shows search snippet. */ - public void showSnippet(Cursor cursor, int summarySnippetColumnIndex) { - if (cursor.getColumnCount() <= summarySnippetColumnIndex - || !SearchSnippets.SNIPPET.equals(cursor.getColumnName(summarySnippetColumnIndex))) { - setSnippet(null); - return; - } - - String snippet = cursor.getString(summarySnippetColumnIndex); - - // Do client side snippeting if provider didn't do it - final Bundle extras = cursor.getExtras(); - if (extras.getBoolean(ContactsContract.DEFERRED_SNIPPETING)) { - - final String query = extras.getString(ContactsContract.DEFERRED_SNIPPETING_QUERY); - - String displayName = null; - int displayNameIndex = cursor.getColumnIndex(Contacts.DISPLAY_NAME); - if (displayNameIndex >= 0) { - displayName = cursor.getString(displayNameIndex); - } - - snippet = updateSnippet(snippet, query, displayName); - - } else { - if (snippet != null) { - int from = 0; - int to = snippet.length(); - int start = snippet.indexOf(SNIPPET_START_MATCH); - if (start == -1) { - snippet = null; - } else { - int firstNl = snippet.lastIndexOf('\n', start); - if (firstNl != -1) { - from = firstNl + 1; - } - int end = snippet.lastIndexOf(SNIPPET_END_MATCH); - if (end != -1) { - int lastNl = snippet.indexOf('\n', end); - if (lastNl != -1) { - to = lastNl; - } - } - - StringBuilder sb = new StringBuilder(); - for (int i = from; i < to; i++) { - char c = snippet.charAt(i); - if (c != SNIPPET_START_MATCH && c != SNIPPET_END_MATCH) { - sb.append(c); - } - } - snippet = sb.toString(); - } - } - } - - setSnippet(snippet); - } - - /** - * Used for deferred snippets from the database. The contents come back as large strings which - * need to be extracted for display. - * - * @param snippet The snippet from the database. - * @param query The search query substring. - * @param displayName The contact display name. - * @return The proper snippet to display. - */ - private String updateSnippet(String snippet, String query, String displayName) { - - if (TextUtils.isEmpty(snippet) || TextUtils.isEmpty(query)) { - return null; - } - query = SearchUtil.cleanStartAndEndOfSearchQuery(query.toLowerCase()); - - // If the display name already contains the query term, return empty - snippets should - // not be needed in that case. - if (!TextUtils.isEmpty(displayName)) { - final String lowerDisplayName = displayName.toLowerCase(); - final List nameTokens = split(lowerDisplayName); - for (String nameToken : nameTokens) { - if (nameToken.startsWith(query)) { - return null; - } - } - } - - // The snippet may contain multiple data lines. - // Show the first line that matches the query. - final SearchUtil.MatchedLine matched = SearchUtil.findMatchingLine(snippet, query); - - if (matched != null && matched.line != null) { - // Tokenize for long strings since the match may be at the end of it. - // Skip this part for short strings since the whole string will be displayed. - // Most contact strings are short so the snippetize method will be called infrequently. - final int lengthThreshold = - getResources().getInteger(R.integer.snippet_length_before_tokenize); - if (matched.line.length() > lengthThreshold) { - return snippetize(matched.line, matched.startIndex, lengthThreshold); - } else { - return matched.line; - } - } - - // No match found. - return null; - } - - private String snippetize(String line, int matchIndex, int maxLength) { - // Show up to maxLength characters. But we only show full tokens so show the last full token - // up to maxLength characters. So as many starting tokens as possible before trying ending - // tokens. - int remainingLength = maxLength; - int tempRemainingLength = remainingLength; - - // Start the end token after the matched query. - int index = matchIndex; - int endTokenIndex = index; - - // Find the match token first. - while (index < line.length()) { - if (!Character.isLetterOrDigit(line.charAt(index))) { - endTokenIndex = index; - remainingLength = tempRemainingLength; - break; - } - tempRemainingLength--; - index++; - } - - // Find as much content before the match. - index = matchIndex - 1; - tempRemainingLength = remainingLength; - int startTokenIndex = matchIndex; - while (index > -1 && tempRemainingLength > 0) { - if (!Character.isLetterOrDigit(line.charAt(index))) { - startTokenIndex = index; - remainingLength = tempRemainingLength; - } - tempRemainingLength--; - index--; - } - - index = endTokenIndex; - tempRemainingLength = remainingLength; - // Find remaining content at after match. - while (index < line.length() && tempRemainingLength > 0) { - if (!Character.isLetterOrDigit(line.charAt(index))) { - endTokenIndex = index; - } - tempRemainingLength--; - index++; - } - // Append ellipse if there is content before or after. - final StringBuilder sb = new StringBuilder(); - if (startTokenIndex > 0) { - sb.append("..."); - } - sb.append(line.substring(startTokenIndex, endTokenIndex)); - if (endTokenIndex < line.length()) { - sb.append("..."); - } - return sb.toString(); - } - - public void setActivatedStateSupported(boolean flag) { - this.mActivatedStateSupported = flag; - } - - public void setAdjustSelectionBoundsEnabled(boolean enabled) { - mAdjustSelectionBoundsEnabled = enabled; - } - - @Override - public void requestLayout() { - // We will assume that once measured this will not need to resize - // itself, so there is no need to pass the layout request to the parent - // view (ListView). - forceLayout(); - } - - /** - * Set drawable resources directly for the drawable resource of the photo view. - * - * @param drawable A drawable resource. - */ - public void setDrawable(Drawable drawable) { - ImageView photo = getPhotoView(); - photo.setScaleType(ImageView.ScaleType.CENTER); - int iconColor = ContextCompat.getColor(getContext(), R.color.search_shortcut_icon_color); - photo.setImageDrawable(drawable); - photo.setImageTintList(ColorStateList.valueOf(iconColor)); - } - - @Override - public boolean onTouchEvent(MotionEvent event) { - final float x = event.getX(); - final float y = event.getY(); - // If the touch event's coordinates are not within the view's header, then delegate - // to super.onTouchEvent so that regular view behavior is preserved. Otherwise, consume - // and ignore the touch event. - if (mBoundsWithoutHeader.contains((int) x, (int) y) || !pointIsInView(x, y)) { - return super.onTouchEvent(event); - } else { - return true; - } - } - - private boolean pointIsInView(float localX, float localY) { - return localX >= mLeftOffset - && localX < mRightOffset - && localY >= 0 - && localY < (getBottom() - getTop()); - } - - /** - * Where to put contact photo. This affects the other Views' layout or look-and-feel. - * - *

TODO: replace enum with int constants - */ - public enum PhotoPosition { - LEFT, - RIGHT - } - - protected static class HighlightSequence { - - private final int start; - private final int end; - - HighlightSequence(int start, int end) { - this.start = start; - this.end = end; - } - } -} diff --git a/java/com/android/contacts/common/list/ContactListPinnedHeaderView.java b/java/com/android/contacts/common/list/ContactListPinnedHeaderView.java deleted file mode 100644 index 1f3e2bfe3..000000000 --- a/java/com/android/contacts/common/list/ContactListPinnedHeaderView.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * 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.res.TypedArray; -import android.graphics.Color; -import android.text.TextUtils; -import android.util.AttributeSet; -import android.view.Gravity; -import android.view.View; -import android.widget.LinearLayout.LayoutParams; -import android.widget.TextView; -import com.android.contacts.common.R; - -/** A custom view for the pinned section header shown at the top of the contact list. */ -public class ContactListPinnedHeaderView extends TextView { - - public ContactListPinnedHeaderView(Context context, AttributeSet attrs, View parent) { - super(context, attrs); - - if (R.styleable.ContactListItemView == null) { - return; - } - TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.ContactListItemView); - int backgroundColor = - a.getColor(R.styleable.ContactListItemView_list_item_background_color, Color.WHITE); - int textOffsetTop = - a.getDimensionPixelSize(R.styleable.ContactListItemView_list_item_text_offset_top, 0); - int paddingStartOffset = - a.getDimensionPixelSize(R.styleable.ContactListItemView_list_item_padding_left, 0); - int textWidth = getResources().getDimensionPixelSize(R.dimen.contact_list_section_header_width); - int widthIncludingPadding = paddingStartOffset + textWidth; - a.recycle(); - - setBackgroundColor(backgroundColor); - setTextAppearance(getContext(), R.style.SectionHeaderStyle); - setLayoutParams(new LayoutParams(textWidth, LayoutParams.WRAP_CONTENT)); - setLayoutDirection(parent.getLayoutDirection()); - setGravity(Gravity.CENTER_VERTICAL | Gravity.CENTER_HORIZONTAL); - - // Apply text top offset. Multiply by two, because we are implementing this by padding for a - // vertically centered view, rather than adjusting the position directly via a layout. - setPaddingRelative( - 0, getPaddingTop() + (textOffsetTop * 2), getPaddingEnd(), getPaddingBottom()); - } - - /** Sets section header or makes it invisible if the title is null. */ - public void setSectionHeaderTitle(String title) { - if (!TextUtils.isEmpty(title)) { - setText(title); - } else { - setVisibility(View.GONE); - } - } -} diff --git a/java/com/android/contacts/common/list/ContactsSectionIndexer.java b/java/com/android/contacts/common/list/ContactsSectionIndexer.java deleted file mode 100644 index 3f0f2b7ee..000000000 --- a/java/com/android/contacts/common/list/ContactsSectionIndexer.java +++ /dev/null @@ -1,119 +0,0 @@ -/* - * 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.text.TextUtils; -import android.widget.SectionIndexer; -import java.util.Arrays; - -/** - * A section indexer that is configured with precomputed section titles and their respective counts. - */ -public class ContactsSectionIndexer implements SectionIndexer { - - private static final String BLANK_HEADER_STRING = " "; - private String[] mSections; - private int[] mPositions; - private int mCount; - - /** - * Constructor. - * - * @param sections a non-null array - * @param counts a non-null array of the same size as sections - */ - public ContactsSectionIndexer(String[] sections, int[] counts) { - if (sections == null || counts == null) { - throw new NullPointerException(); - } - - if (sections.length != counts.length) { - throw new IllegalArgumentException( - "The sections and counts arrays must have the same length"); - } - - // TODO process sections/counts based on current locale and/or specific section titles - - this.mSections = sections; - mPositions = new int[counts.length]; - int position = 0; - for (int i = 0; i < counts.length; i++) { - if (TextUtils.isEmpty(mSections[i])) { - mSections[i] = BLANK_HEADER_STRING; - } else if (!mSections[i].equals(BLANK_HEADER_STRING)) { - mSections[i] = mSections[i].trim(); - } - - mPositions[i] = position; - position += counts[i]; - } - mCount = position; - } - - public Object[] getSections() { - return mSections; - } - - public int getPositionForSection(int section) { - if (section < 0 || section >= mSections.length) { - return -1; - } - - return mPositions[section]; - } - - public int getSectionForPosition(int position) { - if (position < 0 || position >= mCount) { - return -1; - } - - int index = Arrays.binarySearch(mPositions, position); - - /* - * Consider this example: section positions are 0, 3, 5; the supplied - * position is 4. The section corresponding to position 4 starts at - * position 3, so the expected return value is 1. Binary search will not - * find 4 in the array and thus will return -insertPosition-1, i.e. -3. - * To get from that number to the expected value of 1 we need to negate - * and subtract 2. - */ - return index >= 0 ? index : -index - 2; - } - - public void setProfileAndFavoritesHeader(String header, int numberOfItemsToAdd) { - if (mSections != null) { - // Don't do anything if the header is already set properly. - if (mSections.length > 0 && header.equals(mSections[0])) { - return; - } - - // Since the section indexer isn't aware of the profile at the top, we need to add a - // special section at the top for it and shift everything else down. - String[] tempSections = new String[mSections.length + 1]; - int[] tempPositions = new int[mPositions.length + 1]; - tempSections[0] = header; - tempPositions[0] = 0; - for (int i = 1; i <= mPositions.length; i++) { - tempSections[i] = mSections[i - 1]; - tempPositions[i] = mPositions[i - 1] + numberOfItemsToAdd; - } - mSections = tempSections; - mPositions = tempPositions; - mCount = mCount + numberOfItemsToAdd; - } - } -} diff --git a/java/com/android/contacts/common/list/DefaultContactListAdapter.java b/java/com/android/contacts/common/list/DefaultContactListAdapter.java deleted file mode 100644 index 7bcae0e0e..000000000 --- a/java/com/android/contacts/common/list/DefaultContactListAdapter.java +++ /dev/null @@ -1,216 +0,0 @@ -/* - * 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.SharedPreferences; -import android.database.Cursor; -import android.net.Uri; -import android.net.Uri.Builder; -import android.preference.PreferenceManager; -import android.provider.ContactsContract; -import android.provider.ContactsContract.Contacts; -import android.provider.ContactsContract.Directory; -import android.provider.ContactsContract.SearchSnippets; -import android.text.TextUtils; -import android.view.View; -import com.android.contacts.common.compat.ContactsCompat; -import com.android.contacts.common.preference.ContactsPreferences; -import java.util.ArrayList; -import java.util.List; - -/** A cursor adapter for the {@link ContactsContract.Contacts#CONTENT_TYPE} content type. */ -public class DefaultContactListAdapter extends ContactListAdapter { - - public DefaultContactListAdapter(Context context) { - super(context); - } - - @Override - public void configureLoader(CursorLoader loader, long directoryId) { - String sortOrder = null; - if (isSearchMode()) { - String query = getQueryString(); - if (query == null) { - query = ""; - } - query = query.trim(); - if (TextUtils.isEmpty(query)) { - // Regardless of the directory, we don't want anything returned, - // so let's just send a "nothing" query to the local directory. - loader.setUri(Contacts.CONTENT_URI); - loader.setProjection(getProjection(false)); - loader.setSelection("0"); - } else { - final Builder builder = ContactsCompat.getContentUri().buildUpon(); - appendSearchParameters(builder, query, directoryId); - loader.setUri(builder.build()); - loader.setProjection(getProjection(true)); - } - } else { - final ContactListFilter filter = getFilter(); - configureUri(loader, directoryId, filter); - loader.setProjection(getProjection(false)); - configureSelection(loader, directoryId, filter); - } - - if (getSortOrder() == ContactsPreferences.SORT_ORDER_PRIMARY) { - if (sortOrder == null) { - sortOrder = Contacts.SORT_KEY_PRIMARY; - } else { - sortOrder += ", " + Contacts.SORT_KEY_PRIMARY; - } - } else { - if (sortOrder == null) { - sortOrder = Contacts.SORT_KEY_ALTERNATIVE; - } else { - sortOrder += ", " + Contacts.SORT_KEY_ALTERNATIVE; - } - } - loader.setSortOrder(sortOrder); - } - - private void appendSearchParameters(Builder builder, String query, long directoryId) { - builder.appendPath(query); // Builder will encode the query - builder.appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(directoryId)); - if (directoryId != Directory.DEFAULT && directoryId != Directory.LOCAL_INVISIBLE) { - builder.appendQueryParameter( - ContactsContract.LIMIT_PARAM_KEY, - String.valueOf(getDirectoryResultLimit(getDirectoryById(directoryId)))); - } - builder.appendQueryParameter(SearchSnippets.DEFERRED_SNIPPETING_KEY, "1"); - } - - protected void configureUri(CursorLoader loader, long directoryId, ContactListFilter filter) { - Uri uri = Contacts.CONTENT_URI; - - if (directoryId == Directory.DEFAULT && isSectionHeaderDisplayEnabled()) { - uri = ContactListAdapter.buildSectionIndexerUri(uri); - } - - // The "All accounts" filter is the same as the entire contents of Directory.DEFAULT - if (filter != null - && filter.filterType != ContactListFilter.FILTER_TYPE_CUSTOM - && filter.filterType != ContactListFilter.FILTER_TYPE_SINGLE_CONTACT) { - final Uri.Builder builder = uri.buildUpon(); - builder.appendQueryParameter( - ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Directory.DEFAULT)); - if (filter.filterType == ContactListFilter.FILTER_TYPE_ACCOUNT) { - filter.addAccountQueryParameterToUrl(builder); - } - uri = builder.build(); - } - - loader.setUri(uri); - } - - private void configureSelection(CursorLoader loader, long directoryId, ContactListFilter filter) { - if (filter == null) { - return; - } - - if (directoryId != Directory.DEFAULT) { - return; - } - - StringBuilder selection = new StringBuilder(); - List selectionArgs = new ArrayList(); - - switch (filter.filterType) { - case ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS: - { - // We have already added directory=0 to the URI, which takes care of this - // filter - break; - } - case ContactListFilter.FILTER_TYPE_SINGLE_CONTACT: - { - // We have already added the lookup key to the URI, which takes care of this - // filter - break; - } - case ContactListFilter.FILTER_TYPE_STARRED: - { - selection.append(Contacts.STARRED + "!=0"); - break; - } - case ContactListFilter.FILTER_TYPE_WITH_PHONE_NUMBERS_ONLY: - { - selection.append(Contacts.HAS_PHONE_NUMBER + "=1"); - break; - } - case ContactListFilter.FILTER_TYPE_CUSTOM: - { - selection.append(Contacts.IN_VISIBLE_GROUP + "=1"); - if (isCustomFilterForPhoneNumbersOnly()) { - selection.append(" AND " + Contacts.HAS_PHONE_NUMBER + "=1"); - } - break; - } - case ContactListFilter.FILTER_TYPE_ACCOUNT: - { - // We use query parameters for account filter, so no selection to add here. - break; - } - } - loader.setSelection(selection.toString()); - loader.setSelectionArgs(selectionArgs.toArray(new String[0])); - } - - @Override - protected void bindView(View itemView, int partition, Cursor cursor, int position) { - super.bindView(itemView, partition, cursor, position); - final ContactListItemView view = (ContactListItemView) itemView; - - view.setHighlightedPrefix(isSearchMode() ? getUpperCaseQueryString() : null); - - bindSectionHeaderAndDivider(view, position, cursor); - - if (isQuickContactEnabled()) { - bindQuickContact( - view, - partition, - cursor, - ContactQuery.CONTACT_PHOTO_ID, - ContactQuery.CONTACT_PHOTO_URI, - ContactQuery.CONTACT_ID, - ContactQuery.CONTACT_LOOKUP_KEY, - ContactQuery.CONTACT_DISPLAY_NAME); - } else { - if (getDisplayPhotos()) { - bindPhoto(view, partition, cursor); - } - } - - bindNameAndViewId(view, cursor); - bindPresenceAndStatusMessage(view, cursor); - - if (isSearchMode()) { - bindSearchSnippet(view, cursor); - } else { - view.setSnippet(null); - } - } - - private boolean isCustomFilterForPhoneNumbersOnly() { - // TODO: this flag should not be stored in shared prefs. It needs to be in the db. - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); - return prefs.getBoolean( - ContactsPreferences.PREF_DISPLAY_ONLY_PHONES, - ContactsPreferences.PREF_DISPLAY_ONLY_PHONES_DEFAULT); - } -} diff --git a/java/com/android/contacts/common/list/DirectoryListLoader.java b/java/com/android/contacts/common/list/DirectoryListLoader.java deleted file mode 100644 index ce78d2cff..000000000 --- a/java/com/android/contacts/common/list/DirectoryListLoader.java +++ /dev/null @@ -1,210 +0,0 @@ -/* - * 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.AsyncTaskLoader; -import android.content.Context; -import android.content.pm.PackageManager; -import android.database.ContentObserver; -import android.database.Cursor; -import android.database.MatrixCursor; -import android.net.Uri; -import android.os.Handler; -import android.provider.ContactsContract.Directory; -import android.text.TextUtils; -import com.android.contacts.common.R; -import com.android.dialer.common.LogUtil; -import com.android.dialer.common.cp2.DirectoryCompat; -import com.android.dialer.util.PermissionsUtil; - -/** A specialized loader for the list of directories, see {@link Directory}. */ -public class DirectoryListLoader extends AsyncTaskLoader { - - public static final int SEARCH_MODE_NONE = 0; - public static final int SEARCH_MODE_DEFAULT = 1; - public static final int SEARCH_MODE_CONTACT_SHORTCUT = 2; - public static final int SEARCH_MODE_DATA_SHORTCUT = 3; - // This is a virtual column created for a MatrixCursor. - public static final String DIRECTORY_TYPE = "directoryType"; - private static final String[] RESULT_PROJECTION = { - Directory._ID, DIRECTORY_TYPE, Directory.DISPLAY_NAME, Directory.PHOTO_SUPPORT, - }; - private final ContentObserver mObserver = - new ContentObserver(new Handler()) { - @Override - public void onChange(boolean selfChange) { - forceLoad(); - } - }; - private int mDirectorySearchMode; - private boolean mLocalInvisibleDirectoryEnabled; - private MatrixCursor mDefaultDirectoryList; - - public DirectoryListLoader(Context context) { - super(context); - } - - public void setDirectorySearchMode(int mode) { - mDirectorySearchMode = mode; - } - - /** - * A flag that indicates whether the {@link Directory#LOCAL_INVISIBLE} directory should be - * included in the results. - */ - public void setLocalInvisibleDirectoryEnabled(boolean flag) { - this.mLocalInvisibleDirectoryEnabled = flag; - } - - @Override - protected void onStartLoading() { - if (PermissionsUtil.hasContactsReadPermissions(getContext())) { - getContext() - .getContentResolver() - .registerContentObserver(DirectoryQuery.URI, false, mObserver); - } else { - LogUtil.w("DirectoryListLoader.onStartLoading", "contacts permission not available."); - } - forceLoad(); - } - - @Override - protected void onStopLoading() { - getContext().getContentResolver().unregisterContentObserver(mObserver); - } - - @Override - public Cursor loadInBackground() { - if (mDirectorySearchMode == SEARCH_MODE_NONE) { - return getDefaultDirectories(); - } - - MatrixCursor result = new MatrixCursor(RESULT_PROJECTION); - Context context = getContext(); - PackageManager pm = context.getPackageManager(); - String selection; - switch (mDirectorySearchMode) { - case SEARCH_MODE_DEFAULT: - selection = null; - break; - - case SEARCH_MODE_CONTACT_SHORTCUT: - selection = Directory.SHORTCUT_SUPPORT + "=" + Directory.SHORTCUT_SUPPORT_FULL; - break; - - case SEARCH_MODE_DATA_SHORTCUT: - selection = - Directory.SHORTCUT_SUPPORT - + " IN (" - + Directory.SHORTCUT_SUPPORT_FULL - + ", " - + Directory.SHORTCUT_SUPPORT_DATA_ITEMS_ONLY - + ")"; - break; - - default: - throw new RuntimeException("Unsupported directory search mode: " + mDirectorySearchMode); - } - Cursor cursor = null; - try { - cursor = - context - .getContentResolver() - .query( - DirectoryQuery.URI, - DirectoryQuery.PROJECTION, - selection, - null, - DirectoryQuery.ORDER_BY); - - if (cursor == null) { - return result; - } - - while (cursor.moveToNext()) { - long directoryId = cursor.getLong(DirectoryQuery.ID); - if (!mLocalInvisibleDirectoryEnabled && DirectoryCompat.isInvisibleDirectory(directoryId)) { - continue; - } - String directoryType = null; - - String packageName = cursor.getString(DirectoryQuery.PACKAGE_NAME); - int typeResourceId = cursor.getInt(DirectoryQuery.TYPE_RESOURCE_ID); - if (!TextUtils.isEmpty(packageName) && typeResourceId != 0) { - try { - directoryType = pm.getResourcesForApplication(packageName).getString(typeResourceId); - } catch (Exception e) { - LogUtil.e( - "ContactEntryListAdapter.loadInBackground", - "cannot obtain directory type from package: " + packageName); - } - } - String displayName = cursor.getString(DirectoryQuery.DISPLAY_NAME); - int photoSupport = cursor.getInt(DirectoryQuery.PHOTO_SUPPORT); - result.addRow(new Object[] {directoryId, directoryType, displayName, photoSupport}); - } - } catch (RuntimeException e) { - LogUtil.w( - "ContactEntryListAdapter.loadInBackground", "runtime exception when querying directory"); - } finally { - if (cursor != null) { - cursor.close(); - } - } - - return result; - } - - private Cursor getDefaultDirectories() { - if (mDefaultDirectoryList == null) { - mDefaultDirectoryList = new MatrixCursor(RESULT_PROJECTION); - mDefaultDirectoryList.addRow( - new Object[] {Directory.DEFAULT, getContext().getString(R.string.contactsList), null}); - mDefaultDirectoryList.addRow( - new Object[] { - Directory.LOCAL_INVISIBLE, - getContext().getString(R.string.local_invisible_directory), - null - }); - } - return mDefaultDirectoryList; - } - - @Override - protected void onReset() { - stopLoading(); - } - - private static final class DirectoryQuery { - - public static final Uri URI = DirectoryCompat.getContentUri(); - public static final String ORDER_BY = Directory._ID; - - public static final String[] PROJECTION = { - Directory._ID, - Directory.PACKAGE_NAME, - Directory.TYPE_RESOURCE_ID, - Directory.DISPLAY_NAME, - Directory.PHOTO_SUPPORT, - }; - - public static final int ID = 0; - public static final int PACKAGE_NAME = 1; - public static final int TYPE_RESOURCE_ID = 2; - public static final int DISPLAY_NAME = 3; - public static final int PHOTO_SUPPORT = 4; - } -} diff --git a/java/com/android/contacts/common/list/DirectoryPartition.java b/java/com/android/contacts/common/list/DirectoryPartition.java deleted file mode 100644 index 26b851041..000000000 --- a/java/com/android/contacts/common/list/DirectoryPartition.java +++ /dev/null @@ -1,179 +0,0 @@ -/* - * 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.provider.ContactsContract.Directory; -import com.android.common.widget.CompositeCursorAdapter; - -/** Model object for a {@link Directory} row. */ -public final class DirectoryPartition extends CompositeCursorAdapter.Partition { - - public static final int STATUS_NOT_LOADED = 0; - public static final int STATUS_LOADING = 1; - public static final int STATUS_LOADED = 2; - - public static final int RESULT_LIMIT_DEFAULT = -1; - - private long mDirectoryId; - private String mContentUri; - private String mDirectoryType; - private String mDisplayName; - private int mStatus; - private boolean mPriorityDirectory; - private boolean mPhotoSupported; - private int mResultLimit = RESULT_LIMIT_DEFAULT; - private boolean mDisplayNumber = true; - - private String mLabel; - - public DirectoryPartition(boolean showIfEmpty, boolean hasHeader) { - super(showIfEmpty, hasHeader); - } - - /** Directory ID, see {@link Directory}. */ - public long getDirectoryId() { - return mDirectoryId; - } - - public void setDirectoryId(long directoryId) { - this.mDirectoryId = directoryId; - } - - /** - * Directory type resolved from {@link Directory#PACKAGE_NAME} and {@link - * Directory#TYPE_RESOURCE_ID}; - */ - public String getDirectoryType() { - return mDirectoryType; - } - - public void setDirectoryType(String directoryType) { - this.mDirectoryType = directoryType; - } - - /** See {@link Directory#DISPLAY_NAME}. */ - public String getDisplayName() { - return mDisplayName; - } - - public void setDisplayName(String displayName) { - this.mDisplayName = displayName; - } - - public int getStatus() { - return mStatus; - } - - public void setStatus(int status) { - mStatus = status; - } - - public boolean isLoading() { - return mStatus == STATUS_NOT_LOADED || mStatus == STATUS_LOADING; - } - - /** Returns true if this directory should be loaded before non-priority directories. */ - public boolean isPriorityDirectory() { - return mPriorityDirectory; - } - - public void setPriorityDirectory(boolean priorityDirectory) { - mPriorityDirectory = priorityDirectory; - } - - /** Returns true if this directory supports photos. */ - public boolean isPhotoSupported() { - return mPhotoSupported; - } - - public void setPhotoSupported(boolean flag) { - this.mPhotoSupported = flag; - } - - /** - * Max number of results for this directory. Defaults to {@link #RESULT_LIMIT_DEFAULT} which - * implies using the adapter's {@link - * com.android.contacts.common.list.ContactListAdapter#getDirectoryResultLimit()} - */ - public int getResultLimit() { - return mResultLimit; - } - - public void setResultLimit(int resultLimit) { - mResultLimit = resultLimit; - } - - /** - * Used by extended directories to specify a custom content URI. Extended directories MUST have a - * content URI - */ - public String getContentUri() { - return mContentUri; - } - - public void setContentUri(String contentUri) { - mContentUri = contentUri; - } - - /** A label to display in the header next to the display name. */ - public String getLabel() { - return mLabel; - } - - public void setLabel(String label) { - mLabel = label; - } - - @Override - public String toString() { - return "DirectoryPartition{" - + "mDirectoryId=" - + mDirectoryId - + ", mContentUri='" - + mContentUri - + '\'' - + ", mDirectoryType='" - + mDirectoryType - + '\'' - + ", mDisplayName='" - + mDisplayName - + '\'' - + ", mStatus=" - + mStatus - + ", mPriorityDirectory=" - + mPriorityDirectory - + ", mPhotoSupported=" - + mPhotoSupported - + ", mResultLimit=" - + mResultLimit - + ", mLabel='" - + mLabel - + '\'' - + '}'; - } - - /** - * Whether or not to display the phone number in app that have that option - Dialer. If false, - * Phone Label should be used instead of Phone Number. - */ - public boolean isDisplayNumber() { - return mDisplayNumber; - } - - public void setDisplayNumber(boolean displayNumber) { - mDisplayNumber = displayNumber; - } -} diff --git a/java/com/android/contacts/common/list/IndexerListAdapter.java b/java/com/android/contacts/common/list/IndexerListAdapter.java deleted file mode 100644 index 2289f6e59..000000000 --- a/java/com/android/contacts/common/list/IndexerListAdapter.java +++ /dev/null @@ -1,214 +0,0 @@ -/* - * 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.view.View; -import android.view.ViewGroup; -import android.widget.ListView; -import android.widget.SectionIndexer; - -/** A list adapter that supports section indexer and a pinned header. */ -public abstract class IndexerListAdapter extends PinnedHeaderListAdapter implements SectionIndexer { - - protected Context mContext; - private SectionIndexer mIndexer; - private int mIndexedPartition = 0; - private boolean mSectionHeaderDisplayEnabled; - private View mHeader; - private Placement mPlacementCache = new Placement(); - - /** Constructor. */ - public IndexerListAdapter(Context context) { - super(context); - mContext = context; - } - - /** - * Creates a section header view that will be pinned at the top of the list as the user scrolls. - */ - protected abstract View createPinnedSectionHeaderView(Context context, ViewGroup parent); - - /** Sets the title in the pinned header as the user scrolls. */ - protected abstract void setPinnedSectionTitle(View pinnedHeaderView, String title); - - public boolean isSectionHeaderDisplayEnabled() { - return mSectionHeaderDisplayEnabled; - } - - public void setSectionHeaderDisplayEnabled(boolean flag) { - this.mSectionHeaderDisplayEnabled = flag; - } - - public int getIndexedPartition() { - return mIndexedPartition; - } - - public void setIndexedPartition(int partition) { - this.mIndexedPartition = partition; - } - - public SectionIndexer getIndexer() { - return mIndexer; - } - - public void setIndexer(SectionIndexer indexer) { - mIndexer = indexer; - mPlacementCache.invalidate(); - } - - public Object[] getSections() { - if (mIndexer == null) { - return new String[] {" "}; - } else { - return mIndexer.getSections(); - } - } - - /** @return relative position of the section in the indexed partition */ - public int getPositionForSection(int sectionIndex) { - if (mIndexer == null) { - return -1; - } - - return mIndexer.getPositionForSection(sectionIndex); - } - - /** @param position relative position in the indexed partition */ - public int getSectionForPosition(int position) { - if (mIndexer == null) { - return -1; - } - - return mIndexer.getSectionForPosition(position); - } - - @Override - public int getPinnedHeaderCount() { - if (isSectionHeaderDisplayEnabled()) { - return super.getPinnedHeaderCount() + 1; - } else { - return super.getPinnedHeaderCount(); - } - } - - @Override - public View getPinnedHeaderView(int viewIndex, View convertView, ViewGroup parent) { - if (isSectionHeaderDisplayEnabled() && viewIndex == getPinnedHeaderCount() - 1) { - if (mHeader == null) { - mHeader = createPinnedSectionHeaderView(mContext, parent); - } - return mHeader; - } else { - return super.getPinnedHeaderView(viewIndex, convertView, parent); - } - } - - @Override - public void configurePinnedHeaders(PinnedHeaderListView listView) { - super.configurePinnedHeaders(listView); - - if (!isSectionHeaderDisplayEnabled()) { - return; - } - - int index = getPinnedHeaderCount() - 1; - if (mIndexer == null || getCount() == 0) { - listView.setHeaderInvisible(index, false); - } else { - int listPosition = listView.getPositionAt(listView.getTotalTopPinnedHeaderHeight()); - int position = listPosition - listView.getHeaderViewsCount(); - - int section = -1; - int partition = getPartitionForPosition(position); - if (partition == mIndexedPartition) { - int offset = getOffsetInPartition(position); - if (offset != -1) { - section = getSectionForPosition(offset); - } - } - - if (section == -1) { - listView.setHeaderInvisible(index, false); - } else { - View topChild = listView.getChildAt(listPosition); - if (topChild != null) { - // Match the pinned header's height to the height of the list item. - mHeader.setMinimumHeight(topChild.getMeasuredHeight()); - } - setPinnedSectionTitle(mHeader, (String) mIndexer.getSections()[section]); - - // Compute the item position where the current partition begins - int partitionStart = getPositionForPartition(mIndexedPartition); - if (hasHeader(mIndexedPartition)) { - partitionStart++; - } - - // Compute the item position where the next section begins - int nextSectionPosition = partitionStart + getPositionForSection(section + 1); - boolean isLastInSection = position == nextSectionPosition - 1; - listView.setFadingHeader(index, listPosition, isLastInSection); - } - } - } - - /** - * Computes the item's placement within its section and populates the {@code placement} object - * accordingly. Please note that the returned object is volatile and should be copied if the - * result needs to be used later. - */ - public Placement getItemPlacementInSection(int position) { - if (mPlacementCache.position == position) { - return mPlacementCache; - } - - mPlacementCache.position = position; - if (isSectionHeaderDisplayEnabled()) { - int section = getSectionForPosition(position); - if (section != -1 && getPositionForSection(section) == position) { - mPlacementCache.firstInSection = true; - mPlacementCache.sectionHeader = (String) getSections()[section]; - } else { - mPlacementCache.firstInSection = false; - mPlacementCache.sectionHeader = null; - } - - mPlacementCache.lastInSection = (getPositionForSection(section + 1) - 1 == position); - } else { - mPlacementCache.firstInSection = false; - mPlacementCache.lastInSection = false; - mPlacementCache.sectionHeader = null; - } - return mPlacementCache; - } - - /** - * An item view is displayed differently depending on whether it is placed at the beginning, - * middle or end of a section. It also needs to know the section header when it is at the - * beginning of a section. This object captures all this configuration. - */ - public static final class Placement { - - public boolean firstInSection; - public boolean lastInSection; - public String sectionHeader; - private int position = ListView.INVALID_POSITION; - - public void invalidate() { - position = ListView.INVALID_POSITION; - } - } -} diff --git a/java/com/android/contacts/common/list/PhoneNumberListAdapter.java b/java/com/android/contacts/common/list/PhoneNumberListAdapter.java deleted file mode 100644 index 3c45abf37..000000000 --- a/java/com/android/contacts/common/list/PhoneNumberListAdapter.java +++ /dev/null @@ -1,656 +0,0 @@ -/* - * 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.database.Cursor; -import android.net.Uri; -import android.net.Uri.Builder; -import android.provider.ContactsContract; -import android.provider.ContactsContract.CommonDataKinds.Callable; -import android.provider.ContactsContract.CommonDataKinds.Phone; -import android.provider.ContactsContract.CommonDataKinds.SipAddress; -import android.provider.ContactsContract.Contacts; -import android.provider.ContactsContract.Directory; -import android.support.annotation.VisibleForTesting; -import android.text.TextUtils; -import android.view.View; -import android.view.ViewGroup; -import com.android.contacts.common.ContactsUtils; -import com.android.contacts.common.R; -import com.android.contacts.common.compat.CallableCompat; -import com.android.contacts.common.compat.PhoneCompat; -import com.android.contacts.common.extensions.PhoneDirectoryExtenderAccessor; -import com.android.contacts.common.list.ContactListItemView.CallToAction; -import com.android.contacts.common.preference.ContactsPreferences; -import com.android.contacts.common.util.Constants; -import com.android.dialer.common.LogUtil; -import com.android.dialer.common.cp2.DirectoryCompat; -import com.android.dialer.compat.CompatUtils; -import com.android.dialer.configprovider.ConfigProviderBindings; -import com.android.dialer.contactphoto.ContactPhotoManager.DefaultImageRequest; -import com.android.dialer.dialercontact.DialerContact; -import com.android.dialer.duo.DuoComponent; -import com.android.dialer.enrichedcall.EnrichedCallCapabilities; -import com.android.dialer.enrichedcall.EnrichedCallComponent; -import com.android.dialer.enrichedcall.EnrichedCallManager; -import com.android.dialer.lettertile.LetterTileDrawable; -import com.android.dialer.phonenumberutil.PhoneNumberHelper; -import com.android.dialer.util.CallUtil; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -/** - * A cursor adapter for the {@link Phone#CONTENT_ITEM_TYPE} and {@link - * SipAddress#CONTENT_ITEM_TYPE}. - * - *

By default this adapter just handles phone numbers. When {@link #setUseCallableUri(boolean)} - * is called with "true", this adapter starts handling SIP addresses too, by using {@link Callable} - * API instead of {@link Phone}. - */ -public class PhoneNumberListAdapter extends ContactEntryListAdapter { - - private static final String TAG = PhoneNumberListAdapter.class.getSimpleName(); - private static final String IGNORE_NUMBER_TOO_LONG_CLAUSE = "length(" + Phone.NUMBER + ") < 1000"; - // A list of extended directories to add to the directories from the database - private final List mExtendedDirectories; - private final CharSequence mUnknownNameText; - protected final boolean mIsImsVideoEnabled; - - // Extended directories will have ID's that are higher than any of the id's from the database, - // so that we can identify them and set them up properly. If no extended directories - // exist, this will be Long.MAX_VALUE - private long mFirstExtendedDirectoryId = Long.MAX_VALUE; - private boolean mUseCallableUri; - private Listener mListener; - - public PhoneNumberListAdapter(Context context) { - super(context); - setDefaultFilterHeaderText(R.string.list_filter_phones); - mUnknownNameText = context.getText(android.R.string.unknownName); - - mExtendedDirectories = - PhoneDirectoryExtenderAccessor.get(mContext).getExtendedDirectories(mContext); - - int videoCapabilities = CallUtil.getVideoCallingAvailability(context); - mIsImsVideoEnabled = - CallUtil.isVideoEnabled(context) - && (videoCapabilities & CallUtil.VIDEO_CALLING_PRESENCE) != 0; - } - - @Override - public void configureLoader(CursorLoader loader, long directoryId) { - String query = getQueryString(); - if (query == null) { - query = ""; - } - if (isExtendedDirectory(directoryId)) { - final DirectoryPartition directory = getExtendedDirectoryFromId(directoryId); - final String contentUri = directory.getContentUri(); - if (contentUri == null) { - throw new IllegalStateException("Extended directory must have a content URL: " + directory); - } - final Builder builder = Uri.parse(contentUri).buildUpon(); - builder.appendPath(query); - builder.appendQueryParameter( - ContactsContract.LIMIT_PARAM_KEY, String.valueOf(getDirectoryResultLimit(directory))); - loader.setUri(builder.build()); - loader.setProjection(PhoneQuery.PROJECTION_PRIMARY); - } else { - final boolean isRemoteDirectoryQuery = DirectoryCompat.isRemoteDirectoryId(directoryId); - final Builder builder; - if (isSearchMode()) { - final Uri baseUri; - if (isRemoteDirectoryQuery) { - baseUri = PhoneCompat.getContentFilterUri(); - } else if (mUseCallableUri) { - baseUri = CallableCompat.getContentFilterUri(); - } else { - baseUri = PhoneCompat.getContentFilterUri(); - } - builder = baseUri.buildUpon(); - builder.appendPath(query); // Builder will encode the query - builder.appendQueryParameter( - ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(directoryId)); - if (isRemoteDirectoryQuery) { - builder.appendQueryParameter( - ContactsContract.LIMIT_PARAM_KEY, - String.valueOf(getDirectoryResultLimit(getDirectoryById(directoryId)))); - } - } else { - Uri baseUri = mUseCallableUri ? Callable.CONTENT_URI : Phone.CONTENT_URI; - builder = - baseUri - .buildUpon() - .appendQueryParameter( - ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Directory.DEFAULT)); - if (isSectionHeaderDisplayEnabled()) { - builder.appendQueryParameter(Phone.EXTRA_ADDRESS_BOOK_INDEX, "true"); - } - applyFilter(loader, builder, directoryId, getFilter()); - } - - // Ignore invalid phone numbers that are too long. These can potentially cause freezes - // in the UI and there is no reason to display them. - final String prevSelection = loader.getSelection(); - final String newSelection; - if (!TextUtils.isEmpty(prevSelection)) { - newSelection = prevSelection + " AND " + IGNORE_NUMBER_TOO_LONG_CLAUSE; - } else { - newSelection = IGNORE_NUMBER_TOO_LONG_CLAUSE; - } - loader.setSelection(newSelection); - - // Remove duplicates when it is possible. - builder.appendQueryParameter(ContactsContract.REMOVE_DUPLICATE_ENTRIES, "true"); - loader.setUri(builder.build()); - - // TODO a projection that includes the search snippet - if (getContactNameDisplayOrder() == ContactsPreferences.DISPLAY_ORDER_PRIMARY) { - loader.setProjection(PhoneQuery.PROJECTION_PRIMARY); - } else { - loader.setProjection(PhoneQuery.PROJECTION_ALTERNATIVE); - } - - if (getSortOrder() == ContactsPreferences.SORT_ORDER_PRIMARY) { - loader.setSortOrder(Phone.SORT_KEY_PRIMARY); - } else { - loader.setSortOrder(Phone.SORT_KEY_ALTERNATIVE); - } - } - } - - protected boolean isExtendedDirectory(long directoryId) { - return directoryId >= mFirstExtendedDirectoryId; - } - - private DirectoryPartition getExtendedDirectoryFromId(long directoryId) { - final int directoryIndex = (int) (directoryId - mFirstExtendedDirectoryId); - return mExtendedDirectories.get(directoryIndex); - } - - /** - * Configure {@code loader} and {@code uriBuilder} according to {@code directoryId} and {@code - * filter}. - */ - private void applyFilter( - CursorLoader loader, Uri.Builder uriBuilder, long directoryId, ContactListFilter filter) { - if (filter == null || directoryId != Directory.DEFAULT) { - return; - } - - final StringBuilder selection = new StringBuilder(); - final List selectionArgs = new ArrayList(); - - switch (filter.filterType) { - case ContactListFilter.FILTER_TYPE_CUSTOM: - { - selection.append(Contacts.IN_VISIBLE_GROUP + "=1"); - selection.append(" AND " + Contacts.HAS_PHONE_NUMBER + "=1"); - break; - } - case ContactListFilter.FILTER_TYPE_ACCOUNT: - { - filter.addAccountQueryParameterToUrl(uriBuilder); - break; - } - case ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS: - case ContactListFilter.FILTER_TYPE_DEFAULT: - break; // No selection needed. - case ContactListFilter.FILTER_TYPE_WITH_PHONE_NUMBERS_ONLY: - break; // This adapter is always "phone only", so no selection needed either. - default: - LogUtil.w( - TAG, - "Unsupported filter type came " - + "(type: " - + filter.filterType - + ", toString: " - + filter - + ")" - + " showing all contacts."); - // No selection. - break; - } - loader.setSelection(selection.toString()); - loader.setSelectionArgs(selectionArgs.toArray(new String[0])); - } - - public String getPhoneNumber(int position) { - final Cursor item = (Cursor) getItem(position); - return item != null ? item.getString(PhoneQuery.PHONE_NUMBER) : null; - } - - /** - * Retrieves the lookup key for the given cursor position. - * - * @param position The cursor position. - * @return The lookup key. - */ - public String getLookupKey(int position) { - final Cursor item = (Cursor) getItem(position); - return item != null ? item.getString(PhoneQuery.LOOKUP_KEY) : null; - } - - public DialerContact getDialerContact(int position) { - Cursor cursor = (Cursor) getItem(position); - if (cursor == null) { - LogUtil.e("PhoneNumberListAdapter.getDialerContact", "cursor was null."); - return null; - } - - String displayName = cursor.getString(PhoneQuery.DISPLAY_NAME); - String number = cursor.getString(PhoneQuery.PHONE_NUMBER); - String photoUri = cursor.getString(PhoneQuery.PHOTO_URI); - Uri contactUri = - Contacts.getLookupUri( - cursor.getLong(PhoneQuery.CONTACT_ID), cursor.getString(PhoneQuery.LOOKUP_KEY)); - - DialerContact.Builder contact = DialerContact.newBuilder(); - contact - .setNumber(number) - .setPhotoId(cursor.getLong(PhoneQuery.PHOTO_ID)) - .setContactType(LetterTileDrawable.TYPE_DEFAULT) - .setNameOrNumber(displayName) - .setNumberLabel( - Phone.getTypeLabel( - mContext.getResources(), - cursor.getInt(PhoneQuery.PHONE_TYPE), - cursor.getString(PhoneQuery.PHONE_LABEL)) - .toString()); - - if (photoUri != null) { - contact.setPhotoUri(photoUri); - } - - if (contactUri != null) { - contact.setContactUri(contactUri.toString()); - } - - if (!TextUtils.isEmpty(displayName)) { - contact.setDisplayNumber(number); - } - - return contact.build(); - } - - @Override - protected ContactListItemView newView( - Context context, int partition, Cursor cursor, int position, ViewGroup parent) { - ContactListItemView view = super.newView(context, partition, cursor, position, parent); - view.setUnknownNameText(mUnknownNameText); - view.setQuickContactEnabled(isQuickContactEnabled()); - return view; - } - - protected void setHighlight(ContactListItemView view, Cursor cursor) { - view.setHighlightedPrefix(isSearchMode() ? getUpperCaseQueryString() : null); - } - - @Override - protected void bindView(View itemView, int partition, Cursor cursor, int position) { - super.bindView(itemView, partition, cursor, position); - ContactListItemView view = (ContactListItemView) itemView; - - setHighlight(view, cursor); - - // Look at elements before and after this position, checking if contact IDs are same. - // If they have one same contact ID, it means they can be grouped. - // - // In one group, only the first entry will show its photo and its name, and the other - // entries in the group show just their data (e.g. phone number, email address). - cursor.moveToPosition(position); - boolean isFirstEntry = true; - final long currentContactId = cursor.getLong(PhoneQuery.CONTACT_ID); - if (cursor.moveToPrevious() && !cursor.isBeforeFirst()) { - final long previousContactId = cursor.getLong(PhoneQuery.CONTACT_ID); - if (currentContactId == previousContactId) { - isFirstEntry = false; - } - } - cursor.moveToPosition(position); - - bindViewId(view, cursor, PhoneQuery.PHONE_ID); - - bindSectionHeaderAndDivider(view, position); - if (isFirstEntry) { - bindName(view, cursor); - if (isQuickContactEnabled()) { - bindQuickContact( - view, - partition, - cursor, - PhoneQuery.PHOTO_ID, - PhoneQuery.PHOTO_URI, - PhoneQuery.CONTACT_ID, - PhoneQuery.LOOKUP_KEY, - PhoneQuery.DISPLAY_NAME); - } else { - if (getDisplayPhotos()) { - bindPhoto(view, partition, cursor); - } - } - } else { - unbindName(view); - - view.removePhotoView(true, false); - } - - final DirectoryPartition directory = (DirectoryPartition) getPartition(partition); - // All sections have headers, so scroll position is off by 1. - position += getPositionForPartition(partition) + 1; - - bindPhoneNumber(view, cursor, directory.isDisplayNumber(), position); - } - - @VisibleForTesting(otherwise = VisibleForTesting.PROTECTED) - public void bindPhoneNumber( - ContactListItemView view, Cursor cursor, boolean displayNumber, int position) { - CharSequence label = null; - if (displayNumber && !cursor.isNull(PhoneQuery.PHONE_TYPE)) { - final int type = cursor.getInt(PhoneQuery.PHONE_TYPE); - final String customLabel = cursor.getString(PhoneQuery.PHONE_LABEL); - - // TODO cache - label = Phone.getTypeLabel(mContext.getResources(), type, customLabel); - } - view.setLabel(label); - final String text; - String number = cursor.getString(PhoneQuery.PHONE_NUMBER); - if (displayNumber) { - text = number; - } else { - // Display phone label. If that's null, display geocoded location for the number - final String phoneLabel = cursor.getString(PhoneQuery.PHONE_LABEL); - if (phoneLabel != null) { - text = phoneLabel; - } else { - final String phoneNumber = cursor.getString(PhoneQuery.PHONE_NUMBER); - text = - PhoneNumberHelper.getGeoDescription( - mContext, - phoneNumber, - PhoneNumberHelper.getCurrentCountryIso(mContext, null /* PhoneAccountHandle */)); - } - } - view.setPhoneNumber(text); - - @CallToAction int action = ContactListItemView.NONE; - - if (CompatUtils.isVideoCompatible()) { - // Determine if carrier presence indicates the number supports video calling. - int carrierPresence = cursor.getInt(PhoneQuery.CARRIER_PRESENCE); - boolean isPresent = (carrierPresence & Phone.CARRIER_PRESENCE_VT_CAPABLE) != 0; - - boolean showViewIcon = mIsImsVideoEnabled && isPresent; - if (showViewIcon) { - action = ContactListItemView.VIDEO; - } - } - - if (action == ContactListItemView.NONE - && DuoComponent.get(mContext).getDuo().isReachable(mContext, number)) { - action = ContactListItemView.DUO; - } - - if (action == ContactListItemView.NONE) { - EnrichedCallManager manager = EnrichedCallComponent.get(mContext).getEnrichedCallManager(); - EnrichedCallCapabilities capabilities = manager.getCapabilities(number); - if (capabilities != null && capabilities.isCallComposerCapable()) { - action = ContactListItemView.CALL_AND_SHARE; - } else if (capabilities == null - && getQueryString() != null - && getQueryString().length() >= 3) { - manager.requestCapabilities(number); - } - } - - view.setCallToAction(action, mListener, position); - } - - protected void bindSectionHeaderAndDivider(final ContactListItemView view, int position) { - if (isSectionHeaderDisplayEnabled()) { - Placement placement = getItemPlacementInSection(position); - view.setSectionHeader(placement.firstInSection ? placement.sectionHeader : null); - } else { - view.setSectionHeader(null); - } - } - - protected void bindName(final ContactListItemView view, Cursor cursor) { - view.showDisplayName(cursor, PhoneQuery.DISPLAY_NAME); - // Note: we don't show phonetic names any more (see issue 5265330) - } - - protected void unbindName(final ContactListItemView view) { - view.hideDisplayName(); - } - - @Override - protected void bindWorkProfileIcon(final ContactListItemView view, int partition) { - final DirectoryPartition directory = (DirectoryPartition) getPartition(partition); - final long directoryId = directory.getDirectoryId(); - final long userType = ContactsUtils.determineUserType(directoryId, null); - // Work directory must not be a extended directory. An extended directory is custom - // directory in the app, but not a directory provided by framework. So it can't be - // USER_TYPE_WORK. - view.setWorkProfileIconEnabled( - !isExtendedDirectory(directoryId) && userType == ContactsUtils.USER_TYPE_WORK); - } - - protected void bindPhoto(final ContactListItemView view, int partitionIndex, Cursor cursor) { - if (!isPhotoSupported(partitionIndex)) { - view.removePhotoView(); - return; - } - - long photoId = 0; - if (!cursor.isNull(PhoneQuery.PHOTO_ID)) { - photoId = cursor.getLong(PhoneQuery.PHOTO_ID); - } - - if (photoId != 0) { - getPhotoLoader() - .loadThumbnail(view.getPhotoView(), photoId, false, getCircularPhotos(), null); - } else { - final String photoUriString = cursor.getString(PhoneQuery.PHOTO_URI); - final Uri photoUri = photoUriString == null ? null : Uri.parse(photoUriString); - - DefaultImageRequest request = null; - if (photoUri == null) { - final String displayName = cursor.getString(PhoneQuery.DISPLAY_NAME); - final String lookupKey = cursor.getString(PhoneQuery.LOOKUP_KEY); - request = new DefaultImageRequest(displayName, lookupKey, getCircularPhotos()); - } - getPhotoLoader() - .loadDirectoryPhoto(view.getPhotoView(), photoUri, false, getCircularPhotos(), request); - } - } - - public void setUseCallableUri(boolean useCallableUri) { - mUseCallableUri = useCallableUri; - } - - /** - * Override base implementation to inject extended directories between local & remote directories. - * This is done in the following steps: 1. Call base implementation to add directories from the - * cursor. 2. Iterate all base directories and establish the following information: a. The highest - * directory id so that we can assign unused id's to the extended directories. b. The index of the - * last non-remote directory. This is where we will insert extended directories. 3. Iterate the - * extended directories and for each one, assign an ID and insert it in the proper location. - */ - @Override - public void changeDirectories(Cursor cursor) { - super.changeDirectories(cursor); - if (getDirectorySearchMode() == DirectoryListLoader.SEARCH_MODE_NONE) { - return; - } - int numExtendedDirectories = mExtendedDirectories.size(); - - if (ConfigProviderBindings.get(getContext()).getBoolean("p13n_ranker_should_enable", false)) { - // Suggested results wasn't formulated as an extended directory, so manually - // increment the count here when the feature is enabled. Suggestions are - // only shown when the ranker is enabled. - numExtendedDirectories++; - } - - if (getPartitionCount() == cursor.getCount() + numExtendedDirectories) { - // already added all directories; - return; - } - - mFirstExtendedDirectoryId = Long.MAX_VALUE; - if (!mExtendedDirectories.isEmpty()) { - // The Directory.LOCAL_INVISIBLE is not in the cursor but we can't reuse it's - // "special" ID. - long maxId = Directory.LOCAL_INVISIBLE; - int insertIndex = 0; - for (int i = 0, n = getPartitionCount(); i < n; i++) { - final DirectoryPartition partition = (DirectoryPartition) getPartition(i); - final long id = partition.getDirectoryId(); - if (id > maxId) { - maxId = id; - } - if (!DirectoryCompat.isRemoteDirectoryId(id)) { - // assuming remote directories come after local, we will end up with the index - // where we should insert extended directories. This also works if there are no - // remote directories at all. - insertIndex = i + 1; - } - } - // Extended directories ID's cannot collide with base directories - mFirstExtendedDirectoryId = maxId + 1; - for (int i = 0; i < mExtendedDirectories.size(); i++) { - final long id = mFirstExtendedDirectoryId + i; - final DirectoryPartition directory = mExtendedDirectories.get(i); - if (getPartitionByDirectoryId(id) == -1) { - addPartition(insertIndex, directory); - directory.setDirectoryId(id); - } - } - } - } - - @Override - protected Uri getContactUri( - int partitionIndex, Cursor cursor, int contactIdColumn, int lookUpKeyColumn) { - final DirectoryPartition directory = (DirectoryPartition) getPartition(partitionIndex); - final long directoryId = directory.getDirectoryId(); - if (!isExtendedDirectory(directoryId)) { - return super.getContactUri(partitionIndex, cursor, contactIdColumn, lookUpKeyColumn); - } - return Contacts.CONTENT_LOOKUP_URI - .buildUpon() - .appendPath(Constants.LOOKUP_URI_ENCODED) - .appendQueryParameter(Directory.DISPLAY_NAME, directory.getLabel()) - .appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(directoryId)) - .encodedFragment(cursor.getString(lookUpKeyColumn)) - .build(); - } - - public Listener getListener() { - return mListener; - } - - public void setListener(Listener listener) { - mListener = listener; - } - - public interface Listener { - - void onVideoCallIconClicked(int position); - - void onDuoVideoIconClicked(int position); - - void onCallAndShareIconClicked(int position); - } - - public static class PhoneQuery { - - /** - * Optional key used as part of a JSON lookup key to specify an analytics category associated - * with the row. - */ - public static final String ANALYTICS_CATEGORY = "analytics_category"; - - /** - * Optional key used as part of a JSON lookup key to specify an analytics action associated with - * the row. - */ - public static final String ANALYTICS_ACTION = "analytics_action"; - - /** - * Optional key used as part of a JSON lookup key to specify an analytics value associated with - * the row. - */ - public static final String ANALYTICS_VALUE = "analytics_value"; - - public static final String[] PROJECTION_PRIMARY_INTERNAL = - new String[] { - Phone._ID, // 0 - Phone.TYPE, // 1 - Phone.LABEL, // 2 - Phone.NUMBER, // 3 - Phone.CONTACT_ID, // 4 - Phone.LOOKUP_KEY, // 5 - Phone.PHOTO_ID, // 6 - Phone.DISPLAY_NAME_PRIMARY, // 7 - Phone.PHOTO_THUMBNAIL_URI, // 8 - }; - - public static final String[] PROJECTION_PRIMARY; - public static final String[] PROJECTION_ALTERNATIVE_INTERNAL = - new String[] { - Phone._ID, // 0 - Phone.TYPE, // 1 - Phone.LABEL, // 2 - Phone.NUMBER, // 3 - Phone.CONTACT_ID, // 4 - Phone.LOOKUP_KEY, // 5 - Phone.PHOTO_ID, // 6 - Phone.DISPLAY_NAME_ALTERNATIVE, // 7 - Phone.PHOTO_THUMBNAIL_URI, // 8 - }; - public static final String[] PROJECTION_ALTERNATIVE; - public static final int PHONE_ID = 0; - public static final int PHONE_TYPE = 1; - public static final int PHONE_LABEL = 2; - public static final int PHONE_NUMBER = 3; - public static final int CONTACT_ID = 4; - public static final int LOOKUP_KEY = 5; - public static final int PHOTO_ID = 6; - public static final int DISPLAY_NAME = 7; - public static final int PHOTO_URI = 8; - public static final int CARRIER_PRESENCE = 9; - - static { - final List projectionList = - new ArrayList<>(Arrays.asList(PROJECTION_PRIMARY_INTERNAL)); - projectionList.add(Phone.CARRIER_PRESENCE); // 9 - PROJECTION_PRIMARY = projectionList.toArray(new String[projectionList.size()]); - } - - static { - final List projectionList = - new ArrayList<>(Arrays.asList(PROJECTION_ALTERNATIVE_INTERNAL)); - projectionList.add(Phone.CARRIER_PRESENCE); // 9 - PROJECTION_ALTERNATIVE = projectionList.toArray(new String[projectionList.size()]); - } - } -} diff --git a/java/com/android/contacts/common/list/PhoneNumberPickerFragment.java b/java/com/android/contacts/common/list/PhoneNumberPickerFragment.java deleted file mode 100644 index 1a3b80f31..000000000 --- a/java/com/android/contacts/common/list/PhoneNumberPickerFragment.java +++ /dev/null @@ -1,465 +0,0 @@ -/* - * 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.ComponentName; -import android.content.Intent; -import android.content.Loader; -import android.database.Cursor; -import android.os.Bundle; -import android.support.annotation.MainThread; -import android.support.annotation.Nullable; -import android.text.TextUtils; -import android.util.ArraySet; -import android.view.LayoutInflater; -import android.view.MenuItem; -import android.view.View; -import android.view.ViewGroup; -import com.android.contacts.common.R; -import com.android.contacts.common.list.PhoneNumberListAdapter.Listener; -import com.android.contacts.common.util.AccountFilterUtil; -import com.android.dialer.callintent.CallInitiationType; -import com.android.dialer.callintent.CallInitiationType.Type; -import com.android.dialer.callintent.CallSpecificAppData; -import com.android.dialer.common.Assert; -import com.android.dialer.common.LogUtil; -import com.android.dialer.dialercontact.DialerContact; -import com.android.dialer.duo.DuoComponent; -import com.android.dialer.enrichedcall.EnrichedCallComponent; -import com.android.dialer.enrichedcall.EnrichedCallManager; -import com.android.dialer.logging.DialerImpression; -import com.android.dialer.logging.Logger; -import com.android.dialer.performancereport.PerformanceReport; -import com.android.dialer.protos.ProtoParsers; -import java.util.Set; -import org.json.JSONException; -import org.json.JSONObject; - -/** Fragment containing a phone number list for picking. */ -public class PhoneNumberPickerFragment extends ContactEntryListFragment - implements PhoneNumberListAdapter.Listener, EnrichedCallManager.CapabilitiesListener { - - private static final String KEY_FILTER = "filter"; - private OnPhoneNumberPickerActionListener mListener; - private ContactListFilter mFilter; - private View mAccountFilterHeader; - /** - * Lives as ListView's header and is shown when {@link #mAccountFilterHeader} is set to View.GONE. - */ - private View mPaddingView; - /** true if the loader has started at least once. */ - private boolean mLoaderStarted; - - private boolean mUseCallableUri; - - private final Set mLoadFinishedListeners = new ArraySet<>(); - - private CursorReranker mCursorReranker; - - public PhoneNumberPickerFragment() { - setQuickContactEnabled(false); - setPhotoLoaderEnabled(true); - setSectionHeaderDisplayEnabled(false); - setDirectorySearchMode(DirectoryListLoader.SEARCH_MODE_NONE); - - // Show nothing instead of letting caller Activity show something. - setHasOptionsMenu(true); - } - - /** - * Handles a click on the video call icon for a row in the list. - * - * @param position The position in the list where the click ocurred. - */ - @Override - public void onVideoCallIconClicked(int position) { - Logger.get(getContext()).logImpression(DialerImpression.Type.IMS_VIDEO_REQUESTED_FROM_SEARCH); - callNumber(position, true /* isVideoCall */); - } - - @Override - public void onDuoVideoIconClicked(int position) { - PerformanceReport.stopRecording(); - String phoneNumber = getPhoneNumber(position); - Intent intent = DuoComponent.get(getContext()).getDuo().getIntent(getContext(), phoneNumber); - // DialtactsActivity.ACTIVITY_REQUEST_CODE_LIGHTBRINGER - // Cannot reference because of cyclic dependencies - Logger.get(getContext()) - .logImpression(DialerImpression.Type.LIGHTBRINGER_VIDEO_REQUESTED_FROM_SEARCH); - int dialactsActivityRequestCode = 3; - getActivity().startActivityForResult(intent, dialactsActivityRequestCode); - } - - @Override - public void onCallAndShareIconClicked(int position) { - // Required because of cyclic dependencies of everything depending on contacts/common. - String componentName = "com.android.dialer.callcomposer.CallComposerActivity"; - Intent intent = new Intent(); - intent.setComponent(new ComponentName(getContext(), componentName)); - DialerContact contact = ((PhoneNumberListAdapter) getAdapter()).getDialerContact(position); - ProtoParsers.put(intent, "CALL_COMPOSER_CONTACT", contact); - startActivity(intent); - } - - public void setDirectorySearchEnabled(boolean flag) { - setDirectorySearchMode( - flag ? DirectoryListLoader.SEARCH_MODE_DEFAULT : DirectoryListLoader.SEARCH_MODE_NONE); - } - - public void setOnPhoneNumberPickerActionListener(OnPhoneNumberPickerActionListener listener) { - this.mListener = listener; - } - - public OnPhoneNumberPickerActionListener getOnPhoneNumberPickerListener() { - return mListener; - } - - @Override - protected void onCreateView(LayoutInflater inflater, ViewGroup container) { - super.onCreateView(inflater, container); - - View paddingView = inflater.inflate(R.layout.contact_detail_list_padding, null, false); - mPaddingView = paddingView.findViewById(R.id.contact_detail_list_padding); - getListView().addHeaderView(paddingView); - - mAccountFilterHeader = getView().findViewById(R.id.account_filter_header_container); - updateFilterHeaderView(); - - setVisibleScrollbarEnabled(getVisibleScrollbarEnabled()); - } - - @Override - public void onPause() { - super.onPause(); - EnrichedCallComponent.get(getContext()) - .getEnrichedCallManager() - .unregisterCapabilitiesListener(this); - } - - @Override - public void onResume() { - super.onResume(); - EnrichedCallComponent.get(getContext()) - .getEnrichedCallManager() - .registerCapabilitiesListener(this); - } - - protected boolean getVisibleScrollbarEnabled() { - return true; - } - - @Override - protected void setSearchMode(boolean flag) { - super.setSearchMode(flag); - updateFilterHeaderView(); - } - - private void updateFilterHeaderView() { - final ContactListFilter filter = getFilter(); - if (mAccountFilterHeader == null || filter == null) { - return; - } - final boolean shouldShowHeader = - !isSearchMode() - && AccountFilterUtil.updateAccountFilterTitleForPhone( - mAccountFilterHeader, filter, false); - if (shouldShowHeader) { - mPaddingView.setVisibility(View.GONE); - mAccountFilterHeader.setVisibility(View.VISIBLE); - } else { - mPaddingView.setVisibility(View.VISIBLE); - mAccountFilterHeader.setVisibility(View.GONE); - } - } - - @Override - public void restoreSavedState(Bundle savedState) { - super.restoreSavedState(savedState); - - if (savedState == null) { - return; - } - - mFilter = savedState.getParcelable(KEY_FILTER); - } - - @Override - public void onSaveInstanceState(Bundle outState) { - super.onSaveInstanceState(outState); - outState.putParcelable(KEY_FILTER, mFilter); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - final int itemId = item.getItemId(); - if (itemId == android.R.id.home) { // See ActionBar#setDisplayHomeAsUpEnabled() - if (mListener != null) { - mListener.onHomeInActionBarSelected(); - } - return true; - } - return super.onOptionsItemSelected(item); - } - - @Override - protected void onItemClick(int position, long id) { - callNumber(position, false /* isVideoCall */); - } - - /** - * Initiates a call to the number at the specified position. - * - * @param position The position. - * @param isVideoCall {@code true} if the call should be initiated as a video call, {@code false} - * otherwise. - */ - private void callNumber(int position, boolean isVideoCall) { - final String number = getPhoneNumber(position); - if (!TextUtils.isEmpty(number)) { - cacheContactInfo(position); - CallSpecificAppData callSpecificAppData = - CallSpecificAppData.newBuilder() - .setAllowAssistedDialing(true) - .setCallInitiationType(getCallInitiationType(true /* isRemoteDirectory */)) - .setPositionOfSelectedSearchResult(position) - .setCharactersInSearchString(getQueryString() == null ? 0 : getQueryString().length()) - .build(); - mListener.onPickPhoneNumber(number, isVideoCall, callSpecificAppData); - } else { - LogUtil.i( - "PhoneNumberPickerFragment.callNumber", - "item at %d was clicked before adapter is ready, ignoring", - position); - } - - // Get the lookup key and track any analytics - final String lookupKey = getLookupKey(position); - if (!TextUtils.isEmpty(lookupKey)) { - maybeTrackAnalytics(lookupKey); - } - } - - protected void cacheContactInfo(int position) { - // Not implemented. Hook for child classes - } - - protected String getPhoneNumber(int position) { - final PhoneNumberListAdapter adapter = (PhoneNumberListAdapter) getAdapter(); - return adapter.getPhoneNumber(position); - } - - protected String getLookupKey(int position) { - final PhoneNumberListAdapter adapter = (PhoneNumberListAdapter) getAdapter(); - return adapter.getLookupKey(position); - } - - @Override - protected void startLoading() { - mLoaderStarted = true; - super.startLoading(); - } - - @Override - @MainThread - public void onLoadFinished(Loader loader, Cursor data) { - Assert.isMainThread(); - // TODO(strongarm): define and verify behavior for "Nearby places", corp directories, - // and dividers listed in UI between these categories - if (mCursorReranker != null - && data != null - && !data.isClosed() - && data.getCount() > 0 - && loader.getId() == 0) { // only re-rank if a suggestions loader with id of 0. - data = mCursorReranker.rerankCursor(data); - } - super.onLoadFinished(loader, data); - - // disable scroll bar if there is no data - setVisibleScrollbarEnabled(data != null && !data.isClosed() && data.getCount() > 0); - - if (data != null) { - notifyListeners(); - } - } - - /** Ranks cursor data rows and returns reference to new cursor object with reordered data. */ - public interface CursorReranker { - @MainThread - Cursor rerankCursor(Cursor data); - } - - @MainThread - public void setReranker(@Nullable CursorReranker reranker) { - Assert.isMainThread(); - mCursorReranker = reranker; - } - - /** Listener that is notified when cursor has finished loading data. */ - public interface OnLoadFinishedListener { - void onLoadFinished(); - } - - @MainThread - public void addOnLoadFinishedListener(OnLoadFinishedListener listener) { - Assert.isMainThread(); - mLoadFinishedListeners.add(listener); - } - - @MainThread - public void removeOnLoadFinishedListener(OnLoadFinishedListener listener) { - Assert.isMainThread(); - mLoadFinishedListeners.remove(listener); - } - - @MainThread - protected void notifyListeners() { - Assert.isMainThread(); - for (OnLoadFinishedListener listener : mLoadFinishedListeners) { - listener.onLoadFinished(); - } - } - - @Override - public void onCapabilitiesUpdated() { - if (getAdapter() != null) { - EnrichedCallManager manager = - EnrichedCallComponent.get(getContext()).getEnrichedCallManager(); - Listener listener = ((PhoneNumberListAdapter) getAdapter()).getListener(); - - for (int i = 0; i < getListView().getChildCount(); i++) { - if (!(getListView().getChildAt(i) instanceof ContactListItemView)) { - continue; - } - - // Since call and share is the lowest priority call to action, if any others are set, - // do not reset the call to action. Also do not set the call and share call to action if - // the number doesn't support call composer. - ContactListItemView view = (ContactListItemView) getListView().getChildAt(i); - if (view.getCallToAction() != ContactListItemView.NONE - || view.getPhoneNumber() == null - || manager.getCapabilities(view.getPhoneNumber()) == null - || !manager.getCapabilities(view.getPhoneNumber()).isCallComposerCapable()) { - continue; - } - view.setCallToAction(ContactListItemView.CALL_AND_SHARE, listener, view.getPosition()); - } - } - } - - @MainThread - @Override - public void onDetach() { - Assert.isMainThread(); - mLoadFinishedListeners.clear(); - super.onDetach(); - } - - public void setUseCallableUri(boolean useCallableUri) { - mUseCallableUri = useCallableUri; - } - - public boolean usesCallableUri() { - return mUseCallableUri; - } - - @Override - protected ContactEntryListAdapter createListAdapter() { - PhoneNumberListAdapter adapter = new PhoneNumberListAdapter(getActivity()); - adapter.setDisplayPhotos(true); - adapter.setUseCallableUri(mUseCallableUri); - return adapter; - } - - @Override - protected void configureAdapter() { - super.configureAdapter(); - - final ContactEntryListAdapter adapter = getAdapter(); - if (adapter == null) { - return; - } - - if (!isSearchMode() && mFilter != null) { - adapter.setFilter(mFilter); - } - } - - @Override - protected View inflateView(LayoutInflater inflater, ViewGroup container) { - return inflater.inflate(R.layout.contact_list_content, null); - } - - public ContactListFilter getFilter() { - return mFilter; - } - - public void setFilter(ContactListFilter filter) { - if ((mFilter == null && filter == null) || (mFilter != null && mFilter.equals(filter))) { - return; - } - - mFilter = filter; - if (mLoaderStarted) { - reloadData(); - } - updateFilterHeaderView(); - } - - /** - * @param isRemoteDirectory {@code true} if the call was initiated using a contact/phone number - * not in the local contacts database - */ - protected CallInitiationType.Type getCallInitiationType(boolean isRemoteDirectory) { - return Type.UNKNOWN_INITIATION; - } - - /** - * Where a lookup key contains analytic event information, logs the associated analytics event. - * - * @param lookupKey The lookup key JSON object. - */ - private void maybeTrackAnalytics(String lookupKey) { - try { - JSONObject json = new JSONObject(lookupKey); - - String analyticsCategory = - json.getString(PhoneNumberListAdapter.PhoneQuery.ANALYTICS_CATEGORY); - String analyticsAction = json.getString(PhoneNumberListAdapter.PhoneQuery.ANALYTICS_ACTION); - String analyticsValue = json.getString(PhoneNumberListAdapter.PhoneQuery.ANALYTICS_VALUE); - - if (TextUtils.isEmpty(analyticsCategory) - || TextUtils.isEmpty(analyticsAction) - || TextUtils.isEmpty(analyticsValue)) { - return; - } - - // Assume that the analytic value being tracked could be a float value, but just cast - // to a long so that the analytic server can handle it. - long value; - try { - float floatValue = Float.parseFloat(analyticsValue); - value = (long) floatValue; - } catch (NumberFormatException nfe) { - return; - } - - Logger.get(getActivity()) - .sendHitEventAnalytics(analyticsCategory, analyticsAction, "" /* label */, value); - } catch (JSONException e) { - // Not an error; just a lookup key that doesn't have the right information. - } - } -} diff --git a/java/com/android/contacts/common/list/PinnedHeaderListAdapter.java b/java/com/android/contacts/common/list/PinnedHeaderListAdapter.java deleted file mode 100644 index 0bdcef084..000000000 --- a/java/com/android/contacts/common/list/PinnedHeaderListAdapter.java +++ /dev/null @@ -1,159 +0,0 @@ -/* - * 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.view.View; -import android.view.ViewGroup; -import com.android.common.widget.CompositeCursorAdapter; - -/** A subclass of {@link CompositeCursorAdapter} that manages pinned partition headers. */ -public abstract class PinnedHeaderListAdapter extends CompositeCursorAdapter - implements PinnedHeaderListView.PinnedHeaderAdapter { - - public static final int PARTITION_HEADER_TYPE = 0; - - private boolean mPinnedPartitionHeadersEnabled; - private boolean[] mHeaderVisibility; - - public PinnedHeaderListAdapter(Context context) { - super(context); - } - - public boolean getPinnedPartitionHeadersEnabled() { - return mPinnedPartitionHeadersEnabled; - } - - public void setPinnedPartitionHeadersEnabled(boolean flag) { - this.mPinnedPartitionHeadersEnabled = flag; - } - - @Override - public int getPinnedHeaderCount() { - if (mPinnedPartitionHeadersEnabled) { - return getPartitionCount(); - } else { - return 0; - } - } - - protected boolean isPinnedPartitionHeaderVisible(int partition) { - return getPinnedPartitionHeadersEnabled() - && hasHeader(partition) - && !isPartitionEmpty(partition); - } - - /** The default implementation creates the same type of view as a normal partition header. */ - @Override - public View getPinnedHeaderView(int partition, View convertView, ViewGroup parent) { - if (hasHeader(partition)) { - View view = null; - if (convertView != null) { - Integer headerType = (Integer) convertView.getTag(); - if (headerType != null && headerType == PARTITION_HEADER_TYPE) { - view = convertView; - } - } - if (view == null) { - view = newHeaderView(getContext(), partition, null, parent); - view.setTag(PARTITION_HEADER_TYPE); - view.setFocusable(false); - view.setEnabled(false); - } - bindHeaderView(view, partition, getCursor(partition)); - view.setLayoutDirection(parent.getLayoutDirection()); - return view; - } else { - return null; - } - } - - @Override - public void configurePinnedHeaders(PinnedHeaderListView listView) { - if (!getPinnedPartitionHeadersEnabled()) { - return; - } - - int size = getPartitionCount(); - - // Cache visibility bits, because we will need them several times later on - if (mHeaderVisibility == null || mHeaderVisibility.length != size) { - mHeaderVisibility = new boolean[size]; - } - for (int i = 0; i < size; i++) { - boolean visible = isPinnedPartitionHeaderVisible(i); - mHeaderVisibility[i] = visible; - if (!visible) { - listView.setHeaderInvisible(i, true); - } - } - - int headerViewsCount = listView.getHeaderViewsCount(); - - // Starting at the top, find and pin headers for partitions preceding the visible one(s) - int maxTopHeader = -1; - int topHeaderHeight = 0; - for (int i = 0; i < size; i++) { - if (mHeaderVisibility[i]) { - int position = listView.getPositionAt(topHeaderHeight) - headerViewsCount; - int partition = getPartitionForPosition(position); - if (i > partition) { - break; - } - - listView.setHeaderPinnedAtTop(i, topHeaderHeight, false); - topHeaderHeight += listView.getPinnedHeaderHeight(i); - maxTopHeader = i; - } - } - - // Starting at the bottom, find and pin headers for partitions following the visible one(s) - int maxBottomHeader = size; - int bottomHeaderHeight = 0; - int listHeight = listView.getHeight(); - for (int i = size; --i > maxTopHeader; ) { - if (mHeaderVisibility[i]) { - int position = listView.getPositionAt(listHeight - bottomHeaderHeight) - headerViewsCount; - if (position < 0) { - break; - } - - int partition = getPartitionForPosition(position - 1); - if (partition == -1 || i <= partition) { - break; - } - - int height = listView.getPinnedHeaderHeight(i); - bottomHeaderHeight += height; - - listView.setHeaderPinnedAtBottom(i, listHeight - bottomHeaderHeight, false); - maxBottomHeader = i; - } - } - - // Headers in between the top-pinned and bottom-pinned should be hidden - for (int i = maxTopHeader + 1; i < maxBottomHeader; i++) { - if (mHeaderVisibility[i]) { - listView.setHeaderInvisible(i, isPartitionEmpty(i)); - } - } - } - - @Override - public int getScrollPositionForHeader(int viewIndex) { - return getPositionForPartition(viewIndex); - } -} diff --git a/java/com/android/contacts/common/list/PinnedHeaderListView.java b/java/com/android/contacts/common/list/PinnedHeaderListView.java deleted file mode 100644 index a801624b4..000000000 --- a/java/com/android/contacts/common/list/PinnedHeaderListView.java +++ /dev/null @@ -1,563 +0,0 @@ -/* - * 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.graphics.Canvas; -import android.graphics.RectF; -import android.util.AttributeSet; -import android.view.MotionEvent; -import android.view.View; -import android.view.ViewGroup; -import android.widget.AbsListView; -import android.widget.AbsListView.OnScrollListener; -import android.widget.AdapterView; -import android.widget.AdapterView.OnItemSelectedListener; -import android.widget.ListAdapter; -import com.android.dialer.util.ViewUtil; - -/** - * A ListView that maintains a header pinned at the top of the list. The pinned header can be pushed - * up and dissolved as needed. - */ -public class PinnedHeaderListView extends AutoScrollListView - implements OnScrollListener, OnItemSelectedListener { - - private static final int MAX_ALPHA = 255; - private static final int TOP = 0; - private static final int BOTTOM = 1; - private static final int FADING = 2; - private static final int DEFAULT_ANIMATION_DURATION = 20; - private static final int DEFAULT_SMOOTH_SCROLL_DURATION = 100; - private PinnedHeaderAdapter mAdapter; - private int mSize; - private PinnedHeader[] mHeaders; - private RectF mBounds = new RectF(); - private OnScrollListener mOnScrollListener; - private OnItemSelectedListener mOnItemSelectedListener; - private int mScrollState; - private boolean mScrollToSectionOnHeaderTouch = false; - private boolean mHeaderTouched = false; - private int mAnimationDuration = DEFAULT_ANIMATION_DURATION; - private boolean mAnimating; - private long mAnimationTargetTime; - private int mHeaderPaddingStart; - private int mHeaderWidth; - - public PinnedHeaderListView(Context context) { - this(context, null, android.R.attr.listViewStyle); - } - - public PinnedHeaderListView(Context context, AttributeSet attrs) { - this(context, attrs, android.R.attr.listViewStyle); - } - - public PinnedHeaderListView(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - super.setOnScrollListener(this); - super.setOnItemSelectedListener(this); - } - - @Override - protected void onLayout(boolean changed, int l, int t, int r, int b) { - super.onLayout(changed, l, t, r, b); - mHeaderPaddingStart = getPaddingStart(); - mHeaderWidth = r - l - mHeaderPaddingStart - getPaddingEnd(); - } - - @Override - public void setAdapter(ListAdapter adapter) { - mAdapter = (PinnedHeaderAdapter) adapter; - super.setAdapter(adapter); - } - - @Override - public void setOnScrollListener(OnScrollListener onScrollListener) { - mOnScrollListener = onScrollListener; - super.setOnScrollListener(this); - } - - @Override - public void setOnItemSelectedListener(OnItemSelectedListener listener) { - mOnItemSelectedListener = listener; - super.setOnItemSelectedListener(this); - } - - public void setScrollToSectionOnHeaderTouch(boolean value) { - mScrollToSectionOnHeaderTouch = value; - } - - @Override - public void onScroll( - AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { - if (mAdapter != null) { - int count = mAdapter.getPinnedHeaderCount(); - if (count != mSize) { - mSize = count; - if (mHeaders == null) { - mHeaders = new PinnedHeader[mSize]; - } else if (mHeaders.length < mSize) { - PinnedHeader[] headers = mHeaders; - mHeaders = new PinnedHeader[mSize]; - System.arraycopy(headers, 0, mHeaders, 0, headers.length); - } - } - - for (int i = 0; i < mSize; i++) { - if (mHeaders[i] == null) { - mHeaders[i] = new PinnedHeader(); - } - mHeaders[i].view = mAdapter.getPinnedHeaderView(i, mHeaders[i].view, this); - } - - mAnimationTargetTime = System.currentTimeMillis() + mAnimationDuration; - mAdapter.configurePinnedHeaders(this); - invalidateIfAnimating(); - } - if (mOnScrollListener != null) { - mOnScrollListener.onScroll(this, firstVisibleItem, visibleItemCount, totalItemCount); - } - } - - @Override - protected float getTopFadingEdgeStrength() { - // Disable vertical fading at the top when the pinned header is present - return mSize > 0 ? 0 : super.getTopFadingEdgeStrength(); - } - - @Override - public void onScrollStateChanged(AbsListView view, int scrollState) { - mScrollState = scrollState; - if (mOnScrollListener != null) { - mOnScrollListener.onScrollStateChanged(this, scrollState); - } - } - - /** - * Ensures that the selected item is positioned below the top-pinned headers and above the - * bottom-pinned ones. - */ - @Override - public void onItemSelected(AdapterView parent, View view, int position, long id) { - int height = getHeight(); - - int windowTop = 0; - int windowBottom = height; - - for (int i = 0; i < mSize; i++) { - PinnedHeader header = mHeaders[i]; - if (header.visible) { - if (header.state == TOP) { - windowTop = header.y + header.height; - } else if (header.state == BOTTOM) { - windowBottom = header.y; - break; - } - } - } - - View selectedView = getSelectedView(); - if (selectedView != null) { - if (selectedView.getTop() < windowTop) { - setSelectionFromTop(position, windowTop); - } else if (selectedView.getBottom() > windowBottom) { - setSelectionFromTop(position, windowBottom - selectedView.getHeight()); - } - } - - if (mOnItemSelectedListener != null) { - mOnItemSelectedListener.onItemSelected(parent, view, position, id); - } - } - - @Override - public void onNothingSelected(AdapterView parent) { - if (mOnItemSelectedListener != null) { - mOnItemSelectedListener.onNothingSelected(parent); - } - } - - public int getPinnedHeaderHeight(int viewIndex) { - ensurePinnedHeaderLayout(viewIndex); - return mHeaders[viewIndex].view.getHeight(); - } - - /** - * Set header to be pinned at the top. - * - * @param viewIndex index of the header view - * @param y is position of the header in pixels. - * @param animate true if the transition to the new coordinate should be animated - */ - public void setHeaderPinnedAtTop(int viewIndex, int y, boolean animate) { - ensurePinnedHeaderLayout(viewIndex); - PinnedHeader header = mHeaders[viewIndex]; - header.visible = true; - header.y = y; - header.state = TOP; - - // TODO perhaps we should animate at the top as well - header.animating = false; - } - - /** - * Set header to be pinned at the bottom. - * - * @param viewIndex index of the header view - * @param y is position of the header in pixels. - * @param animate true if the transition to the new coordinate should be animated - */ - public void setHeaderPinnedAtBottom(int viewIndex, int y, boolean animate) { - ensurePinnedHeaderLayout(viewIndex); - PinnedHeader header = mHeaders[viewIndex]; - header.state = BOTTOM; - if (header.animating) { - header.targetTime = mAnimationTargetTime; - header.sourceY = header.y; - header.targetY = y; - } else if (animate && (header.y != y || !header.visible)) { - if (header.visible) { - header.sourceY = header.y; - } else { - header.visible = true; - header.sourceY = y + header.height; - } - header.animating = true; - header.targetVisible = true; - header.targetTime = mAnimationTargetTime; - header.targetY = y; - } else { - header.visible = true; - header.y = y; - } - } - - /** - * Set header to be pinned at the top of the first visible item. - * - * @param viewIndex index of the header view - * @param position is position of the header in pixels. - */ - public void setFadingHeader(int viewIndex, int position, boolean fade) { - ensurePinnedHeaderLayout(viewIndex); - - View child = getChildAt(position - getFirstVisiblePosition()); - if (child == null) { - return; - } - - PinnedHeader header = mHeaders[viewIndex]; - header.visible = true; - header.state = FADING; - header.alpha = MAX_ALPHA; - header.animating = false; - - int top = getTotalTopPinnedHeaderHeight(); - header.y = top; - if (fade) { - int bottom = child.getBottom() - top; - int headerHeight = header.height; - if (bottom < headerHeight) { - int portion = bottom - headerHeight; - header.alpha = MAX_ALPHA * (headerHeight + portion) / headerHeight; - header.y = top + portion; - } - } - } - - /** - * Makes header invisible. - * - * @param viewIndex index of the header view - * @param animate true if the transition to the new coordinate should be animated - */ - public void setHeaderInvisible(int viewIndex, boolean animate) { - PinnedHeader header = mHeaders[viewIndex]; - if (header.visible && (animate || header.animating) && header.state == BOTTOM) { - header.sourceY = header.y; - if (!header.animating) { - header.visible = true; - header.targetY = getBottom() + header.height; - } - header.animating = true; - header.targetTime = mAnimationTargetTime; - header.targetVisible = false; - } else { - header.visible = false; - } - } - - private void ensurePinnedHeaderLayout(int viewIndex) { - View view = mHeaders[viewIndex].view; - if (view.isLayoutRequested()) { - ViewGroup.LayoutParams layoutParams = view.getLayoutParams(); - int widthSpec; - int heightSpec; - - if (layoutParams != null && layoutParams.width > 0) { - widthSpec = View.MeasureSpec.makeMeasureSpec(layoutParams.width, View.MeasureSpec.EXACTLY); - } else { - widthSpec = View.MeasureSpec.makeMeasureSpec(mHeaderWidth, View.MeasureSpec.EXACTLY); - } - - if (layoutParams != null && layoutParams.height > 0) { - heightSpec = - View.MeasureSpec.makeMeasureSpec(layoutParams.height, View.MeasureSpec.EXACTLY); - } else { - heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); - } - view.measure(widthSpec, heightSpec); - int height = view.getMeasuredHeight(); - mHeaders[viewIndex].height = height; - view.layout(0, 0, view.getMeasuredWidth(), height); - } - } - - /** Returns the sum of heights of headers pinned to the top. */ - public int getTotalTopPinnedHeaderHeight() { - for (int i = mSize; --i >= 0; ) { - PinnedHeader header = mHeaders[i]; - if (header.visible && header.state == TOP) { - return header.y + header.height; - } - } - return 0; - } - - /** Returns the list item position at the specified y coordinate. */ - public int getPositionAt(int y) { - do { - int position = pointToPosition(getPaddingLeft() + 1, y); - if (position != -1) { - return position; - } - // If position == -1, we must have hit a separator. Let's examine - // a nearby pixel - y--; - } while (y > 0); - return 0; - } - - @Override - public boolean onInterceptTouchEvent(MotionEvent ev) { - mHeaderTouched = false; - if (super.onInterceptTouchEvent(ev)) { - return true; - } - - if (mScrollState == SCROLL_STATE_IDLE) { - final int y = (int) ev.getY(); - final int x = (int) ev.getX(); - for (int i = mSize; --i >= 0; ) { - PinnedHeader header = mHeaders[i]; - // For RTL layouts, this also takes into account that the scrollbar is on the left - // side. - final int padding = getPaddingLeft(); - if (header.visible - && header.y <= y - && header.y + header.height > y - && x >= padding - && padding + header.view.getWidth() >= x) { - mHeaderTouched = true; - if (mScrollToSectionOnHeaderTouch && ev.getAction() == MotionEvent.ACTION_DOWN) { - return smoothScrollToPartition(i); - } else { - return true; - } - } - } - } - - return false; - } - - @Override - public boolean onTouchEvent(MotionEvent ev) { - if (mHeaderTouched) { - if (ev.getAction() == MotionEvent.ACTION_UP) { - mHeaderTouched = false; - } - return true; - } - return super.onTouchEvent(ev); - } - - private boolean smoothScrollToPartition(int partition) { - if (mAdapter == null) { - return false; - } - final int position = mAdapter.getScrollPositionForHeader(partition); - if (position == -1) { - return false; - } - - int offset = 0; - for (int i = 0; i < partition; i++) { - PinnedHeader header = mHeaders[i]; - if (header.visible) { - offset += header.height; - } - } - smoothScrollToPositionFromTop( - position + getHeaderViewsCount(), offset, DEFAULT_SMOOTH_SCROLL_DURATION); - return true; - } - - private void invalidateIfAnimating() { - mAnimating = false; - for (int i = 0; i < mSize; i++) { - if (mHeaders[i].animating) { - mAnimating = true; - invalidate(); - return; - } - } - } - - @Override - protected void dispatchDraw(Canvas canvas) { - long currentTime = mAnimating ? System.currentTimeMillis() : 0; - - int top = 0; - int bottom = getBottom(); - boolean hasVisibleHeaders = false; - for (int i = 0; i < mSize; i++) { - PinnedHeader header = mHeaders[i]; - if (header.visible) { - hasVisibleHeaders = true; - if (header.state == BOTTOM && header.y < bottom) { - bottom = header.y; - } else if (header.state == TOP || header.state == FADING) { - int newTop = header.y + header.height; - if (newTop > top) { - top = newTop; - } - } - } - } - - if (hasVisibleHeaders) { - canvas.save(); - } - - super.dispatchDraw(canvas); - - if (hasVisibleHeaders) { - canvas.restore(); - - // If the first item is visible and if it has a positive top that is greater than the - // first header's assigned y-value, use that for the first header's y value. This way, - // the header inherits any padding applied to the list view. - if (mSize > 0 && getFirstVisiblePosition() == 0) { - View firstChild = getChildAt(0); - PinnedHeader firstHeader = mHeaders[0]; - - if (firstHeader != null) { - int firstHeaderTop = firstChild != null ? firstChild.getTop() : 0; - firstHeader.y = Math.max(firstHeader.y, firstHeaderTop); - } - } - - // First draw top headers, then the bottom ones to handle the Z axis correctly - for (int i = mSize; --i >= 0; ) { - PinnedHeader header = mHeaders[i]; - if (header.visible && (header.state == TOP || header.state == FADING)) { - drawHeader(canvas, header, currentTime); - } - } - - for (int i = 0; i < mSize; i++) { - PinnedHeader header = mHeaders[i]; - if (header.visible && header.state == BOTTOM) { - drawHeader(canvas, header, currentTime); - } - } - } - - invalidateIfAnimating(); - } - - private void drawHeader(Canvas canvas, PinnedHeader header, long currentTime) { - if (header.animating) { - int timeLeft = (int) (header.targetTime - currentTime); - if (timeLeft <= 0) { - header.y = header.targetY; - header.visible = header.targetVisible; - header.animating = false; - } else { - header.y = - header.targetY + (header.sourceY - header.targetY) * timeLeft / mAnimationDuration; - } - } - if (header.visible) { - View view = header.view; - int saveCount = canvas.save(); - int translateX = - ViewUtil.isViewLayoutRtl(this) - ? getWidth() - mHeaderPaddingStart - view.getWidth() - : mHeaderPaddingStart; - canvas.translate(translateX, header.y); - if (header.state == FADING) { - mBounds.set(0, 0, view.getWidth(), view.getHeight()); - canvas.saveLayerAlpha(mBounds, header.alpha); - } - view.draw(canvas); - canvas.restoreToCount(saveCount); - } - } - - /** Adapter interface. The list adapter must implement this interface. */ - public interface PinnedHeaderAdapter { - - /** Returns the overall number of pinned headers, visible or not. */ - int getPinnedHeaderCount(); - - /** Creates or updates the pinned header view. */ - View getPinnedHeaderView(int viewIndex, View convertView, ViewGroup parent); - - /** - * Configures the pinned headers to match the visible list items. The adapter should call {@link - * PinnedHeaderListView#setHeaderPinnedAtTop}, {@link - * PinnedHeaderListView#setHeaderPinnedAtBottom}, {@link PinnedHeaderListView#setFadingHeader} - * or {@link PinnedHeaderListView#setHeaderInvisible}, for each header that needs to change its - * position or visibility. - */ - void configurePinnedHeaders(PinnedHeaderListView listView); - - /** - * Returns the list position to scroll to if the pinned header is touched. Return -1 if the list - * does not need to be scrolled. - */ - int getScrollPositionForHeader(int viewIndex); - } - - private static final class PinnedHeader { - - View view; - boolean visible; - int y; - int height; - int alpha; - int state; - - boolean animating; - boolean targetVisible; - int sourceY; - int targetY; - long targetTime; - } -} -- cgit v1.2.3