• 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.common.list;
18 
19 import android.app.Activity;
20 import android.app.Fragment;
21 import android.app.LoaderManager;
22 import android.app.LoaderManager.LoaderCallbacks;
23 import android.content.Context;
24 import android.content.CursorLoader;
25 import android.content.Intent;
26 import android.content.Loader;
27 import android.database.Cursor;
28 import android.os.Bundle;
29 import android.os.Handler;
30 import android.os.Message;
31 import android.os.Parcelable;
32 import android.provider.ContactsContract.Directory;
33 import android.text.TextUtils;
34 import android.view.LayoutInflater;
35 import android.view.MotionEvent;
36 import android.view.View;
37 import android.view.View.OnFocusChangeListener;
38 import android.view.View.OnTouchListener;
39 import android.view.ViewGroup;
40 import android.view.inputmethod.InputMethodManager;
41 import android.widget.AbsListView;
42 import android.widget.AbsListView.OnScrollListener;
43 import android.widget.AdapterView;
44 import android.widget.AdapterView.OnItemClickListener;
45 import android.widget.ListView;
46 
47 import com.android.common.widget.CompositeCursorAdapter.Partition;
48 import com.android.contacts.common.ContactPhotoManager;
49 import com.android.contacts.common.R;
50 import com.android.contacts.common.preference.ContactsPreferences;
51 
52 import java.util.Locale;
53 
54 /**
55  * Common base class for various contact-related list fragments.
56  */
57 public 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 
inflateView(LayoutInflater inflater, ViewGroup container)154     protected abstract View inflateView(LayoutInflater inflater, ViewGroup container);
createListAdapter()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      */
onItemClick(int position, long id)162     protected abstract void onItemClick(int position, long id);
163 
164     @Override
onAttach(Activity activity)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      */
setContext(Context context)174     public void setContext(Context context) {
175         mContext = context;
176         configurePhotoLoader();
177     }
178 
getContext()179     public Context getContext() {
180         return mContext;
181     }
182 
setEnabled(boolean enabled)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      */
setLoaderManager(LoaderManager loaderManager)199     public void setLoaderManager(LoaderManager loaderManager) {
200         mLoaderManager = loaderManager;
201     }
202 
203     @Override
getLoaderManager()204     public LoaderManager getLoaderManager() {
205         return mLoaderManager;
206     }
207 
getAdapter()208     public T getAdapter() {
209         return mAdapter;
210     }
211 
212     @Override
getView()213     public View getView() {
214         return mView;
215     }
216 
getListView()217     public ListView getListView() {
218         return mListView;
219     }
220 
221     @Override
onSaveInstanceState(Bundle outState)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
onCreate(Bundle savedState)244     public void onCreate(Bundle savedState) {
245         super.onCreate(savedState);
246         mContactsPrefs = new ContactsPreferences(mContext);
247         restoreSavedState(savedState);
248     }
249 
restoreSavedState(Bundle savedState)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
onStart()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 
startLoading()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
onCreateLoader(int id, Bundle args)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 
createCursorLoader()331     public CursorLoader createCursorLoader() {
332         return new CursorLoader(mContext, null, null, null, null, null);
333     }
334 
startLoadingDirectoryPartition(int partitionIndex)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      */
loadDirectoryPartitionDelayed(int partitionIndex, DirectoryPartition partition)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      */
loadDirectoryPartition(int partitionIndex, DirectoryPartition partition)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      */
removePendingDirectorySearchRequests()376     private void removePendingDirectorySearchRequests() {
377         mDelayedDirectorySearchHandler.removeMessages(DIRECTORY_SEARCH_MESSAGE);
378     }
379 
380     @Override
onLoadFinished(Loader<Cursor> loader, Cursor data)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 
onLoaderReset(Loader<Cursor> loader)410     public void onLoaderReset(Loader<Cursor> loader) {
411     }
412 
onPartitionLoaded(int partitionIndex, Cursor data)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 
isLoading()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 
isLoadingDirectoryList()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
onStop()448     public void onStop() {
449         super.onStop();
450         mContactsPrefs.unregisterChangeListener();
451         mAdapter.clearPartitions();
452     }
453 
reloadData()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      */
showCount(int partitionIndex, Cursor data)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      */
setProfileHeader()473     protected void setProfileHeader() {
474         mUserProfileExists = false;
475     }
476 
477     /**
478      * Provides logic that dismisses this fragment. The default implementation
479      * does nothing.
480      */
finish()481     protected void finish() {
482     }
483 
setSectionHeaderDisplayEnabled(boolean flag)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 
isSectionHeaderDisplayEnabled()494     public boolean isSectionHeaderDisplayEnabled() {
495         return mSectionHeaderDisplayEnabled;
496     }
497 
setVisibleScrollbarEnabled(boolean flag)498     public void setVisibleScrollbarEnabled(boolean flag) {
499         if (mVisibleScrollbarEnabled != flag) {
500             mVisibleScrollbarEnabled = flag;
501             configureVerticalScrollbar();
502         }
503     }
504 
isVisibleScrollbarEnabled()505     public boolean isVisibleScrollbarEnabled() {
506         return mVisibleScrollbarEnabled;
507     }
508 
setVerticalScrollbarPosition(int position)509     public void setVerticalScrollbarPosition(int position) {
510         if (mVerticalScrollbarPosition != position) {
511             mVerticalScrollbarPosition = position;
512             configureVerticalScrollbar();
513         }
514     }
515 
configureVerticalScrollbar()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 
setPhotoLoaderEnabled(boolean flag)538     public void setPhotoLoaderEnabled(boolean flag) {
539         mPhotoLoaderEnabled = flag;
540         configurePhotoLoader();
541     }
542 
isPhotoLoaderEnabled()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      */
isSelectionVisible()550     public boolean isSelectionVisible() {
551         return mSelectionVisible;
552     }
553 
setSelectionVisible(boolean flag)554     public void setSelectionVisible(boolean flag) {
555         this.mSelectionVisible = flag;
556     }
557 
setQuickContactEnabled(boolean flag)558     public void setQuickContactEnabled(boolean flag) {
559         this.mQuickContactEnabled = flag;
560     }
561 
setIncludeProfile(boolean flag)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      */
setSearchMode(boolean flag)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 
isSearchMode()606     public final boolean isSearchMode() {
607         return mSearchMode;
608     }
609 
getQueryString()610     public final String getQueryString() {
611         return mQueryString;
612     }
613 
setQueryString(String queryString, boolean delaySelection)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 
getDirectorySearchMode()629     public int getDirectorySearchMode() {
630         return mDirectorySearchMode;
631     }
632 
setDirectorySearchMode(int mode)633     public void setDirectorySearchMode(int mode) {
634         mDirectorySearchMode = mode;
635     }
636 
isLegacyCompatibilityMode()637     public boolean isLegacyCompatibilityMode() {
638         return mLegacyCompatibility;
639     }
640 
setLegacyCompatibilityMode(boolean flag)641     public void setLegacyCompatibilityMode(boolean flag) {
642         mLegacyCompatibility = flag;
643     }
644 
getContactNameDisplayOrder()645     protected int getContactNameDisplayOrder() {
646         return mDisplayOrder;
647     }
648 
setContactNameDisplayOrder(int displayOrder)649     protected void setContactNameDisplayOrder(int displayOrder) {
650         mDisplayOrder = displayOrder;
651         if (mAdapter != null) {
652             mAdapter.setContactNameDisplayOrder(displayOrder);
653         }
654     }
655 
getSortOrder()656     public int getSortOrder() {
657         return mSortOrder;
658     }
659 
setSortOrder(int sortOrder)660     public void setSortOrder(int sortOrder) {
661         mSortOrder = sortOrder;
662         if (mAdapter != null) {
663             mAdapter.setSortOrder(sortOrder);
664         }
665     }
666 
setDirectoryResultLimit(int limit)667     public void setDirectoryResultLimit(int limit) {
668         mDirectoryResultLimit = limit;
669     }
670 
loadPreferences()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
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)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 
onCreateView(LayoutInflater inflater, ViewGroup container)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 
configurePhotoLoader()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 
configureAdapter()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
onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount)771     public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
772             int totalItemCount) {
773     }
774 
775     @Override
onScrollStateChanged(AbsListView view, int scrollState)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
onItemClick(AdapterView<?> parent, View view, int position, long id)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 
hideSoftKeyboard()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
onFocusChange(View view, boolean hasFocus)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
onTouch(View view, MotionEvent event)815     public boolean onTouch(View view, MotionEvent event) {
816         if (view == mListView) {
817             hideSoftKeyboard();
818         }
819         return false;
820     }
821 
822     @Override
onPause()823     public void onPause() {
824         super.onPause();
825         removePendingDirectorySearchRequests();
826     }
827 
828     /**
829      * Restore the list state after the adapter is populated.
830      */
completeRestoreInstanceState()831     protected void completeRestoreInstanceState() {
832         if (mListState != null) {
833             mListView.onRestoreInstanceState(mListState);
834             mListState = null;
835         }
836     }
837 
setDarkTheme(boolean value)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      */
onPickerResult(Intent data)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 
getDefaultVerticalScrollbarPosition()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