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