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