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