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