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