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 */ 16package com.android.contacts.list; 17 18import android.app.Activity; 19import android.content.ContentResolver; 20import android.content.ContentUris; 21import android.content.Loader; 22import android.content.SharedPreferences; 23import android.content.SharedPreferences.Editor; 24import android.database.Cursor; 25import android.net.Uri; 26import android.os.AsyncTask; 27import android.os.Bundle; 28import android.os.Handler; 29import android.os.Message; 30import android.preference.PreferenceManager; 31import android.provider.ContactsContract; 32import android.provider.ContactsContract.Contacts; 33import android.provider.ContactsContract.Directory; 34import android.text.TextUtils; 35import android.util.Log; 36 37import com.android.common.widget.CompositeCursorAdapter.Partition; 38import com.android.contacts.R; 39import com.android.contacts.util.ContactLoaderUtils; 40import com.android.contacts.widget.AutoScrollListView; 41 42import java.util.List; 43 44/** 45 * Fragment containing a contact list used for browsing (as compared to 46 * picking a contact with one of the PICK intents). 47 */ 48public abstract class ContactBrowseListFragment extends 49 ContactEntryListFragment<ContactListAdapter> { 50 51 private static final String TAG = "ContactList"; 52 53 private static final String KEY_SELECTED_URI = "selectedUri"; 54 private static final String KEY_SELECTION_VERIFIED = "selectionVerified"; 55 private static final String KEY_FILTER = "filter"; 56 private static final String KEY_LAST_SELECTED_POSITION = "lastSelected"; 57 58 private static final String PERSISTENT_SELECTION_PREFIX = "defaultContactBrowserSelection"; 59 60 /** 61 * The id for a delayed message that triggers automatic selection of the first 62 * found contact in search mode. 63 */ 64 private static final int MESSAGE_AUTOSELECT_FIRST_FOUND_CONTACT = 1; 65 66 /** 67 * The delay that is used for automatically selecting the first found contact. 68 */ 69 private static final int DELAY_AUTOSELECT_FIRST_FOUND_CONTACT_MILLIS = 500; 70 71 /** 72 * The minimum number of characters in the search query that is required 73 * before we automatically select the first found contact. 74 */ 75 private static final int AUTOSELECT_FIRST_FOUND_CONTACT_MIN_QUERY_LENGTH = 2; 76 77 private SharedPreferences mPrefs; 78 private Handler mHandler; 79 80 private boolean mStartedLoading; 81 private boolean mSelectionRequired; 82 private boolean mSelectionToScreenRequested; 83 private boolean mSmoothScrollRequested; 84 private boolean mSelectionPersistenceRequested; 85 private Uri mSelectedContactUri; 86 private long mSelectedContactDirectoryId; 87 private String mSelectedContactLookupKey; 88 private long mSelectedContactId; 89 private boolean mSelectionVerified; 90 private int mLastSelectedPosition = -1; 91 private boolean mRefreshingContactUri; 92 private ContactListFilter mFilter; 93 private String mPersistentSelectionPrefix = PERSISTENT_SELECTION_PREFIX; 94 95 protected OnContactBrowserActionListener mListener; 96 private ContactLookupTask mContactLookupTask; 97 98 private final class ContactLookupTask extends AsyncTask<Void, Void, Uri> { 99 100 private final Uri mUri; 101 private boolean mIsCancelled; 102 103 public ContactLookupTask(Uri uri) { 104 mUri = uri; 105 } 106 107 @Override 108 protected Uri doInBackground(Void... args) { 109 Cursor cursor = null; 110 try { 111 final ContentResolver resolver = getContext().getContentResolver(); 112 final Uri uriCurrentFormat = ContactLoaderUtils.ensureIsContactUri(resolver, mUri); 113 cursor = resolver.query(uriCurrentFormat, 114 new String[] { Contacts._ID, Contacts.LOOKUP_KEY }, null, null, null); 115 116 if (cursor != null && cursor.moveToFirst()) { 117 final long contactId = cursor.getLong(0); 118 final String lookupKey = cursor.getString(1); 119 if (contactId != 0 && !TextUtils.isEmpty(lookupKey)) { 120 return Contacts.getLookupUri(contactId, lookupKey); 121 } 122 } 123 124 Log.e(TAG, "Error: No contact ID or lookup key for contact " + mUri); 125 return null; 126 } finally { 127 if (cursor != null) { 128 cursor.close(); 129 } 130 } 131 } 132 133 public void cancel() { 134 super.cancel(true); 135 // Use a flag to keep track of whether the {@link AsyncTask} was cancelled or not in 136 // order to ensure onPostExecute() is not executed after the cancel request. The flag is 137 // necessary because {@link AsyncTask} still calls onPostExecute() if the cancel request 138 // came after the worker thread was finished. 139 mIsCancelled = true; 140 } 141 142 @Override 143 protected void onPostExecute(Uri uri) { 144 // Make sure the {@link Fragment} is at least still attached to the {@link Activity} 145 // before continuing. Null URIs should still be allowed so that the list can be 146 // refreshed and a default contact can be selected (i.e. the case of deleted 147 // contacts). 148 if (mIsCancelled || !isAdded()) { 149 return; 150 } 151 onContactUriQueryFinished(uri); 152 } 153 } 154 155 private boolean mDelaySelection; 156 157 private Handler getHandler() { 158 if (mHandler == null) { 159 mHandler = new Handler() { 160 @Override 161 public void handleMessage(Message msg) { 162 switch (msg.what) { 163 case MESSAGE_AUTOSELECT_FIRST_FOUND_CONTACT: 164 selectDefaultContact(); 165 break; 166 } 167 } 168 }; 169 } 170 return mHandler; 171 } 172 173 @Override 174 public void onAttach(Activity activity) { 175 super.onAttach(activity); 176 mPrefs = PreferenceManager.getDefaultSharedPreferences(activity); 177 restoreFilter(); 178 restoreSelectedUri(false); 179 } 180 181 @Override 182 protected void setSearchMode(boolean flag) { 183 if (isSearchMode() != flag) { 184 if (!flag) { 185 restoreSelectedUri(true); 186 } 187 super.setSearchMode(flag); 188 } 189 } 190 191 public void setFilter(ContactListFilter filter) { 192 setFilter(filter, true); 193 } 194 195 public void setFilter(ContactListFilter filter, boolean restoreSelectedUri) { 196 if (mFilter == null && filter == null) { 197 return; 198 } 199 200 if (mFilter != null && mFilter.equals(filter)) { 201 return; 202 } 203 204 Log.v(TAG, "New filter: " + filter); 205 206 mFilter = filter; 207 mLastSelectedPosition = -1; 208 saveFilter(); 209 if (restoreSelectedUri) { 210 mSelectedContactUri = null; 211 restoreSelectedUri(true); 212 } 213 reloadData(); 214 } 215 216 public ContactListFilter getFilter() { 217 return mFilter; 218 } 219 220 @Override 221 public void restoreSavedState(Bundle savedState) { 222 super.restoreSavedState(savedState); 223 224 if (savedState == null) { 225 return; 226 } 227 228 mFilter = savedState.getParcelable(KEY_FILTER); 229 mSelectedContactUri = savedState.getParcelable(KEY_SELECTED_URI); 230 mSelectionVerified = savedState.getBoolean(KEY_SELECTION_VERIFIED); 231 mLastSelectedPosition = savedState.getInt(KEY_LAST_SELECTED_POSITION); 232 parseSelectedContactUri(); 233 } 234 235 @Override 236 public void onSaveInstanceState(Bundle outState) { 237 super.onSaveInstanceState(outState); 238 outState.putParcelable(KEY_FILTER, mFilter); 239 outState.putParcelable(KEY_SELECTED_URI, mSelectedContactUri); 240 outState.putBoolean(KEY_SELECTION_VERIFIED, mSelectionVerified); 241 outState.putInt(KEY_LAST_SELECTED_POSITION, mLastSelectedPosition); 242 } 243 244 protected void refreshSelectedContactUri() { 245 if (mContactLookupTask != null) { 246 mContactLookupTask.cancel(); 247 } 248 249 if (!isSelectionVisible()) { 250 return; 251 } 252 253 mRefreshingContactUri = true; 254 255 if (mSelectedContactUri == null) { 256 onContactUriQueryFinished(null); 257 return; 258 } 259 260 if (mSelectedContactDirectoryId != Directory.DEFAULT 261 && mSelectedContactDirectoryId != Directory.LOCAL_INVISIBLE) { 262 onContactUriQueryFinished(mSelectedContactUri); 263 } else { 264 mContactLookupTask = new ContactLookupTask(mSelectedContactUri); 265 mContactLookupTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Void[])null); 266 } 267 } 268 269 protected void onContactUriQueryFinished(Uri uri) { 270 mRefreshingContactUri = false; 271 mSelectedContactUri = uri; 272 parseSelectedContactUri(); 273 checkSelection(); 274 } 275 276 @Override 277 protected void prepareEmptyView() { 278 if (isSearchMode()) { 279 return; 280 } else if (isSyncActive()) { 281 if (hasIccCard()) { 282 setEmptyText(R.string.noContactsHelpTextWithSync); 283 } else { 284 setEmptyText(R.string.noContactsNoSimHelpTextWithSync); 285 } 286 } else { 287 if (hasIccCard()) { 288 setEmptyText(R.string.noContactsHelpText); 289 } else { 290 setEmptyText(R.string.noContactsNoSimHelpText); 291 } 292 } 293 } 294 295 public Uri getSelectedContactUri() { 296 return mSelectedContactUri; 297 } 298 299 /** 300 * Sets the new selection for the list. 301 */ 302 public void setSelectedContactUri(Uri uri) { 303 setSelectedContactUri(uri, true, false /* no smooth scroll */, true, false); 304 } 305 306 @Override 307 public void setQueryString(String queryString, boolean delaySelection) { 308 mDelaySelection = delaySelection; 309 super.setQueryString(queryString, delaySelection); 310 } 311 312 /** 313 * Sets whether or not a contact selection must be made. 314 * @param required if true, we need to check if the selection is present in 315 * the list and if not notify the listener so that it can load a 316 * different list. 317 * TODO: Figure out how to reconcile this with {@link #setSelectedContactUri}, 318 * without causing unnecessary loading of the list if the selected contact URI is 319 * the same as before. 320 */ 321 public void setSelectionRequired(boolean required) { 322 mSelectionRequired = required; 323 } 324 325 /** 326 * Sets the new contact selection. 327 * 328 * @param uri the new selection 329 * @param required if true, we need to check if the selection is present in 330 * the list and if not notify the listener so that it can load a 331 * different list 332 * @param smoothScroll if true, the UI will roll smoothly to the new 333 * selection 334 * @param persistent if true, the selection will be stored in shared 335 * preferences. 336 * @param willReloadData if true, the selection will be remembered but not 337 * actually shown, because we are expecting that the data will be 338 * reloaded momentarily 339 */ 340 private void setSelectedContactUri(Uri uri, boolean required, boolean smoothScroll, 341 boolean persistent, boolean willReloadData) { 342 mSmoothScrollRequested = smoothScroll; 343 mSelectionToScreenRequested = true; 344 345 if ((mSelectedContactUri == null && uri != null) 346 || (mSelectedContactUri != null && !mSelectedContactUri.equals(uri))) { 347 mSelectionVerified = false; 348 mSelectionRequired = required; 349 mSelectionPersistenceRequested = persistent; 350 mSelectedContactUri = uri; 351 parseSelectedContactUri(); 352 353 if (!willReloadData) { 354 // Configure the adapter to show the selection based on the 355 // lookup key extracted from the URI 356 ContactListAdapter adapter = getAdapter(); 357 if (adapter != null) { 358 adapter.setSelectedContact(mSelectedContactDirectoryId, 359 mSelectedContactLookupKey, mSelectedContactId); 360 getListView().invalidateViews(); 361 } 362 } 363 364 // Also, launch a loader to pick up a new lookup URI in case it has changed 365 refreshSelectedContactUri(); 366 } 367 } 368 369 private void parseSelectedContactUri() { 370 if (mSelectedContactUri != null) { 371 String directoryParam = 372 mSelectedContactUri.getQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY); 373 mSelectedContactDirectoryId = TextUtils.isEmpty(directoryParam) ? Directory.DEFAULT 374 : Long.parseLong(directoryParam); 375 if (mSelectedContactUri.toString().startsWith(Contacts.CONTENT_LOOKUP_URI.toString())) { 376 List<String> pathSegments = mSelectedContactUri.getPathSegments(); 377 mSelectedContactLookupKey = Uri.encode(pathSegments.get(2)); 378 if (pathSegments.size() == 4) { 379 mSelectedContactId = ContentUris.parseId(mSelectedContactUri); 380 } 381 } else if (mSelectedContactUri.toString().startsWith(Contacts.CONTENT_URI.toString()) && 382 mSelectedContactUri.getPathSegments().size() >= 2) { 383 mSelectedContactLookupKey = null; 384 mSelectedContactId = ContentUris.parseId(mSelectedContactUri); 385 } else { 386 Log.e(TAG, "Unsupported contact URI: " + mSelectedContactUri); 387 mSelectedContactLookupKey = null; 388 mSelectedContactId = 0; 389 } 390 391 } else { 392 mSelectedContactDirectoryId = Directory.DEFAULT; 393 mSelectedContactLookupKey = null; 394 mSelectedContactId = 0; 395 } 396 } 397 398 @Override 399 protected void configureAdapter() { 400 super.configureAdapter(); 401 402 ContactListAdapter adapter = getAdapter(); 403 if (adapter == null) { 404 return; 405 } 406 407 boolean searchMode = isSearchMode(); 408 if (!searchMode && mFilter != null) { 409 adapter.setFilter(mFilter); 410 if (mSelectionRequired 411 || mFilter.filterType == ContactListFilter.FILTER_TYPE_SINGLE_CONTACT) { 412 adapter.setSelectedContact( 413 mSelectedContactDirectoryId, mSelectedContactLookupKey, mSelectedContactId); 414 } 415 } 416 417 // Display the user's profile if not in search mode 418 adapter.setIncludeProfile(!searchMode); 419 } 420 421 @Override 422 public void onLoadFinished(Loader<Cursor> loader, Cursor data) { 423 super.onLoadFinished(loader, data); 424 mSelectionVerified = false; 425 426 // Refresh the currently selected lookup in case it changed while we were sleeping 427 refreshSelectedContactUri(); 428 } 429 430 @Override 431 public void onLoaderReset(Loader<Cursor> loader) { 432 } 433 434 private void checkSelection() { 435 if (mSelectionVerified) { 436 return; 437 } 438 439 if (mRefreshingContactUri) { 440 return; 441 } 442 443 if (isLoadingDirectoryList()) { 444 return; 445 } 446 447 ContactListAdapter adapter = getAdapter(); 448 if (adapter == null) { 449 return; 450 } 451 452 boolean directoryLoading = true; 453 int count = adapter.getPartitionCount(); 454 for (int i = 0; i < count; i++) { 455 Partition partition = adapter.getPartition(i); 456 if (partition instanceof DirectoryPartition) { 457 DirectoryPartition directory = (DirectoryPartition) partition; 458 if (directory.getDirectoryId() == mSelectedContactDirectoryId) { 459 directoryLoading = directory.isLoading(); 460 break; 461 } 462 } 463 } 464 465 if (directoryLoading) { 466 return; 467 } 468 469 adapter.setSelectedContact( 470 mSelectedContactDirectoryId, mSelectedContactLookupKey, mSelectedContactId); 471 472 final int selectedPosition = adapter.getSelectedContactPosition(); 473 if (selectedPosition != -1) { 474 mLastSelectedPosition = selectedPosition; 475 } else { 476 if (isSearchMode()) { 477 if (mDelaySelection) { 478 selectFirstFoundContactAfterDelay(); 479 if (mListener != null) { 480 mListener.onSelectionChange(); 481 } 482 return; 483 } 484 } else if (mSelectionRequired) { 485 // A specific contact was requested, but it's not in the loaded list. 486 487 // Try reconfiguring and reloading the list that will hopefully contain 488 // the requested contact. Only take one attempt to avoid an infinite loop 489 // in case the contact cannot be found at all. 490 mSelectionRequired = false; 491 492 // If we were looking at a different specific contact, just reload 493 // FILTER_TYPE_ALL_ACCOUNTS is needed for the case where a new contact is added 494 // on a tablet and the loader is returning a stale list. In this case, the contact 495 // will not be found until the next load. b/7621855 This will only fix the most 496 // common case where all accounts are shown. It will not fix the one account case. 497 // TODO: we may want to add more FILTER_TYPEs or relax this check to fix all other 498 // FILTER_TYPE cases. 499 if (mFilter != null 500 && (mFilter.filterType == ContactListFilter.FILTER_TYPE_SINGLE_CONTACT 501 || mFilter.filterType == ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS)) { 502 reloadData(); 503 } else { 504 // Otherwise, call the listener, which will adjust the filter. 505 notifyInvalidSelection(); 506 } 507 return; 508 } else if (mFilter != null 509 && mFilter.filterType == ContactListFilter.FILTER_TYPE_SINGLE_CONTACT) { 510 // If we were trying to load a specific contact, but that contact no longer 511 // exists, call the listener, which will adjust the filter. 512 notifyInvalidSelection(); 513 return; 514 } 515 516 saveSelectedUri(null); 517 selectDefaultContact(); 518 } 519 520 mSelectionRequired = false; 521 mSelectionVerified = true; 522 523 if (mSelectionPersistenceRequested) { 524 saveSelectedUri(mSelectedContactUri); 525 mSelectionPersistenceRequested = false; 526 } 527 528 if (mSelectionToScreenRequested) { 529 requestSelectionToScreen(selectedPosition); 530 } 531 532 getListView().invalidateViews(); 533 534 if (mListener != null) { 535 mListener.onSelectionChange(); 536 } 537 } 538 539 /** 540 * Automatically selects the first found contact in search mode. The selection 541 * is updated after a delay to allow the user to type without to much UI churn 542 * and to save bandwidth on directory queries. 543 */ 544 public void selectFirstFoundContactAfterDelay() { 545 Handler handler = getHandler(); 546 handler.removeMessages(MESSAGE_AUTOSELECT_FIRST_FOUND_CONTACT); 547 548 String queryString = getQueryString(); 549 if (queryString != null 550 && queryString.length() >= AUTOSELECT_FIRST_FOUND_CONTACT_MIN_QUERY_LENGTH) { 551 handler.sendEmptyMessageDelayed(MESSAGE_AUTOSELECT_FIRST_FOUND_CONTACT, 552 DELAY_AUTOSELECT_FIRST_FOUND_CONTACT_MILLIS); 553 } else { 554 setSelectedContactUri(null, false, false, false, false); 555 } 556 } 557 558 protected void selectDefaultContact() { 559 Uri contactUri = null; 560 ContactListAdapter adapter = getAdapter(); 561 if (mLastSelectedPosition != -1) { 562 int count = adapter.getCount(); 563 int pos = mLastSelectedPosition; 564 if (pos >= count && count > 0) { 565 pos = count - 1; 566 } 567 contactUri = adapter.getContactUri(pos); 568 } 569 570 if (contactUri == null) { 571 contactUri = adapter.getFirstContactUri(); 572 } 573 574 setSelectedContactUri(contactUri, false, mSmoothScrollRequested, false, false); 575 } 576 577 protected void requestSelectionToScreen(int selectedPosition) { 578 if (selectedPosition != -1) { 579 AutoScrollListView listView = (AutoScrollListView)getListView(); 580 listView.requestPositionToScreen( 581 selectedPosition + listView.getHeaderViewsCount(), mSmoothScrollRequested); 582 mSelectionToScreenRequested = false; 583 } 584 } 585 586 @Override 587 public boolean isLoading() { 588 return mRefreshingContactUri || super.isLoading(); 589 } 590 591 @Override 592 protected void startLoading() { 593 mStartedLoading = true; 594 mSelectionVerified = false; 595 super.startLoading(); 596 } 597 598 public void reloadDataAndSetSelectedUri(Uri uri) { 599 setSelectedContactUri(uri, true, true, true, true); 600 reloadData(); 601 } 602 603 @Override 604 public void reloadData() { 605 if (mStartedLoading) { 606 mSelectionVerified = false; 607 mLastSelectedPosition = -1; 608 super.reloadData(); 609 } 610 } 611 612 public void setOnContactListActionListener(OnContactBrowserActionListener listener) { 613 mListener = listener; 614 } 615 616 public void createNewContact() { 617 if (mListener != null) mListener.onCreateNewContactAction(); 618 } 619 620 public void viewContact(Uri contactUri) { 621 setSelectedContactUri(contactUri, false, false, true, false); 622 if (mListener != null) mListener.onViewContactAction(contactUri); 623 } 624 625 public void editContact(Uri contactUri) { 626 if (mListener != null) mListener.onEditContactAction(contactUri); 627 } 628 629 public void deleteContact(Uri contactUri) { 630 if (mListener != null) mListener.onDeleteContactAction(contactUri); 631 } 632 633 public void addToFavorites(Uri contactUri) { 634 if (mListener != null) mListener.onAddToFavoritesAction(contactUri); 635 } 636 637 public void removeFromFavorites(Uri contactUri) { 638 if (mListener != null) mListener.onRemoveFromFavoritesAction(contactUri); 639 } 640 641 public void callContact(Uri contactUri) { 642 if (mListener != null) mListener.onCallContactAction(contactUri); 643 } 644 645 public void smsContact(Uri contactUri) { 646 if (mListener != null) mListener.onSmsContactAction(contactUri); 647 } 648 649 private void notifyInvalidSelection() { 650 if (mListener != null) mListener.onInvalidSelection(); 651 } 652 653 @Override 654 protected void finish() { 655 super.finish(); 656 if (mListener != null) mListener.onFinishAction(); 657 } 658 659 private void saveSelectedUri(Uri contactUri) { 660 if (isSearchMode()) { 661 return; 662 } 663 664 ContactListFilter.storeToPreferences(mPrefs, mFilter); 665 666 Editor editor = mPrefs.edit(); 667 if (contactUri == null) { 668 editor.remove(getPersistentSelectionKey()); 669 } else { 670 editor.putString(getPersistentSelectionKey(), contactUri.toString()); 671 } 672 editor.apply(); 673 } 674 675 private void restoreSelectedUri(boolean willReloadData) { 676 // The meaning of mSelectionRequired is that we need to show some 677 // selection other than the previous selection saved in shared preferences 678 if (mSelectionRequired) { 679 return; 680 } 681 682 String selectedUri = mPrefs.getString(getPersistentSelectionKey(), null); 683 if (selectedUri == null) { 684 setSelectedContactUri(null, false, false, false, willReloadData); 685 } else { 686 setSelectedContactUri(Uri.parse(selectedUri), false, false, false, willReloadData); 687 } 688 } 689 690 private void saveFilter() { 691 ContactListFilter.storeToPreferences(mPrefs, mFilter); 692 } 693 694 private void restoreFilter() { 695 mFilter = ContactListFilter.restoreDefaultPreferences(mPrefs); 696 } 697 698 private String getPersistentSelectionKey() { 699 if (mFilter == null) { 700 return mPersistentSelectionPrefix; 701 } else { 702 return mPersistentSelectionPrefix + "-" + mFilter.getId(); 703 } 704 } 705 706 public boolean isOptionsMenuChanged() { 707 // This fragment does not have an option menu of its own 708 return false; 709 } 710} 711