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