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