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