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