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