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 
     17 package com.android.contacts.common.list;
     18 
     19 import android.app.Fragment;
     20 import android.app.LoaderManager;
     21 import android.app.LoaderManager.LoaderCallbacks;
     22 import android.content.Context;
     23 import android.content.CursorLoader;
     24 import android.content.Loader;
     25 import android.database.Cursor;
     26 import android.os.Bundle;
     27 import android.os.Handler;
     28 import android.os.Message;
     29 import android.os.Parcelable;
     30 import android.provider.ContactsContract.Directory;
     31 import android.support.annotation.Nullable;
     32 import android.text.TextUtils;
     33 import android.view.LayoutInflater;
     34 import android.view.MotionEvent;
     35 import android.view.View;
     36 import android.view.View.OnFocusChangeListener;
     37 import android.view.View.OnTouchListener;
     38 import android.view.ViewGroup;
     39 import android.view.inputmethod.InputMethodManager;
     40 import android.widget.AbsListView;
     41 import android.widget.AbsListView.OnScrollListener;
     42 import android.widget.AdapterView;
     43 import android.widget.AdapterView.OnItemClickListener;
     44 import android.widget.ListView;
     45 import com.android.common.widget.CompositeCursorAdapter.Partition;
     46 import com.android.contacts.common.ContactPhotoManager;
     47 import com.android.contacts.common.preference.ContactsPreferences;
     48 import com.android.contacts.common.util.ContactListViewUtils;
     49 import com.android.dialer.common.LogUtil;
     50 import com.android.dialer.performancereport.PerformanceReport;
     51 import java.lang.ref.WeakReference;
     52 import java.util.Locale;
     53 
     54 /** Common base class for various contact-related list fragments. */
     55 public abstract class ContactEntryListFragment<T extends ContactEntryListAdapter> extends Fragment
     56     implements OnItemClickListener,
     57         OnScrollListener,
     58         OnFocusChangeListener,
     59         OnTouchListener,
     60         LoaderCallbacks<Cursor> {
     61   private static final String KEY_LIST_STATE = "liststate";
     62   private static final String KEY_SECTION_HEADER_DISPLAY_ENABLED = "sectionHeaderDisplayEnabled";
     63   private static final String KEY_PHOTO_LOADER_ENABLED = "photoLoaderEnabled";
     64   private static final String KEY_QUICK_CONTACT_ENABLED = "quickContactEnabled";
     65   private static final String KEY_ADJUST_SELECTION_BOUNDS_ENABLED = "adjustSelectionBoundsEnabled";
     66   private static final String KEY_INCLUDE_PROFILE = "includeProfile";
     67   private static final String KEY_SEARCH_MODE = "searchMode";
     68   private static final String KEY_VISIBLE_SCROLLBAR_ENABLED = "visibleScrollbarEnabled";
     69   private static final String KEY_SCROLLBAR_POSITION = "scrollbarPosition";
     70   private static final String KEY_QUERY_STRING = "queryString";
     71   private static final String KEY_DIRECTORY_SEARCH_MODE = "directorySearchMode";
     72   private static final String KEY_SELECTION_VISIBLE = "selectionVisible";
     73   private static final String KEY_DARK_THEME = "darkTheme";
     74   private static final String KEY_LEGACY_COMPATIBILITY = "legacyCompatibility";
     75   private static final String KEY_DIRECTORY_RESULT_LIMIT = "directoryResultLimit";
     76 
     77   private static final String DIRECTORY_ID_ARG_KEY = "directoryId";
     78 
     79   private static final int DIRECTORY_LOADER_ID = -1;
     80 
     81   private static final int DIRECTORY_SEARCH_DELAY_MILLIS = 300;
     82   private static final int DIRECTORY_SEARCH_MESSAGE = 1;
     83 
     84   private static final int DEFAULT_DIRECTORY_RESULT_LIMIT = 20;
     85   private static final int STATUS_NOT_LOADED = 0;
     86   private static final int STATUS_LOADING = 1;
     87   private static final int STATUS_LOADED = 2;
     88   protected boolean mUserProfileExists;
     89   private boolean mSectionHeaderDisplayEnabled;
     90   private boolean mPhotoLoaderEnabled;
     91   private boolean mQuickContactEnabled = true;
     92   private boolean mAdjustSelectionBoundsEnabled = true;
     93   private boolean mIncludeProfile;
     94   private boolean mSearchMode;
     95   private boolean mVisibleScrollbarEnabled;
     96   private boolean mShowEmptyListForEmptyQuery;
     97   private int mVerticalScrollbarPosition = getDefaultVerticalScrollbarPosition();
     98   private String mQueryString;
     99   private int mDirectorySearchMode = DirectoryListLoader.SEARCH_MODE_NONE;
    100   private boolean mSelectionVisible;
    101   private boolean mLegacyCompatibility;
    102   private boolean mEnabled = true;
    103   private T mAdapter;
    104   private View mView;
    105   private ListView mListView;
    106   /** Used to save the scrolling state of the list when the fragment is not recreated. */
    107   private int mListViewTopIndex;
    108 
    109   private int mListViewTopOffset;
    110   /** Used for keeping track of the scroll state of the list. */
    111   private Parcelable mListState;
    112 
    113   private int mDisplayOrder;
    114   private int mSortOrder;
    115   private int mDirectoryResultLimit = DEFAULT_DIRECTORY_RESULT_LIMIT;
    116   private ContactPhotoManager mPhotoManager;
    117   private ContactsPreferences mContactsPrefs;
    118   private boolean mForceLoad;
    119   private boolean mDarkTheme;
    120   private int mDirectoryListStatus = STATUS_NOT_LOADED;
    121 
    122   /**
    123    * Indicates whether we are doing the initial complete load of data (false) or a refresh caused by
    124    * a change notification (true)
    125    */
    126   private boolean mLoadPriorityDirectoriesOnly;
    127 
    128   private Context mContext;
    129 
    130   private LoaderManager mLoaderManager;
    131 
    132   private Handler mDelayedDirectorySearchHandler;
    133 
    134   private static class DelayedDirectorySearchHandler extends Handler {
    135     private final WeakReference<ContactEntryListFragment<?>> contactEntryListFragmentRef;
    136 
    137     private DelayedDirectorySearchHandler(ContactEntryListFragment<?> contactEntryListFragment) {
    138       this.contactEntryListFragmentRef = new WeakReference<>(contactEntryListFragment);
    139     }
    140 
    141     @Override
    142     public void handleMessage(Message msg) {
    143       ContactEntryListFragment<?> contactEntryListFragment = contactEntryListFragmentRef.get();
    144       if (contactEntryListFragment == null) {
    145         return;
    146       }
    147       if (msg.what == DIRECTORY_SEARCH_MESSAGE) {
    148         contactEntryListFragment.loadDirectoryPartition(msg.arg1, (DirectoryPartition) msg.obj);
    149       }
    150     }
    151   }
    152 
    153   private ContactsPreferences.ChangeListener mPreferencesChangeListener =
    154       new ContactsPreferences.ChangeListener() {
    155         @Override
    156         public void onChange() {
    157           loadPreferences();
    158           reloadData();
    159         }
    160       };
    161 
    162   protected ContactEntryListFragment() {
    163     mDelayedDirectorySearchHandler = new DelayedDirectorySearchHandler(this);
    164   }
    165 
    166   protected abstract View inflateView(LayoutInflater inflater, ViewGroup container);
    167 
    168   protected abstract T createListAdapter();
    169 
    170   /**
    171    * @param position Please note that the position is already adjusted for header views, so "0"
    172    *     means the first list item below header views.
    173    */
    174   protected abstract void onItemClick(int position, long id);
    175 
    176   @Override
    177   public void onAttach(Context context) {
    178     super.onAttach(context);
    179     setContext(context);
    180     setLoaderManager(super.getLoaderManager());
    181   }
    182 
    183   @Override
    184   public Context getContext() {
    185     return mContext;
    186   }
    187 
    188   /** Sets a context for the fragment in the unit test environment. */
    189   public void setContext(Context context) {
    190     mContext = context;
    191     configurePhotoLoader();
    192   }
    193 
    194   public void setEnabled(boolean enabled) {
    195     if (mEnabled != enabled) {
    196       mEnabled = enabled;
    197       if (mAdapter != null) {
    198         if (mEnabled) {
    199           reloadData();
    200         } else {
    201           mAdapter.clearPartitions();
    202         }
    203       }
    204     }
    205   }
    206 
    207   @Override
    208   public LoaderManager getLoaderManager() {
    209     return mLoaderManager;
    210   }
    211 
    212   /** Overrides a loader manager for use in unit tests. */
    213   public void setLoaderManager(LoaderManager loaderManager) {
    214     mLoaderManager = loaderManager;
    215   }
    216 
    217   public T getAdapter() {
    218     return mAdapter;
    219   }
    220 
    221   @Override
    222   public View getView() {
    223     return mView;
    224   }
    225 
    226   public ListView getListView() {
    227     return mListView;
    228   }
    229 
    230   @Override
    231   public void onSaveInstanceState(Bundle outState) {
    232     super.onSaveInstanceState(outState);
    233     outState.putBoolean(KEY_SECTION_HEADER_DISPLAY_ENABLED, mSectionHeaderDisplayEnabled);
    234     outState.putBoolean(KEY_PHOTO_LOADER_ENABLED, mPhotoLoaderEnabled);
    235     outState.putBoolean(KEY_QUICK_CONTACT_ENABLED, mQuickContactEnabled);
    236     outState.putBoolean(KEY_ADJUST_SELECTION_BOUNDS_ENABLED, mAdjustSelectionBoundsEnabled);
    237     outState.putBoolean(KEY_INCLUDE_PROFILE, mIncludeProfile);
    238     outState.putBoolean(KEY_SEARCH_MODE, mSearchMode);
    239     outState.putBoolean(KEY_VISIBLE_SCROLLBAR_ENABLED, mVisibleScrollbarEnabled);
    240     outState.putInt(KEY_SCROLLBAR_POSITION, mVerticalScrollbarPosition);
    241     outState.putInt(KEY_DIRECTORY_SEARCH_MODE, mDirectorySearchMode);
    242     outState.putBoolean(KEY_SELECTION_VISIBLE, mSelectionVisible);
    243     outState.putBoolean(KEY_LEGACY_COMPATIBILITY, mLegacyCompatibility);
    244     outState.putString(KEY_QUERY_STRING, mQueryString);
    245     outState.putInt(KEY_DIRECTORY_RESULT_LIMIT, mDirectoryResultLimit);
    246     outState.putBoolean(KEY_DARK_THEME, mDarkTheme);
    247 
    248     if (mListView != null) {
    249       outState.putParcelable(KEY_LIST_STATE, mListView.onSaveInstanceState());
    250     }
    251   }
    252 
    253   @Override
    254   public void onCreate(Bundle savedState) {
    255     super.onCreate(savedState);
    256     restoreSavedState(savedState);
    257     mAdapter = createListAdapter();
    258     mContactsPrefs = new ContactsPreferences(mContext);
    259   }
    260 
    261   public void restoreSavedState(Bundle savedState) {
    262     if (savedState == null) {
    263       return;
    264     }
    265 
    266     mSectionHeaderDisplayEnabled = savedState.getBoolean(KEY_SECTION_HEADER_DISPLAY_ENABLED);
    267     mPhotoLoaderEnabled = savedState.getBoolean(KEY_PHOTO_LOADER_ENABLED);
    268     mQuickContactEnabled = savedState.getBoolean(KEY_QUICK_CONTACT_ENABLED);
    269     mAdjustSelectionBoundsEnabled = savedState.getBoolean(KEY_ADJUST_SELECTION_BOUNDS_ENABLED);
    270     mIncludeProfile = savedState.getBoolean(KEY_INCLUDE_PROFILE);
    271     mSearchMode = savedState.getBoolean(KEY_SEARCH_MODE);
    272     mVisibleScrollbarEnabled = savedState.getBoolean(KEY_VISIBLE_SCROLLBAR_ENABLED);
    273     mVerticalScrollbarPosition = savedState.getInt(KEY_SCROLLBAR_POSITION);
    274     mDirectorySearchMode = savedState.getInt(KEY_DIRECTORY_SEARCH_MODE);
    275     mSelectionVisible = savedState.getBoolean(KEY_SELECTION_VISIBLE);
    276     mLegacyCompatibility = savedState.getBoolean(KEY_LEGACY_COMPATIBILITY);
    277     mQueryString = savedState.getString(KEY_QUERY_STRING);
    278     mDirectoryResultLimit = savedState.getInt(KEY_DIRECTORY_RESULT_LIMIT);
    279     mDarkTheme = savedState.getBoolean(KEY_DARK_THEME);
    280 
    281     // Retrieve list state. This will be applied in onLoadFinished
    282     mListState = savedState.getParcelable(KEY_LIST_STATE);
    283   }
    284 
    285   @Override
    286   public void onStart() {
    287     super.onStart();
    288 
    289     mContactsPrefs.registerChangeListener(mPreferencesChangeListener);
    290 
    291     mForceLoad = loadPreferences();
    292 
    293     mDirectoryListStatus = STATUS_NOT_LOADED;
    294     mLoadPriorityDirectoriesOnly = true;
    295 
    296     startLoading();
    297   }
    298 
    299   protected void startLoading() {
    300     if (mAdapter == null) {
    301       // The method was called before the fragment was started
    302       return;
    303     }
    304 
    305     configureAdapter();
    306     int partitionCount = mAdapter.getPartitionCount();
    307     for (int i = 0; i < partitionCount; i++) {
    308       Partition partition = mAdapter.getPartition(i);
    309       if (partition instanceof DirectoryPartition) {
    310         DirectoryPartition directoryPartition = (DirectoryPartition) partition;
    311         if (directoryPartition.getStatus() == DirectoryPartition.STATUS_NOT_LOADED) {
    312           if (directoryPartition.isPriorityDirectory() || !mLoadPriorityDirectoriesOnly) {
    313             startLoadingDirectoryPartition(i);
    314           }
    315         }
    316       } else {
    317         getLoaderManager().initLoader(i, null, this);
    318       }
    319     }
    320 
    321     // Next time this method is called, we should start loading non-priority directories
    322     mLoadPriorityDirectoriesOnly = false;
    323   }
    324 
    325   @Override
    326   public Loader<Cursor> onCreateLoader(int id, Bundle args) {
    327     if (id == DIRECTORY_LOADER_ID) {
    328       DirectoryListLoader loader = new DirectoryListLoader(mContext);
    329       loader.setDirectorySearchMode(mAdapter.getDirectorySearchMode());
    330       loader.setLocalInvisibleDirectoryEnabled(
    331           ContactEntryListAdapter.LOCAL_INVISIBLE_DIRECTORY_ENABLED);
    332       return loader;
    333     } else {
    334       CursorLoader loader = createCursorLoader(mContext);
    335       long directoryId =
    336           args != null && args.containsKey(DIRECTORY_ID_ARG_KEY)
    337               ? args.getLong(DIRECTORY_ID_ARG_KEY)
    338               : Directory.DEFAULT;
    339       mAdapter.configureLoader(loader, directoryId);
    340       return loader;
    341     }
    342   }
    343 
    344   public CursorLoader createCursorLoader(Context context) {
    345     return new CursorLoader(context, null, null, null, null, null) {
    346       @Override
    347       protected Cursor onLoadInBackground() {
    348         try {
    349           return super.onLoadInBackground();
    350         } catch (RuntimeException e) {
    351           // We don't even know what the projection should be, so no point trying to
    352           // return an empty MatrixCursor with the correct projection here.
    353           LogUtil.w(
    354               "ContactEntryListFragment.onLoadInBackground",
    355               "RuntimeException while trying to query ContactsProvider.");
    356           return null;
    357         }
    358       }
    359     };
    360   }
    361 
    362   private void startLoadingDirectoryPartition(int partitionIndex) {
    363     DirectoryPartition partition = (DirectoryPartition) mAdapter.getPartition(partitionIndex);
    364     partition.setStatus(DirectoryPartition.STATUS_LOADING);
    365     long directoryId = partition.getDirectoryId();
    366     if (mForceLoad) {
    367       if (directoryId == Directory.DEFAULT) {
    368         loadDirectoryPartition(partitionIndex, partition);
    369       } else {
    370         loadDirectoryPartitionDelayed(partitionIndex, partition);
    371       }
    372     } else {
    373       Bundle args = new Bundle();
    374       args.putLong(DIRECTORY_ID_ARG_KEY, directoryId);
    375       getLoaderManager().initLoader(partitionIndex, args, this);
    376     }
    377   }
    378 
    379   /**
    380    * Queues up a delayed request to search the specified directory. Since directory search will
    381    * likely introduce a lot of network traffic, we want to wait for a pause in the user's typing
    382    * before sending a directory request.
    383    */
    384   private void loadDirectoryPartitionDelayed(int partitionIndex, DirectoryPartition partition) {
    385     mDelayedDirectorySearchHandler.removeMessages(DIRECTORY_SEARCH_MESSAGE, partition);
    386     Message msg =
    387         mDelayedDirectorySearchHandler.obtainMessage(
    388             DIRECTORY_SEARCH_MESSAGE, partitionIndex, 0, partition);
    389     mDelayedDirectorySearchHandler.sendMessageDelayed(msg, DIRECTORY_SEARCH_DELAY_MILLIS);
    390   }
    391 
    392   /** Loads the directory partition. */
    393   protected void loadDirectoryPartition(int partitionIndex, DirectoryPartition partition) {
    394     Bundle args = new Bundle();
    395     args.putLong(DIRECTORY_ID_ARG_KEY, partition.getDirectoryId());
    396     getLoaderManager().restartLoader(partitionIndex, args, this);
    397   }
    398 
    399   /** Cancels all queued directory loading requests. */
    400   private void removePendingDirectorySearchRequests() {
    401     mDelayedDirectorySearchHandler.removeMessages(DIRECTORY_SEARCH_MESSAGE);
    402   }
    403 
    404   @Override
    405   public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
    406     if (!mEnabled) {
    407       return;
    408     }
    409 
    410     int loaderId = loader.getId();
    411     if (loaderId == DIRECTORY_LOADER_ID) {
    412       mDirectoryListStatus = STATUS_LOADED;
    413       mAdapter.changeDirectories(data);
    414       startLoading();
    415     } else {
    416       onPartitionLoaded(loaderId, data);
    417       if (isSearchMode()) {
    418         int directorySearchMode = getDirectorySearchMode();
    419         if (directorySearchMode != DirectoryListLoader.SEARCH_MODE_NONE) {
    420           if (mDirectoryListStatus == STATUS_NOT_LOADED) {
    421             mDirectoryListStatus = STATUS_LOADING;
    422             getLoaderManager().initLoader(DIRECTORY_LOADER_ID, null, this);
    423           } else {
    424             startLoading();
    425           }
    426         }
    427       } else {
    428         mDirectoryListStatus = STATUS_NOT_LOADED;
    429         getLoaderManager().destroyLoader(DIRECTORY_LOADER_ID);
    430       }
    431     }
    432   }
    433 
    434   @Override
    435   public void onLoaderReset(Loader<Cursor> loader) {}
    436 
    437   protected void onPartitionLoaded(int partitionIndex, Cursor data) {
    438     if (partitionIndex >= mAdapter.getPartitionCount()) {
    439       // When we get unsolicited data, ignore it.  This could happen
    440       // when we are switching from search mode to the default mode.
    441       return;
    442     }
    443 
    444     // Return for non-"Suggestions" if on the zero-suggest screen.
    445     if (TextUtils.isEmpty(mQueryString) && partitionIndex > 0) {
    446       return;
    447     }
    448 
    449     mAdapter.changeCursor(partitionIndex, data);
    450     setProfileHeader();
    451 
    452     if (!isLoading()) {
    453       completeRestoreInstanceState();
    454     }
    455   }
    456 
    457   public boolean isLoading() {
    458     //noinspection SimplifiableIfStatement
    459     if (mAdapter != null && mAdapter.isLoading()) {
    460       return true;
    461     }
    462 
    463     return isLoadingDirectoryList();
    464 
    465   }
    466 
    467   public boolean isLoadingDirectoryList() {
    468     return isSearchMode()
    469         && getDirectorySearchMode() != DirectoryListLoader.SEARCH_MODE_NONE
    470         && (mDirectoryListStatus == STATUS_NOT_LOADED || mDirectoryListStatus == STATUS_LOADING);
    471   }
    472 
    473   @Override
    474   public void onStop() {
    475     super.onStop();
    476     mContactsPrefs.unregisterChangeListener();
    477     mAdapter.clearPartitions();
    478   }
    479 
    480   protected void reloadData() {
    481     removePendingDirectorySearchRequests();
    482     mAdapter.onDataReload();
    483     mLoadPriorityDirectoriesOnly = true;
    484     mForceLoad = true;
    485     startLoading();
    486   }
    487 
    488   /**
    489    * Shows a view at the top of the list with a pseudo local profile prompting the user to add a
    490    * local profile. Default implementation does nothing.
    491    */
    492   protected void setProfileHeader() {
    493     mUserProfileExists = false;
    494   }
    495 
    496   /** Provides logic that dismisses this fragment. The default implementation does nothing. */
    497   protected void finish() {}
    498 
    499   public boolean isSectionHeaderDisplayEnabled() {
    500     return mSectionHeaderDisplayEnabled;
    501   }
    502 
    503   public void setSectionHeaderDisplayEnabled(boolean flag) {
    504     if (mSectionHeaderDisplayEnabled != flag) {
    505       mSectionHeaderDisplayEnabled = flag;
    506       if (mAdapter != null) {
    507         mAdapter.setSectionHeaderDisplayEnabled(flag);
    508       }
    509       configureVerticalScrollbar();
    510     }
    511   }
    512 
    513   public boolean isVisibleScrollbarEnabled() {
    514     return mVisibleScrollbarEnabled;
    515   }
    516 
    517   public void setVisibleScrollbarEnabled(boolean flag) {
    518     if (mVisibleScrollbarEnabled != flag) {
    519       mVisibleScrollbarEnabled = flag;
    520       configureVerticalScrollbar();
    521     }
    522   }
    523 
    524   private void configureVerticalScrollbar() {
    525     boolean hasScrollbar = isVisibleScrollbarEnabled() && isSectionHeaderDisplayEnabled();
    526 
    527     if (mListView != null) {
    528       mListView.setFastScrollEnabled(hasScrollbar);
    529       mListView.setVerticalScrollbarPosition(mVerticalScrollbarPosition);
    530       mListView.setScrollBarStyle(ListView.SCROLLBARS_OUTSIDE_OVERLAY);
    531     }
    532   }
    533 
    534   public boolean isPhotoLoaderEnabled() {
    535     return mPhotoLoaderEnabled;
    536   }
    537 
    538   public void setPhotoLoaderEnabled(boolean flag) {
    539     mPhotoLoaderEnabled = flag;
    540     configurePhotoLoader();
    541   }
    542 
    543   public void setQuickContactEnabled(boolean flag) {
    544     this.mQuickContactEnabled = flag;
    545   }
    546 
    547   public void setAdjustSelectionBoundsEnabled(boolean flag) {
    548     mAdjustSelectionBoundsEnabled = flag;
    549   }
    550 
    551   public final boolean isSearchMode() {
    552     return mSearchMode;
    553   }
    554 
    555   /**
    556    * Enter/exit search mode. This is method is tightly related to the current query, and should only
    557    * be called by {@link #setQueryString}.
    558    *
    559    * <p>Also note this method doesn't call {@link #reloadData()}; {@link #setQueryString} does it.
    560    */
    561   protected void setSearchMode(boolean flag) {
    562     if (mSearchMode != flag) {
    563       mSearchMode = flag;
    564 
    565       if (!flag) {
    566         mDirectoryListStatus = STATUS_NOT_LOADED;
    567         getLoaderManager().destroyLoader(DIRECTORY_LOADER_ID);
    568       }
    569 
    570       if (mAdapter != null) {
    571         mAdapter.setSearchMode(flag);
    572 
    573         mAdapter.clearPartitions();
    574         if (!flag) {
    575           // If we are switching from search to regular display, remove all directory
    576           // partitions after default one, assuming they are remote directories which
    577           // should be cleaned up on exiting the search mode.
    578           mAdapter.removeDirectoriesAfterDefault();
    579         }
    580         mAdapter.configurePartitionsVisibility(flag);
    581       }
    582 
    583       if (mListView != null) {
    584         mListView.setFastScrollEnabled(!flag);
    585       }
    586     }
    587   }
    588 
    589   @Nullable
    590   public final String getQueryString() {
    591     return mQueryString;
    592   }
    593 
    594   public void setQueryString(String queryString) {
    595     if (!TextUtils.equals(mQueryString, queryString)) {
    596       if (mShowEmptyListForEmptyQuery && mAdapter != null && mListView != null) {
    597         if (TextUtils.isEmpty(mQueryString)) {
    598           // Restore the adapter if the query used to be empty.
    599           mListView.setAdapter(mAdapter);
    600         } else if (TextUtils.isEmpty(queryString)) {
    601           // Instantly clear the list view if the new query is empty.
    602           mListView.setAdapter(null);
    603         }
    604       }
    605 
    606       mQueryString = queryString;
    607       setSearchMode(!TextUtils.isEmpty(mQueryString) || mShowEmptyListForEmptyQuery);
    608 
    609       if (mAdapter != null) {
    610         mAdapter.setQueryString(queryString);
    611         reloadData();
    612       }
    613     }
    614   }
    615 
    616   public void setShowEmptyListForNullQuery(boolean show) {
    617     mShowEmptyListForEmptyQuery = show;
    618   }
    619 
    620   public boolean getShowEmptyListForNullQuery() {
    621     return mShowEmptyListForEmptyQuery;
    622   }
    623 
    624   public int getDirectoryLoaderId() {
    625     return DIRECTORY_LOADER_ID;
    626   }
    627 
    628   public int getDirectorySearchMode() {
    629     return mDirectorySearchMode;
    630   }
    631 
    632   public void setDirectorySearchMode(int mode) {
    633     mDirectorySearchMode = mode;
    634   }
    635 
    636   protected int getContactNameDisplayOrder() {
    637     return mDisplayOrder;
    638   }
    639 
    640   protected void setContactNameDisplayOrder(int displayOrder) {
    641     mDisplayOrder = displayOrder;
    642     if (mAdapter != null) {
    643       mAdapter.setContactNameDisplayOrder(displayOrder);
    644     }
    645   }
    646 
    647   public int getSortOrder() {
    648     return mSortOrder;
    649   }
    650 
    651   public void setSortOrder(int sortOrder) {
    652     mSortOrder = sortOrder;
    653     if (mAdapter != null) {
    654       mAdapter.setSortOrder(sortOrder);
    655     }
    656   }
    657 
    658   public void setDirectoryResultLimit(int limit) {
    659     mDirectoryResultLimit = limit;
    660   }
    661 
    662   protected boolean loadPreferences() {
    663     boolean changed = false;
    664     if (getContactNameDisplayOrder() != mContactsPrefs.getDisplayOrder()) {
    665       setContactNameDisplayOrder(mContactsPrefs.getDisplayOrder());
    666       changed = true;
    667     }
    668 
    669     if (getSortOrder() != mContactsPrefs.getSortOrder()) {
    670       setSortOrder(mContactsPrefs.getSortOrder());
    671       changed = true;
    672     }
    673 
    674     return changed;
    675   }
    676 
    677   @Override
    678   public View onCreateView(
    679       LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
    680     onCreateView(inflater, container);
    681 
    682     boolean searchMode = isSearchMode();
    683     mAdapter.setSearchMode(searchMode);
    684     mAdapter.configurePartitionsVisibility(searchMode);
    685     mAdapter.setPhotoLoader(mPhotoManager);
    686     mListView.setAdapter(mAdapter);
    687     return mView;
    688   }
    689 
    690   protected void onCreateView(LayoutInflater inflater, ViewGroup container) {
    691     mView = inflateView(inflater, container);
    692 
    693     mListView = mView.findViewById(android.R.id.list);
    694     if (mListView == null) {
    695       throw new RuntimeException(
    696           "Your content must have a ListView whose id attribute is " + "'android.R.id.list'");
    697     }
    698 
    699     View emptyView = mView.findViewById(android.R.id.empty);
    700     if (emptyView != null) {
    701       mListView.setEmptyView(emptyView);
    702     }
    703 
    704     mListView.setOnItemClickListener(this);
    705     mListView.setOnFocusChangeListener(this);
    706     mListView.setOnTouchListener(this);
    707     mListView.setFastScrollEnabled(!isSearchMode());
    708 
    709     // Tell list view to not show dividers. We'll do it ourself so that we can *not* show
    710     // them when an A-Z headers is visible.
    711     mListView.setDividerHeight(0);
    712 
    713     // We manually save/restore the listview state
    714     mListView.setSaveEnabled(false);
    715 
    716     configureVerticalScrollbar();
    717     configurePhotoLoader();
    718 
    719     getAdapter().setFragmentRootView(getView());
    720 
    721     ContactListViewUtils.applyCardPaddingToView(getResources(), mListView, mView);
    722   }
    723 
    724   @Override
    725   public void onHiddenChanged(boolean hidden) {
    726     super.onHiddenChanged(hidden);
    727     if (getActivity() != null && getView() != null && !hidden) {
    728       // If the padding was last applied when in a hidden state, it may have been applied
    729       // incorrectly. Therefore we need to reapply it.
    730       ContactListViewUtils.applyCardPaddingToView(getResources(), mListView, getView());
    731     }
    732   }
    733 
    734   protected void configurePhotoLoader() {
    735     if (isPhotoLoaderEnabled() && mContext != null) {
    736       if (mPhotoManager == null) {
    737         mPhotoManager = ContactPhotoManager.getInstance(mContext);
    738       }
    739       if (mListView != null) {
    740         mListView.setOnScrollListener(this);
    741       }
    742       if (mAdapter != null) {
    743         mAdapter.setPhotoLoader(mPhotoManager);
    744       }
    745     }
    746   }
    747 
    748   protected void configureAdapter() {
    749     if (mAdapter == null) {
    750       return;
    751     }
    752 
    753     mAdapter.setQuickContactEnabled(mQuickContactEnabled);
    754     mAdapter.setAdjustSelectionBoundsEnabled(mAdjustSelectionBoundsEnabled);
    755     mAdapter.setQueryString(mQueryString);
    756     mAdapter.setDirectorySearchMode(mDirectorySearchMode);
    757     mAdapter.setPinnedPartitionHeadersEnabled(false);
    758     mAdapter.setContactNameDisplayOrder(mDisplayOrder);
    759     mAdapter.setSortOrder(mSortOrder);
    760     mAdapter.setSectionHeaderDisplayEnabled(mSectionHeaderDisplayEnabled);
    761     mAdapter.setSelectionVisible(mSelectionVisible);
    762     mAdapter.setDirectoryResultLimit(mDirectoryResultLimit);
    763     mAdapter.setDarkTheme(mDarkTheme);
    764   }
    765 
    766   @Override
    767   public void onScroll(
    768       AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {}
    769 
    770   @Override
    771   public void onScrollStateChanged(AbsListView view, int scrollState) {
    772     PerformanceReport.recordScrollStateChange(scrollState);
    773     if (scrollState == OnScrollListener.SCROLL_STATE_FLING) {
    774       mPhotoManager.pause();
    775     } else if (isPhotoLoaderEnabled()) {
    776       mPhotoManager.resume();
    777     }
    778   }
    779 
    780   @Override
    781   public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
    782     hideSoftKeyboard();
    783 
    784     int adjPosition = position - mListView.getHeaderViewsCount();
    785     if (adjPosition >= 0) {
    786       onItemClick(adjPosition, id);
    787     }
    788   }
    789 
    790   private void hideSoftKeyboard() {
    791     // Hide soft keyboard, if visible
    792     InputMethodManager inputMethodManager =
    793         (InputMethodManager) mContext.getSystemService(Context.INPUT_METHOD_SERVICE);
    794     inputMethodManager.hideSoftInputFromWindow(mListView.getWindowToken(), 0);
    795   }
    796 
    797   /** Dismisses the soft keyboard when the list takes focus. */
    798   @Override
    799   public void onFocusChange(View view, boolean hasFocus) {
    800     if (view == mListView && hasFocus) {
    801       hideSoftKeyboard();
    802     }
    803   }
    804 
    805   /** Dismisses the soft keyboard when the list is touched. */
    806   @Override
    807   public boolean onTouch(View view, MotionEvent event) {
    808     if (view == mListView) {
    809       hideSoftKeyboard();
    810     }
    811     return false;
    812   }
    813 
    814   @Override
    815   public void onPause() {
    816     // Save the scrolling state of the list view
    817     mListViewTopIndex = mListView.getFirstVisiblePosition();
    818     View v = mListView.getChildAt(0);
    819     mListViewTopOffset = (v == null) ? 0 : (v.getTop() - mListView.getPaddingTop());
    820 
    821     super.onPause();
    822     removePendingDirectorySearchRequests();
    823   }
    824 
    825   @Override
    826   public void onResume() {
    827     super.onResume();
    828     // Restore the selection of the list view. See b/19982820.
    829     // This has to be done manually because if the list view has its emptyView set,
    830     // the scrolling state will be reset when clearPartitions() is called on the adapter.
    831     mListView.setSelectionFromTop(mListViewTopIndex, mListViewTopOffset);
    832   }
    833 
    834   /** Restore the list state after the adapter is populated. */
    835   protected void completeRestoreInstanceState() {
    836     if (mListState != null) {
    837       mListView.onRestoreInstanceState(mListState);
    838       mListState = null;
    839     }
    840   }
    841 
    842   public void setDarkTheme(boolean value) {
    843     mDarkTheme = value;
    844     if (mAdapter != null) {
    845       mAdapter.setDarkTheme(value);
    846     }
    847   }
    848 
    849   private int getDefaultVerticalScrollbarPosition() {
    850     final Locale locale = Locale.getDefault();
    851     final int layoutDirection = TextUtils.getLayoutDirectionFromLocale(locale);
    852     switch (layoutDirection) {
    853       case View.LAYOUT_DIRECTION_RTL:
    854         return View.SCROLLBAR_POSITION_LEFT;
    855       case View.LAYOUT_DIRECTION_LTR:
    856       default:
    857         return View.SCROLLBAR_POSITION_RIGHT;
    858     }
    859   }
    860 }
    861