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