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