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