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