1 /* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.contacts.common.list; 18 19 import android.app.Activity; 20 import android.app.Fragment; 21 import android.app.LoaderManager; 22 import android.app.LoaderManager.LoaderCallbacks; 23 import android.content.Context; 24 import android.content.CursorLoader; 25 import android.content.Intent; 26 import android.content.Loader; 27 import android.database.Cursor; 28 import android.os.Bundle; 29 import android.os.Handler; 30 import android.os.Message; 31 import android.os.Parcelable; 32 import android.provider.ContactsContract.Directory; 33 import android.text.TextUtils; 34 import android.view.LayoutInflater; 35 import android.view.MotionEvent; 36 import android.view.View; 37 import android.view.View.OnFocusChangeListener; 38 import android.view.View.OnTouchListener; 39 import android.view.ViewGroup; 40 import android.view.inputmethod.InputMethodManager; 41 import android.widget.AbsListView; 42 import android.widget.AbsListView.OnScrollListener; 43 import android.widget.AdapterView; 44 import android.widget.AdapterView.OnItemClickListener; 45 import android.widget.ListView; 46 47 import com.android.common.widget.CompositeCursorAdapter.Partition; 48 import com.android.contacts.common.ContactPhotoManager; 49 import com.android.contacts.common.preference.ContactsPreferences; 50 import com.android.contacts.common.util.ContactListViewUtils; 51 52 import java.util.Locale; 53 54 /** 55 * Common base class for various contact-related list fragments. 56 */ 57 public 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"; 71 private static final String KEY_ADJUST_SELECTION_BOUNDS_ENABLED = 72 "adjustSelectionBoundsEnabled"; 73 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; 97 private boolean mAdjustSelectionBoundsEnabled = true; 98 private boolean mIncludeProfile; 99 private boolean mSearchMode; 100 private boolean mVisibleScrollbarEnabled; 101 private boolean mShowEmptyListForEmptyQuery; 102 private int mVerticalScrollbarPosition = getDefaultVerticalScrollbarPosition(); 103 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 }; 156 private int defaultVerticalScrollbarPosition; 157 inflateView(LayoutInflater inflater, ViewGroup container)158 protected abstract View inflateView(LayoutInflater inflater, ViewGroup container); createListAdapter()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 */ onItemClick(int position, long id)166 protected abstract void onItemClick(int position, long id); 167 168 @Override onAttach(Activity activity)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 */ setContext(Context context)178 public void setContext(Context context) { 179 mContext = context; 180 configurePhotoLoader(); 181 } 182 getContext()183 public Context getContext() { 184 return mContext; 185 } 186 setEnabled(boolean enabled)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 */ setLoaderManager(LoaderManager loaderManager)203 public void setLoaderManager(LoaderManager loaderManager) { 204 mLoaderManager = loaderManager; 205 } 206 207 @Override getLoaderManager()208 public LoaderManager getLoaderManager() { 209 return mLoaderManager; 210 } 211 getAdapter()212 public T getAdapter() { 213 return mAdapter; 214 } 215 216 @Override getView()217 public View getView() { 218 return mView; 219 } 220 getListView()221 public ListView getListView() { 222 return mListView; 223 } 224 225 @Override onSaveInstanceState(Bundle outState)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); 231 outState.putBoolean(KEY_ADJUST_SELECTION_BOUNDS_ENABLED, mAdjustSelectionBoundsEnabled); 232 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 onCreate(Bundle savedState)249 public void onCreate(Bundle savedState) { 250 super.onCreate(savedState); 251 restoreSavedState(savedState); 252 mAdapter = createListAdapter(); 253 mContactsPrefs = new ContactsPreferences(mContext); 254 } 255 restoreSavedState(Bundle savedState)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); 264 mAdjustSelectionBoundsEnabled = savedState.getBoolean(KEY_ADJUST_SELECTION_BOUNDS_ENABLED); 265 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 onStart()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 startLoading()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 onCreateLoader(int id, Bundle args)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 { 329 CursorLoader loader = createCursorLoader(mContext); 330 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 createCursorLoader(Context context)338 public CursorLoader createCursorLoader(Context context) { 339 return new CursorLoader(context, null, null, null, null, null); 340 } 341 startLoadingDirectoryPartition(int partitionIndex)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 */ loadDirectoryPartitionDelayed(int partitionIndex, DirectoryPartition partition)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 */ loadDirectoryPartition(int partitionIndex, DirectoryPartition partition)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 */ removePendingDirectorySearchRequests()383 private void removePendingDirectorySearchRequests() { 384 mDelayedDirectorySearchHandler.removeMessages(DIRECTORY_SEARCH_MESSAGE); 385 } 386 387 @Override onLoadFinished(Loader<Cursor> loader, Cursor data)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 onLoaderReset(Loader<Cursor> loader)417 public void onLoaderReset(Loader<Cursor> loader) { 418 } 419 onPartitionLoaded(int partitionIndex, Cursor data)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(); 429 430 if (!isLoading()) { 431 completeRestoreInstanceState(); 432 } 433 } 434 isLoading()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 isLoadingDirectoryList()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 onStop()454 public void onStop() { 455 super.onStop(); 456 mContactsPrefs.unregisterChangeListener(); 457 mAdapter.clearPartitions(); 458 } 459 reloadData()460 protected void reloadData() { 461 removePendingDirectorySearchRequests(); 462 mAdapter.onDataReload(); 463 mLoadPriorityDirectoriesOnly = true; 464 mForceLoad = true; 465 startLoading(); 466 } 467 468 /** 469 * 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 */ setProfileHeader()472 protected void setProfileHeader() { 473 mUserProfileExists = false; 474 } 475 476 /** 477 * Provides logic that dismisses this fragment. The default implementation 478 * does nothing. 479 */ finish()480 protected void finish() { 481 } 482 setSectionHeaderDisplayEnabled(boolean flag)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 isSectionHeaderDisplayEnabled()493 public boolean isSectionHeaderDisplayEnabled() { 494 return mSectionHeaderDisplayEnabled; 495 } 496 setVisibleScrollbarEnabled(boolean flag)497 public void setVisibleScrollbarEnabled(boolean flag) { 498 if (mVisibleScrollbarEnabled != flag) { 499 mVisibleScrollbarEnabled = flag; 500 configureVerticalScrollbar(); 501 } 502 } 503 isVisibleScrollbarEnabled()504 public boolean isVisibleScrollbarEnabled() { 505 return mVisibleScrollbarEnabled; 506 } 507 setVerticalScrollbarPosition(int position)508 public void setVerticalScrollbarPosition(int position) { 509 if (mVerticalScrollbarPosition != position) { 510 mVerticalScrollbarPosition = position; 511 configureVerticalScrollbar(); 512 } 513 } 514 configureVerticalScrollbar()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); 523 } 524 } 525 setPhotoLoaderEnabled(boolean flag)526 public void setPhotoLoaderEnabled(boolean flag) { 527 mPhotoLoaderEnabled = flag; 528 configurePhotoLoader(); 529 } 530 isPhotoLoaderEnabled()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 */ isSelectionVisible()538 public boolean isSelectionVisible() { 539 return mSelectionVisible; 540 } 541 setSelectionVisible(boolean flag)542 public void setSelectionVisible(boolean flag) { 543 this.mSelectionVisible = flag; 544 } 545 setQuickContactEnabled(boolean flag)546 public void setQuickContactEnabled(boolean flag) { 547 this.mQuickContactEnabled = flag; 548 } 549 setAdjustSelectionBoundsEnabled(boolean flag)550 public void setAdjustSelectionBoundsEnabled(boolean flag) { 551 mAdjustSelectionBoundsEnabled = flag; 552 } 553 setIncludeProfile(boolean flag)554 public void setIncludeProfile(boolean flag) { 555 mIncludeProfile = flag; 556 if(mAdapter != null) { 557 mAdapter.setIncludeProfile(flag); 558 } 559 } 560 561 /** 562 * Enter/exit search mode. This is method is tightly related to the current query, and should 563 * only be called by {@link #setQueryString}. 564 * 565 * Also note this method doesn't call {@link #reloadData()}; {@link #setQueryString} does it. 566 */ setSearchMode(boolean flag)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.setSearchMode(flag); 579 580 mAdapter.clearPartitions(); 581 if (!flag) { 582 // If we are switching from search to regular display, remove all directory 583 // partitions after default one, assuming they are remote directories which 584 // should be cleaned up on exiting the search mode. 585 mAdapter.removeDirectoriesAfterDefault(); 586 } 587 mAdapter.configureDefaultPartition(false, flag); 588 } 589 590 if (mListView != null) { 591 mListView.setFastScrollEnabled(!flag); 592 } 593 } 594 } 595 isSearchMode()596 public final boolean isSearchMode() { 597 return mSearchMode; 598 } 599 getQueryString()600 public final String getQueryString() { 601 return mQueryString; 602 } 603 setQueryString(String queryString, boolean delaySelection)604 public void setQueryString(String queryString, boolean delaySelection) { 605 if (!TextUtils.equals(mQueryString, queryString)) { 606 if (mShowEmptyListForEmptyQuery && mAdapter != null && mListView != null) { 607 if (TextUtils.isEmpty(mQueryString)) { 608 // Restore the adapter if the query used to be empty. 609 mListView.setAdapter(mAdapter); 610 } else if (TextUtils.isEmpty(queryString)) { 611 // Instantly clear the list view if the new query is empty. 612 mListView.setAdapter(null); 613 } 614 } 615 616 mQueryString = queryString; 617 setSearchMode(!TextUtils.isEmpty(mQueryString) || mShowEmptyListForEmptyQuery); 618 619 if (mAdapter != null) { 620 mAdapter.setQueryString(queryString); 621 reloadData(); 622 } 623 } 624 } 625 setShowEmptyListForNullQuery(boolean show)626 public void setShowEmptyListForNullQuery(boolean show) { 627 mShowEmptyListForEmptyQuery = show; 628 } 629 getDirectoryLoaderId()630 public int getDirectoryLoaderId() { 631 return DIRECTORY_LOADER_ID; 632 } 633 getDirectorySearchMode()634 public int getDirectorySearchMode() { 635 return mDirectorySearchMode; 636 } 637 setDirectorySearchMode(int mode)638 public void setDirectorySearchMode(int mode) { 639 mDirectorySearchMode = mode; 640 } 641 isLegacyCompatibilityMode()642 public boolean isLegacyCompatibilityMode() { 643 return mLegacyCompatibility; 644 } 645 setLegacyCompatibilityMode(boolean flag)646 public void setLegacyCompatibilityMode(boolean flag) { 647 mLegacyCompatibility = flag; 648 } 649 getContactNameDisplayOrder()650 protected int getContactNameDisplayOrder() { 651 return mDisplayOrder; 652 } 653 setContactNameDisplayOrder(int displayOrder)654 protected void setContactNameDisplayOrder(int displayOrder) { 655 mDisplayOrder = displayOrder; 656 if (mAdapter != null) { 657 mAdapter.setContactNameDisplayOrder(displayOrder); 658 } 659 } 660 getSortOrder()661 public int getSortOrder() { 662 return mSortOrder; 663 } 664 setSortOrder(int sortOrder)665 public void setSortOrder(int sortOrder) { 666 mSortOrder = sortOrder; 667 if (mAdapter != null) { 668 mAdapter.setSortOrder(sortOrder); 669 } 670 } 671 setDirectoryResultLimit(int limit)672 public void setDirectoryResultLimit(int limit) { 673 mDirectoryResultLimit = limit; 674 } 675 loadPreferences()676 protected boolean loadPreferences() { 677 boolean changed = false; 678 if (getContactNameDisplayOrder() != mContactsPrefs.getDisplayOrder()) { 679 setContactNameDisplayOrder(mContactsPrefs.getDisplayOrder()); 680 changed = true; 681 } 682 683 if (getSortOrder() != mContactsPrefs.getSortOrder()) { 684 setSortOrder(mContactsPrefs.getSortOrder()); 685 changed = true; 686 } 687 688 return changed; 689 } 690 691 @Override onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)692 public View onCreateView(LayoutInflater inflater, ViewGroup container, 693 Bundle savedInstanceState) { 694 onCreateView(inflater, container); 695 696 boolean searchMode = isSearchMode(); 697 mAdapter.setSearchMode(searchMode); 698 mAdapter.configureDefaultPartition(false, searchMode); 699 mAdapter.setPhotoLoader(mPhotoManager); 700 mListView.setAdapter(mAdapter); 701 702 if (!isSearchMode()) { 703 mListView.setFocusableInTouchMode(true); 704 mListView.requestFocus(); 705 } 706 707 return mView; 708 } 709 onCreateView(LayoutInflater inflater, ViewGroup container)710 protected void onCreateView(LayoutInflater inflater, ViewGroup container) { 711 mView = inflateView(inflater, container); 712 713 mListView = (ListView)mView.findViewById(android.R.id.list); 714 if (mListView == null) { 715 throw new RuntimeException( 716 "Your content must have a ListView whose id attribute is " + 717 "'android.R.id.list'"); 718 } 719 720 View emptyView = mView.findViewById(android.R.id.empty); 721 if (emptyView != null) { 722 mListView.setEmptyView(emptyView); 723 } 724 725 mListView.setOnItemClickListener(this); 726 mListView.setOnFocusChangeListener(this); 727 mListView.setOnTouchListener(this); 728 mListView.setFastScrollEnabled(!isSearchMode()); 729 730 // Tell list view to not show dividers. We'll do it ourself so that we can *not* show 731 // them when an A-Z headers is visible. 732 mListView.setDividerHeight(0); 733 734 // We manually save/restore the listview state 735 mListView.setSaveEnabled(false); 736 737 configureVerticalScrollbar(); 738 configurePhotoLoader(); 739 740 getAdapter().setFragmentRootView(getView()); 741 742 ContactListViewUtils.applyCardPaddingToView(getResources(), mListView, mView); 743 } 744 745 @Override onHiddenChanged(boolean hidden)746 public void onHiddenChanged(boolean hidden) { 747 super.onHiddenChanged(hidden); 748 if (getActivity() != null && getView() != null && !hidden) { 749 // If the padding was last applied when in a hidden state, it may have been applied 750 // incorrectly. Therefore we need to reapply it. 751 ContactListViewUtils.applyCardPaddingToView(getResources(), mListView, getView()); 752 } 753 } 754 configurePhotoLoader()755 protected void configurePhotoLoader() { 756 if (isPhotoLoaderEnabled() && mContext != null) { 757 if (mPhotoManager == null) { 758 mPhotoManager = ContactPhotoManager.getInstance(mContext); 759 } 760 if (mListView != null) { 761 mListView.setOnScrollListener(this); 762 } 763 if (mAdapter != null) { 764 mAdapter.setPhotoLoader(mPhotoManager); 765 } 766 } 767 } 768 configureAdapter()769 protected void configureAdapter() { 770 if (mAdapter == null) { 771 return; 772 } 773 774 mAdapter.setQuickContactEnabled(mQuickContactEnabled); 775 mAdapter.setAdjustSelectionBoundsEnabled(mAdjustSelectionBoundsEnabled); 776 mAdapter.setIncludeProfile(mIncludeProfile); 777 mAdapter.setQueryString(mQueryString); 778 mAdapter.setDirectorySearchMode(mDirectorySearchMode); 779 mAdapter.setPinnedPartitionHeadersEnabled(false); 780 mAdapter.setContactNameDisplayOrder(mDisplayOrder); 781 mAdapter.setSortOrder(mSortOrder); 782 mAdapter.setSectionHeaderDisplayEnabled(mSectionHeaderDisplayEnabled); 783 mAdapter.setSelectionVisible(mSelectionVisible); 784 mAdapter.setDirectoryResultLimit(mDirectoryResultLimit); 785 mAdapter.setDarkTheme(mDarkTheme); 786 } 787 788 @Override onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount)789 public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, 790 int totalItemCount) { 791 } 792 793 @Override onScrollStateChanged(AbsListView view, int scrollState)794 public void onScrollStateChanged(AbsListView view, int scrollState) { 795 if (scrollState == OnScrollListener.SCROLL_STATE_FLING) { 796 mPhotoManager.pause(); 797 } else if (isPhotoLoaderEnabled()) { 798 mPhotoManager.resume(); 799 } 800 } 801 802 @Override onItemClick(AdapterView<?> parent, View view, int position, long id)803 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 804 hideSoftKeyboard(); 805 806 int adjPosition = position - mListView.getHeaderViewsCount(); 807 if (adjPosition >= 0) { 808 onItemClick(adjPosition, id); 809 } 810 } 811 hideSoftKeyboard()812 private void hideSoftKeyboard() { 813 // Hide soft keyboard, if visible 814 InputMethodManager inputMethodManager = (InputMethodManager) 815 mContext.getSystemService(Context.INPUT_METHOD_SERVICE); 816 inputMethodManager.hideSoftInputFromWindow(mListView.getWindowToken(), 0); 817 } 818 819 /** 820 * Dismisses the soft keyboard when the list takes focus. 821 */ 822 @Override onFocusChange(View view, boolean hasFocus)823 public void onFocusChange(View view, boolean hasFocus) { 824 if (view == mListView && hasFocus) { 825 hideSoftKeyboard(); 826 } 827 } 828 829 /** 830 * Dismisses the soft keyboard when the list is touched. 831 */ 832 @Override onTouch(View view, MotionEvent event)833 public boolean onTouch(View view, MotionEvent event) { 834 if (view == mListView) { 835 hideSoftKeyboard(); 836 } 837 return false; 838 } 839 840 @Override onPause()841 public void onPause() { 842 super.onPause(); 843 removePendingDirectorySearchRequests(); 844 } 845 846 /** 847 * Restore the list state after the adapter is populated. 848 */ completeRestoreInstanceState()849 protected void completeRestoreInstanceState() { 850 if (mListState != null) { 851 mListView.onRestoreInstanceState(mListState); 852 mListState = null; 853 } 854 } 855 setDarkTheme(boolean value)856 public void setDarkTheme(boolean value) { 857 mDarkTheme = value; 858 if (mAdapter != null) mAdapter.setDarkTheme(value); 859 } 860 861 /** 862 * Processes a result returned by the contact picker. 863 */ onPickerResult(Intent data)864 public void onPickerResult(Intent data) { 865 throw new UnsupportedOperationException("Picker result handler is not implemented."); 866 } 867 868 private ContactsPreferences.ChangeListener mPreferencesChangeListener = 869 new ContactsPreferences.ChangeListener() { 870 @Override 871 public void onChange() { 872 loadPreferences(); 873 reloadData(); 874 } 875 }; 876 getDefaultVerticalScrollbarPosition()877 private int getDefaultVerticalScrollbarPosition() { 878 final Locale locale = Locale.getDefault(); 879 final int layoutDirection = TextUtils.getLayoutDirectionFromLocale(locale); 880 switch (layoutDirection) { 881 case View.LAYOUT_DIRECTION_RTL: 882 return View.SCROLLBAR_POSITION_LEFT; 883 case View.LAYOUT_DIRECTION_LTR: 884 default: 885 return View.SCROLLBAR_POSITION_RIGHT; 886 } 887 } 888 } 889