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