Home | History | Annotate | Download | only in list
      1 /*
      2  * Copyright (C) 2010 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 package com.android.contacts.common.list;
     17 
     18 import android.content.Context;
     19 import android.content.CursorLoader;
     20 import android.content.res.Resources;
     21 import android.database.Cursor;
     22 import android.net.Uri;
     23 import android.os.Bundle;
     24 import android.provider.ContactsContract;
     25 import android.provider.ContactsContract.CommonDataKinds.Phone;
     26 import android.provider.ContactsContract.Contacts;
     27 import android.provider.ContactsContract.Directory;
     28 import android.text.TextUtils;
     29 import android.view.LayoutInflater;
     30 import android.view.View;
     31 import android.view.ViewGroup;
     32 import android.widget.QuickContactBadge;
     33 import android.widget.SectionIndexer;
     34 import android.widget.TextView;
     35 import com.android.contacts.common.ContactsUtils;
     36 import com.android.contacts.common.R;
     37 import com.android.contacts.common.util.SearchUtil;
     38 import com.android.dialer.common.LogUtil;
     39 import com.android.dialer.common.cp2.DirectoryCompat;
     40 import com.android.dialer.compat.CompatUtils;
     41 import com.android.dialer.configprovider.ConfigProviderBindings;
     42 import com.android.dialer.contactphoto.ContactPhotoManager;
     43 import com.android.dialer.contactphoto.ContactPhotoManager.DefaultImageRequest;
     44 import com.android.dialer.logging.InteractionEvent;
     45 import com.android.dialer.logging.Logger;
     46 import java.util.HashSet;
     47 
     48 /**
     49  * Common base class for various contact-related lists, e.g. contact list, phone number list etc.
     50  */
     51 public abstract class ContactEntryListAdapter extends IndexerListAdapter {
     52 
     53   /**
     54    * Indicates whether the {@link Directory#LOCAL_INVISIBLE} directory should be included in the
     55    * search.
     56    */
     57   public static final boolean LOCAL_INVISIBLE_DIRECTORY_ENABLED = false;
     58 
     59   private int mDisplayOrder;
     60   private int mSortOrder;
     61 
     62   private boolean mDisplayPhotos;
     63   private boolean mCircularPhotos = true;
     64   private boolean mQuickContactEnabled;
     65   private boolean mAdjustSelectionBoundsEnabled;
     66 
     67   /** The root view of the fragment that this adapter is associated with. */
     68   private View mFragmentRootView;
     69 
     70   private ContactPhotoManager mPhotoLoader;
     71 
     72   private String mQueryString;
     73   private String mUpperCaseQueryString;
     74   private boolean mSearchMode;
     75   private int mDirectorySearchMode;
     76   private int mDirectoryResultLimit = Integer.MAX_VALUE;
     77 
     78   private boolean mEmptyListEnabled = true;
     79 
     80   private boolean mSelectionVisible;
     81 
     82   private ContactListFilter mFilter;
     83   private boolean mDarkTheme = false;
     84 
     85   public static final int SUGGESTIONS_LOADER_ID = 0;
     86 
     87   /** Resource used to provide header-text for default filter. */
     88   private CharSequence mDefaultFilterHeaderText;
     89 
     90   public ContactEntryListAdapter(Context context) {
     91     super(context);
     92     setDefaultFilterHeaderText(R.string.local_search_label);
     93     addPartitions();
     94   }
     95 
     96   /**
     97    * @param fragmentRootView Root view of the fragment. This is used to restrict the scope of image
     98    *     loading requests that get cancelled on cursor changes.
     99    */
    100   protected void setFragmentRootView(View fragmentRootView) {
    101     mFragmentRootView = fragmentRootView;
    102   }
    103 
    104   protected void setDefaultFilterHeaderText(int resourceId) {
    105     mDefaultFilterHeaderText = getContext().getResources().getText(resourceId);
    106   }
    107 
    108   @Override
    109   protected ContactListItemView newView(
    110       Context context, int partition, Cursor cursor, int position, ViewGroup parent) {
    111     final ContactListItemView view = new ContactListItemView(context, null);
    112     view.setIsSectionHeaderEnabled(isSectionHeaderDisplayEnabled());
    113     view.setAdjustSelectionBoundsEnabled(isAdjustSelectionBoundsEnabled());
    114     return view;
    115   }
    116 
    117   @Override
    118   protected void bindView(View itemView, int partition, Cursor cursor, int position) {
    119     final ContactListItemView view = (ContactListItemView) itemView;
    120     view.setIsSectionHeaderEnabled(isSectionHeaderDisplayEnabled());
    121     bindWorkProfileIcon(view, partition);
    122   }
    123 
    124   @Override
    125   protected View createPinnedSectionHeaderView(Context context, ViewGroup parent) {
    126     return new ContactListPinnedHeaderView(context, null, parent);
    127   }
    128 
    129   @Override
    130   protected void setPinnedSectionTitle(View pinnedHeaderView, String title) {
    131     ((ContactListPinnedHeaderView) pinnedHeaderView).setSectionHeaderTitle(title);
    132   }
    133 
    134   protected void addPartitions() {
    135     if (ConfigProviderBindings.get(getContext()).getBoolean("p13n_ranker_should_enable", false)) {
    136       addPartition(createSuggestionsDirectoryPartition());
    137     }
    138     addPartition(createDefaultDirectoryPartition());
    139   }
    140 
    141   protected DirectoryPartition createSuggestionsDirectoryPartition() {
    142     DirectoryPartition partition = new DirectoryPartition(true, true);
    143     partition.setDirectoryId(SUGGESTIONS_LOADER_ID);
    144     partition.setDirectoryType(getContext().getString(R.string.contact_suggestions));
    145     partition.setPriorityDirectory(true);
    146     partition.setPhotoSupported(true);
    147     partition.setLabel(getContext().getString(R.string.local_suggestions_search_label));
    148     return partition;
    149   }
    150 
    151   protected DirectoryPartition createDefaultDirectoryPartition() {
    152     DirectoryPartition partition = new DirectoryPartition(true, true);
    153     partition.setDirectoryId(Directory.DEFAULT);
    154     partition.setDirectoryType(getContext().getString(R.string.contactsList));
    155     partition.setPriorityDirectory(true);
    156     partition.setPhotoSupported(true);
    157     partition.setLabel(mDefaultFilterHeaderText.toString());
    158     return partition;
    159   }
    160 
    161   /**
    162    * Remove all directories after the default directory. This is typically used when contacts list
    163    * screens are asked to exit the search mode and thus need to remove all remote directory results
    164    * for the search.
    165    *
    166    * <p>This code assumes that the default directory and directories before that should not be
    167    * deleted (e.g. Join screen has "suggested contacts" directory before the default director, and
    168    * we should not remove the directory).
    169    */
    170   public void removeDirectoriesAfterDefault() {
    171     final int partitionCount = getPartitionCount();
    172     for (int i = partitionCount - 1; i >= 0; i--) {
    173       final Partition partition = getPartition(i);
    174       if ((partition instanceof DirectoryPartition)
    175           && ((DirectoryPartition) partition).getDirectoryId() == Directory.DEFAULT) {
    176         break;
    177       } else {
    178         removePartition(i);
    179       }
    180     }
    181   }
    182 
    183   protected int getPartitionByDirectoryId(long id) {
    184     int count = getPartitionCount();
    185     for (int i = 0; i < count; i++) {
    186       Partition partition = getPartition(i);
    187       if (partition instanceof DirectoryPartition) {
    188         if (((DirectoryPartition) partition).getDirectoryId() == id) {
    189           return i;
    190         }
    191       }
    192     }
    193     return -1;
    194   }
    195 
    196   protected DirectoryPartition getDirectoryById(long id) {
    197     int count = getPartitionCount();
    198     for (int i = 0; i < count; i++) {
    199       Partition partition = getPartition(i);
    200       if (partition instanceof DirectoryPartition) {
    201         final DirectoryPartition directoryPartition = (DirectoryPartition) partition;
    202         if (directoryPartition.getDirectoryId() == id) {
    203           return directoryPartition;
    204         }
    205       }
    206     }
    207     return null;
    208   }
    209 
    210   public abstract void configureLoader(CursorLoader loader, long directoryId);
    211 
    212   /** Marks all partitions as "loading" */
    213   public void onDataReload() {
    214     boolean notify = false;
    215     int count = getPartitionCount();
    216     for (int i = 0; i < count; i++) {
    217       Partition partition = getPartition(i);
    218       if (partition instanceof DirectoryPartition) {
    219         DirectoryPartition directoryPartition = (DirectoryPartition) partition;
    220         if (!directoryPartition.isLoading()) {
    221           notify = true;
    222         }
    223         directoryPartition.setStatus(DirectoryPartition.STATUS_NOT_LOADED);
    224       }
    225     }
    226     if (notify) {
    227       notifyDataSetChanged();
    228     }
    229   }
    230 
    231   @Override
    232   public void clearPartitions() {
    233     int count = getPartitionCount();
    234     for (int i = 0; i < count; i++) {
    235       Partition partition = getPartition(i);
    236       if (partition instanceof DirectoryPartition) {
    237         DirectoryPartition directoryPartition = (DirectoryPartition) partition;
    238         directoryPartition.setStatus(DirectoryPartition.STATUS_NOT_LOADED);
    239       }
    240     }
    241     super.clearPartitions();
    242   }
    243 
    244   public boolean isSearchMode() {
    245     return mSearchMode;
    246   }
    247 
    248   public void setSearchMode(boolean flag) {
    249     mSearchMode = flag;
    250   }
    251 
    252   public String getQueryString() {
    253     return mQueryString;
    254   }
    255 
    256   public void setQueryString(String queryString) {
    257     mQueryString = queryString;
    258     if (TextUtils.isEmpty(queryString)) {
    259       mUpperCaseQueryString = null;
    260     } else {
    261       mUpperCaseQueryString = SearchUtil.cleanStartAndEndOfSearchQuery(queryString.toUpperCase());
    262     }
    263 
    264     // Enable default partition header if in search mode (including zero-suggest).
    265     if (mQueryString != null) {
    266       setDefaultPartitionHeader(true);
    267     }
    268   }
    269 
    270   public String getUpperCaseQueryString() {
    271     return mUpperCaseQueryString;
    272   }
    273 
    274   public int getDirectorySearchMode() {
    275     return mDirectorySearchMode;
    276   }
    277 
    278   public void setDirectorySearchMode(int mode) {
    279     mDirectorySearchMode = mode;
    280   }
    281 
    282   public int getDirectoryResultLimit() {
    283     return mDirectoryResultLimit;
    284   }
    285 
    286   public void setDirectoryResultLimit(int limit) {
    287     this.mDirectoryResultLimit = limit;
    288   }
    289 
    290   public int getDirectoryResultLimit(DirectoryPartition directoryPartition) {
    291     final int limit = directoryPartition.getResultLimit();
    292     return limit == DirectoryPartition.RESULT_LIMIT_DEFAULT ? mDirectoryResultLimit : limit;
    293   }
    294 
    295   public int getContactNameDisplayOrder() {
    296     return mDisplayOrder;
    297   }
    298 
    299   public void setContactNameDisplayOrder(int displayOrder) {
    300     mDisplayOrder = displayOrder;
    301   }
    302 
    303   public int getSortOrder() {
    304     return mSortOrder;
    305   }
    306 
    307   public void setSortOrder(int sortOrder) {
    308     mSortOrder = sortOrder;
    309   }
    310 
    311   protected ContactPhotoManager getPhotoLoader() {
    312     return mPhotoLoader;
    313   }
    314 
    315   public void setPhotoLoader(ContactPhotoManager photoLoader) {
    316     mPhotoLoader = photoLoader;
    317   }
    318 
    319   public boolean getDisplayPhotos() {
    320     return mDisplayPhotos;
    321   }
    322 
    323   public void setDisplayPhotos(boolean displayPhotos) {
    324     mDisplayPhotos = displayPhotos;
    325   }
    326 
    327   public boolean getCircularPhotos() {
    328     return mCircularPhotos;
    329   }
    330 
    331   public boolean isSelectionVisible() {
    332     return mSelectionVisible;
    333   }
    334 
    335   public void setSelectionVisible(boolean flag) {
    336     this.mSelectionVisible = flag;
    337   }
    338 
    339   public boolean isQuickContactEnabled() {
    340     return mQuickContactEnabled;
    341   }
    342 
    343   public void setQuickContactEnabled(boolean quickContactEnabled) {
    344     mQuickContactEnabled = quickContactEnabled;
    345   }
    346 
    347   public boolean isAdjustSelectionBoundsEnabled() {
    348     return mAdjustSelectionBoundsEnabled;
    349   }
    350 
    351   public void setAdjustSelectionBoundsEnabled(boolean enabled) {
    352     mAdjustSelectionBoundsEnabled = enabled;
    353   }
    354 
    355   public void setProfileExists(boolean exists) {
    356     // Stick the "ME" header for the profile
    357     if (exists) {
    358       setSectionHeader(R.string.user_profile_contacts_list_header, /* # of ME */ 1);
    359     }
    360   }
    361 
    362   private void setSectionHeader(int resId, int numberOfItems) {
    363     SectionIndexer indexer = getIndexer();
    364     if (indexer != null) {
    365       ((ContactsSectionIndexer) indexer)
    366           .setProfileAndFavoritesHeader(getContext().getString(resId), numberOfItems);
    367     }
    368   }
    369 
    370   public void setDarkTheme(boolean value) {
    371     mDarkTheme = value;
    372   }
    373 
    374   /** Updates partitions according to the directory meta-data contained in the supplied cursor. */
    375   public void changeDirectories(Cursor cursor) {
    376     if (cursor.getCount() == 0) {
    377       // Directory table must have at least local directory, without which this adapter will
    378       // enter very weird state.
    379       LogUtil.i(
    380           "ContactEntryListAdapter.changeDirectories",
    381           "directory search loader returned an empty cursor, which implies we have "
    382               + "no directory entries.",
    383           new RuntimeException());
    384       return;
    385     }
    386     HashSet<Long> directoryIds = new HashSet<Long>();
    387 
    388     int idColumnIndex = cursor.getColumnIndex(Directory._ID);
    389     int directoryTypeColumnIndex = cursor.getColumnIndex(DirectoryListLoader.DIRECTORY_TYPE);
    390     int displayNameColumnIndex = cursor.getColumnIndex(Directory.DISPLAY_NAME);
    391     int photoSupportColumnIndex = cursor.getColumnIndex(Directory.PHOTO_SUPPORT);
    392 
    393     // TODO preserve the order of partition to match those of the cursor
    394     // Phase I: add new directories
    395     cursor.moveToPosition(-1);
    396     while (cursor.moveToNext()) {
    397       long id = cursor.getLong(idColumnIndex);
    398       directoryIds.add(id);
    399       if (getPartitionByDirectoryId(id) == -1) {
    400         DirectoryPartition partition = new DirectoryPartition(false, true);
    401         partition.setDirectoryId(id);
    402         if (DirectoryCompat.isRemoteDirectoryId(id)) {
    403           if (DirectoryCompat.isEnterpriseDirectoryId(id)) {
    404             partition.setLabel(mContext.getString(R.string.directory_search_label_work));
    405           } else {
    406             partition.setLabel(mContext.getString(R.string.directory_search_label));
    407           }
    408         } else {
    409           if (DirectoryCompat.isEnterpriseDirectoryId(id)) {
    410             partition.setLabel(mContext.getString(R.string.list_filter_phones_work));
    411           } else {
    412             partition.setLabel(mDefaultFilterHeaderText.toString());
    413           }
    414         }
    415         partition.setDirectoryType(cursor.getString(directoryTypeColumnIndex));
    416         partition.setDisplayName(cursor.getString(displayNameColumnIndex));
    417         int photoSupport = cursor.getInt(photoSupportColumnIndex);
    418         partition.setPhotoSupported(
    419             photoSupport == Directory.PHOTO_SUPPORT_THUMBNAIL_ONLY
    420                 || photoSupport == Directory.PHOTO_SUPPORT_FULL);
    421         addPartition(partition);
    422       }
    423     }
    424 
    425     // Phase II: remove deleted directories
    426     int count = getPartitionCount();
    427     for (int i = count; --i >= 0; ) {
    428       Partition partition = getPartition(i);
    429       if (partition instanceof DirectoryPartition) {
    430         long id = ((DirectoryPartition) partition).getDirectoryId();
    431         if (!directoryIds.contains(id)) {
    432           removePartition(i);
    433         }
    434       }
    435     }
    436 
    437     invalidate();
    438     notifyDataSetChanged();
    439   }
    440 
    441   @Override
    442   public void changeCursor(int partitionIndex, Cursor cursor) {
    443     if (partitionIndex >= getPartitionCount()) {
    444       // There is no partition for this data
    445       return;
    446     }
    447 
    448     Partition partition = getPartition(partitionIndex);
    449     if (partition instanceof DirectoryPartition) {
    450       ((DirectoryPartition) partition).setStatus(DirectoryPartition.STATUS_LOADED);
    451     }
    452 
    453     if (mDisplayPhotos && mPhotoLoader != null && isPhotoSupported(partitionIndex)) {
    454       mPhotoLoader.refreshCache();
    455     }
    456 
    457     super.changeCursor(partitionIndex, cursor);
    458 
    459     if (isSectionHeaderDisplayEnabled() && partitionIndex == getIndexedPartition()) {
    460       updateIndexer(cursor);
    461     }
    462 
    463     // When the cursor changes, cancel any pending asynchronous photo loads.
    464     mPhotoLoader.cancelPendingRequests(mFragmentRootView);
    465   }
    466 
    467   public void changeCursor(Cursor cursor) {
    468     changeCursor(0, cursor);
    469   }
    470 
    471   /** Updates the indexer, which is used to produce section headers. */
    472   private void updateIndexer(Cursor cursor) {
    473     if (cursor == null || cursor.isClosed()) {
    474       setIndexer(null);
    475       return;
    476     }
    477 
    478     Bundle bundle = cursor.getExtras();
    479     if (bundle.containsKey(Contacts.EXTRA_ADDRESS_BOOK_INDEX_TITLES)
    480         && bundle.containsKey(Contacts.EXTRA_ADDRESS_BOOK_INDEX_COUNTS)) {
    481       String[] sections = bundle.getStringArray(Contacts.EXTRA_ADDRESS_BOOK_INDEX_TITLES);
    482       int[] counts = bundle.getIntArray(Contacts.EXTRA_ADDRESS_BOOK_INDEX_COUNTS);
    483 
    484       if (getExtraStartingSection()) {
    485         // Insert an additional unnamed section at the top of the list.
    486         String[] allSections = new String[sections.length + 1];
    487         int[] allCounts = new int[counts.length + 1];
    488         for (int i = 0; i < sections.length; i++) {
    489           allSections[i + 1] = sections[i];
    490           allCounts[i + 1] = counts[i];
    491         }
    492         allCounts[0] = 1;
    493         allSections[0] = "";
    494         setIndexer(new ContactsSectionIndexer(allSections, allCounts));
    495       } else {
    496         setIndexer(new ContactsSectionIndexer(sections, counts));
    497       }
    498     } else {
    499       setIndexer(null);
    500     }
    501   }
    502 
    503   protected boolean getExtraStartingSection() {
    504     return false;
    505   }
    506 
    507   @Override
    508   public int getViewTypeCount() {
    509     // We need a separate view type for each item type, plus another one for
    510     // each type with header, plus one for "other".
    511     return getItemViewTypeCount() * 2 + 1;
    512   }
    513 
    514   @Override
    515   public int getItemViewType(int partitionIndex, int position) {
    516     int type = super.getItemViewType(partitionIndex, position);
    517     if (!isUserProfile(position)
    518         && isSectionHeaderDisplayEnabled()
    519         && partitionIndex == getIndexedPartition()) {
    520       Placement placement = getItemPlacementInSection(position);
    521       return placement.firstInSection ? type : getItemViewTypeCount() + type;
    522     } else {
    523       return type;
    524     }
    525   }
    526 
    527   @Override
    528   public boolean isEmpty() {
    529     // TODO
    530     //        if (contactsListActivity.mProviderStatus != ProviderStatus.STATUS_NORMAL) {
    531     //            return true;
    532     //        }
    533 
    534     if (!mEmptyListEnabled) {
    535       return false;
    536     } else if (isSearchMode()) {
    537       return TextUtils.isEmpty(getQueryString());
    538     } else {
    539       return super.isEmpty();
    540     }
    541   }
    542 
    543   public boolean isLoading() {
    544     int count = getPartitionCount();
    545     for (int i = 0; i < count; i++) {
    546       Partition partition = getPartition(i);
    547       if (partition instanceof DirectoryPartition && ((DirectoryPartition) partition).isLoading()) {
    548         return true;
    549       }
    550     }
    551     return false;
    552   }
    553 
    554   /** Configures visibility parameters for the directory partitions. */
    555   public void configurePartitionsVisibility(boolean isInSearchMode) {
    556     for (int i = 0; i < getPartitionCount(); i++) {
    557       setShowIfEmpty(i, false);
    558       setHasHeader(i, isInSearchMode);
    559     }
    560   }
    561 
    562   // Sets header for the default partition.
    563   private void setDefaultPartitionHeader(boolean setHeader) {
    564     // Iterate in reverse here to ensure the first DEFAULT directory has header.
    565     // Both "Suggestions" and "All Contacts" directories have DEFAULT id.
    566     int defaultPartitionIndex = -1;
    567     for (int i = getPartitionCount() - 1; i >= 0; i--) {
    568       Partition partition = getPartition(i);
    569       if (partition instanceof DirectoryPartition
    570           && ((DirectoryPartition) partition).getDirectoryId() == Directory.DEFAULT) {
    571         defaultPartitionIndex = i;
    572       }
    573     }
    574     setHasHeader(defaultPartitionIndex, setHeader);
    575   }
    576 
    577   @Override
    578   protected View newHeaderView(Context context, int partition, Cursor cursor, ViewGroup parent) {
    579     LayoutInflater inflater = LayoutInflater.from(context);
    580     View view = inflater.inflate(R.layout.directory_header, parent, false);
    581     if (!getPinnedPartitionHeadersEnabled()) {
    582       // If the headers are unpinned, there is no need for their background
    583       // color to be non-transparent. Setting this transparent reduces maintenance for
    584       // non-pinned headers. We don't need to bother synchronizing the activity's
    585       // background color with the header background color.
    586       view.setBackground(null);
    587     }
    588     return view;
    589   }
    590 
    591   protected void bindWorkProfileIcon(final ContactListItemView view, int partitionId) {
    592     final Partition partition = getPartition(partitionId);
    593     if (partition instanceof DirectoryPartition) {
    594       final DirectoryPartition directoryPartition = (DirectoryPartition) partition;
    595       final long directoryId = directoryPartition.getDirectoryId();
    596       final long userType = ContactsUtils.determineUserType(directoryId, null);
    597       view.setWorkProfileIconEnabled(userType == ContactsUtils.USER_TYPE_WORK);
    598     }
    599   }
    600 
    601   @Override
    602   protected void bindHeaderView(View view, int partitionIndex, Cursor cursor) {
    603     Partition partition = getPartition(partitionIndex);
    604     if (!(partition instanceof DirectoryPartition)) {
    605       return;
    606     }
    607 
    608     DirectoryPartition directoryPartition = (DirectoryPartition) partition;
    609     long directoryId = directoryPartition.getDirectoryId();
    610     TextView labelTextView = (TextView) view.findViewById(R.id.label);
    611     TextView displayNameTextView = (TextView) view.findViewById(R.id.display_name);
    612     labelTextView.setText(directoryPartition.getLabel());
    613     if (!DirectoryCompat.isRemoteDirectoryId(directoryId)) {
    614       displayNameTextView.setText(null);
    615     } else {
    616       String directoryName = directoryPartition.getDisplayName();
    617       String displayName =
    618           !TextUtils.isEmpty(directoryName) ? directoryName : directoryPartition.getDirectoryType();
    619       displayNameTextView.setText(displayName);
    620     }
    621 
    622     final Resources res = getContext().getResources();
    623     final int headerPaddingTop =
    624         partitionIndex == 1 && getPartition(0).isEmpty()
    625             ? 0
    626             : res.getDimensionPixelOffset(R.dimen.directory_header_extra_top_padding);
    627     // There should be no extra padding at the top of the first directory header
    628     view.setPaddingRelative(
    629         view.getPaddingStart(), headerPaddingTop, view.getPaddingEnd(), view.getPaddingBottom());
    630   }
    631 
    632   /** Checks whether the contact entry at the given position represents the user's profile. */
    633   protected boolean isUserProfile(int position) {
    634     // The profile only ever appears in the first position if it is present.  So if the position
    635     // is anything beyond 0, it can't be the profile.
    636     boolean isUserProfile = false;
    637     if (position == 0) {
    638       int partition = getPartitionForPosition(position);
    639       if (partition >= 0) {
    640         // Save the old cursor position - the call to getItem() may modify the cursor
    641         // position.
    642         int offset = getCursor(partition).getPosition();
    643         Cursor cursor = (Cursor) getItem(position);
    644         if (cursor != null) {
    645           int profileColumnIndex = cursor.getColumnIndex(Contacts.IS_USER_PROFILE);
    646           if (profileColumnIndex != -1) {
    647             isUserProfile = cursor.getInt(profileColumnIndex) == 1;
    648           }
    649           // Restore the old cursor position.
    650           cursor.moveToPosition(offset);
    651         }
    652       }
    653     }
    654     return isUserProfile;
    655   }
    656 
    657   public boolean isPhotoSupported(int partitionIndex) {
    658     Partition partition = getPartition(partitionIndex);
    659     if (partition instanceof DirectoryPartition) {
    660       return ((DirectoryPartition) partition).isPhotoSupported();
    661     }
    662     return true;
    663   }
    664 
    665   /** Returns the currently selected filter. */
    666   public ContactListFilter getFilter() {
    667     return mFilter;
    668   }
    669 
    670   public void setFilter(ContactListFilter filter) {
    671     mFilter = filter;
    672   }
    673 
    674   // TODO: move sharable logic (bindXX() methods) to here with extra arguments
    675 
    676   /**
    677    * Loads the photo for the quick contact view and assigns the contact uri.
    678    *
    679    * @param photoIdColumn Index of the photo id column
    680    * @param photoUriColumn Index of the photo uri column. Optional: Can be -1
    681    * @param contactIdColumn Index of the contact id column
    682    * @param lookUpKeyColumn Index of the lookup key column
    683    * @param displayNameColumn Index of the display name column
    684    */
    685   protected void bindQuickContact(
    686       final ContactListItemView view,
    687       int partitionIndex,
    688       Cursor cursor,
    689       int photoIdColumn,
    690       int photoUriColumn,
    691       int contactIdColumn,
    692       int lookUpKeyColumn,
    693       int displayNameColumn) {
    694     long photoId = 0;
    695     if (!cursor.isNull(photoIdColumn)) {
    696       photoId = cursor.getLong(photoIdColumn);
    697     }
    698 
    699     QuickContactBadge quickContact = view.getQuickContact();
    700     quickContact.assignContactUri(
    701         getContactUri(partitionIndex, cursor, contactIdColumn, lookUpKeyColumn));
    702     if (CompatUtils.hasPrioritizedMimeType()) {
    703       // The Contacts app never uses the QuickContactBadge. Therefore, it is safe to assume
    704       // that only Dialer will use this QuickContact badge. This means prioritizing the phone
    705       // mimetype here is reasonable.
    706       quickContact.setPrioritizedMimeType(Phone.CONTENT_ITEM_TYPE);
    707     }
    708     Logger.get(mContext)
    709         .logQuickContactOnTouch(
    710             quickContact, InteractionEvent.Type.OPEN_QUICK_CONTACT_FROM_SEARCH, true);
    711 
    712     if (photoId != 0 || photoUriColumn == -1) {
    713       getPhotoLoader().loadThumbnail(quickContact, photoId, mDarkTheme, mCircularPhotos, null);
    714     } else {
    715       final String photoUriString = cursor.getString(photoUriColumn);
    716       final Uri photoUri = photoUriString == null ? null : Uri.parse(photoUriString);
    717       DefaultImageRequest request = null;
    718       if (photoUri == null) {
    719         request = getDefaultImageRequestFromCursor(cursor, displayNameColumn, lookUpKeyColumn);
    720       }
    721       getPhotoLoader().loadPhoto(quickContact, photoUri, -1, mDarkTheme, mCircularPhotos, request);
    722     }
    723   }
    724 
    725   @Override
    726   public boolean hasStableIds() {
    727     // Whenever bindViewId() is called, the values passed into setId() are stable or
    728     // stable-ish. For example, when one contact is modified we don't expect a second
    729     // contact's Contact._ID values to change.
    730     return true;
    731   }
    732 
    733   protected void bindViewId(final ContactListItemView view, Cursor cursor, int idColumn) {
    734     // Set a semi-stable id, so that talkback won't get confused when the list gets
    735     // refreshed. There is little harm in inserting the same ID twice.
    736     long contactId = cursor.getLong(idColumn);
    737     view.setId((int) (contactId % Integer.MAX_VALUE));
    738   }
    739 
    740   protected Uri getContactUri(
    741       int partitionIndex, Cursor cursor, int contactIdColumn, int lookUpKeyColumn) {
    742     long contactId = cursor.getLong(contactIdColumn);
    743     String lookupKey = cursor.getString(lookUpKeyColumn);
    744     long directoryId = ((DirectoryPartition) getPartition(partitionIndex)).getDirectoryId();
    745     Uri uri = Contacts.getLookupUri(contactId, lookupKey);
    746     if (uri != null && directoryId != Directory.DEFAULT) {
    747       uri =
    748           uri.buildUpon()
    749               .appendQueryParameter(
    750                   ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(directoryId))
    751               .build();
    752     }
    753     return uri;
    754   }
    755 
    756   /**
    757    * Retrieves the lookup key and display name from a cursor, and returns a {@link
    758    * DefaultImageRequest} containing these contact details
    759    *
    760    * @param cursor Contacts cursor positioned at the current row to retrieve contact details for
    761    * @param displayNameColumn Column index of the display name
    762    * @param lookupKeyColumn Column index of the lookup key
    763    * @return {@link DefaultImageRequest} with the displayName and identifier fields set to the
    764    *     display name and lookup key of the contact.
    765    */
    766   public DefaultImageRequest getDefaultImageRequestFromCursor(
    767       Cursor cursor, int displayNameColumn, int lookupKeyColumn) {
    768     final String displayName = cursor.getString(displayNameColumn);
    769     final String lookupKey = cursor.getString(lookupKeyColumn);
    770     return new DefaultImageRequest(displayName, lookupKey, mCircularPhotos);
    771   }
    772 }
    773