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