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