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