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