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