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