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