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.R; 50 import com.android.contacts.common.preference.ContactsPreferences; 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_INCLUDE_PROFILE = "includeProfile"; 72 private static final String KEY_SEARCH_MODE = "searchMode"; 73 private static final String KEY_VISIBLE_SCROLLBAR_ENABLED = "visibleScrollbarEnabled"; 74 private static final String KEY_SCROLLBAR_POSITION = "scrollbarPosition"; 75 private static final String KEY_QUERY_STRING = "queryString"; 76 private static final String KEY_DIRECTORY_SEARCH_MODE = "directorySearchMode"; 77 private static final String KEY_SELECTION_VISIBLE = "selectionVisible"; 78 private static final String KEY_REQUEST = "request"; 79 private static final String KEY_DARK_THEME = "darkTheme"; 80 private static final String KEY_LEGACY_COMPATIBILITY = "legacyCompatibility"; 81 private static final String KEY_DIRECTORY_RESULT_LIMIT = "directoryResultLimit"; 82 83 private static final String DIRECTORY_ID_ARG_KEY = "directoryId"; 84 85 private static final int DIRECTORY_LOADER_ID = -1; 86 87 private static final int DIRECTORY_SEARCH_DELAY_MILLIS = 300; 88 private static final int DIRECTORY_SEARCH_MESSAGE = 1; 89 90 private static final int DEFAULT_DIRECTORY_RESULT_LIMIT = 20; 91 92 private boolean mSectionHeaderDisplayEnabled; 93 private boolean mPhotoLoaderEnabled; 94 private boolean mQuickContactEnabled = true; 95 private boolean mIncludeProfile; 96 private boolean mSearchMode; 97 private boolean mVisibleScrollbarEnabled; 98 private int mVerticalScrollbarPosition = getDefaultVerticalScrollbarPosition(); 99 private String mQueryString; 100 private int mDirectorySearchMode = DirectoryListLoader.SEARCH_MODE_NONE; 101 private boolean mSelectionVisible; 102 private boolean mLegacyCompatibility; 103 104 private boolean mEnabled = true; 105 106 private T mAdapter; 107 private View mView; 108 private ListView mListView; 109 110 /** 111 * Used for keeping track of the scroll state of the list. 112 */ 113 private Parcelable mListState; 114 115 private int mDisplayOrder; 116 private int mSortOrder; 117 private int mDirectoryResultLimit = DEFAULT_DIRECTORY_RESULT_LIMIT; 118 119 private ContactPhotoManager mPhotoManager; 120 private ContactsPreferences mContactsPrefs; 121 122 private boolean mForceLoad; 123 124 private boolean mDarkTheme; 125 126 protected boolean mUserProfileExists; 127 128 private static final int STATUS_NOT_LOADED = 0; 129 private static final int STATUS_LOADING = 1; 130 private static final int STATUS_LOADED = 2; 131 132 private int mDirectoryListStatus = STATUS_NOT_LOADED; 133 134 /** 135 * Indicates whether we are doing the initial complete load of data (false) or 136 * a refresh caused by a change notification (true) 137 */ 138 private boolean mLoadPriorityDirectoriesOnly; 139 140 private Context mContext; 141 142 private LoaderManager mLoaderManager; 143 144 private Handler mDelayedDirectorySearchHandler = new Handler() { 145 @Override 146 public void handleMessage(Message msg) { 147 if (msg.what == DIRECTORY_SEARCH_MESSAGE) { 148 loadDirectoryPartition(msg.arg1, (DirectoryPartition) msg.obj); 149 } 150 } 151 }; 152 private int defaultVerticalScrollbarPosition; 153 154 protected abstract View inflateView(LayoutInflater inflater, ViewGroup container); 155 protected abstract T createListAdapter(); 156 157 /** 158 * @param position Please note that the position is already adjusted for 159 * header views, so "0" means the first list item below header 160 * views. 161 */ 162 protected abstract void onItemClick(int position, long id); 163 164 @Override 165 public void onAttach(Activity activity) { 166 super.onAttach(activity); 167 setContext(activity); 168 setLoaderManager(super.getLoaderManager()); 169 } 170 171 /** 172 * Sets a context for the fragment in the unit test environment. 173 */ 174 public void setContext(Context context) { 175 mContext = context; 176 configurePhotoLoader(); 177 } 178 179 public Context getContext() { 180 return mContext; 181 } 182 183 public void setEnabled(boolean enabled) { 184 if (mEnabled != enabled) { 185 mEnabled = enabled; 186 if (mAdapter != null) { 187 if (mEnabled) { 188 reloadData(); 189 } else { 190 mAdapter.clearPartitions(); 191 } 192 } 193 } 194 } 195 196 /** 197 * Overrides a loader manager for use in unit tests. 198 */ 199 public void setLoaderManager(LoaderManager loaderManager) { 200 mLoaderManager = loaderManager; 201 } 202 203 @Override 204 public LoaderManager getLoaderManager() { 205 return mLoaderManager; 206 } 207 208 public T getAdapter() { 209 return mAdapter; 210 } 211 212 @Override 213 public View getView() { 214 return mView; 215 } 216 217 public ListView getListView() { 218 return mListView; 219 } 220 221 @Override 222 public void onSaveInstanceState(Bundle outState) { 223 super.onSaveInstanceState(outState); 224 outState.putBoolean(KEY_SECTION_HEADER_DISPLAY_ENABLED, mSectionHeaderDisplayEnabled); 225 outState.putBoolean(KEY_PHOTO_LOADER_ENABLED, mPhotoLoaderEnabled); 226 outState.putBoolean(KEY_QUICK_CONTACT_ENABLED, mQuickContactEnabled); 227 outState.putBoolean(KEY_INCLUDE_PROFILE, mIncludeProfile); 228 outState.putBoolean(KEY_SEARCH_MODE, mSearchMode); 229 outState.putBoolean(KEY_VISIBLE_SCROLLBAR_ENABLED, mVisibleScrollbarEnabled); 230 outState.putInt(KEY_SCROLLBAR_POSITION, mVerticalScrollbarPosition); 231 outState.putInt(KEY_DIRECTORY_SEARCH_MODE, mDirectorySearchMode); 232 outState.putBoolean(KEY_SELECTION_VISIBLE, mSelectionVisible); 233 outState.putBoolean(KEY_LEGACY_COMPATIBILITY, mLegacyCompatibility); 234 outState.putString(KEY_QUERY_STRING, mQueryString); 235 outState.putInt(KEY_DIRECTORY_RESULT_LIMIT, mDirectoryResultLimit); 236 outState.putBoolean(KEY_DARK_THEME, mDarkTheme); 237 238 if (mListView != null) { 239 outState.putParcelable(KEY_LIST_STATE, mListView.onSaveInstanceState()); 240 } 241 } 242 243 @Override 244 public void onCreate(Bundle savedState) { 245 super.onCreate(savedState); 246 mAdapter = createListAdapter(); 247 mContactsPrefs = new ContactsPreferences(mContext); 248 restoreSavedState(savedState); 249 } 250 251 public void restoreSavedState(Bundle savedState) { 252 if (savedState == null) { 253 return; 254 } 255 256 mSectionHeaderDisplayEnabled = savedState.getBoolean(KEY_SECTION_HEADER_DISPLAY_ENABLED); 257 mPhotoLoaderEnabled = savedState.getBoolean(KEY_PHOTO_LOADER_ENABLED); 258 mQuickContactEnabled = savedState.getBoolean(KEY_QUICK_CONTACT_ENABLED); 259 mIncludeProfile = savedState.getBoolean(KEY_INCLUDE_PROFILE); 260 mSearchMode = savedState.getBoolean(KEY_SEARCH_MODE); 261 mVisibleScrollbarEnabled = savedState.getBoolean(KEY_VISIBLE_SCROLLBAR_ENABLED); 262 mVerticalScrollbarPosition = savedState.getInt(KEY_SCROLLBAR_POSITION); 263 mDirectorySearchMode = savedState.getInt(KEY_DIRECTORY_SEARCH_MODE); 264 mSelectionVisible = savedState.getBoolean(KEY_SELECTION_VISIBLE); 265 mLegacyCompatibility = savedState.getBoolean(KEY_LEGACY_COMPATIBILITY); 266 mQueryString = savedState.getString(KEY_QUERY_STRING); 267 mDirectoryResultLimit = savedState.getInt(KEY_DIRECTORY_RESULT_LIMIT); 268 mDarkTheme = savedState.getBoolean(KEY_DARK_THEME); 269 270 // Retrieve list state. This will be applied in onLoadFinished 271 mListState = savedState.getParcelable(KEY_LIST_STATE); 272 } 273 274 @Override 275 public void onStart() { 276 super.onStart(); 277 278 mContactsPrefs.registerChangeListener(mPreferencesChangeListener); 279 280 mForceLoad = loadPreferences(); 281 282 mDirectoryListStatus = STATUS_NOT_LOADED; 283 mLoadPriorityDirectoriesOnly = true; 284 285 startLoading(); 286 } 287 288 protected void startLoading() { 289 if (mAdapter == null) { 290 // The method was called before the fragment was started 291 return; 292 } 293 294 configureAdapter(); 295 int partitionCount = mAdapter.getPartitionCount(); 296 for (int i = 0; i < partitionCount; i++) { 297 Partition partition = mAdapter.getPartition(i); 298 if (partition instanceof DirectoryPartition) { 299 DirectoryPartition directoryPartition = (DirectoryPartition)partition; 300 if (directoryPartition.getStatus() == DirectoryPartition.STATUS_NOT_LOADED) { 301 if (directoryPartition.isPriorityDirectory() || !mLoadPriorityDirectoriesOnly) { 302 startLoadingDirectoryPartition(i); 303 } 304 } 305 } else { 306 getLoaderManager().initLoader(i, null, this); 307 } 308 } 309 310 // Next time this method is called, we should start loading non-priority directories 311 mLoadPriorityDirectoriesOnly = false; 312 } 313 314 @Override 315 public Loader<Cursor> onCreateLoader(int id, Bundle args) { 316 if (id == DIRECTORY_LOADER_ID) { 317 DirectoryListLoader loader = new DirectoryListLoader(mContext); 318 loader.setDirectorySearchMode(mAdapter.getDirectorySearchMode()); 319 loader.setLocalInvisibleDirectoryEnabled( 320 ContactEntryListAdapter.LOCAL_INVISIBLE_DIRECTORY_ENABLED); 321 return loader; 322 } else { 323 CursorLoader loader = createCursorLoader(mContext); 324 long directoryId = args != null && args.containsKey(DIRECTORY_ID_ARG_KEY) 325 ? args.getLong(DIRECTORY_ID_ARG_KEY) 326 : Directory.DEFAULT; 327 mAdapter.configureLoader(loader, directoryId); 328 return loader; 329 } 330 } 331 332 public CursorLoader createCursorLoader(Context context) { 333 return new CursorLoader(context, null, null, null, null, null); 334 } 335 336 private void startLoadingDirectoryPartition(int partitionIndex) { 337 DirectoryPartition partition = (DirectoryPartition)mAdapter.getPartition(partitionIndex); 338 partition.setStatus(DirectoryPartition.STATUS_LOADING); 339 long directoryId = partition.getDirectoryId(); 340 if (mForceLoad) { 341 if (directoryId == Directory.DEFAULT) { 342 loadDirectoryPartition(partitionIndex, partition); 343 } else { 344 loadDirectoryPartitionDelayed(partitionIndex, partition); 345 } 346 } else { 347 Bundle args = new Bundle(); 348 args.putLong(DIRECTORY_ID_ARG_KEY, directoryId); 349 getLoaderManager().initLoader(partitionIndex, args, this); 350 } 351 } 352 353 /** 354 * Queues up a delayed request to search the specified directory. Since 355 * directory search will likely introduce a lot of network traffic, we want 356 * to wait for a pause in the user's typing before sending a directory request. 357 */ 358 private void loadDirectoryPartitionDelayed(int partitionIndex, DirectoryPartition partition) { 359 mDelayedDirectorySearchHandler.removeMessages(DIRECTORY_SEARCH_MESSAGE, partition); 360 Message msg = mDelayedDirectorySearchHandler.obtainMessage( 361 DIRECTORY_SEARCH_MESSAGE, partitionIndex, 0, partition); 362 mDelayedDirectorySearchHandler.sendMessageDelayed(msg, DIRECTORY_SEARCH_DELAY_MILLIS); 363 } 364 365 /** 366 * Loads the directory partition. 367 */ 368 protected void loadDirectoryPartition(int partitionIndex, DirectoryPartition partition) { 369 Bundle args = new Bundle(); 370 args.putLong(DIRECTORY_ID_ARG_KEY, partition.getDirectoryId()); 371 getLoaderManager().restartLoader(partitionIndex, args, this); 372 } 373 374 /** 375 * Cancels all queued directory loading requests. 376 */ 377 private void removePendingDirectorySearchRequests() { 378 mDelayedDirectorySearchHandler.removeMessages(DIRECTORY_SEARCH_MESSAGE); 379 } 380 381 @Override 382 public void onLoadFinished(Loader<Cursor> loader, Cursor data) { 383 if (!mEnabled) { 384 return; 385 } 386 387 int loaderId = loader.getId(); 388 if (loaderId == DIRECTORY_LOADER_ID) { 389 mDirectoryListStatus = STATUS_LOADED; 390 mAdapter.changeDirectories(data); 391 startLoading(); 392 } else { 393 onPartitionLoaded(loaderId, data); 394 if (isSearchMode()) { 395 int directorySearchMode = getDirectorySearchMode(); 396 if (directorySearchMode != DirectoryListLoader.SEARCH_MODE_NONE) { 397 if (mDirectoryListStatus == STATUS_NOT_LOADED) { 398 mDirectoryListStatus = STATUS_LOADING; 399 getLoaderManager().initLoader(DIRECTORY_LOADER_ID, null, this); 400 } else { 401 startLoading(); 402 } 403 } 404 } else { 405 mDirectoryListStatus = STATUS_NOT_LOADED; 406 getLoaderManager().destroyLoader(DIRECTORY_LOADER_ID); 407 } 408 } 409 } 410 411 public void onLoaderReset(Loader<Cursor> loader) { 412 } 413 414 protected void onPartitionLoaded(int partitionIndex, Cursor data) { 415 if (partitionIndex >= mAdapter.getPartitionCount()) { 416 // When we get unsolicited data, ignore it. This could happen 417 // when we are switching from search mode to the default mode. 418 return; 419 } 420 421 mAdapter.changeCursor(partitionIndex, data); 422 setProfileHeader(); 423 showCount(partitionIndex, data); 424 425 if (!isLoading()) { 426 completeRestoreInstanceState(); 427 } 428 } 429 430 public boolean isLoading() { 431 if (mAdapter != null && mAdapter.isLoading()) { 432 return true; 433 } 434 435 if (isLoadingDirectoryList()) { 436 return true; 437 } 438 439 return false; 440 } 441 442 public boolean isLoadingDirectoryList() { 443 return isSearchMode() && getDirectorySearchMode() != DirectoryListLoader.SEARCH_MODE_NONE 444 && (mDirectoryListStatus == STATUS_NOT_LOADED 445 || mDirectoryListStatus == STATUS_LOADING); 446 } 447 448 @Override 449 public void onStop() { 450 super.onStop(); 451 mContactsPrefs.unregisterChangeListener(); 452 mAdapter.clearPartitions(); 453 } 454 455 protected void reloadData() { 456 removePendingDirectorySearchRequests(); 457 mAdapter.onDataReload(); 458 mLoadPriorityDirectoriesOnly = true; 459 mForceLoad = true; 460 startLoading(); 461 } 462 463 /** 464 * Shows the count of entries included in the list. The default 465 * implementation does nothing. 466 */ 467 protected void showCount(int partitionIndex, Cursor data) { 468 } 469 470 /** 471 * Shows a view at the top of the list with a pseudo local profile prompting the user to add 472 * a local profile. Default implementation does nothing. 473 */ 474 protected void setProfileHeader() { 475 mUserProfileExists = false; 476 } 477 478 /** 479 * Provides logic that dismisses this fragment. The default implementation 480 * does nothing. 481 */ 482 protected void finish() { 483 } 484 485 public void setSectionHeaderDisplayEnabled(boolean flag) { 486 if (mSectionHeaderDisplayEnabled != flag) { 487 mSectionHeaderDisplayEnabled = flag; 488 if (mAdapter != null) { 489 mAdapter.setSectionHeaderDisplayEnabled(flag); 490 } 491 configureVerticalScrollbar(); 492 } 493 } 494 495 public boolean isSectionHeaderDisplayEnabled() { 496 return mSectionHeaderDisplayEnabled; 497 } 498 499 public void setVisibleScrollbarEnabled(boolean flag) { 500 if (mVisibleScrollbarEnabled != flag) { 501 mVisibleScrollbarEnabled = flag; 502 configureVerticalScrollbar(); 503 } 504 } 505 506 public boolean isVisibleScrollbarEnabled() { 507 return mVisibleScrollbarEnabled; 508 } 509 510 public void setVerticalScrollbarPosition(int position) { 511 if (mVerticalScrollbarPosition != position) { 512 mVerticalScrollbarPosition = position; 513 configureVerticalScrollbar(); 514 } 515 } 516 517 private void configureVerticalScrollbar() { 518 boolean hasScrollbar = isVisibleScrollbarEnabled() && isSectionHeaderDisplayEnabled(); 519 520 if (mListView != null) { 521 mListView.setFastScrollEnabled(hasScrollbar); 522 mListView.setFastScrollAlwaysVisible(hasScrollbar); 523 mListView.setVerticalScrollbarPosition(mVerticalScrollbarPosition); 524 mListView.setScrollBarStyle(ListView.SCROLLBARS_OUTSIDE_OVERLAY); 525 int leftPadding = 0; 526 int rightPadding = 0; 527 if (mVerticalScrollbarPosition == View.SCROLLBAR_POSITION_LEFT) { 528 leftPadding = mContext.getResources().getDimensionPixelOffset( 529 R.dimen.list_visible_scrollbar_padding); 530 } else { 531 rightPadding = mContext.getResources().getDimensionPixelOffset( 532 R.dimen.list_visible_scrollbar_padding); 533 } 534 mListView.setPadding(leftPadding, mListView.getPaddingTop(), 535 rightPadding, mListView.getPaddingBottom()); 536 } 537 } 538 539 public void setPhotoLoaderEnabled(boolean flag) { 540 mPhotoLoaderEnabled = flag; 541 configurePhotoLoader(); 542 } 543 544 public boolean isPhotoLoaderEnabled() { 545 return mPhotoLoaderEnabled; 546 } 547 548 /** 549 * Returns true if the list is supposed to visually highlight the selected item. 550 */ 551 public boolean isSelectionVisible() { 552 return mSelectionVisible; 553 } 554 555 public void setSelectionVisible(boolean flag) { 556 this.mSelectionVisible = flag; 557 } 558 559 public void setQuickContactEnabled(boolean flag) { 560 this.mQuickContactEnabled = flag; 561 } 562 563 public void setIncludeProfile(boolean flag) { 564 mIncludeProfile = flag; 565 if(mAdapter != null) { 566 mAdapter.setIncludeProfile(flag); 567 } 568 } 569 570 /** 571 * Enter/exit search mode. By design, a fragment enters search mode only when it has a 572 * non-empty query text, so the mode must be tightly related to the current query. 573 * For this reason this method must only be called by {@link #setQueryString}. 574 * 575 * Also note this method doesn't call {@link #reloadData()}; {@link #setQueryString} does it. 576 */ 577 protected void setSearchMode(boolean flag) { 578 if (mSearchMode != flag) { 579 mSearchMode = flag; 580 setSectionHeaderDisplayEnabled(!mSearchMode); 581 582 if (!flag) { 583 mDirectoryListStatus = STATUS_NOT_LOADED; 584 getLoaderManager().destroyLoader(DIRECTORY_LOADER_ID); 585 } 586 587 if (mAdapter != null) { 588 mAdapter.setPinnedPartitionHeadersEnabled(flag); 589 mAdapter.setSearchMode(flag); 590 591 mAdapter.clearPartitions(); 592 if (!flag) { 593 // If we are switching from search to regular display, remove all directory 594 // partitions after default one, assuming they are remote directories which 595 // should be cleaned up on exiting the search mode. 596 mAdapter.removeDirectoriesAfterDefault(); 597 } 598 mAdapter.configureDefaultPartition(false, flag); 599 } 600 601 if (mListView != null) { 602 mListView.setFastScrollEnabled(!flag); 603 } 604 } 605 } 606 607 public final boolean isSearchMode() { 608 return mSearchMode; 609 } 610 611 public final String getQueryString() { 612 return mQueryString; 613 } 614 615 public void setQueryString(String queryString, boolean delaySelection) { 616 // Normalize the empty query. 617 if (TextUtils.isEmpty(queryString)) queryString = null; 618 619 if (!TextUtils.equals(mQueryString, queryString)) { 620 mQueryString = queryString; 621 setSearchMode(!TextUtils.isEmpty(mQueryString)); 622 623 if (mAdapter != null) { 624 mAdapter.setQueryString(queryString); 625 reloadData(); 626 } 627 } 628 } 629 630 public int getDirectoryLoaderId() { 631 return DIRECTORY_LOADER_ID; 632 } 633 634 public int getDirectorySearchMode() { 635 return mDirectorySearchMode; 636 } 637 638 public void setDirectorySearchMode(int mode) { 639 mDirectorySearchMode = mode; 640 } 641 642 public boolean isLegacyCompatibilityMode() { 643 return mLegacyCompatibility; 644 } 645 646 public void setLegacyCompatibilityMode(boolean flag) { 647 mLegacyCompatibility = flag; 648 } 649 650 protected int getContactNameDisplayOrder() { 651 return mDisplayOrder; 652 } 653 654 protected void setContactNameDisplayOrder(int displayOrder) { 655 mDisplayOrder = displayOrder; 656 if (mAdapter != null) { 657 mAdapter.setContactNameDisplayOrder(displayOrder); 658 } 659 } 660 661 public int getSortOrder() { 662 return mSortOrder; 663 } 664 665 public void setSortOrder(int sortOrder) { 666 mSortOrder = sortOrder; 667 if (mAdapter != null) { 668 mAdapter.setSortOrder(sortOrder); 669 } 670 } 671 672 public void setDirectoryResultLimit(int limit) { 673 mDirectoryResultLimit = limit; 674 } 675 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 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 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 741 protected void configurePhotoLoader() { 742 if (isPhotoLoaderEnabled() && mContext != null) { 743 if (mPhotoManager == null) { 744 mPhotoManager = ContactPhotoManager.getInstance(mContext); 745 } 746 if (mListView != null) { 747 mListView.setOnScrollListener(this); 748 } 749 if (mAdapter != null) { 750 mAdapter.setPhotoLoader(mPhotoManager); 751 } 752 } 753 } 754 755 protected void configureAdapter() { 756 if (mAdapter == null) { 757 return; 758 } 759 760 mAdapter.setQuickContactEnabled(mQuickContactEnabled); 761 mAdapter.setIncludeProfile(mIncludeProfile); 762 mAdapter.setQueryString(mQueryString); 763 mAdapter.setDirectorySearchMode(mDirectorySearchMode); 764 mAdapter.setPinnedPartitionHeadersEnabled(mSearchMode); 765 mAdapter.setContactNameDisplayOrder(mDisplayOrder); 766 mAdapter.setSortOrder(mSortOrder); 767 mAdapter.setSectionHeaderDisplayEnabled(mSectionHeaderDisplayEnabled); 768 mAdapter.setSelectionVisible(mSelectionVisible); 769 mAdapter.setDirectoryResultLimit(mDirectoryResultLimit); 770 mAdapter.setDarkTheme(mDarkTheme); 771 } 772 773 @Override 774 public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, 775 int totalItemCount) { 776 } 777 778 @Override 779 public void onScrollStateChanged(AbsListView view, int scrollState) { 780 if (scrollState == OnScrollListener.SCROLL_STATE_FLING) { 781 mPhotoManager.pause(); 782 } else if (isPhotoLoaderEnabled()) { 783 mPhotoManager.resume(); 784 } 785 } 786 787 @Override 788 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 789 hideSoftKeyboard(); 790 791 int adjPosition = position - mListView.getHeaderViewsCount(); 792 if (adjPosition >= 0) { 793 onItemClick(adjPosition, id); 794 } 795 } 796 797 private void hideSoftKeyboard() { 798 // Hide soft keyboard, if visible 799 InputMethodManager inputMethodManager = (InputMethodManager) 800 mContext.getSystemService(Context.INPUT_METHOD_SERVICE); 801 inputMethodManager.hideSoftInputFromWindow(mListView.getWindowToken(), 0); 802 } 803 804 /** 805 * Dismisses the soft keyboard when the list takes focus. 806 */ 807 @Override 808 public void onFocusChange(View view, boolean hasFocus) { 809 if (view == mListView && hasFocus) { 810 hideSoftKeyboard(); 811 } 812 } 813 814 /** 815 * Dismisses the soft keyboard when the list is touched. 816 */ 817 @Override 818 public boolean onTouch(View view, MotionEvent event) { 819 if (view == mListView) { 820 hideSoftKeyboard(); 821 } 822 return false; 823 } 824 825 @Override 826 public void onPause() { 827 super.onPause(); 828 removePendingDirectorySearchRequests(); 829 } 830 831 /** 832 * Restore the list state after the adapter is populated. 833 */ 834 protected void completeRestoreInstanceState() { 835 if (mListState != null) { 836 mListView.onRestoreInstanceState(mListState); 837 mListState = null; 838 } 839 } 840 841 public void setDarkTheme(boolean value) { 842 mDarkTheme = value; 843 if (mAdapter != null) mAdapter.setDarkTheme(value); 844 } 845 846 /** 847 * Processes a result returned by the contact picker. 848 */ 849 public void onPickerResult(Intent data) { 850 throw new UnsupportedOperationException("Picker result handler is not implemented."); 851 } 852 853 private ContactsPreferences.ChangeListener mPreferencesChangeListener = 854 new ContactsPreferences.ChangeListener() { 855 @Override 856 public void onChange() { 857 loadPreferences(); 858 reloadData(); 859 } 860 }; 861 862 private int getDefaultVerticalScrollbarPosition() { 863 final Locale locale = Locale.getDefault(); 864 final int layoutDirection = TextUtils.getLayoutDirectionFromLocale(locale); 865 switch (layoutDirection) { 866 case View.LAYOUT_DIRECTION_RTL: 867 return View.SCROLLBAR_POSITION_LEFT; 868 case View.LAYOUT_DIRECTION_LTR: 869 default: 870 return View.SCROLLBAR_POSITION_RIGHT; 871 } 872 } 873 } 874