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