ContactEntryListFragment.java revision fc9221ef57bfb9311dda798f67030d40215be859
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 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 52import java.util.Locale; 53 54/** 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"; 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 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); 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 249 public void onCreate(Bundle savedState) { 250 super.onCreate(savedState); 251 mAdapter = createListAdapter(); 252 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); 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 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 { 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 338 public CursorLoader createCursorLoader(Context context) { 339 return new CursorLoader(context, null, null, null, null, null); 340 } 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(); 429 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 /** 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 */ 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); 523 } 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 550 public void setAdjustSelectionBoundsEnabled(boolean flag) { 551 mAdjustSelectionBoundsEnabled = flag; 552 } 553 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 */ 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) { 606 if (!TextUtils.equals(mQueryString, queryString)) { 607 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 617 mQueryString = queryString; 618 setSearchMode(!TextUtils.isEmpty(mQueryString) || mShowEmptyListForEmptyQuery); 619 620 if (mAdapter != null) { 621 mAdapter.setQueryString(queryString); 622 reloadData(); 623 } 624 } 625 } 626 627 public void setShowEmptyListForNullQuery(boolean show) { 628 mShowEmptyListForEmptyQuery = show; 629 } 630 631 public int getDirectoryLoaderId() { 632 return DIRECTORY_LOADER_ID; 633 } 634 635 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 697 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); 762 mAdapter.setAdjustSelectionBoundsEnabled(mAdjustSelectionBoundsEnabled); 763 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 }; 863 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 } 875} 876