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