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