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