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