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