blob: 387b303270780fc8d71e32cdbdded52d18368400 [file] [log] [blame]
Chiao Chengfed477c2012-12-04 17:40:46 -08001/*
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
17package com.android.contacts.common.list;
18
19import android.app.Activity;
Brian Attwellc2e912c2014-10-27 12:29:44 -070020import android.app.Fragment;
Chiao Chengfed477c2012-12-04 17:40:46 -080021import android.app.LoaderManager;
22import android.app.LoaderManager.LoaderCallbacks;
23import android.content.Context;
24import android.content.CursorLoader;
25import android.content.Intent;
26import android.content.Loader;
27import android.database.Cursor;
28import android.os.Bundle;
29import android.os.Handler;
30import android.os.Message;
31import android.os.Parcelable;
32import android.provider.ContactsContract.Directory;
33import android.text.TextUtils;
Yorke Lee35821292015-06-23 16:53:29 -070034import android.util.Log;
Chiao Chengfed477c2012-12-04 17:40:46 -080035import android.view.LayoutInflater;
36import android.view.MotionEvent;
37import android.view.View;
38import android.view.View.OnFocusChangeListener;
39import android.view.View.OnTouchListener;
40import android.view.ViewGroup;
41import android.view.inputmethod.InputMethodManager;
42import android.widget.AbsListView;
43import android.widget.AbsListView.OnScrollListener;
44import android.widget.AdapterView;
45import android.widget.AdapterView.OnItemClickListener;
Brian Attwell207a8772015-02-27 16:35:00 -080046import android.widget.AdapterView.OnItemLongClickListener;
Chiao Chengfed477c2012-12-04 17:40:46 -080047import android.widget.ListView;
48
49import com.android.common.widget.CompositeCursorAdapter.Partition;
Zheng Fu0dd2bdb2014-08-26 14:24:41 -070050import com.android.contacts.common.ContactPhotoManager;
Wenyi Wangc9ad9b12016-05-20 15:10:12 -070051import com.android.contacts.common.logging.ListEvent.ActionType;
Gary Maif69e8562016-08-01 16:54:44 -070052import com.android.contacts.common.logging.Logger;
Chiao Chengfed477c2012-12-04 17:40:46 -080053import com.android.contacts.common.preference.ContactsPreferences;
54
Fabrice Di Meglio29a5cf92013-04-03 20:59:09 -070055import java.util.Locale;
56
Chiao Chengfed477c2012-12-04 17:40:46 -080057/**
58 * Common base class for various contact-related list fragments.
59 */
60public abstract class ContactEntryListFragment<T extends ContactEntryListAdapter>
Brian Attwellc2e912c2014-10-27 12:29:44 -070061 extends Fragment
Chiao Chengfed477c2012-12-04 17:40:46 -080062 implements OnItemClickListener, OnScrollListener, OnFocusChangeListener, OnTouchListener,
Brian Attwell207a8772015-02-27 16:35:00 -080063 OnItemLongClickListener, LoaderCallbacks<Cursor> {
Chiao Chengfed477c2012-12-04 17:40:46 -080064 private static final String TAG = "ContactEntryListFragment";
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";
Andrew Lee4683e542014-06-09 16:24:10 -070074 private static final String KEY_ADJUST_SELECTION_BOUNDS_ENABLED =
75 "adjustSelectionBoundsEnabled";
Chiao Chengfed477c2012-12-04 17:40:46 -080076 private static final String KEY_SEARCH_MODE = "searchMode";
Tingting Wang33768352016-06-27 17:48:55 -070077 private static final String KEY_DISPLAY_DIRECTORY_HEADER = "displayDirectoryHeader";
Chiao Chengfed477c2012-12-04 17:40:46 -080078 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";
Wenyi Wangc9ad9b12016-05-20 15:10:12 -070087 private static final String KEY_LOGS_LIST_EVENTS = "logsListEvents";
88 private static final String KEY_DATA_LOADED = "dataLoaded";
Chiao Chengfed477c2012-12-04 17:40:46 -080089
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;
Andrew Lee4683e542014-06-09 16:24:10 -0700102 private boolean mAdjustSelectionBoundsEnabled = true;
Wenyi Wang25774d22016-04-08 11:15:11 -0700103 private boolean mIncludeFavorites;
Chiao Chengfed477c2012-12-04 17:40:46 -0800104 private boolean mSearchMode;
Tingting Wang33768352016-06-27 17:48:55 -0700105 private boolean mDisplayDirectoryHeader = true;
Chiao Chengfed477c2012-12-04 17:40:46 -0800106 private boolean mVisibleScrollbarEnabled;
Andrew Leed5506982014-05-15 14:12:34 -0700107 private boolean mShowEmptyListForEmptyQuery;
Fabrice Di Meglio29a5cf92013-04-03 20:59:09 -0700108 private int mVerticalScrollbarPosition = getDefaultVerticalScrollbarPosition();
Chiao Chengfed477c2012-12-04 17:40:46 -0800109 private String mQueryString;
110 private int mDirectorySearchMode = DirectoryListLoader.SEARCH_MODE_NONE;
111 private boolean mSelectionVisible;
112 private boolean mLegacyCompatibility;
Wenyi Wangc9ad9b12016-05-20 15:10:12 -0700113 // 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;
Chiao Chengfed477c2012-12-04 17:40:46 -0800118
119 private boolean mEnabled = true;
120
121 private T mAdapter;
Walter Jangf7d733a2016-08-12 14:23:25 -0700122 protected View mView;
Chiao Chengfed477c2012-12-04 17:40:46 -0800123 private ListView mListView;
124
125 /**
Ta-wei Yen47757162015-11-02 16:20:01 -0800126 * 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 /**
Chiao Chengfed477c2012-12-04 17:40:46 -0800132 * Used for keeping track of the scroll state of the list.
133 */
134 private Parcelable mListState;
135
Wenyi Wangc9ad9b12016-05-20 15:10:12 -0700136 /**
137 * The type of the contacts list.
138 */
139 private int mListType;
140
Chiao Chengfed477c2012-12-04 17:40:46 -0800141 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
Chiao Chengfed477c2012-12-04 17:40:46 -0800152 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 };
Fabrice Di Meglio29a5cf92013-04-03 20:59:09 -0700176 private int defaultVerticalScrollbarPosition;
Chiao Chengfed477c2012-12-04 17:40:46 -0800177
178 protected abstract View inflateView(LayoutInflater inflater, ViewGroup container);
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 */
186 protected abstract void onItemClick(int position, long id);
187
Brian Attwell207a8772015-02-27 16:35:00 -0800188 /**
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 */
193 protected boolean onItemLongClick(int position, long id) {
194 return false;
195 }
196
Chiao Chengfed477c2012-12-04 17:40:46 -0800197 @Override
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 */
207 public void setContext(Context context) {
208 mContext = context;
209 configurePhotoLoader();
210 }
211
212 public Context getContext() {
213 return mContext;
214 }
215
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 */
232 public void setLoaderManager(LoaderManager loaderManager) {
233 mLoaderManager = loaderManager;
234 }
235
236 @Override
237 public LoaderManager getLoaderManager() {
238 return mLoaderManager;
239 }
240
241 public T getAdapter() {
242 return mAdapter;
243 }
244
245 @Override
246 public View getView() {
247 return mView;
248 }
249
250 public ListView getListView() {
251 return mListView;
252 }
253
254 @Override
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);
Andrew Lee4683e542014-06-09 16:24:10 -0700260 outState.putBoolean(KEY_ADJUST_SELECTION_BOUNDS_ENABLED, mAdjustSelectionBoundsEnabled);
Chiao Chengfed477c2012-12-04 17:40:46 -0800261 outState.putBoolean(KEY_SEARCH_MODE, mSearchMode);
Tingting Wang33768352016-06-27 17:48:55 -0700262 outState.putBoolean(KEY_DISPLAY_DIRECTORY_HEADER, mDisplayDirectoryHeader);
Chiao Chengfed477c2012-12-04 17:40:46 -0800263 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);
Wenyi Wangc9ad9b12016-05-20 15:10:12 -0700271 outState.putBoolean(KEY_LOGS_LIST_EVENTS, mLogListEvents);
272 outState.putBoolean(KEY_DATA_LOADED, mDataLoaded);
Chiao Chengfed477c2012-12-04 17:40:46 -0800273
274 if (mListView != null) {
275 outState.putParcelable(KEY_LIST_STATE, mListView.onSaveInstanceState());
276 }
277 }
278
279 @Override
280 public void onCreate(Bundle savedState) {
281 super.onCreate(savedState);
Brian Attwell2101c3d2014-07-16 16:10:30 -0700282 restoreSavedState(savedState);
Yorke Leea9602e72013-10-01 09:18:27 -0700283 mAdapter = createListAdapter();
Chiao Chengfed477c2012-12-04 17:40:46 -0800284 mContactsPrefs = new ContactsPreferences(mContext);
Chiao Chengfed477c2012-12-04 17:40:46 -0800285 }
286
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);
Andrew Lee4683e542014-06-09 16:24:10 -0700295 mAdjustSelectionBoundsEnabled = savedState.getBoolean(KEY_ADJUST_SELECTION_BOUNDS_ENABLED);
Chiao Chengfed477c2012-12-04 17:40:46 -0800296 mSearchMode = savedState.getBoolean(KEY_SEARCH_MODE);
Tingting Wang33768352016-06-27 17:48:55 -0700297 mDisplayDirectoryHeader = savedState.getBoolean(KEY_DISPLAY_DIRECTORY_HEADER);
Chiao Chengfed477c2012-12-04 17:40:46 -0800298 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
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
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
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 {
Jay Shrauner9afe4942013-08-15 09:34:24 -0700360 CursorLoader loader = createCursorLoader(mContext);
Chiao Chengfed477c2012-12-04 17:40:46 -0800361 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
Jay Shrauner9afe4942013-08-15 09:34:24 -0700369 public CursorLoader createCursorLoader(Context context) {
Yorke Lee35821292015-06-23 16:53:29 -0700370 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 };
Chiao Chengfed477c2012-12-04 17:40:46 -0800383 }
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
Gary Maif69e8562016-08-01 16:54:44 -0700436 getListView().setVisibility(View.VISIBLE);
437 getView().setVisibility(View.VISIBLE);
438
Chiao Chengfed477c2012-12-04 17:40:46 -0800439 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 {
Walter Jang1bdef2f2016-07-19 16:00:26 -0700457 maybeLogListEvent();
Chiao Chengfed477c2012-12-04 17:40:46 -0800458 mDirectoryListStatus = STATUS_NOT_LOADED;
459 getLoaderManager().destroyLoader(DIRECTORY_LOADER_ID);
460 }
461 }
462 }
463
Walter Jang1bdef2f2016-07-19 16:00:26 -0700464 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
Chiao Chengfed477c2012-12-04 17:40:46 -0800473 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);
Wenyi Wangbe88bed2016-05-13 12:04:14 -0700484 setListHeader();
Chiao Chengfed477c2012-12-04 17:40:46 -0800485
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 /**
Wenyi Wangbe88bed2016-05-13 12:04:14 -0700525 * Shows a view at the top of the list.
Chiao Chengfed477c2012-12-04 17:40:46 -0800526 */
Wenyi Wangbe88bed2016-05-13 12:04:14 -0700527 protected void setListHeader() {}
Chiao Chengfed477c2012-12-04 17:40:46 -0800528
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);
Chiao Chengfed477c2012-12-04 17:40:46 -0800573 mListView.setVerticalScrollbarPosition(mVerticalScrollbarPosition);
574 mListView.setScrollBarStyle(ListView.SCROLLBARS_OUTSIDE_OVERLAY);
Chiao Chengfed477c2012-12-04 17:40:46 -0800575 }
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
Andrew Lee4683e542014-06-09 16:24:10 -0700602 public void setAdjustSelectionBoundsEnabled(boolean flag) {
603 mAdjustSelectionBoundsEnabled = flag;
604 }
605
Wenyi Wang25774d22016-04-08 11:15:11 -0700606 public void setIncludeFavorites(boolean flag) {
607 mIncludeFavorites = flag;
608 if (mAdapter != null) {
609 mAdapter.setIncludeFavorites(flag);
610 }
611 }
612
Tingting Wang33768352016-06-27 17:48:55 -0700613 public void setDisplayDirectoryHeader(boolean flag) {
614 mDisplayDirectoryHeader = flag;
615 }
616
Chiao Chengfed477c2012-12-04 17:40:46 -0800617 /**
Andrew Leed5506982014-05-15 14:12:34 -0700618 * Enter/exit search mode. This is method is tightly related to the current query, and should
619 * only be called by {@link #setQueryString}.
Chiao Chengfed477c2012-12-04 17:40:46 -0800620 *
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) {
Chiao Chengfed477c2012-12-04 17:40:46 -0800634 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 }
Tingting Wang33768352016-06-27 17:48:55 -0700643 mAdapter.configureDefaultPartition(false, shouldDisplayDirectoryHeader());
Chiao Chengfed477c2012-12-04 17:40:46 -0800644 }
645
646 if (mListView != null) {
647 mListView.setFastScrollEnabled(!flag);
648 }
649 }
650 }
651
Tingting Wang33768352016-06-27 17:48:55 -0700652 /**
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
Chiao Chengfed477c2012-12-04 17:40:46 -0800664 public final boolean isSearchMode() {
665 return mSearchMode;
666 }
667
668 public final String getQueryString() {
669 return mQueryString;
670 }
671
Wenyi Wanga5cae5d2016-06-23 12:25:31 -0700672 // TODO: the paramter delaySelection is not in use, and let's remove it.
Chiao Chengfed477c2012-12-04 17:40:46 -0800673 public void setQueryString(String queryString, boolean delaySelection) {
Chiao Chengfed477c2012-12-04 17:40:46 -0800674 if (!TextUtils.equals(mQueryString, queryString)) {
Andrew Leed5506982014-05-15 14:12:34 -0700675 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
Chiao Chengfed477c2012-12-04 17:40:46 -0800685 mQueryString = queryString;
Andrew Leed5506982014-05-15 14:12:34 -0700686 setSearchMode(!TextUtils.isEmpty(mQueryString) || mShowEmptyListForEmptyQuery);
Chiao Chengfed477c2012-12-04 17:40:46 -0800687
688 if (mAdapter != null) {
689 mAdapter.setQueryString(queryString);
690 reloadData();
691 }
692 }
693 }
694
Andrew Leed5506982014-05-15 14:12:34 -0700695 public void setShowEmptyListForNullQuery(boolean show) {
696 mShowEmptyListForEmptyQuery = show;
697 }
698
Christine Chen3efbe592013-07-08 18:05:03 -0700699 public int getDirectoryLoaderId() {
700 return DIRECTORY_LOADER_ID;
701 }
702
Chiao Chengfed477c2012-12-04 17:40:46 -0800703 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
Chiao Chengfed477c2012-12-04 17:40:46 -0800765 boolean searchMode = isSearchMode();
766 mAdapter.setSearchMode(searchMode);
Tingting Wang33768352016-06-27 17:48:55 -0700767 mAdapter.configureDefaultPartition(false, shouldDisplayDirectoryHeader());
Chiao Chengfed477c2012-12-04 17:40:46 -0800768 mAdapter.setPhotoLoader(mPhotoManager);
769 mListView.setAdapter(mAdapter);
770
771 if (!isSearchMode()) {
772 mListView.setFocusableInTouchMode(true);
773 mListView.requestFocus();
774 }
775
Wenyi Wangc9ad9b12016-05-20 15:10:12 -0700776 if (savedInstanceState != null) {
777 mLogListEvents = savedInstanceState.getBoolean(KEY_LOGS_LIST_EVENTS, true);
778 mDataLoaded = savedInstanceState.getBoolean(KEY_DATA_LOADED, false);
779 }
780
Chiao Chengfed477c2012-12-04 17:40:46 -0800781 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);
Brian Attwell207a8772015-02-27 16:35:00 -0800800 mListView.setOnItemLongClickListener(this);
Chiao Chengfed477c2012-12-04 17:40:46 -0800801 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();
Brian Attwellb92b6372014-07-21 23:39:35 -0700814
815 getAdapter().setFragmentRootView(getView());
Chiao Chengfed477c2012-12-04 17:40:46 -0800816 }
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);
Andrew Lee4683e542014-06-09 16:24:10 -0700838 mAdapter.setAdjustSelectionBoundsEnabled(mAdjustSelectionBoundsEnabled);
Wenyi Wang25774d22016-04-08 11:15:11 -0700839 mAdapter.setIncludeFavorites(mIncludeFavorites);
Chiao Chengfed477c2012-12-04 17:40:46 -0800840 mAdapter.setQueryString(mQueryString);
841 mAdapter.setDirectorySearchMode(mDirectorySearchMode);
Nancy Chen92528252014-08-07 17:07:57 -0700842 mAdapter.setPinnedPartitionHeadersEnabled(false);
Chiao Chengfed477c2012-12-04 17:40:46 -0800843 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
Brian Attwell207a8772015-02-27 16:35:00 -0800875 @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
Chiao Chengfed477c2012-12-04 17:40:46 -0800885 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() {
Ta-wei Yen47757162015-11-02 16:20:01 -0800915 // 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
Chiao Chengfed477c2012-12-04 17:40:46 -0800920 super.onPause();
921 removePendingDirectorySearchRequests();
922 }
923
Wenyi Wang72198662015-12-10 15:18:15 -0800924 @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
Chiao Chengfed477c2012-12-04 17:40:46 -0800933 /**
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 };
Fabrice Di Meglio29a5cf92013-04-03 20:59:09 -0700963
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 }
Wenyi Wangc9ad9b12016-05-20 15:10:12 -0700975
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 }
Chiao Chengfed477c2012-12-04 17:40:46 -0800987}