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