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