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