ContactBrowseListFragment.java revision 682e152f65a14971d7df191ff849f9db9d50d617
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 (mSelectionRequired) { 439 mSelectionRequired = false; 440 notifyInvalidSelection(); 441 return; 442 } 443 444 if (isSearchMode()) { 445 selectFirstFoundContactAfterDelay(); 446 if (mListener != null) { 447 mListener.onSelectionChange(); 448 } 449 return; 450 } 451 452 saveSelectedUri(null); 453 selectDefaultContact(); 454 } 455 456 mSelectionRequired = false; 457 mSelectionVerified = true; 458 459 if (mSelectionPersistenceRequested) { 460 saveSelectedUri(mSelectedContactUri); 461 mSelectionPersistenceRequested = false; 462 } 463 464 if (mSelectionToScreenRequested) { 465 requestSelectionToScreen(); 466 } 467 468 getListView().invalidateViews(); 469 470 if (mListener != null) { 471 mListener.onSelectionChange(); 472 } 473 } 474 475 /** 476 * Automatically selects the first found contact in search mode. The selection 477 * is updated after a delay to allow the user to type without to much UI churn 478 * and to save bandwidth on directory queries. 479 */ 480 public void selectFirstFoundContactAfterDelay() { 481 Handler handler = getHandler(); 482 handler.removeMessages(MESSAGE_AUTOSELECT_FIRST_FOUND_CONTACT); 483 484 String queryString = getQueryString(); 485 if (queryString != null 486 && queryString.length() >= AUTOSELECT_FIRST_FOUND_CONTACT_MIN_QUERY_LENGTH) { 487 handler.sendEmptyMessageDelayed(MESSAGE_AUTOSELECT_FIRST_FOUND_CONTACT, 488 DELAY_AUTOSELECT_FIRST_FOUND_CONTACT_MILLIS); 489 } else { 490 setSelectedContactUri(null, false, false, false, false); 491 } 492 } 493 494 protected void selectDefaultContact() { 495 Uri contactUri = null; 496 if (mLastSelectedPosition != -1) { 497 contactUri = getAdapter().getContactUri(mLastSelectedPosition); 498 } 499 500 if (contactUri == null) { 501 contactUri = getAdapter().getFirstContactUri(); 502 } 503 504 setSelectedContactUri(contactUri, false, mSmoothScrollRequested, false, false); 505 } 506 507 protected void requestSelectionToScreen() { 508 int selectedPosition = getAdapter().getSelectedContactPosition(); 509 if (selectedPosition != -1) { 510 AutoScrollListView listView = (AutoScrollListView)getListView(); 511 listView.requestPositionToScreen( 512 selectedPosition + listView.getHeaderViewsCount(), mSmoothScrollRequested); 513 mSelectionToScreenRequested = false; 514 } 515 } 516 517 @Override 518 public boolean isLoading() { 519 return mRefreshingContactUri || super.isLoading(); 520 } 521 522 @Override 523 protected void startLoading() { 524 mStartedLoading = true; 525 mSelectionVerified = false; 526 super.startLoading(); 527 } 528 529 public void reloadDataAndSetSelectedUri(Uri uri) { 530 setSelectedContactUri(uri, true, true, true, true); 531 reloadData(); 532 } 533 534 @Override 535 public void reloadData() { 536 if (mStartedLoading) { 537 mSelectionVerified = false; 538 super.reloadData(); 539 } 540 } 541 542 public void setOnContactListActionListener(OnContactBrowserActionListener listener) { 543 mListener = listener; 544 } 545 546 public void createNewContact() { 547 if (mListener != null) mListener.onCreateNewContactAction(); 548 } 549 550 public void viewContact(Uri contactUri) { 551 setSelectedContactUri(contactUri, false, false, true, false); 552 if (mListener != null) { mListener.onViewContactAction(contactUri); } 553 } 554 555 public void editContact(Uri contactUri) { 556 if (mListener != null) mListener.onEditContactAction(contactUri); 557 } 558 559 public void deleteContact(Uri contactUri) { 560 if (mListener != null) mListener.onDeleteContactAction(contactUri); 561 } 562 563 public void addToFavorites(Uri contactUri) { 564 if (mListener != null) mListener.onAddToFavoritesAction(contactUri); 565 } 566 567 public void removeFromFavorites(Uri contactUri) { 568 if (mListener != null) mListener.onRemoveFromFavoritesAction(contactUri); 569 } 570 571 public void callContact(Uri contactUri) { 572 if (mListener != null) mListener.onCallContactAction(contactUri); 573 } 574 575 public void smsContact(Uri contactUri) { 576 if (mListener != null) mListener.onSmsContactAction(contactUri); 577 } 578 579 private void notifyInvalidSelection() { 580 if (mListener != null) mListener.onInvalidSelection(); 581 } 582 583 @Override 584 protected void finish() { 585 super.finish(); 586 if (mListener != null) mListener.onFinishAction(); 587 } 588 589 private void saveSelectedUri(Uri contactUri) { 590 if (isSearchMode()) { 591 return; 592 } 593 594 ContactListFilter.storeToPreferences(mPrefs, mFilter); 595 596 Editor editor = mPrefs.edit(); 597 if (contactUri == null) { 598 editor.remove(getPersistentSelectionKey()); 599 } else { 600 editor.putString(getPersistentSelectionKey(), contactUri.toString()); 601 } 602 editor.apply(); 603 } 604 605 private void restoreSelectedUri(boolean willReloadData) { 606 // The meaning of mSelectionRequired is that we need to show some 607 // selection other than the previous selection saved in shared preferences 608 if (mSelectionRequired) { 609 return; 610 } 611 612 String selectedUri = mPrefs.getString(getPersistentSelectionKey(), null); 613 if (selectedUri == null) { 614 setSelectedContactUri(null, false, false, false, willReloadData); 615 } else { 616 setSelectedContactUri(Uri.parse(selectedUri), false, false, false, willReloadData); 617 } 618 } 619 620 private void saveFilter() { 621 ContactListFilter.storeToPreferences(mPrefs, mFilter); 622 } 623 624 private void restoreFilter() { 625 mFilter = ContactListFilter.restoreFromPreferences(mPrefs); 626 if (mFilter == null) { 627 mFilter = new ContactListFilter(ContactListFilter.FILTER_TYPE_ALL_ACCOUNTS); 628 } 629 } 630 631 private String getPersistentSelectionKey() { 632 if (mFilter == null) { 633 return mPersistentSelectionPrefix; 634 } else { 635 return mPersistentSelectionPrefix + "-" + mFilter.getId(); 636 } 637 } 638 639 public boolean isOptionsMenuChanged() { 640 // This fragment does not have an option menu of its own 641 return false; 642 } 643} 644