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