ContactBrowseListFragment.java revision a5a2744ab8102cf4ff5fbd3e1fa074a45257b3dd
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.Loader; 26import android.content.SharedPreferences; 27import android.content.SharedPreferences.Editor; 28import android.database.Cursor; 29import android.net.Uri; 30import android.os.Bundle; 31import android.os.Handler; 32import android.os.Message; 33import android.preference.PreferenceManager; 34import android.provider.ContactsContract; 35import android.provider.ContactsContract.Contacts; 36import android.provider.ContactsContract.Directory; 37import android.text.TextUtils; 38import android.util.Log; 39 40import 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 */ 46public abstract class ContactBrowseListFragment extends 47 ContactEntryListFragment<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_ENABLED = "filterEnabled"; 54 private static final String KEY_FILTER = "filter"; 55 56 private static final String KEY_PERSISTENT_SELECTION_ENABLED = "persistentSelectionEnabled"; 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 boolean mSelectionVerified; 88 private boolean mRefreshingContactUri; 89 private boolean mFilterEnabled; 90 private ContactListFilter mFilter; 91 private boolean mPersistentSelectionEnabled; 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 public void setPersistentSelectionEnabled(boolean flag) { 166 this.mPersistentSelectionEnabled = flag; 167 } 168 169 public void setFilter(ContactListFilter filter) { 170 setFilter(filter, true); 171 } 172 173 public void setFilter(ContactListFilter filter, boolean restoreSelectedUri) { 174 if (mFilter == null && filter == null) { 175 return; 176 } 177 178 if (mFilter != null && mFilter.equals(filter)) { 179 return; 180 } 181 182 Log.v(TAG, "New filter: " + filter); 183 184 mFilter = filter; 185 saveFilter(); 186 if (restoreSelectedUri) { 187 mSelectedContactUri = null; 188 restoreSelectedUri(true); 189 } 190 reloadData(); 191 } 192 193 public ContactListFilter getFilter() { 194 return mFilter; 195 } 196 197 public boolean isFilterEnabled() { 198 return mFilterEnabled; 199 } 200 201 public void setFilterEnabled(boolean flag) { 202 this.mFilterEnabled = flag; 203 } 204 205 @Override 206 public void restoreSavedState(Bundle savedState) { 207 super.restoreSavedState(savedState); 208 209 if (savedState == null) { 210 return; 211 } 212 213 mPersistentSelectionEnabled = savedState.getBoolean(KEY_PERSISTENT_SELECTION_ENABLED); 214 mFilterEnabled = savedState.getBoolean(KEY_FILTER_ENABLED); 215 mFilter = savedState.getParcelable(KEY_FILTER); 216 mSelectedContactUri = savedState.getParcelable(KEY_SELECTED_URI); 217 mSelectionVerified = savedState.getBoolean(KEY_SELECTION_VERIFIED); 218 parseSelectedContactUri(); 219 } 220 221 @Override 222 public void onSaveInstanceState(Bundle outState) { 223 super.onSaveInstanceState(outState); 224 outState.putBoolean(KEY_PERSISTENT_SELECTION_ENABLED, mPersistentSelectionEnabled); 225 outState.putBoolean(KEY_FILTER_ENABLED, mFilterEnabled); 226 outState.putParcelable(KEY_FILTER, mFilter); 227 outState.putParcelable(KEY_SELECTED_URI, mSelectedContactUri); 228 outState.putBoolean(KEY_SELECTION_VERIFIED, mSelectionVerified); 229 } 230 231 protected void refreshSelectedContactUri() { 232 if (mQueryHandler == null) { 233 return; 234 } 235 236 mQueryHandler.cancelOperation(0); 237 238 if (!isSelectionVisible()) { 239 return; 240 } 241 242 mRefreshingContactUri = true; 243 244 if (mSelectedContactUri == null) { 245 onContactUriQueryFinished(null); 246 return; 247 } 248 249 if (mSelectedContactDirectoryId != Directory.DEFAULT 250 && mSelectedContactDirectoryId != Directory.LOCAL_INVISIBLE) { 251 onContactUriQueryFinished(mSelectedContactUri); 252 } else { 253 mQueryHandler.runQuery(); 254 } 255 } 256 257 protected void onContactUriQueryFinished(Uri uri) { 258 mRefreshingContactUri = false; 259 mSelectedContactUri = uri; 260 parseSelectedContactUri(); 261 checkSelection(); 262 } 263 264 @Override 265 protected void prepareEmptyView() { 266 if (isSearchMode()) { 267 return; 268 } else if (isSyncActive()) { 269 if (hasIccCard()) { 270 setEmptyText(R.string.noContactsHelpTextWithSync); 271 } else { 272 setEmptyText(R.string.noContactsNoSimHelpTextWithSync); 273 } 274 } else { 275 if (hasIccCard()) { 276 setEmptyText(R.string.noContactsHelpText); 277 } else { 278 setEmptyText(R.string.noContactsNoSimHelpText); 279 } 280 } 281 } 282 283 public Uri getSelectedContactUri() { 284 return mSelectedContactUri; 285 } 286 287 /** 288 * Sets the new selection for the list. 289 */ 290 public void setSelectedContactUri(Uri uri) { 291 setSelectedContactUri(uri, true, true, true, false); 292 } 293 294 /** 295 * Sets the new contact selection. 296 * 297 * @param uri the new selection 298 * @param required if true, we need to check if the selection is present in 299 * the list and if not notify the listener so that it can load a 300 * different list 301 * @param smoothScroll if true, the UI will roll smoothly to the new 302 * selection 303 * @param persistent if true, the selection will be stored in shared 304 * preferences. 305 * @param willReloadData if true, the selection will be remembered but not 306 * actually shown, because we are expecting that the data will be 307 * reloaded momentarily 308 */ 309 private void setSelectedContactUri(Uri uri, boolean required, boolean smoothScroll, 310 boolean persistent, boolean willReloadData) { 311 mSmoothScrollRequested = smoothScroll; 312 mSelectionToScreenRequested = true; 313 314 if ((mSelectedContactUri == null && uri != null) 315 || (mSelectedContactUri != null && !mSelectedContactUri.equals(uri))) { 316 mSelectionVerified = false; 317 mSelectionRequired = required; 318 mSelectionPersistenceRequested = persistent; 319 mSelectedContactUri = uri; 320 parseSelectedContactUri(); 321 322 if (!willReloadData) { 323 // Configure the adapter to show the selection based on the 324 // lookup key extracted from the URI 325 ContactListAdapter adapter = getAdapter(); 326 if (adapter != null) { 327 adapter.setSelectedContact( 328 mSelectedContactDirectoryId, mSelectedContactLookupKey); 329 getListView().invalidateViews(); 330 } 331 } 332 333 // Also, launch a loader to pick up a new lookup URI in case it has changed 334 refreshSelectedContactUri(); 335 } 336 } 337 338 private void parseSelectedContactUri() { 339 if (mSelectedContactUri != null) { 340 String directoryParam = 341 mSelectedContactUri.getQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY); 342 mSelectedContactDirectoryId = TextUtils.isEmpty(directoryParam) ? Directory.DEFAULT 343 : Long.parseLong(directoryParam); 344 if (mSelectedContactUri.toString().startsWith(Contacts.CONTENT_LOOKUP_URI.toString())) { 345 List<String> pathSegments = mSelectedContactUri.getPathSegments(); 346 mSelectedContactLookupKey = Uri.encode(pathSegments.get(2)); 347 } else { 348 mSelectedContactLookupKey = null; 349 } 350 351 } else { 352 mSelectedContactDirectoryId = Directory.DEFAULT; 353 mSelectedContactLookupKey = null; 354 } 355 } 356 357 @Override 358 protected void configureAdapter() { 359 super.configureAdapter(); 360 361 ContactListAdapter adapter = getAdapter(); 362 if (adapter == null) { 363 return; 364 } 365 366 if (mFilterEnabled && mFilter != null) { 367 adapter.setFilter(mFilter); 368 if (mSelectionRequired 369 || mFilter.filterType == ContactListFilter.FILTER_TYPE_SINGLE_CONTACT) { 370 adapter.setSelectedContact(mSelectedContactDirectoryId, mSelectedContactLookupKey); 371 } 372 } 373 } 374 375 @Override 376 public void onLoadFinished(Loader<Cursor> loader, Cursor data) { 377 super.onLoadFinished(loader, data); 378 mSelectionVerified = false; 379 380 // Refresh the currently selected lookup in case it changed while we were sleeping 381 refreshSelectedContactUri(); 382 } 383 384 @Override 385 public void onLoaderReset(Loader<Cursor> loader) { 386 } 387 388 private void checkSelection() { 389 if (mSelectionVerified) { 390 return; 391 } 392 393 if (mRefreshingContactUri) { 394 return; 395 } 396 397 if (isLoadingDirectoryList()) { 398 return; 399 } 400 401 ContactListAdapter adapter = getAdapter(); 402 if (adapter == null) { 403 return; 404 } 405 406 boolean directoryLoading = true; 407 int count = adapter.getPartitionCount(); 408 for (int i = 0; i < count; i++) { 409 Partition partition = adapter.getPartition(i); 410 if (partition instanceof DirectoryPartition) { 411 DirectoryPartition directory = (DirectoryPartition) partition; 412 if (directory.getDirectoryId() == mSelectedContactDirectoryId) { 413 directoryLoading = directory.isLoading(); 414 break; 415 } 416 } 417 } 418 419 if (directoryLoading) { 420 return; 421 } 422 423 adapter.setSelectedContact(mSelectedContactDirectoryId, mSelectedContactLookupKey); 424 425 int selectedPosition = adapter.getSelectedContactPosition(); 426 if (selectedPosition == -1) { 427 if (mSelectionRequired) { 428 mSelectionRequired = false; 429 notifyInvalidSelection(); 430 return; 431 } 432 433 if (isSearchMode()) { 434 selectFirstFoundContactAfterDelay(); 435 if (mListener != null) { 436 mListener.onSelectionChange(); 437 } 438 return; 439 } 440 441 saveSelectedUri(null); 442 selectDefaultContact(); 443 } 444 445 mSelectionRequired = false; 446 mSelectionVerified = true; 447 448 if (mSelectionPersistenceRequested) { 449 saveSelectedUri(mSelectedContactUri); 450 mSelectionPersistenceRequested = false; 451 } 452 453 if (mSelectionToScreenRequested) { 454 requestSelectionToScreen(); 455 } 456 457 getListView().invalidateViews(); 458 459 if (mListener != null) { 460 mListener.onSelectionChange(); 461 } 462 } 463 464 /** 465 * Automatically selects the first found contact in search mode. The selection 466 * is updated after a delay to allow the user to type without to much UI churn 467 * and to save bandwidth on directory queries. 468 */ 469 public void selectFirstFoundContactAfterDelay() { 470 Handler handler = getHandler(); 471 handler.removeMessages(MESSAGE_AUTOSELECT_FIRST_FOUND_CONTACT); 472 473 String queryString = getQueryString(); 474 if (queryString != null 475 && queryString.length() >= AUTOSELECT_FIRST_FOUND_CONTACT_MIN_QUERY_LENGTH) { 476 handler.sendEmptyMessageDelayed(MESSAGE_AUTOSELECT_FIRST_FOUND_CONTACT, 477 DELAY_AUTOSELECT_FIRST_FOUND_CONTACT_MILLIS); 478 } else { 479 setSelectedContactUri(null, false, false, false, false); 480 } 481 } 482 483 protected void selectDefaultContact() { 484 Uri firstContactUri = getAdapter().getFirstContactUri(); 485 setSelectedContactUri(firstContactUri, false, mSmoothScrollRequested, false, false); 486 } 487 488 protected void requestSelectionToScreen() { 489 int selectedPosition = getAdapter().getSelectedContactPosition(); 490 if (selectedPosition != -1) { 491 AutoScrollListView listView = (AutoScrollListView)getListView(); 492 listView.requestPositionToScreen( 493 selectedPosition + listView.getHeaderViewsCount(), mSmoothScrollRequested); 494 mSelectionToScreenRequested = false; 495 } 496 } 497 498 @Override 499 public boolean isLoading() { 500 return mRefreshingContactUri || super.isLoading(); 501 } 502 503 @Override 504 protected void startLoading() { 505 mStartedLoading = true; 506 mSelectionVerified = false; 507 super.startLoading(); 508 } 509 510 public void reloadDataAndSetSelectedUri(Uri uri) { 511 setSelectedContactUri(uri, true, true, true, true); 512 reloadData(); 513 } 514 515 @Override 516 public void reloadData() { 517 if (mStartedLoading) { 518 mSelectionVerified = false; 519 super.reloadData(); 520 } 521 } 522 523 public void setOnContactListActionListener(OnContactBrowserActionListener listener) { 524 mListener = listener; 525 } 526 527 public void createNewContact() { 528 if (mListener != null) mListener.onCreateNewContactAction(); 529 } 530 531 public void viewContact(Uri contactUri) { 532 setSelectedContactUri(contactUri, false, false, true, false); 533 if (mListener != null) { mListener.onViewContactAction(contactUri); } 534 } 535 536 public void editContact(Uri contactUri) { 537 if (mListener != null) mListener.onEditContactAction(contactUri); 538 } 539 540 public void deleteContact(Uri contactUri) { 541 if (mListener != null) mListener.onDeleteContactAction(contactUri); 542 } 543 544 public void addToFavorites(Uri contactUri) { 545 if (mListener != null) mListener.onAddToFavoritesAction(contactUri); 546 } 547 548 public void removeFromFavorites(Uri contactUri) { 549 if (mListener != null) mListener.onRemoveFromFavoritesAction(contactUri); 550 } 551 552 public void callContact(Uri contactUri) { 553 if (mListener != null) mListener.onCallContactAction(contactUri); 554 } 555 556 public void smsContact(Uri contactUri) { 557 if (mListener != null) mListener.onSmsContactAction(contactUri); 558 } 559 560 private void notifyInvalidSelection() { 561 if (mListener != null) mListener.onInvalidSelection(); 562 } 563 564 @Override 565 protected void finish() { 566 super.finish(); 567 if (mListener != null) mListener.onFinishAction(); 568 } 569 570 private void saveSelectedUri(Uri contactUri) { 571 if (mFilterEnabled) { 572 ContactListFilter.storeToPreferences(mPrefs, mFilter); 573 } 574 575 if (mPersistentSelectionEnabled) { 576 Editor editor = mPrefs.edit(); 577 if (contactUri == null) { 578 editor.remove(getPersistentSelectionKey()); 579 } else { 580 editor.putString(getPersistentSelectionKey(), contactUri.toString()); 581 } 582 editor.apply(); 583 } 584 } 585 586 private void restoreSelectedUri(boolean willReloadData) { 587 if (!mPersistentSelectionEnabled) { 588 return; 589 } 590 591 // The meaning of mSelectionRequired is that we need to show some 592 // selection other than the previous selection saved in shared preferences 593 if (mSelectionRequired) { 594 return; 595 } 596 597 String selectedUri = mPrefs.getString(getPersistentSelectionKey(), null); 598 if (selectedUri == null) { 599 setSelectedContactUri(null, false, false, false, willReloadData); 600 } else { 601 setSelectedContactUri(Uri.parse(selectedUri), false, false, false, willReloadData); 602 } 603 } 604 605 private void saveFilter() { 606 if (mFilterEnabled) { 607 ContactListFilter.storeToPreferences(mPrefs, mFilter); 608 } 609 } 610 611 private void restoreFilter() { 612 if (mFilterEnabled) { 613 mFilter = ContactListFilter.restoreFromPreferences(mPrefs); 614 } 615 } 616 617 private String getPersistentSelectionKey() { 618 if (mFilter == null) { 619 return mPersistentSelectionPrefix; 620 } else { 621 return mPersistentSelectionPrefix + "-" + mFilter.getId(); 622 } 623 } 624 625 public boolean isOptionsMenuChanged() { 626 // This fragment does not have an option menu of its own 627 return false; 628 } 629} 630