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