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