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