1 /* 2 * Copyright (C) 2007 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 */ 16 17 package com.android.contacts; 18 19 import com.android.contacts.TextHighlightingAnimation.TextWithHighlighting; 20 import com.android.contacts.model.ContactsSource; 21 import com.android.contacts.model.Sources; 22 import com.android.contacts.ui.ContactsPreferences; 23 import com.android.contacts.ui.ContactsPreferencesActivity; 24 import com.android.contacts.ui.ContactsPreferencesActivity.Prefs; 25 import com.android.contacts.util.AccountSelectionUtil; 26 import com.android.contacts.util.Constants; 27 28 import android.accounts.Account; 29 import android.accounts.AccountManager; 30 import android.app.Activity; 31 import android.app.AlertDialog; 32 import android.app.Dialog; 33 import android.app.ListActivity; 34 import android.app.SearchManager; 35 import android.content.AsyncQueryHandler; 36 import android.content.ContentResolver; 37 import android.content.ContentUris; 38 import android.content.ContentValues; 39 import android.content.Context; 40 import android.content.DialogInterface; 41 import android.content.IContentService; 42 import android.content.Intent; 43 import android.content.SharedPreferences; 44 import android.content.UriMatcher; 45 import android.content.res.ColorStateList; 46 import android.content.res.Resources; 47 import android.database.CharArrayBuffer; 48 import android.database.ContentObserver; 49 import android.database.Cursor; 50 import android.database.MatrixCursor; 51 import android.graphics.Bitmap; 52 import android.graphics.BitmapFactory; 53 import android.graphics.Canvas; 54 import android.graphics.Color; 55 import android.graphics.Paint; 56 import android.graphics.Rect; 57 import android.graphics.Typeface; 58 import android.graphics.drawable.BitmapDrawable; 59 import android.graphics.drawable.Drawable; 60 import android.net.Uri; 61 import android.net.Uri.Builder; 62 import android.os.Bundle; 63 import android.os.Handler; 64 import android.os.Parcelable; 65 import android.os.RemoteException; 66 import android.preference.PreferenceManager; 67 import android.provider.ContactsContract; 68 import android.provider.Settings; 69 import android.provider.Contacts.ContactMethods; 70 import android.provider.Contacts.People; 71 import android.provider.Contacts.PeopleColumns; 72 import android.provider.Contacts.Phones; 73 import android.provider.ContactsContract.ContactCounts; 74 import android.provider.ContactsContract.Contacts; 75 import android.provider.ContactsContract.Data; 76 import android.provider.ContactsContract.Intents; 77 import android.provider.ContactsContract.ProviderStatus; 78 import android.provider.ContactsContract.RawContacts; 79 import android.provider.ContactsContract.SearchSnippetColumns; 80 import android.provider.ContactsContract.CommonDataKinds.Email; 81 import android.provider.ContactsContract.CommonDataKinds.Nickname; 82 import android.provider.ContactsContract.CommonDataKinds.Organization; 83 import android.provider.ContactsContract.CommonDataKinds.Phone; 84 import android.provider.ContactsContract.CommonDataKinds.Photo; 85 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; 86 import android.provider.ContactsContract.Contacts.AggregationSuggestions; 87 import android.provider.ContactsContract.Intents.Insert; 88 import android.provider.ContactsContract.Intents.UI; 89 import android.telephony.TelephonyManager; 90 import android.text.Editable; 91 import android.text.Html; 92 import android.text.TextUtils; 93 import android.text.TextWatcher; 94 import android.util.Log; 95 import android.view.ContextMenu; 96 import android.view.ContextThemeWrapper; 97 import android.view.KeyEvent; 98 import android.view.LayoutInflater; 99 import android.view.Menu; 100 import android.view.MenuInflater; 101 import android.view.MenuItem; 102 import android.view.MotionEvent; 103 import android.view.View; 104 import android.view.ViewGroup; 105 import android.view.ContextMenu.ContextMenuInfo; 106 import android.view.View.OnClickListener; 107 import android.view.View.OnFocusChangeListener; 108 import android.view.View.OnTouchListener; 109 import android.view.inputmethod.EditorInfo; 110 import android.view.inputmethod.InputMethodManager; 111 import android.widget.AbsListView; 112 import android.widget.AdapterView; 113 import android.widget.ArrayAdapter; 114 import android.widget.Button; 115 import android.widget.CursorAdapter; 116 import android.widget.Filter; 117 import android.widget.ImageView; 118 import android.widget.ListView; 119 import android.widget.QuickContactBadge; 120 import android.widget.SectionIndexer; 121 import android.widget.TextView; 122 import android.widget.Toast; 123 import android.widget.AbsListView.OnScrollListener; 124 125 import java.lang.ref.WeakReference; 126 import java.util.ArrayList; 127 import java.util.List; 128 import java.util.Random; 129 130 /** 131 * Displays a list of contacts. Usually is embedded into the ContactsActivity. 132 */ 133 @SuppressWarnings("deprecation") 134 public class ContactsListActivity extends ListActivity implements View.OnCreateContextMenuListener, 135 View.OnClickListener, View.OnKeyListener, TextWatcher, TextView.OnEditorActionListener, 136 OnFocusChangeListener, OnTouchListener { 137 138 public static class JoinContactActivity extends ContactsListActivity { 139 140 } 141 142 public static class ContactsSearchActivity extends ContactsListActivity { 143 144 } 145 146 private static final String TAG = "ContactsListActivity"; 147 148 private static final boolean ENABLE_ACTION_ICON_OVERLAYS = true; 149 150 private static final String LIST_STATE_KEY = "liststate"; 151 private static final String SHORTCUT_ACTION_KEY = "shortcutAction"; 152 153 static final int MENU_ITEM_VIEW_CONTACT = 1; 154 static final int MENU_ITEM_CALL = 2; 155 static final int MENU_ITEM_EDIT_BEFORE_CALL = 3; 156 static final int MENU_ITEM_SEND_SMS = 4; 157 static final int MENU_ITEM_SEND_IM = 5; 158 static final int MENU_ITEM_EDIT = 6; 159 static final int MENU_ITEM_DELETE = 7; 160 static final int MENU_ITEM_TOGGLE_STAR = 8; 161 162 private static final int SUBACTIVITY_NEW_CONTACT = 1; 163 private static final int SUBACTIVITY_VIEW_CONTACT = 2; 164 private static final int SUBACTIVITY_DISPLAY_GROUP = 3; 165 private static final int SUBACTIVITY_SEARCH = 4; 166 private static final int SUBACTIVITY_FILTER = 5; 167 168 private static final int TEXT_HIGHLIGHTING_ANIMATION_DURATION = 350; 169 170 /** 171 * The action for the join contact activity. 172 * <p> 173 * Input: extra field {@link #EXTRA_AGGREGATE_ID} is the aggregate ID. 174 * 175 * TODO: move to {@link ContactsContract}. 176 */ 177 public static final String JOIN_AGGREGATE = 178 "com.android.contacts.action.JOIN_AGGREGATE"; 179 180 /** 181 * Used with {@link #JOIN_AGGREGATE} to give it the target for aggregation. 182 * <p> 183 * Type: LONG 184 */ 185 public static final String EXTRA_AGGREGATE_ID = 186 "com.android.contacts.action.AGGREGATE_ID"; 187 188 /** 189 * Used with {@link #JOIN_AGGREGATE} to give it the name of the aggregation target. 190 * <p> 191 * Type: STRING 192 */ 193 @Deprecated 194 public static final String EXTRA_AGGREGATE_NAME = 195 "com.android.contacts.action.AGGREGATE_NAME"; 196 197 public static final String AUTHORITIES_FILTER_KEY = "authorities"; 198 199 private static final Uri CONTACTS_CONTENT_URI_WITH_LETTER_COUNTS = 200 buildSectionIndexerUri(Contacts.CONTENT_URI); 201 202 /** Mask for picker mode */ 203 static final int MODE_MASK_PICKER = 0x80000000; 204 /** Mask for no presence mode */ 205 static final int MODE_MASK_NO_PRESENCE = 0x40000000; 206 /** Mask for enabling list filtering */ 207 static final int MODE_MASK_NO_FILTER = 0x20000000; 208 /** Mask for having a "create new contact" header in the list */ 209 static final int MODE_MASK_CREATE_NEW = 0x10000000; 210 /** Mask for showing photos in the list */ 211 static final int MODE_MASK_SHOW_PHOTOS = 0x08000000; 212 /** Mask for hiding additional information e.g. primary phone number in the list */ 213 static final int MODE_MASK_NO_DATA = 0x04000000; 214 /** Mask for showing a call button in the list */ 215 static final int MODE_MASK_SHOW_CALL_BUTTON = 0x02000000; 216 /** Mask to disable quickcontact (images will show as normal images) */ 217 static final int MODE_MASK_DISABLE_QUIKCCONTACT = 0x01000000; 218 /** Mask to show the total number of contacts at the top */ 219 static final int MODE_MASK_SHOW_NUMBER_OF_CONTACTS = 0x00800000; 220 221 /** Unknown mode */ 222 static final int MODE_UNKNOWN = 0; 223 /** Default mode */ 224 static final int MODE_DEFAULT = 4 | MODE_MASK_SHOW_PHOTOS | MODE_MASK_SHOW_NUMBER_OF_CONTACTS; 225 /** Custom mode */ 226 static final int MODE_CUSTOM = 8; 227 /** Show all starred contacts */ 228 static final int MODE_STARRED = 20 | MODE_MASK_SHOW_PHOTOS; 229 /** Show frequently contacted contacts */ 230 static final int MODE_FREQUENT = 30 | MODE_MASK_SHOW_PHOTOS; 231 /** Show starred and the frequent */ 232 static final int MODE_STREQUENT = 35 | MODE_MASK_SHOW_PHOTOS | MODE_MASK_SHOW_CALL_BUTTON; 233 /** Show all contacts and pick them when clicking */ 234 static final int MODE_PICK_CONTACT = 40 | MODE_MASK_PICKER | MODE_MASK_SHOW_PHOTOS 235 | MODE_MASK_DISABLE_QUIKCCONTACT; 236 /** Show all contacts as well as the option to create a new one */ 237 static final int MODE_PICK_OR_CREATE_CONTACT = 42 | MODE_MASK_PICKER | MODE_MASK_CREATE_NEW 238 | MODE_MASK_SHOW_PHOTOS | MODE_MASK_DISABLE_QUIKCCONTACT; 239 /** Show all people through the legacy provider and pick them when clicking */ 240 static final int MODE_LEGACY_PICK_PERSON = 43 | MODE_MASK_PICKER 241 | MODE_MASK_DISABLE_QUIKCCONTACT; 242 /** Show all people through the legacy provider as well as the option to create a new one */ 243 static final int MODE_LEGACY_PICK_OR_CREATE_PERSON = 44 | MODE_MASK_PICKER 244 | MODE_MASK_CREATE_NEW | MODE_MASK_DISABLE_QUIKCCONTACT; 245 /** Show all contacts and pick them when clicking, and allow creating a new contact */ 246 static final int MODE_INSERT_OR_EDIT_CONTACT = 45 | MODE_MASK_PICKER | MODE_MASK_CREATE_NEW 247 | MODE_MASK_SHOW_PHOTOS | MODE_MASK_DISABLE_QUIKCCONTACT; 248 /** Show all phone numbers and pick them when clicking */ 249 static final int MODE_PICK_PHONE = 50 | MODE_MASK_PICKER | MODE_MASK_NO_PRESENCE; 250 /** Show all phone numbers through the legacy provider and pick them when clicking */ 251 static final int MODE_LEGACY_PICK_PHONE = 252 51 | MODE_MASK_PICKER | MODE_MASK_NO_PRESENCE | MODE_MASK_NO_FILTER; 253 /** Show all postal addresses and pick them when clicking */ 254 static final int MODE_PICK_POSTAL = 255 55 | MODE_MASK_PICKER | MODE_MASK_NO_PRESENCE | MODE_MASK_NO_FILTER; 256 /** Show all postal addresses and pick them when clicking */ 257 static final int MODE_LEGACY_PICK_POSTAL = 258 56 | MODE_MASK_PICKER | MODE_MASK_NO_PRESENCE | MODE_MASK_NO_FILTER; 259 static final int MODE_GROUP = 57 | MODE_MASK_SHOW_PHOTOS; 260 /** Run a search query */ 261 static final int MODE_QUERY = 60 | MODE_MASK_SHOW_PHOTOS | MODE_MASK_NO_FILTER 262 | MODE_MASK_SHOW_NUMBER_OF_CONTACTS; 263 /** Run a search query in PICK mode, but that still launches to VIEW */ 264 static final int MODE_QUERY_PICK_TO_VIEW = 65 | MODE_MASK_SHOW_PHOTOS | MODE_MASK_PICKER 265 | MODE_MASK_SHOW_NUMBER_OF_CONTACTS; 266 267 /** Show join suggestions followed by an A-Z list */ 268 static final int MODE_JOIN_CONTACT = 70 | MODE_MASK_PICKER | MODE_MASK_NO_PRESENCE 269 | MODE_MASK_NO_DATA | MODE_MASK_SHOW_PHOTOS | MODE_MASK_DISABLE_QUIKCCONTACT; 270 271 /** Run a search query in a PICK mode */ 272 static final int MODE_QUERY_PICK = 75 | MODE_MASK_SHOW_PHOTOS | MODE_MASK_NO_FILTER 273 | MODE_MASK_PICKER | MODE_MASK_DISABLE_QUIKCCONTACT | MODE_MASK_SHOW_NUMBER_OF_CONTACTS; 274 275 /** Run a search query in a PICK_PHONE mode */ 276 static final int MODE_QUERY_PICK_PHONE = 80 | MODE_MASK_NO_FILTER | MODE_MASK_PICKER 277 | MODE_MASK_SHOW_NUMBER_OF_CONTACTS; 278 279 /** Run a search query in PICK mode, but that still launches to EDIT */ 280 static final int MODE_QUERY_PICK_TO_EDIT = 85 | MODE_MASK_NO_FILTER | MODE_MASK_SHOW_PHOTOS 281 | MODE_MASK_PICKER | MODE_MASK_SHOW_NUMBER_OF_CONTACTS; 282 283 /** 284 * An action used to do perform search while in a contact picker. It is initiated 285 * by the ContactListActivity itself. 286 */ 287 private static final String ACTION_SEARCH_INTERNAL = "com.android.contacts.INTERNAL_SEARCH"; 288 289 /** Maximum number of suggestions shown for joining aggregates */ 290 static final int MAX_SUGGESTIONS = 4; 291 292 static final String[] CONTACTS_SUMMARY_PROJECTION = new String[] { 293 Contacts._ID, // 0 294 Contacts.DISPLAY_NAME_PRIMARY, // 1 295 Contacts.DISPLAY_NAME_ALTERNATIVE, // 2 296 Contacts.SORT_KEY_PRIMARY, // 3 297 Contacts.STARRED, // 4 298 Contacts.TIMES_CONTACTED, // 5 299 Contacts.CONTACT_PRESENCE, // 6 300 Contacts.PHOTO_ID, // 7 301 Contacts.LOOKUP_KEY, // 8 302 Contacts.PHONETIC_NAME, // 9 303 Contacts.HAS_PHONE_NUMBER, // 10 304 }; 305 static final String[] CONTACTS_SUMMARY_PROJECTION_FROM_EMAIL = new String[] { 306 Contacts._ID, // 0 307 Contacts.DISPLAY_NAME_PRIMARY, // 1 308 Contacts.DISPLAY_NAME_ALTERNATIVE, // 2 309 Contacts.SORT_KEY_PRIMARY, // 3 310 Contacts.STARRED, // 4 311 Contacts.TIMES_CONTACTED, // 5 312 Contacts.CONTACT_PRESENCE, // 6 313 Contacts.PHOTO_ID, // 7 314 Contacts.LOOKUP_KEY, // 8 315 Contacts.PHONETIC_NAME, // 9 316 // email lookup doesn't included HAS_PHONE_NUMBER in projection 317 }; 318 319 static final String[] CONTACTS_SUMMARY_FILTER_PROJECTION = new String[] { 320 Contacts._ID, // 0 321 Contacts.DISPLAY_NAME_PRIMARY, // 1 322 Contacts.DISPLAY_NAME_ALTERNATIVE, // 2 323 Contacts.SORT_KEY_PRIMARY, // 3 324 Contacts.STARRED, // 4 325 Contacts.TIMES_CONTACTED, // 5 326 Contacts.CONTACT_PRESENCE, // 6 327 Contacts.PHOTO_ID, // 7 328 Contacts.LOOKUP_KEY, // 8 329 Contacts.PHONETIC_NAME, // 9 330 Contacts.HAS_PHONE_NUMBER, // 10 331 SearchSnippetColumns.SNIPPET_MIMETYPE, // 11 332 SearchSnippetColumns.SNIPPET_DATA1, // 12 333 SearchSnippetColumns.SNIPPET_DATA4, // 13 334 }; 335 336 static final String[] LEGACY_PEOPLE_PROJECTION = new String[] { 337 People._ID, // 0 338 People.DISPLAY_NAME, // 1 339 People.DISPLAY_NAME, // 2 340 People.DISPLAY_NAME, // 3 341 People.STARRED, // 4 342 PeopleColumns.TIMES_CONTACTED, // 5 343 People.PRESENCE_STATUS, // 6 344 }; 345 static final int SUMMARY_ID_COLUMN_INDEX = 0; 346 static final int SUMMARY_DISPLAY_NAME_PRIMARY_COLUMN_INDEX = 1; 347 static final int SUMMARY_DISPLAY_NAME_ALTERNATIVE_COLUMN_INDEX = 2; 348 static final int SUMMARY_SORT_KEY_PRIMARY_COLUMN_INDEX = 3; 349 static final int SUMMARY_STARRED_COLUMN_INDEX = 4; 350 static final int SUMMARY_TIMES_CONTACTED_COLUMN_INDEX = 5; 351 static final int SUMMARY_PRESENCE_STATUS_COLUMN_INDEX = 6; 352 static final int SUMMARY_PHOTO_ID_COLUMN_INDEX = 7; 353 static final int SUMMARY_LOOKUP_KEY_COLUMN_INDEX = 8; 354 static final int SUMMARY_PHONETIC_NAME_COLUMN_INDEX = 9; 355 static final int SUMMARY_HAS_PHONE_COLUMN_INDEX = 10; 356 static final int SUMMARY_SNIPPET_MIMETYPE_COLUMN_INDEX = 11; 357 static final int SUMMARY_SNIPPET_DATA1_COLUMN_INDEX = 12; 358 static final int SUMMARY_SNIPPET_DATA4_COLUMN_INDEX = 13; 359 360 static final String[] PHONES_PROJECTION = new String[] { 361 Phone._ID, //0 362 Phone.TYPE, //1 363 Phone.LABEL, //2 364 Phone.NUMBER, //3 365 Phone.DISPLAY_NAME, // 4 366 Phone.CONTACT_ID, // 5 367 }; 368 static final String[] LEGACY_PHONES_PROJECTION = new String[] { 369 Phones._ID, //0 370 Phones.TYPE, //1 371 Phones.LABEL, //2 372 Phones.NUMBER, //3 373 People.DISPLAY_NAME, // 4 374 }; 375 static final int PHONE_ID_COLUMN_INDEX = 0; 376 static final int PHONE_TYPE_COLUMN_INDEX = 1; 377 static final int PHONE_LABEL_COLUMN_INDEX = 2; 378 static final int PHONE_NUMBER_COLUMN_INDEX = 3; 379 static final int PHONE_DISPLAY_NAME_COLUMN_INDEX = 4; 380 static final int PHONE_CONTACT_ID_COLUMN_INDEX = 5; 381 382 static final String[] POSTALS_PROJECTION = new String[] { 383 StructuredPostal._ID, //0 384 StructuredPostal.TYPE, //1 385 StructuredPostal.LABEL, //2 386 StructuredPostal.DATA, //3 387 StructuredPostal.DISPLAY_NAME, // 4 388 }; 389 static final String[] LEGACY_POSTALS_PROJECTION = new String[] { 390 ContactMethods._ID, //0 391 ContactMethods.TYPE, //1 392 ContactMethods.LABEL, //2 393 ContactMethods.DATA, //3 394 People.DISPLAY_NAME, // 4 395 }; 396 static final String[] RAW_CONTACTS_PROJECTION = new String[] { 397 RawContacts._ID, //0 398 RawContacts.CONTACT_ID, //1 399 RawContacts.ACCOUNT_TYPE, //2 400 }; 401 402 static final int POSTAL_ID_COLUMN_INDEX = 0; 403 static final int POSTAL_TYPE_COLUMN_INDEX = 1; 404 static final int POSTAL_LABEL_COLUMN_INDEX = 2; 405 static final int POSTAL_ADDRESS_COLUMN_INDEX = 3; 406 static final int POSTAL_DISPLAY_NAME_COLUMN_INDEX = 4; 407 408 private static final int QUERY_TOKEN = 42; 409 410 static final String KEY_PICKER_MODE = "picker_mode"; 411 412 private ContactItemListAdapter mAdapter; 413 414 int mMode = MODE_DEFAULT; 415 416 private QueryHandler mQueryHandler; 417 private boolean mJustCreated; 418 private boolean mSyncEnabled; 419 Uri mSelectedContactUri; 420 421 // private boolean mDisplayAll; 422 private boolean mDisplayOnlyPhones; 423 424 private Uri mGroupUri; 425 426 private long mQueryAggregateId; 427 428 private ArrayList<Long> mWritableRawContactIds = new ArrayList<Long>(); 429 private int mWritableSourcesCnt; 430 private int mReadOnlySourcesCnt; 431 432 /** 433 * Used to keep track of the scroll state of the list. 434 */ 435 private Parcelable mListState = null; 436 437 private String mShortcutAction; 438 439 /** 440 * Internal query type when in mode {@link #MODE_QUERY_PICK_TO_VIEW}. 441 */ 442 private int mQueryMode = QUERY_MODE_NONE; 443 444 private static final int QUERY_MODE_NONE = -1; 445 private static final int QUERY_MODE_MAILTO = 1; 446 private static final int QUERY_MODE_TEL = 2; 447 448 private int mProviderStatus = ProviderStatus.STATUS_NORMAL; 449 450 private boolean mSearchMode; 451 private boolean mSearchResultsMode; 452 private boolean mShowNumberOfContacts; 453 454 private boolean mShowSearchSnippets; 455 private boolean mSearchInitiated; 456 457 private String mInitialFilter; 458 459 private static final String CLAUSE_ONLY_VISIBLE = Contacts.IN_VISIBLE_GROUP + "=1"; 460 private static final String CLAUSE_ONLY_PHONES = Contacts.HAS_PHONE_NUMBER + "=1"; 461 462 /** 463 * In the {@link #MODE_JOIN_CONTACT} determines whether we display a list item with the label 464 * "Show all contacts" or actually show all contacts 465 */ 466 private boolean mJoinModeShowAllContacts; 467 468 /** 469 * The ID of the special item described above. 470 */ 471 private static final long JOIN_MODE_SHOW_ALL_CONTACTS_ID = -2; 472 473 // Uri matcher for contact id 474 private static final int CONTACTS_ID = 1001; 475 private static final UriMatcher sContactsIdMatcher; 476 477 private ContactPhotoLoader mPhotoLoader; 478 479 final String[] sLookupProjection = new String[] { 480 Contacts.LOOKUP_KEY 481 }; 482 483 static { 484 sContactsIdMatcher = new UriMatcher(UriMatcher.NO_MATCH); sContactsIdMatcher.addURI(ContactsContract.AUTHORITY, "contacts/#", CONTACTS_ID)485 sContactsIdMatcher.addURI(ContactsContract.AUTHORITY, "contacts/#", CONTACTS_ID); 486 } 487 488 private class DeleteClickListener implements DialogInterface.OnClickListener { onClick(DialogInterface dialog, int which)489 public void onClick(DialogInterface dialog, int which) { 490 if (mSelectedContactUri != null) { 491 getContentResolver().delete(mSelectedContactUri, null, null); 492 } 493 } 494 } 495 496 /** 497 * A {@link TextHighlightingAnimation} that redraws just the contact display name in a 498 * list item. 499 */ 500 private static class NameHighlightingAnimation extends TextHighlightingAnimation { 501 private final ListView mListView; 502 NameHighlightingAnimation(ListView listView, int duration)503 private NameHighlightingAnimation(ListView listView, int duration) { 504 super(duration); 505 this.mListView = listView; 506 } 507 508 /** 509 * Redraws all visible items of the list corresponding to contacts 510 */ 511 @Override invalidate()512 protected void invalidate() { 513 int childCount = mListView.getChildCount(); 514 for (int i = 0; i < childCount; i++) { 515 View itemView = mListView.getChildAt(i); 516 if (itemView instanceof ContactListItemView) { 517 final ContactListItemView view = (ContactListItemView)itemView; 518 view.getNameTextView().invalidate(); 519 } 520 } 521 } 522 523 @Override onAnimationStarted()524 protected void onAnimationStarted() { 525 mListView.setScrollingCacheEnabled(false); 526 } 527 528 @Override onAnimationEnded()529 protected void onAnimationEnded() { 530 mListView.setScrollingCacheEnabled(true); 531 } 532 } 533 534 // The size of a home screen shortcut icon. 535 private int mIconSize; 536 private ContactsPreferences mContactsPrefs; 537 private int mDisplayOrder; 538 private int mSortOrder; 539 private boolean mHighlightWhenScrolling; 540 private TextHighlightingAnimation mHighlightingAnimation; 541 private SearchEditText mSearchEditText; 542 543 /** 544 * An approximation of the background color of the pinned header. This color 545 * is used when the pinned header is being pushed up. At that point the header 546 * "fades away". Rather than computing a faded bitmap based on the 9-patch 547 * normally used for the background, we will use a solid color, which will 548 * provide better performance and reduced complexity. 549 */ 550 private int mPinnedHeaderBackgroundColor; 551 552 private ContentObserver mProviderStatusObserver = new ContentObserver(new Handler()) { 553 554 @Override 555 public void onChange(boolean selfChange) { 556 checkProviderState(true); 557 } 558 }; 559 560 @Override onCreate(Bundle icicle)561 protected void onCreate(Bundle icicle) { 562 super.onCreate(icicle); 563 564 mIconSize = getResources().getDimensionPixelSize(android.R.dimen.app_icon_size); 565 mContactsPrefs = new ContactsPreferences(this); 566 mPhotoLoader = new ContactPhotoLoader(this, R.drawable.ic_contact_list_picture); 567 568 // Resolve the intent 569 final Intent intent = getIntent(); 570 571 // Allow the title to be set to a custom String using an extra on the intent 572 String title = intent.getStringExtra(UI.TITLE_EXTRA_KEY); 573 if (title != null) { 574 setTitle(title); 575 } 576 577 String action = intent.getAction(); 578 String component = intent.getComponent().getClassName(); 579 580 // When we get a FILTER_CONTACTS_ACTION, it represents search in the context 581 // of some other action. Let's retrieve the original action to provide proper 582 // context for the search queries. 583 if (UI.FILTER_CONTACTS_ACTION.equals(action)) { 584 mSearchMode = true; 585 mShowSearchSnippets = true; 586 Bundle extras = intent.getExtras(); 587 if (extras != null) { 588 mInitialFilter = extras.getString(UI.FILTER_TEXT_EXTRA_KEY); 589 String originalAction = 590 extras.getString(ContactsSearchManager.ORIGINAL_ACTION_EXTRA_KEY); 591 if (originalAction != null) { 592 action = originalAction; 593 } 594 String originalComponent = 595 extras.getString(ContactsSearchManager.ORIGINAL_COMPONENT_EXTRA_KEY); 596 if (originalComponent != null) { 597 component = originalComponent; 598 } 599 } else { 600 mInitialFilter = null; 601 } 602 } 603 604 Log.i(TAG, "Called with action: " + action); 605 mMode = MODE_UNKNOWN; 606 if (UI.LIST_DEFAULT.equals(action) || UI.FILTER_CONTACTS_ACTION.equals(action)) { 607 mMode = MODE_DEFAULT; 608 // When mDefaultMode is true the mode is set in onResume(), since the preferneces 609 // activity may change it whenever this activity isn't running 610 } else if (UI.LIST_GROUP_ACTION.equals(action)) { 611 mMode = MODE_GROUP; 612 String groupName = intent.getStringExtra(UI.GROUP_NAME_EXTRA_KEY); 613 if (TextUtils.isEmpty(groupName)) { 614 finish(); 615 return; 616 } 617 buildUserGroupUri(groupName); 618 } else if (UI.LIST_ALL_CONTACTS_ACTION.equals(action)) { 619 mMode = MODE_CUSTOM; 620 mDisplayOnlyPhones = false; 621 } else if (UI.LIST_STARRED_ACTION.equals(action)) { 622 mMode = mSearchMode ? MODE_DEFAULT : MODE_STARRED; 623 } else if (UI.LIST_FREQUENT_ACTION.equals(action)) { 624 mMode = mSearchMode ? MODE_DEFAULT : MODE_FREQUENT; 625 } else if (UI.LIST_STREQUENT_ACTION.equals(action)) { 626 mMode = mSearchMode ? MODE_DEFAULT : MODE_STREQUENT; 627 } else if (UI.LIST_CONTACTS_WITH_PHONES_ACTION.equals(action)) { 628 mMode = MODE_CUSTOM; 629 mDisplayOnlyPhones = true; 630 } else if (Intent.ACTION_PICK.equals(action)) { 631 // XXX These should be showing the data from the URI given in 632 // the Intent. 633 final String type = intent.resolveType(this); 634 if (Contacts.CONTENT_TYPE.equals(type)) { 635 mMode = MODE_PICK_CONTACT; 636 } else if (People.CONTENT_TYPE.equals(type)) { 637 mMode = MODE_LEGACY_PICK_PERSON; 638 } else if (Phone.CONTENT_TYPE.equals(type)) { 639 mMode = MODE_PICK_PHONE; 640 } else if (Phones.CONTENT_TYPE.equals(type)) { 641 mMode = MODE_LEGACY_PICK_PHONE; 642 } else if (StructuredPostal.CONTENT_TYPE.equals(type)) { 643 mMode = MODE_PICK_POSTAL; 644 } else if (ContactMethods.CONTENT_POSTAL_TYPE.equals(type)) { 645 mMode = MODE_LEGACY_PICK_POSTAL; 646 } 647 } else if (Intent.ACTION_CREATE_SHORTCUT.equals(action)) { 648 if (component.equals("alias.DialShortcut")) { 649 mMode = MODE_PICK_PHONE; 650 mShortcutAction = Intent.ACTION_CALL; 651 mShowSearchSnippets = false; 652 setTitle(R.string.callShortcutActivityTitle); 653 } else if (component.equals("alias.MessageShortcut")) { 654 mMode = MODE_PICK_PHONE; 655 mShortcutAction = Intent.ACTION_SENDTO; 656 mShowSearchSnippets = false; 657 setTitle(R.string.messageShortcutActivityTitle); 658 } else if (mSearchMode) { 659 mMode = MODE_PICK_CONTACT; 660 mShortcutAction = Intent.ACTION_VIEW; 661 setTitle(R.string.shortcutActivityTitle); 662 } else { 663 mMode = MODE_PICK_OR_CREATE_CONTACT; 664 mShortcutAction = Intent.ACTION_VIEW; 665 setTitle(R.string.shortcutActivityTitle); 666 } 667 } else if (Intent.ACTION_GET_CONTENT.equals(action)) { 668 final String type = intent.resolveType(this); 669 if (Contacts.CONTENT_ITEM_TYPE.equals(type)) { 670 if (mSearchMode) { 671 mMode = MODE_PICK_CONTACT; 672 } else { 673 mMode = MODE_PICK_OR_CREATE_CONTACT; 674 } 675 } else if (Phone.CONTENT_ITEM_TYPE.equals(type)) { 676 mMode = MODE_PICK_PHONE; 677 } else if (Phones.CONTENT_ITEM_TYPE.equals(type)) { 678 mMode = MODE_LEGACY_PICK_PHONE; 679 } else if (StructuredPostal.CONTENT_ITEM_TYPE.equals(type)) { 680 mMode = MODE_PICK_POSTAL; 681 } else if (ContactMethods.CONTENT_POSTAL_ITEM_TYPE.equals(type)) { 682 mMode = MODE_LEGACY_PICK_POSTAL; 683 } else if (People.CONTENT_ITEM_TYPE.equals(type)) { 684 if (mSearchMode) { 685 mMode = MODE_LEGACY_PICK_PERSON; 686 } else { 687 mMode = MODE_LEGACY_PICK_OR_CREATE_PERSON; 688 } 689 } 690 691 } else if (Intent.ACTION_INSERT_OR_EDIT.equals(action)) { 692 mMode = MODE_INSERT_OR_EDIT_CONTACT; 693 } else if (Intent.ACTION_SEARCH.equals(action)) { 694 // See if the suggestion was clicked with a search action key (call button) 695 if ("call".equals(intent.getStringExtra(SearchManager.ACTION_MSG))) { 696 String query = intent.getStringExtra(SearchManager.QUERY); 697 if (!TextUtils.isEmpty(query)) { 698 Intent newIntent = new Intent(Intent.ACTION_CALL_PRIVILEGED, 699 Uri.fromParts("tel", query, null)); 700 startActivity(newIntent); 701 } 702 finish(); 703 return; 704 } 705 706 // See if search request has extras to specify query 707 if (intent.hasExtra(Insert.EMAIL)) { 708 mMode = MODE_QUERY_PICK_TO_VIEW; 709 mQueryMode = QUERY_MODE_MAILTO; 710 mInitialFilter = intent.getStringExtra(Insert.EMAIL); 711 } else if (intent.hasExtra(Insert.PHONE)) { 712 mMode = MODE_QUERY_PICK_TO_VIEW; 713 mQueryMode = QUERY_MODE_TEL; 714 mInitialFilter = intent.getStringExtra(Insert.PHONE); 715 } else { 716 // Otherwise handle the more normal search case 717 mMode = MODE_QUERY; 718 mShowSearchSnippets = true; 719 mInitialFilter = getIntent().getStringExtra(SearchManager.QUERY); 720 } 721 mSearchResultsMode = true; 722 } else if (ACTION_SEARCH_INTERNAL.equals(action)) { 723 String originalAction = null; 724 Bundle extras = intent.getExtras(); 725 if (extras != null) { 726 originalAction = extras.getString(ContactsSearchManager.ORIGINAL_ACTION_EXTRA_KEY); 727 } 728 mShortcutAction = intent.getStringExtra(SHORTCUT_ACTION_KEY); 729 730 if (Intent.ACTION_INSERT_OR_EDIT.equals(originalAction)) { 731 mMode = MODE_QUERY_PICK_TO_EDIT; 732 mShowSearchSnippets = true; 733 mInitialFilter = getIntent().getStringExtra(SearchManager.QUERY); 734 } else if (mShortcutAction != null && intent.hasExtra(Insert.PHONE)) { 735 mMode = MODE_QUERY_PICK_PHONE; 736 mQueryMode = QUERY_MODE_TEL; 737 mInitialFilter = intent.getStringExtra(Insert.PHONE); 738 } else { 739 mMode = MODE_QUERY_PICK; 740 mQueryMode = QUERY_MODE_NONE; 741 mShowSearchSnippets = true; 742 mInitialFilter = getIntent().getStringExtra(SearchManager.QUERY); 743 } 744 mSearchResultsMode = true; 745 // Since this is the filter activity it receives all intents 746 // dispatched from the SearchManager for security reasons 747 // so we need to re-dispatch from here to the intended target. 748 } else if (Intents.SEARCH_SUGGESTION_CLICKED.equals(action)) { 749 Uri data = intent.getData(); 750 Uri telUri = null; 751 if (sContactsIdMatcher.match(data) == CONTACTS_ID) { 752 long contactId = Long.valueOf(data.getLastPathSegment()); 753 final Cursor cursor = queryPhoneNumbers(contactId); 754 if (cursor != null) { 755 if (cursor.getCount() == 1 && cursor.moveToFirst()) { 756 int phoneNumberIndex = cursor.getColumnIndex(Phone.NUMBER); 757 String phoneNumber = cursor.getString(phoneNumberIndex); 758 telUri = Uri.parse("tel:" + phoneNumber); 759 } 760 cursor.close(); 761 } 762 } 763 // See if the suggestion was clicked with a search action key (call button) 764 Intent newIntent; 765 if ("call".equals(intent.getStringExtra(SearchManager.ACTION_MSG)) && telUri != null) { 766 newIntent = new Intent(Intent.ACTION_CALL_PRIVILEGED, telUri); 767 } else { 768 newIntent = new Intent(Intent.ACTION_VIEW, data); 769 } 770 startActivity(newIntent); 771 finish(); 772 return; 773 } else if (Intents.SEARCH_SUGGESTION_DIAL_NUMBER_CLICKED.equals(action)) { 774 Intent newIntent = new Intent(Intent.ACTION_CALL_PRIVILEGED, intent.getData()); 775 startActivity(newIntent); 776 finish(); 777 return; 778 } else if (Intents.SEARCH_SUGGESTION_CREATE_CONTACT_CLICKED.equals(action)) { 779 // TODO actually support this in EditContactActivity. 780 String number = intent.getData().getSchemeSpecificPart(); 781 Intent newIntent = new Intent(Intent.ACTION_INSERT, Contacts.CONTENT_URI); 782 newIntent.putExtra(Intents.Insert.PHONE, number); 783 startActivity(newIntent); 784 finish(); 785 return; 786 } 787 788 if (JOIN_AGGREGATE.equals(action)) { 789 if (mSearchMode) { 790 mMode = MODE_PICK_CONTACT; 791 } else { 792 mMode = MODE_JOIN_CONTACT; 793 mQueryAggregateId = intent.getLongExtra(EXTRA_AGGREGATE_ID, -1); 794 if (mQueryAggregateId == -1) { 795 Log.e(TAG, "Intent " + action + " is missing required extra: " 796 + EXTRA_AGGREGATE_ID); 797 setResult(RESULT_CANCELED); 798 finish(); 799 } 800 } 801 } 802 803 if (mMode == MODE_UNKNOWN) { 804 mMode = MODE_DEFAULT; 805 } 806 807 if (((mMode & MODE_MASK_SHOW_NUMBER_OF_CONTACTS) != 0 || mSearchMode) 808 && !mSearchResultsMode) { 809 mShowNumberOfContacts = true; 810 } 811 812 if (mMode == MODE_JOIN_CONTACT) { 813 setContentView(R.layout.contacts_list_content_join); 814 TextView blurbView = (TextView)findViewById(R.id.join_contact_blurb); 815 816 String blurb = getString(R.string.blurbJoinContactDataWith, 817 getContactDisplayName(mQueryAggregateId)); 818 blurbView.setText(blurb); 819 mJoinModeShowAllContacts = true; 820 } else if (mSearchMode) { 821 setContentView(R.layout.contacts_search_content); 822 } else if (mSearchResultsMode) { 823 setContentView(R.layout.contacts_list_search_results); 824 TextView titleText = (TextView)findViewById(R.id.search_results_for); 825 titleText.setText(Html.fromHtml(getString(R.string.search_results_for, 826 "<b>" + mInitialFilter + "</b>"))); 827 } else { 828 setContentView(R.layout.contacts_list_content); 829 } 830 831 setupListView(); 832 if (mSearchMode) { 833 setupSearchView(); 834 } 835 836 mQueryHandler = new QueryHandler(this); 837 mJustCreated = true; 838 839 mSyncEnabled = true; 840 } 841 842 /** 843 * Register an observer for provider status changes - we will need to 844 * reflect them in the UI. 845 */ registerProviderStatusObserver()846 private void registerProviderStatusObserver() { 847 getContentResolver().registerContentObserver(ProviderStatus.CONTENT_URI, 848 false, mProviderStatusObserver); 849 } 850 851 /** 852 * Register an observer for provider status changes - we will need to 853 * reflect them in the UI. 854 */ unregisterProviderStatusObserver()855 private void unregisterProviderStatusObserver() { 856 getContentResolver().unregisterContentObserver(mProviderStatusObserver); 857 } 858 setupListView()859 private void setupListView() { 860 final ListView list = getListView(); 861 final LayoutInflater inflater = getLayoutInflater(); 862 863 mHighlightingAnimation = 864 new NameHighlightingAnimation(list, TEXT_HIGHLIGHTING_ANIMATION_DURATION); 865 866 // Tell list view to not show dividers. We'll do it ourself so that we can *not* show 867 // them when an A-Z headers is visible. 868 list.setDividerHeight(0); 869 list.setOnCreateContextMenuListener(this); 870 871 mAdapter = new ContactItemListAdapter(this); 872 setListAdapter(mAdapter); 873 874 if (list instanceof PinnedHeaderListView && mAdapter.getDisplaySectionHeadersEnabled()) { 875 mPinnedHeaderBackgroundColor = 876 getResources().getColor(R.color.pinned_header_background); 877 PinnedHeaderListView pinnedHeaderList = (PinnedHeaderListView)list; 878 View pinnedHeader = inflater.inflate(R.layout.list_section, list, false); 879 pinnedHeaderList.setPinnedHeaderView(pinnedHeader); 880 } 881 882 list.setOnScrollListener(mAdapter); 883 list.setOnKeyListener(this); 884 list.setOnFocusChangeListener(this); 885 list.setOnTouchListener(this); 886 887 // We manually save/restore the listview state 888 list.setSaveEnabled(false); 889 } 890 891 /** 892 * Configures search UI. 893 */ setupSearchView()894 private void setupSearchView() { 895 mSearchEditText = (SearchEditText)findViewById(R.id.search_src_text); 896 mSearchEditText.addTextChangedListener(this); 897 mSearchEditText.setOnEditorActionListener(this); 898 mSearchEditText.setText(mInitialFilter); 899 } 900 getContactDisplayName(long contactId)901 private String getContactDisplayName(long contactId) { 902 String contactName = null; 903 Cursor c = getContentResolver().query( 904 ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId), 905 new String[] {Contacts.DISPLAY_NAME}, null, null, null); 906 try { 907 if (c != null && c.moveToFirst()) { 908 contactName = c.getString(0); 909 } 910 } finally { 911 if (c != null) { 912 c.close(); 913 } 914 } 915 916 if (contactName == null) { 917 contactName = ""; 918 } 919 920 return contactName; 921 } 922 getSummaryDisplayNameColumnIndex()923 private int getSummaryDisplayNameColumnIndex() { 924 if (mDisplayOrder == ContactsContract.Preferences.DISPLAY_ORDER_PRIMARY) { 925 return SUMMARY_DISPLAY_NAME_PRIMARY_COLUMN_INDEX; 926 } else { 927 return SUMMARY_DISPLAY_NAME_ALTERNATIVE_COLUMN_INDEX; 928 } 929 } 930 931 /** {@inheritDoc} */ onClick(View v)932 public void onClick(View v) { 933 int id = v.getId(); 934 switch (id) { 935 // TODO a better way of identifying the button 936 case android.R.id.button1: { 937 final int position = (Integer)v.getTag(); 938 Cursor c = mAdapter.getCursor(); 939 if (c != null) { 940 c.moveToPosition(position); 941 callContact(c); 942 } 943 break; 944 } 945 } 946 } 947 setEmptyText()948 private void setEmptyText() { 949 if (mMode == MODE_JOIN_CONTACT || mSearchMode) { 950 return; 951 } 952 953 TextView empty = (TextView) findViewById(R.id.emptyText); 954 if (mDisplayOnlyPhones) { 955 empty.setText(getText(R.string.noContactsWithPhoneNumbers)); 956 } else if (mMode == MODE_STREQUENT || mMode == MODE_STARRED) { 957 empty.setText(getText(R.string.noFavoritesHelpText)); 958 } else if (mMode == MODE_QUERY || mMode == MODE_QUERY_PICK 959 || mMode == MODE_QUERY_PICK_PHONE || mMode == MODE_QUERY_PICK_TO_VIEW 960 || mMode == MODE_QUERY_PICK_TO_EDIT) { 961 empty.setText(getText(R.string.noMatchingContacts)); 962 } else { 963 boolean hasSim = ((TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE)) 964 .hasIccCard(); 965 boolean createShortcut = Intent.ACTION_CREATE_SHORTCUT.equals(getIntent().getAction()); 966 if (isSyncActive()) { 967 if (createShortcut) { 968 // Help text is the same no matter whether there is SIM or not. 969 empty.setText(getText(R.string.noContactsHelpTextWithSyncForCreateShortcut)); 970 } else if (hasSim) { 971 empty.setText(getText(R.string.noContactsHelpTextWithSync)); 972 } else { 973 empty.setText(getText(R.string.noContactsNoSimHelpTextWithSync)); 974 } 975 } else { 976 if (createShortcut) { 977 // Help text is the same no matter whether there is SIM or not. 978 empty.setText(getText(R.string.noContactsHelpTextForCreateShortcut)); 979 } else if (hasSim) { 980 empty.setText(getText(R.string.noContactsHelpText)); 981 } else { 982 empty.setText(getText(R.string.noContactsNoSimHelpText)); 983 } 984 } 985 } 986 } 987 isSyncActive()988 private boolean isSyncActive() { 989 Account[] accounts = AccountManager.get(this).getAccounts(); 990 if (accounts != null && accounts.length > 0) { 991 IContentService contentService = ContentResolver.getContentService(); 992 for (Account account : accounts) { 993 try { 994 if (contentService.isSyncActive(account, ContactsContract.AUTHORITY)) { 995 return true; 996 } 997 } catch (RemoteException e) { 998 Log.e(TAG, "Could not get the sync status"); 999 } 1000 } 1001 } 1002 return false; 1003 } 1004 buildUserGroupUri(String group)1005 private void buildUserGroupUri(String group) { 1006 mGroupUri = Uri.withAppendedPath(Contacts.CONTENT_GROUP_URI, group); 1007 } 1008 1009 /** 1010 * Sets the mode when the request is for "default" 1011 */ setDefaultMode()1012 private void setDefaultMode() { 1013 // Load the preferences 1014 SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); 1015 1016 mDisplayOnlyPhones = prefs.getBoolean(Prefs.DISPLAY_ONLY_PHONES, 1017 Prefs.DISPLAY_ONLY_PHONES_DEFAULT); 1018 } 1019 1020 @Override onDestroy()1021 protected void onDestroy() { 1022 super.onDestroy(); 1023 mPhotoLoader.stop(); 1024 } 1025 1026 @Override onStart()1027 protected void onStart() { 1028 super.onStart(); 1029 1030 mContactsPrefs.registerChangeListener(mPreferencesChangeListener); 1031 } 1032 1033 @Override onPause()1034 protected void onPause() { 1035 super.onPause(); 1036 unregisterProviderStatusObserver(); 1037 } 1038 1039 @Override onResume()1040 protected void onResume() { 1041 super.onResume(); 1042 1043 registerProviderStatusObserver(); 1044 mPhotoLoader.resume(); 1045 1046 Activity parent = getParent(); 1047 1048 // Do this before setting the filter. The filter thread relies 1049 // on some state that is initialized in setDefaultMode 1050 if (mMode == MODE_DEFAULT) { 1051 // If we're in default mode we need to possibly reset the mode due to a change 1052 // in the preferences activity while we weren't running 1053 setDefaultMode(); 1054 } 1055 1056 // See if we were invoked with a filter 1057 if (mSearchMode) { 1058 mSearchEditText.requestFocus(); 1059 } 1060 1061 if (!mSearchMode && !checkProviderState(mJustCreated)) { 1062 return; 1063 } 1064 1065 if (mJustCreated) { 1066 // We need to start a query here the first time the activity is launched, as long 1067 // as we aren't doing a filter. 1068 startQuery(); 1069 } 1070 mJustCreated = false; 1071 mSearchInitiated = false; 1072 } 1073 1074 /** 1075 * Obtains the contacts provider status and configures the UI accordingly. 1076 * 1077 * @param loadData true if the method needs to start a query when the 1078 * provider is in the normal state 1079 * @return true if the provider status is normal 1080 */ checkProviderState(boolean loadData)1081 private boolean checkProviderState(boolean loadData) { 1082 View importFailureView = findViewById(R.id.import_failure); 1083 if (importFailureView == null) { 1084 return true; 1085 } 1086 1087 TextView messageView = (TextView) findViewById(R.id.emptyText); 1088 1089 // This query can be performed on the UI thread because 1090 // the API explicitly allows such use. 1091 Cursor cursor = getContentResolver().query(ProviderStatus.CONTENT_URI, new String[] { 1092 ProviderStatus.STATUS, ProviderStatus.DATA1 1093 }, null, null, null); 1094 try { 1095 if (cursor.moveToFirst()) { 1096 int status = cursor.getInt(0); 1097 if (status != mProviderStatus) { 1098 mProviderStatus = status; 1099 switch (status) { 1100 case ProviderStatus.STATUS_NORMAL: 1101 mAdapter.notifyDataSetInvalidated(); 1102 if (loadData) { 1103 startQuery(); 1104 } 1105 break; 1106 1107 case ProviderStatus.STATUS_CHANGING_LOCALE: 1108 messageView.setText(R.string.locale_change_in_progress); 1109 mAdapter.changeCursor(null); 1110 mAdapter.notifyDataSetInvalidated(); 1111 break; 1112 1113 case ProviderStatus.STATUS_UPGRADING: 1114 messageView.setText(R.string.upgrade_in_progress); 1115 mAdapter.changeCursor(null); 1116 mAdapter.notifyDataSetInvalidated(); 1117 break; 1118 1119 case ProviderStatus.STATUS_UPGRADE_OUT_OF_MEMORY: 1120 long size = cursor.getLong(1); 1121 String message = getResources().getString( 1122 R.string.upgrade_out_of_memory, new Object[] {size}); 1123 messageView.setText(message); 1124 configureImportFailureView(importFailureView); 1125 mAdapter.changeCursor(null); 1126 mAdapter.notifyDataSetInvalidated(); 1127 break; 1128 } 1129 } 1130 } 1131 } finally { 1132 cursor.close(); 1133 } 1134 1135 importFailureView.setVisibility( 1136 mProviderStatus == ProviderStatus.STATUS_UPGRADE_OUT_OF_MEMORY 1137 ? View.VISIBLE 1138 : View.GONE); 1139 return mProviderStatus == ProviderStatus.STATUS_NORMAL; 1140 } 1141 configureImportFailureView(View importFailureView)1142 private void configureImportFailureView(View importFailureView) { 1143 1144 OnClickListener listener = new OnClickListener(){ 1145 1146 public void onClick(View v) { 1147 switch(v.getId()) { 1148 case R.id.import_failure_uninstall_apps: { 1149 startActivity(new Intent(Settings.ACTION_MANAGE_APPLICATIONS_SETTINGS)); 1150 break; 1151 } 1152 case R.id.import_failure_retry_upgrade: { 1153 // Send a provider status update, which will trigger a retry 1154 ContentValues values = new ContentValues(); 1155 values.put(ProviderStatus.STATUS, ProviderStatus.STATUS_UPGRADING); 1156 getContentResolver().update(ProviderStatus.CONTENT_URI, values, null, null); 1157 break; 1158 } 1159 } 1160 }}; 1161 1162 Button uninstallApps = (Button) findViewById(R.id.import_failure_uninstall_apps); 1163 uninstallApps.setOnClickListener(listener); 1164 1165 Button retryUpgrade = (Button) findViewById(R.id.import_failure_retry_upgrade); 1166 retryUpgrade.setOnClickListener(listener); 1167 } 1168 getTextFilter()1169 private String getTextFilter() { 1170 if (mSearchEditText != null) { 1171 return mSearchEditText.getText().toString(); 1172 } 1173 return null; 1174 } 1175 1176 @Override onRestart()1177 protected void onRestart() { 1178 super.onRestart(); 1179 1180 if (!checkProviderState(false)) { 1181 return; 1182 } 1183 1184 // The cursor was killed off in onStop(), so we need to get a new one here 1185 // We do not perform the query if a filter is set on the list because the 1186 // filter will cause the query to happen anyway 1187 if (TextUtils.isEmpty(getTextFilter())) { 1188 startQuery(); 1189 } else { 1190 // Run the filtered query on the adapter 1191 mAdapter.onContentChanged(); 1192 } 1193 } 1194 1195 @Override onSaveInstanceState(Bundle icicle)1196 protected void onSaveInstanceState(Bundle icicle) { 1197 super.onSaveInstanceState(icicle); 1198 // Save list state in the bundle so we can restore it after the QueryHandler has run 1199 if (mList != null) { 1200 icicle.putParcelable(LIST_STATE_KEY, mList.onSaveInstanceState()); 1201 } 1202 } 1203 1204 @Override onRestoreInstanceState(Bundle icicle)1205 protected void onRestoreInstanceState(Bundle icicle) { 1206 super.onRestoreInstanceState(icicle); 1207 // Retrieve list state. This will be applied after the QueryHandler has run 1208 mListState = icicle.getParcelable(LIST_STATE_KEY); 1209 } 1210 1211 @Override onStop()1212 protected void onStop() { 1213 super.onStop(); 1214 1215 mContactsPrefs.unregisterChangeListener(); 1216 mAdapter.setSuggestionsCursor(null); 1217 mAdapter.changeCursor(null); 1218 1219 if (mMode == MODE_QUERY) { 1220 // Make sure the search box is closed 1221 SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE); 1222 searchManager.stopSearch(); 1223 } 1224 } 1225 1226 @Override onCreateOptionsMenu(Menu menu)1227 public boolean onCreateOptionsMenu(Menu menu) { 1228 super.onCreateOptionsMenu(menu); 1229 1230 // If Contacts was invoked by another Activity simply as a way of 1231 // picking a contact, don't show the options menu 1232 if ((mMode & MODE_MASK_PICKER) == MODE_MASK_PICKER) { 1233 return false; 1234 } 1235 1236 MenuInflater inflater = getMenuInflater(); 1237 inflater.inflate(R.menu.list, menu); 1238 return true; 1239 } 1240 1241 @Override onPrepareOptionsMenu(Menu menu)1242 public boolean onPrepareOptionsMenu(Menu menu) { 1243 final boolean defaultMode = (mMode == MODE_DEFAULT); 1244 menu.findItem(R.id.menu_display_groups).setVisible(defaultMode); 1245 return true; 1246 } 1247 1248 @Override onOptionsItemSelected(MenuItem item)1249 public boolean onOptionsItemSelected(MenuItem item) { 1250 switch (item.getItemId()) { 1251 case R.id.menu_display_groups: { 1252 final Intent intent = new Intent(this, ContactsPreferencesActivity.class); 1253 startActivityForResult(intent, SUBACTIVITY_DISPLAY_GROUP); 1254 return true; 1255 } 1256 case R.id.menu_search: { 1257 onSearchRequested(); 1258 return true; 1259 } 1260 case R.id.menu_add: { 1261 final Intent intent = new Intent(Intent.ACTION_INSERT, Contacts.CONTENT_URI); 1262 startActivity(intent); 1263 return true; 1264 } 1265 case R.id.menu_import_export: { 1266 displayImportExportDialog(); 1267 return true; 1268 } 1269 case R.id.menu_accounts: { 1270 final Intent intent = new Intent(Settings.ACTION_SYNC_SETTINGS); 1271 intent.putExtra(AUTHORITIES_FILTER_KEY, new String[] { 1272 ContactsContract.AUTHORITY 1273 }); 1274 startActivity(intent); 1275 return true; 1276 } 1277 } 1278 return false; 1279 } 1280 1281 @Override startSearch(String initialQuery, boolean selectInitialQuery, Bundle appSearchData, boolean globalSearch)1282 public void startSearch(String initialQuery, boolean selectInitialQuery, Bundle appSearchData, 1283 boolean globalSearch) { 1284 if (mProviderStatus != ProviderStatus.STATUS_NORMAL) { 1285 return; 1286 } 1287 1288 if (globalSearch) { 1289 super.startSearch(initialQuery, selectInitialQuery, appSearchData, globalSearch); 1290 } else { 1291 if (!mSearchMode && (mMode & MODE_MASK_NO_FILTER) == 0) { 1292 if ((mMode & MODE_MASK_PICKER) != 0) { 1293 ContactsSearchManager.startSearchForResult(this, initialQuery, 1294 SUBACTIVITY_FILTER); 1295 } else { 1296 ContactsSearchManager.startSearch(this, initialQuery); 1297 } 1298 } 1299 } 1300 } 1301 1302 /** 1303 * Performs filtering of the list based on the search query entered in the 1304 * search text edit. 1305 */ onSearchTextChanged()1306 protected void onSearchTextChanged() { 1307 // Set the proper empty string 1308 setEmptyText(); 1309 1310 Filter filter = mAdapter.getFilter(); 1311 filter.filter(getTextFilter()); 1312 } 1313 1314 /** 1315 * Starts a new activity that will run a search query and display search results. 1316 */ doSearch()1317 private void doSearch() { 1318 String query = getTextFilter(); 1319 if (TextUtils.isEmpty(query)) { 1320 return; 1321 } 1322 1323 Intent intent = new Intent(this, SearchResultsActivity.class); 1324 Intent originalIntent = getIntent(); 1325 Bundle originalExtras = originalIntent.getExtras(); 1326 if (originalExtras != null) { 1327 intent.putExtras(originalExtras); 1328 } 1329 1330 intent.putExtra(SearchManager.QUERY, query); 1331 if ((mMode & MODE_MASK_PICKER) != 0) { 1332 intent.setAction(ACTION_SEARCH_INTERNAL); 1333 intent.putExtra(SHORTCUT_ACTION_KEY, mShortcutAction); 1334 if (mShortcutAction != null) { 1335 if (Intent.ACTION_CALL.equals(mShortcutAction) 1336 || Intent.ACTION_SENDTO.equals(mShortcutAction)) { 1337 intent.putExtra(Insert.PHONE, query); 1338 } 1339 } else { 1340 switch (mQueryMode) { 1341 case QUERY_MODE_MAILTO: 1342 intent.putExtra(Insert.EMAIL, query); 1343 break; 1344 case QUERY_MODE_TEL: 1345 intent.putExtra(Insert.PHONE, query); 1346 break; 1347 } 1348 } 1349 startActivityForResult(intent, SUBACTIVITY_SEARCH); 1350 } else { 1351 intent.setAction(Intent.ACTION_SEARCH); 1352 startActivity(intent); 1353 } 1354 } 1355 1356 @Override onCreateDialog(int id, Bundle bundle)1357 protected Dialog onCreateDialog(int id, Bundle bundle) { 1358 switch (id) { 1359 case R.string.import_from_sim: 1360 case R.string.import_from_sdcard: { 1361 return AccountSelectionUtil.getSelectAccountDialog(this, id); 1362 } 1363 case R.id.dialog_sdcard_not_found: { 1364 return new AlertDialog.Builder(this) 1365 .setTitle(R.string.no_sdcard_title) 1366 .setIcon(android.R.drawable.ic_dialog_alert) 1367 .setMessage(R.string.no_sdcard_message) 1368 .setPositiveButton(android.R.string.ok, null).create(); 1369 } 1370 case R.id.dialog_delete_contact_confirmation: { 1371 return new AlertDialog.Builder(this) 1372 .setTitle(R.string.deleteConfirmation_title) 1373 .setIcon(android.R.drawable.ic_dialog_alert) 1374 .setMessage(R.string.deleteConfirmation) 1375 .setNegativeButton(android.R.string.cancel, null) 1376 .setPositiveButton(android.R.string.ok, 1377 new DeleteClickListener()).create(); 1378 } 1379 case R.id.dialog_readonly_contact_hide_confirmation: { 1380 return new AlertDialog.Builder(this) 1381 .setTitle(R.string.deleteConfirmation_title) 1382 .setIcon(android.R.drawable.ic_dialog_alert) 1383 .setMessage(R.string.readOnlyContactWarning) 1384 .setNegativeButton(android.R.string.cancel, null) 1385 .setPositiveButton(android.R.string.ok, 1386 new DeleteClickListener()).create(); 1387 } 1388 case R.id.dialog_readonly_contact_delete_confirmation: { 1389 return new AlertDialog.Builder(this) 1390 .setTitle(R.string.deleteConfirmation_title) 1391 .setIcon(android.R.drawable.ic_dialog_alert) 1392 .setMessage(R.string.readOnlyContactDeleteConfirmation) 1393 .setNegativeButton(android.R.string.cancel, null) 1394 .setPositiveButton(android.R.string.ok, 1395 new DeleteClickListener()).create(); 1396 } 1397 case R.id.dialog_multiple_contact_delete_confirmation: { 1398 return new AlertDialog.Builder(this) 1399 .setTitle(R.string.deleteConfirmation_title) 1400 .setIcon(android.R.drawable.ic_dialog_alert) 1401 .setMessage(R.string.multipleContactDeleteConfirmation) 1402 .setNegativeButton(android.R.string.cancel, null) 1403 .setPositiveButton(android.R.string.ok, 1404 new DeleteClickListener()).create(); 1405 } 1406 } 1407 return super.onCreateDialog(id, bundle); 1408 } 1409 1410 /** 1411 * Create a {@link Dialog} that allows the user to pick from a bulk import 1412 * or bulk export task across all contacts. 1413 */ displayImportExportDialog()1414 private void displayImportExportDialog() { 1415 // Wrap our context to inflate list items using correct theme 1416 final Context dialogContext = new ContextThemeWrapper(this, android.R.style.Theme_Light); 1417 final Resources res = dialogContext.getResources(); 1418 final LayoutInflater dialogInflater = (LayoutInflater)dialogContext 1419 .getSystemService(Context.LAYOUT_INFLATER_SERVICE); 1420 1421 // Adapter that shows a list of string resources 1422 final ArrayAdapter<Integer> adapter = new ArrayAdapter<Integer>(this, 1423 android.R.layout.simple_list_item_1) { 1424 @Override 1425 public View getView(int position, View convertView, ViewGroup parent) { 1426 if (convertView == null) { 1427 convertView = dialogInflater.inflate(android.R.layout.simple_list_item_1, 1428 parent, false); 1429 } 1430 1431 final int resId = this.getItem(position); 1432 ((TextView)convertView).setText(resId); 1433 return convertView; 1434 } 1435 }; 1436 1437 if (TelephonyManager.getDefault().hasIccCard()) { 1438 adapter.add(R.string.import_from_sim); 1439 } 1440 if (res.getBoolean(R.bool.config_allow_import_from_sdcard)) { 1441 adapter.add(R.string.import_from_sdcard); 1442 } 1443 if (res.getBoolean(R.bool.config_allow_export_to_sdcard)) { 1444 adapter.add(R.string.export_to_sdcard); 1445 } 1446 if (res.getBoolean(R.bool.config_allow_share_visible_contacts)) { 1447 adapter.add(R.string.share_visible_contacts); 1448 } 1449 1450 final DialogInterface.OnClickListener clickListener = 1451 new DialogInterface.OnClickListener() { 1452 public void onClick(DialogInterface dialog, int which) { 1453 dialog.dismiss(); 1454 1455 final int resId = adapter.getItem(which); 1456 switch (resId) { 1457 case R.string.import_from_sim: 1458 case R.string.import_from_sdcard: { 1459 handleImportRequest(resId); 1460 break; 1461 } 1462 case R.string.export_to_sdcard: { 1463 Context context = ContactsListActivity.this; 1464 Intent exportIntent = new Intent(context, ExportVCardActivity.class); 1465 context.startActivity(exportIntent); 1466 break; 1467 } 1468 case R.string.share_visible_contacts: { 1469 doShareVisibleContacts(); 1470 break; 1471 } 1472 default: { 1473 Log.e(TAG, "Unexpected resource: " + 1474 getResources().getResourceEntryName(resId)); 1475 } 1476 } 1477 } 1478 }; 1479 1480 new AlertDialog.Builder(this) 1481 .setTitle(R.string.dialog_import_export) 1482 .setNegativeButton(android.R.string.cancel, null) 1483 .setSingleChoiceItems(adapter, -1, clickListener) 1484 .show(); 1485 } 1486 doShareVisibleContacts()1487 private void doShareVisibleContacts() { 1488 final Cursor cursor = getContentResolver().query(Contacts.CONTENT_URI, 1489 sLookupProjection, getContactSelection(), null, null); 1490 try { 1491 if (!cursor.moveToFirst()) { 1492 Toast.makeText(this, R.string.share_error, Toast.LENGTH_SHORT).show(); 1493 return; 1494 } 1495 1496 StringBuilder uriListBuilder = new StringBuilder(); 1497 int index = 0; 1498 for (;!cursor.isAfterLast(); cursor.moveToNext()) { 1499 if (index != 0) 1500 uriListBuilder.append(':'); 1501 uriListBuilder.append(cursor.getString(0)); 1502 index++; 1503 } 1504 Uri uri = Uri.withAppendedPath( 1505 Contacts.CONTENT_MULTI_VCARD_URI, 1506 Uri.encode(uriListBuilder.toString())); 1507 1508 final Intent intent = new Intent(Intent.ACTION_SEND); 1509 intent.setType(Contacts.CONTENT_VCARD_TYPE); 1510 intent.putExtra(Intent.EXTRA_STREAM, uri); 1511 startActivity(intent); 1512 } finally { 1513 cursor.close(); 1514 } 1515 } 1516 handleImportRequest(int resId)1517 private void handleImportRequest(int resId) { 1518 // There's three possibilities: 1519 // - more than one accounts -> ask the user 1520 // - just one account -> use the account without asking the user 1521 // - no account -> use phone-local storage without asking the user 1522 final Sources sources = Sources.getInstance(this); 1523 final List<Account> accountList = sources.getAccounts(true); 1524 final int size = accountList.size(); 1525 if (size > 1) { 1526 showDialog(resId); 1527 return; 1528 } 1529 1530 AccountSelectionUtil.doImport(this, resId, (size == 1 ? accountList.get(0) : null)); 1531 } 1532 1533 @Override onActivityResult(int requestCode, int resultCode, Intent data)1534 protected void onActivityResult(int requestCode, int resultCode, Intent data) { 1535 switch (requestCode) { 1536 case SUBACTIVITY_NEW_CONTACT: 1537 if (resultCode == RESULT_OK) { 1538 returnPickerResult(null, data.getStringExtra(Intent.EXTRA_SHORTCUT_NAME), 1539 data.getData(), (mMode & MODE_MASK_PICKER) != 0 1540 ? Intent.FLAG_GRANT_READ_URI_PERMISSION : 0); 1541 } 1542 break; 1543 1544 case SUBACTIVITY_VIEW_CONTACT: 1545 if (resultCode == RESULT_OK) { 1546 mAdapter.notifyDataSetChanged(); 1547 } 1548 break; 1549 1550 case SUBACTIVITY_DISPLAY_GROUP: 1551 // Mark as just created so we re-run the view query 1552 mJustCreated = true; 1553 break; 1554 1555 case SUBACTIVITY_FILTER: 1556 case SUBACTIVITY_SEARCH: 1557 // Pass through results of filter or search UI 1558 if (resultCode == RESULT_OK) { 1559 setResult(RESULT_OK, data); 1560 finish(); 1561 } 1562 break; 1563 } 1564 } 1565 1566 @Override onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo)1567 public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) { 1568 // If Contacts was invoked by another Activity simply as a way of 1569 // picking a contact, don't show the context menu 1570 if ((mMode & MODE_MASK_PICKER) == MODE_MASK_PICKER) { 1571 return; 1572 } 1573 1574 AdapterView.AdapterContextMenuInfo info; 1575 try { 1576 info = (AdapterView.AdapterContextMenuInfo) menuInfo; 1577 } catch (ClassCastException e) { 1578 Log.e(TAG, "bad menuInfo", e); 1579 return; 1580 } 1581 1582 Cursor cursor = (Cursor) getListAdapter().getItem(info.position); 1583 if (cursor == null) { 1584 // For some reason the requested item isn't available, do nothing 1585 return; 1586 } 1587 long id = info.id; 1588 Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, id); 1589 long rawContactId = ContactsUtils.queryForRawContactId(getContentResolver(), id); 1590 Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId); 1591 1592 // Setup the menu header 1593 menu.setHeaderTitle(cursor.getString(getSummaryDisplayNameColumnIndex())); 1594 1595 // View contact details 1596 final Intent viewContactIntent = new Intent(Intent.ACTION_VIEW, contactUri); 1597 StickyTabs.setTab(viewContactIntent, getIntent()); 1598 menu.add(0, MENU_ITEM_VIEW_CONTACT, 0, R.string.menu_viewContact) 1599 .setIntent(viewContactIntent); 1600 1601 if (cursor.getInt(SUMMARY_HAS_PHONE_COLUMN_INDEX) != 0) { 1602 // Calling contact 1603 menu.add(0, MENU_ITEM_CALL, 0, getString(R.string.menu_call)); 1604 // Send SMS item 1605 menu.add(0, MENU_ITEM_SEND_SMS, 0, getString(R.string.menu_sendSMS)); 1606 } 1607 1608 // Star toggling 1609 int starState = cursor.getInt(SUMMARY_STARRED_COLUMN_INDEX); 1610 if (starState == 0) { 1611 menu.add(0, MENU_ITEM_TOGGLE_STAR, 0, R.string.menu_addStar); 1612 } else { 1613 menu.add(0, MENU_ITEM_TOGGLE_STAR, 0, R.string.menu_removeStar); 1614 } 1615 1616 // Contact editing 1617 menu.add(0, MENU_ITEM_EDIT, 0, R.string.menu_editContact) 1618 .setIntent(new Intent(Intent.ACTION_EDIT, rawContactUri)); 1619 menu.add(0, MENU_ITEM_DELETE, 0, R.string.menu_deleteContact); 1620 } 1621 1622 @Override onContextItemSelected(MenuItem item)1623 public boolean onContextItemSelected(MenuItem item) { 1624 AdapterView.AdapterContextMenuInfo info; 1625 try { 1626 info = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo(); 1627 } catch (ClassCastException e) { 1628 Log.e(TAG, "bad menuInfo", e); 1629 return false; 1630 } 1631 1632 Cursor cursor = (Cursor) getListAdapter().getItem(info.position); 1633 1634 switch (item.getItemId()) { 1635 case MENU_ITEM_TOGGLE_STAR: { 1636 // Toggle the star 1637 ContentValues values = new ContentValues(1); 1638 values.put(Contacts.STARRED, cursor.getInt(SUMMARY_STARRED_COLUMN_INDEX) == 0 ? 1 : 0); 1639 final Uri selectedUri = this.getContactUri(info.position); 1640 getContentResolver().update(selectedUri, values, null, null); 1641 return true; 1642 } 1643 1644 case MENU_ITEM_CALL: { 1645 callContact(cursor); 1646 return true; 1647 } 1648 1649 case MENU_ITEM_SEND_SMS: { 1650 smsContact(cursor); 1651 return true; 1652 } 1653 1654 case MENU_ITEM_DELETE: { 1655 doContactDelete(getContactUri(info.position)); 1656 return true; 1657 } 1658 } 1659 1660 return super.onContextItemSelected(item); 1661 } 1662 1663 /** 1664 * Event handler for the use case where the user starts typing without 1665 * bringing up the search UI first. 1666 */ onKey(View v, int keyCode, KeyEvent event)1667 public boolean onKey(View v, int keyCode, KeyEvent event) { 1668 if (!mSearchMode && (mMode & MODE_MASK_NO_FILTER) == 0 && !mSearchInitiated) { 1669 int unicodeChar = event.getUnicodeChar(); 1670 if (unicodeChar != 0) { 1671 mSearchInitiated = true; 1672 startSearch(new String(new int[]{unicodeChar}, 0, 1), false, null, false); 1673 return true; 1674 } 1675 } 1676 return false; 1677 } 1678 1679 /** 1680 * Event handler for search UI. 1681 */ afterTextChanged(Editable s)1682 public void afterTextChanged(Editable s) { 1683 onSearchTextChanged(); 1684 } 1685 beforeTextChanged(CharSequence s, int start, int count, int after)1686 public void beforeTextChanged(CharSequence s, int start, int count, int after) { 1687 } 1688 onTextChanged(CharSequence s, int start, int before, int count)1689 public void onTextChanged(CharSequence s, int start, int before, int count) { 1690 } 1691 1692 /** 1693 * Event handler for search UI. 1694 */ onEditorAction(TextView v, int actionId, KeyEvent event)1695 public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { 1696 if (actionId == EditorInfo.IME_ACTION_DONE) { 1697 hideSoftKeyboard(); 1698 if (TextUtils.isEmpty(getTextFilter())) { 1699 finish(); 1700 } 1701 return true; 1702 } 1703 return false; 1704 } 1705 1706 @Override onKeyDown(int keyCode, KeyEvent event)1707 public boolean onKeyDown(int keyCode, KeyEvent event) { 1708 switch (keyCode) { 1709 case KeyEvent.KEYCODE_CALL: { 1710 if (callSelection()) { 1711 return true; 1712 } 1713 break; 1714 } 1715 1716 case KeyEvent.KEYCODE_DEL: { 1717 if (deleteSelection()) { 1718 return true; 1719 } 1720 break; 1721 } 1722 } 1723 1724 return super.onKeyDown(keyCode, event); 1725 } 1726 deleteSelection()1727 private boolean deleteSelection() { 1728 if ((mMode & MODE_MASK_PICKER) != 0) { 1729 return false; 1730 } 1731 1732 final int position = getListView().getSelectedItemPosition(); 1733 if (position != ListView.INVALID_POSITION) { 1734 Uri contactUri = getContactUri(position); 1735 if (contactUri != null) { 1736 doContactDelete(contactUri); 1737 return true; 1738 } 1739 } 1740 return false; 1741 } 1742 1743 /** 1744 * Prompt the user before deleting the given {@link Contacts} entry. 1745 */ doContactDelete(Uri contactUri)1746 protected void doContactDelete(Uri contactUri) { 1747 mReadOnlySourcesCnt = 0; 1748 mWritableSourcesCnt = 0; 1749 mWritableRawContactIds.clear(); 1750 1751 Sources sources = Sources.getInstance(ContactsListActivity.this); 1752 Cursor c = getContentResolver().query(RawContacts.CONTENT_URI, RAW_CONTACTS_PROJECTION, 1753 RawContacts.CONTACT_ID + "=" + ContentUris.parseId(contactUri), null, 1754 null); 1755 if (c != null) { 1756 try { 1757 while (c.moveToNext()) { 1758 final String accountType = c.getString(2); 1759 final long rawContactId = c.getLong(0); 1760 ContactsSource contactsSource = sources.getInflatedSource(accountType, 1761 ContactsSource.LEVEL_SUMMARY); 1762 if (contactsSource != null && contactsSource.readOnly) { 1763 mReadOnlySourcesCnt += 1; 1764 } else { 1765 mWritableSourcesCnt += 1; 1766 mWritableRawContactIds.add(rawContactId); 1767 } 1768 } 1769 } finally { 1770 c.close(); 1771 } 1772 } 1773 1774 mSelectedContactUri = contactUri; 1775 if (mReadOnlySourcesCnt > 0 && mWritableSourcesCnt > 0) { 1776 showDialog(R.id.dialog_readonly_contact_delete_confirmation); 1777 } else if (mReadOnlySourcesCnt > 0 && mWritableSourcesCnt == 0) { 1778 showDialog(R.id.dialog_readonly_contact_hide_confirmation); 1779 } else if (mReadOnlySourcesCnt == 0 && mWritableSourcesCnt > 1) { 1780 showDialog(R.id.dialog_multiple_contact_delete_confirmation); 1781 } else { 1782 showDialog(R.id.dialog_delete_contact_confirmation); 1783 } 1784 } 1785 1786 /** 1787 * Dismisses the soft keyboard when the list takes focus. 1788 */ onFocusChange(View view, boolean hasFocus)1789 public void onFocusChange(View view, boolean hasFocus) { 1790 if (view == getListView() && hasFocus) { 1791 hideSoftKeyboard(); 1792 } 1793 } 1794 1795 /** 1796 * Dismisses the soft keyboard when the list takes focus. 1797 */ onTouch(View view, MotionEvent event)1798 public boolean onTouch(View view, MotionEvent event) { 1799 if (view == getListView()) { 1800 hideSoftKeyboard(); 1801 } 1802 return false; 1803 } 1804 1805 /** 1806 * Dismisses the search UI along with the keyboard if the filter text is empty. 1807 */ onKeyPreIme(int keyCode, KeyEvent event)1808 public boolean onKeyPreIme(int keyCode, KeyEvent event) { 1809 if (mSearchMode && keyCode == KeyEvent.KEYCODE_BACK && TextUtils.isEmpty(getTextFilter())) { 1810 hideSoftKeyboard(); 1811 onBackPressed(); 1812 return true; 1813 } 1814 return false; 1815 } 1816 1817 @Override onListItemClick(ListView l, View v, int position, long id)1818 protected void onListItemClick(ListView l, View v, int position, long id) { 1819 hideSoftKeyboard(); 1820 1821 if (mSearchMode && mAdapter.isSearchAllContactsItemPosition(position)) { 1822 doSearch(); 1823 } else if (mMode == MODE_INSERT_OR_EDIT_CONTACT || mMode == MODE_QUERY_PICK_TO_EDIT) { 1824 Intent intent; 1825 if (position == 0 && !mSearchMode && mMode != MODE_QUERY_PICK_TO_EDIT) { 1826 intent = new Intent(Intent.ACTION_INSERT, Contacts.CONTENT_URI); 1827 } else { 1828 intent = new Intent(Intent.ACTION_EDIT, getSelectedUri(position)); 1829 } 1830 intent.setFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT); 1831 Bundle extras = getIntent().getExtras(); 1832 if (extras != null) { 1833 intent.putExtras(extras); 1834 } 1835 intent.putExtra(KEY_PICKER_MODE, (mMode & MODE_MASK_PICKER) == MODE_MASK_PICKER); 1836 1837 startActivity(intent); 1838 finish(); 1839 } else if ((mMode & MODE_MASK_CREATE_NEW) == MODE_MASK_CREATE_NEW 1840 && position == 0) { 1841 Intent newContact = new Intent(Intents.Insert.ACTION, Contacts.CONTENT_URI); 1842 startActivityForResult(newContact, SUBACTIVITY_NEW_CONTACT); 1843 } else if (mMode == MODE_JOIN_CONTACT && id == JOIN_MODE_SHOW_ALL_CONTACTS_ID) { 1844 mJoinModeShowAllContacts = false; 1845 startQuery(); 1846 } else if (id > 0) { 1847 final Uri uri = getSelectedUri(position); 1848 if ((mMode & MODE_MASK_PICKER) == 0) { 1849 final Intent intent = new Intent(Intent.ACTION_VIEW, uri); 1850 StickyTabs.setTab(intent, getIntent()); 1851 startActivityForResult(intent, SUBACTIVITY_VIEW_CONTACT); 1852 } else if (mMode == MODE_JOIN_CONTACT) { 1853 returnPickerResult(null, null, uri, 0); 1854 } else if (mMode == MODE_QUERY_PICK_TO_VIEW) { 1855 // Started with query that should launch to view contact 1856 final Intent intent = new Intent(Intent.ACTION_VIEW, uri); 1857 startActivity(intent); 1858 finish(); 1859 } else if (mMode == MODE_PICK_PHONE || mMode == MODE_QUERY_PICK_PHONE) { 1860 Cursor c = (Cursor) mAdapter.getItem(position); 1861 returnPickerResult(c, c.getString(PHONE_DISPLAY_NAME_COLUMN_INDEX), uri, 1862 Intent.FLAG_GRANT_READ_URI_PERMISSION); 1863 } else if ((mMode & MODE_MASK_PICKER) != 0) { 1864 Cursor c = (Cursor) mAdapter.getItem(position); 1865 returnPickerResult(c, c.getString(getSummaryDisplayNameColumnIndex()), uri, 1866 Intent.FLAG_GRANT_READ_URI_PERMISSION); 1867 } else if (mMode == MODE_PICK_POSTAL 1868 || mMode == MODE_LEGACY_PICK_POSTAL 1869 || mMode == MODE_LEGACY_PICK_PHONE) { 1870 returnPickerResult(null, null, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION); 1871 } 1872 } else { 1873 signalError(); 1874 } 1875 } 1876 hideSoftKeyboard()1877 private void hideSoftKeyboard() { 1878 // Hide soft keyboard, if visible 1879 InputMethodManager inputMethodManager = (InputMethodManager) 1880 getSystemService(Context.INPUT_METHOD_SERVICE); 1881 inputMethodManager.hideSoftInputFromWindow(mList.getWindowToken(), 0); 1882 } 1883 1884 /** 1885 * @param selectedUri In most cases, this should be a lookup {@link Uri}, possibly 1886 * generated through {@link Contacts#getLookupUri(long, String)}. 1887 */ returnPickerResult(Cursor c, String name, Uri selectedUri, int uriPerms)1888 private void returnPickerResult(Cursor c, String name, Uri selectedUri, int uriPerms) { 1889 final Intent intent = new Intent(); 1890 1891 if (mShortcutAction != null) { 1892 Intent shortcutIntent; 1893 if (Intent.ACTION_VIEW.equals(mShortcutAction)) { 1894 // This is a simple shortcut to view a contact. 1895 shortcutIntent = new Intent(ContactsContract.QuickContact.ACTION_QUICK_CONTACT); 1896 shortcutIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | 1897 Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED); 1898 1899 shortcutIntent.setData(selectedUri); 1900 shortcutIntent.putExtra(ContactsContract.QuickContact.EXTRA_MODE, 1901 ContactsContract.QuickContact.MODE_LARGE); 1902 shortcutIntent.putExtra(ContactsContract.QuickContact.EXTRA_EXCLUDE_MIMES, 1903 (String[]) null); 1904 1905 final Bitmap icon = framePhoto(loadContactPhoto(selectedUri, null)); 1906 if (icon != null) { 1907 intent.putExtra(Intent.EXTRA_SHORTCUT_ICON, scaleToAppIconSize(icon)); 1908 } else { 1909 intent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, 1910 Intent.ShortcutIconResource.fromContext(this, 1911 R.drawable.ic_launcher_shortcut_contact)); 1912 } 1913 } else { 1914 // This is a direct dial or sms shortcut. 1915 String number = c.getString(PHONE_NUMBER_COLUMN_INDEX); 1916 int type = c.getInt(PHONE_TYPE_COLUMN_INDEX); 1917 String scheme; 1918 int resid; 1919 if (Intent.ACTION_CALL.equals(mShortcutAction)) { 1920 scheme = Constants.SCHEME_TEL; 1921 resid = R.drawable.badge_action_call; 1922 } else { 1923 scheme = Constants.SCHEME_SMSTO; 1924 resid = R.drawable.badge_action_sms; 1925 } 1926 1927 // Make the URI a direct tel: URI so that it will always continue to work 1928 Uri phoneUri = Uri.fromParts(scheme, number, null); 1929 shortcutIntent = new Intent(mShortcutAction, phoneUri); 1930 1931 intent.putExtra(Intent.EXTRA_SHORTCUT_ICON, 1932 generatePhoneNumberIcon(selectedUri, type, resid)); 1933 } 1934 shortcutIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); 1935 intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent); 1936 intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, name); 1937 setResult(RESULT_OK, intent); 1938 } else { 1939 intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, name); 1940 intent.addFlags(uriPerms); 1941 setResult(RESULT_OK, intent.setData(selectedUri)); 1942 } 1943 finish(); 1944 } 1945 framePhoto(Bitmap photo)1946 private Bitmap framePhoto(Bitmap photo) { 1947 final Resources r = getResources(); 1948 final Drawable frame = r.getDrawable(com.android.internal.R.drawable.quickcontact_badge); 1949 1950 final int width = r.getDimensionPixelSize(R.dimen.contact_shortcut_frame_width); 1951 final int height = r.getDimensionPixelSize(R.dimen.contact_shortcut_frame_height); 1952 1953 frame.setBounds(0, 0, width, height); 1954 1955 final Rect padding = new Rect(); 1956 frame.getPadding(padding); 1957 1958 final Rect source = new Rect(0, 0, photo.getWidth(), photo.getHeight()); 1959 final Rect destination = new Rect(padding.left, padding.top, 1960 width - padding.right, height - padding.bottom); 1961 1962 final int d = Math.max(width, height); 1963 final Bitmap b = Bitmap.createBitmap(d, d, Bitmap.Config.ARGB_8888); 1964 final Canvas c = new Canvas(b); 1965 1966 c.translate((d - width) / 2.0f, (d - height) / 2.0f); 1967 frame.draw(c); 1968 c.drawBitmap(photo, source, destination, new Paint(Paint.FILTER_BITMAP_FLAG)); 1969 1970 return b; 1971 } 1972 1973 /** 1974 * Generates a phone number shortcut icon. Adds an overlay describing the type of the phone 1975 * number, and if there is a photo also adds the call action icon. 1976 * 1977 * @param lookupUri The person the phone number belongs to 1978 * @param type The type of the phone number 1979 * @param actionResId The ID for the action resource 1980 * @return The bitmap for the icon 1981 */ generatePhoneNumberIcon(Uri lookupUri, int type, int actionResId)1982 private Bitmap generatePhoneNumberIcon(Uri lookupUri, int type, int actionResId) { 1983 final Resources r = getResources(); 1984 boolean drawPhoneOverlay = true; 1985 final float scaleDensity = getResources().getDisplayMetrics().scaledDensity; 1986 1987 Bitmap photo = loadContactPhoto(lookupUri, null); 1988 if (photo == null) { 1989 // If there isn't a photo use the generic phone action icon instead 1990 Bitmap phoneIcon = getPhoneActionIcon(r, actionResId); 1991 if (phoneIcon != null) { 1992 photo = phoneIcon; 1993 drawPhoneOverlay = false; 1994 } else { 1995 return null; 1996 } 1997 } 1998 1999 // Setup the drawing classes 2000 Bitmap icon = createShortcutBitmap(); 2001 Canvas canvas = new Canvas(icon); 2002 2003 // Copy in the photo 2004 Paint photoPaint = new Paint(); 2005 photoPaint.setDither(true); 2006 photoPaint.setFilterBitmap(true); 2007 Rect src = new Rect(0,0, photo.getWidth(),photo.getHeight()); 2008 Rect dst = new Rect(0,0, mIconSize, mIconSize); 2009 canvas.drawBitmap(photo, src, dst, photoPaint); 2010 2011 // Create an overlay for the phone number type 2012 String overlay = null; 2013 switch (type) { 2014 case Phone.TYPE_HOME: 2015 overlay = getString(R.string.type_short_home); 2016 break; 2017 2018 case Phone.TYPE_MOBILE: 2019 overlay = getString(R.string.type_short_mobile); 2020 break; 2021 2022 case Phone.TYPE_WORK: 2023 overlay = getString(R.string.type_short_work); 2024 break; 2025 2026 case Phone.TYPE_PAGER: 2027 overlay = getString(R.string.type_short_pager); 2028 break; 2029 2030 case Phone.TYPE_OTHER: 2031 overlay = getString(R.string.type_short_other); 2032 break; 2033 } 2034 if (overlay != null) { 2035 Paint textPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DEV_KERN_TEXT_FLAG); 2036 textPaint.setTextSize(20.0f * scaleDensity); 2037 textPaint.setTypeface(Typeface.DEFAULT_BOLD); 2038 textPaint.setColor(r.getColor(R.color.textColorIconOverlay)); 2039 textPaint.setShadowLayer(3f, 1, 1, r.getColor(R.color.textColorIconOverlayShadow)); 2040 canvas.drawText(overlay, 2 * scaleDensity, 16 * scaleDensity, textPaint); 2041 } 2042 2043 // Draw the phone action icon as an overlay 2044 if (ENABLE_ACTION_ICON_OVERLAYS && drawPhoneOverlay) { 2045 Bitmap phoneIcon = getPhoneActionIcon(r, actionResId); 2046 if (phoneIcon != null) { 2047 src.set(0, 0, phoneIcon.getWidth(), phoneIcon.getHeight()); 2048 int iconWidth = icon.getWidth(); 2049 dst.set(iconWidth - ((int) (20 * scaleDensity)), -1, 2050 iconWidth, ((int) (19 * scaleDensity))); 2051 canvas.drawBitmap(phoneIcon, src, dst, photoPaint); 2052 } 2053 } 2054 2055 return icon; 2056 } 2057 scaleToAppIconSize(Bitmap photo)2058 private Bitmap scaleToAppIconSize(Bitmap photo) { 2059 // Setup the drawing classes 2060 Bitmap icon = createShortcutBitmap(); 2061 Canvas canvas = new Canvas(icon); 2062 2063 // Copy in the photo 2064 Paint photoPaint = new Paint(); 2065 photoPaint.setDither(true); 2066 photoPaint.setFilterBitmap(true); 2067 Rect src = new Rect(0,0, photo.getWidth(),photo.getHeight()); 2068 Rect dst = new Rect(0,0, mIconSize, mIconSize); 2069 canvas.drawBitmap(photo, src, dst, photoPaint); 2070 2071 return icon; 2072 } 2073 createShortcutBitmap()2074 private Bitmap createShortcutBitmap() { 2075 return Bitmap.createBitmap(mIconSize, mIconSize, Bitmap.Config.ARGB_8888); 2076 } 2077 2078 /** 2079 * Returns the icon for the phone call action. 2080 * 2081 * @param r The resources to load the icon from 2082 * @param resId The resource ID to load 2083 * @return the icon for the phone call action 2084 */ getPhoneActionIcon(Resources r, int resId)2085 private Bitmap getPhoneActionIcon(Resources r, int resId) { 2086 Drawable phoneIcon = r.getDrawable(resId); 2087 if (phoneIcon instanceof BitmapDrawable) { 2088 BitmapDrawable bd = (BitmapDrawable) phoneIcon; 2089 return bd.getBitmap(); 2090 } else { 2091 return null; 2092 } 2093 } 2094 getUriToQuery()2095 private Uri getUriToQuery() { 2096 switch(mMode) { 2097 case MODE_JOIN_CONTACT: 2098 return getJoinSuggestionsUri(null); 2099 case MODE_FREQUENT: 2100 case MODE_STARRED: 2101 return Contacts.CONTENT_URI; 2102 2103 case MODE_DEFAULT: 2104 case MODE_CUSTOM: 2105 case MODE_INSERT_OR_EDIT_CONTACT: 2106 case MODE_PICK_CONTACT: 2107 case MODE_PICK_OR_CREATE_CONTACT:{ 2108 return CONTACTS_CONTENT_URI_WITH_LETTER_COUNTS; 2109 } 2110 case MODE_STREQUENT: { 2111 return Contacts.CONTENT_STREQUENT_URI; 2112 } 2113 case MODE_LEGACY_PICK_PERSON: 2114 case MODE_LEGACY_PICK_OR_CREATE_PERSON: { 2115 return People.CONTENT_URI; 2116 } 2117 case MODE_PICK_PHONE: { 2118 return buildSectionIndexerUri(Phone.CONTENT_URI); 2119 } 2120 case MODE_LEGACY_PICK_PHONE: { 2121 return Phones.CONTENT_URI; 2122 } 2123 case MODE_PICK_POSTAL: { 2124 return buildSectionIndexerUri(StructuredPostal.CONTENT_URI); 2125 } 2126 case MODE_LEGACY_PICK_POSTAL: { 2127 return ContactMethods.CONTENT_URI; 2128 } 2129 case MODE_QUERY_PICK_TO_VIEW: { 2130 if (mQueryMode == QUERY_MODE_MAILTO) { 2131 return Uri.withAppendedPath(Email.CONTENT_FILTER_URI, 2132 Uri.encode(mInitialFilter)); 2133 } else if (mQueryMode == QUERY_MODE_TEL) { 2134 return Uri.withAppendedPath(Phone.CONTENT_FILTER_URI, 2135 Uri.encode(mInitialFilter)); 2136 } 2137 return CONTACTS_CONTENT_URI_WITH_LETTER_COUNTS; 2138 } 2139 case MODE_QUERY: 2140 case MODE_QUERY_PICK: 2141 case MODE_QUERY_PICK_TO_EDIT: { 2142 return getContactFilterUri(mInitialFilter); 2143 } 2144 case MODE_QUERY_PICK_PHONE: { 2145 return Uri.withAppendedPath(Phone.CONTENT_FILTER_URI, 2146 Uri.encode(mInitialFilter)); 2147 } 2148 case MODE_GROUP: { 2149 return mGroupUri; 2150 } 2151 default: { 2152 throw new IllegalStateException("Can't generate URI: Unsupported Mode."); 2153 } 2154 } 2155 } 2156 2157 /** 2158 * Build the {@link Contacts#CONTENT_LOOKUP_URI} for the given 2159 * {@link ListView} position, using {@link #mAdapter}. 2160 */ getContactUri(int position)2161 private Uri getContactUri(int position) { 2162 if (position == ListView.INVALID_POSITION) { 2163 throw new IllegalArgumentException("Position not in list bounds"); 2164 } 2165 2166 final Cursor cursor = (Cursor)mAdapter.getItem(position); 2167 if (cursor == null) { 2168 return null; 2169 } 2170 2171 switch(mMode) { 2172 case MODE_LEGACY_PICK_PERSON: 2173 case MODE_LEGACY_PICK_OR_CREATE_PERSON: { 2174 final long personId = cursor.getLong(SUMMARY_ID_COLUMN_INDEX); 2175 return ContentUris.withAppendedId(People.CONTENT_URI, personId); 2176 } 2177 2178 default: { 2179 // Build and return soft, lookup reference 2180 final long contactId = cursor.getLong(SUMMARY_ID_COLUMN_INDEX); 2181 final String lookupKey = cursor.getString(SUMMARY_LOOKUP_KEY_COLUMN_INDEX); 2182 return Contacts.getLookupUri(contactId, lookupKey); 2183 } 2184 } 2185 } 2186 2187 /** 2188 * Build the {@link Uri} for the given {@link ListView} position, which can 2189 * be used as result when in {@link #MODE_MASK_PICKER} mode. 2190 */ getSelectedUri(int position)2191 private Uri getSelectedUri(int position) { 2192 if (position == ListView.INVALID_POSITION) { 2193 throw new IllegalArgumentException("Position not in list bounds"); 2194 } 2195 2196 final long id = mAdapter.getItemId(position); 2197 switch(mMode) { 2198 case MODE_LEGACY_PICK_PERSON: 2199 case MODE_LEGACY_PICK_OR_CREATE_PERSON: { 2200 return ContentUris.withAppendedId(People.CONTENT_URI, id); 2201 } 2202 case MODE_PICK_PHONE: 2203 case MODE_QUERY_PICK_PHONE: { 2204 return ContentUris.withAppendedId(Data.CONTENT_URI, id); 2205 } 2206 case MODE_LEGACY_PICK_PHONE: { 2207 return ContentUris.withAppendedId(Phones.CONTENT_URI, id); 2208 } 2209 case MODE_PICK_POSTAL: { 2210 return ContentUris.withAppendedId(Data.CONTENT_URI, id); 2211 } 2212 case MODE_LEGACY_PICK_POSTAL: { 2213 return ContentUris.withAppendedId(ContactMethods.CONTENT_URI, id); 2214 } 2215 default: { 2216 return getContactUri(position); 2217 } 2218 } 2219 } 2220 getProjectionForQuery()2221 String[] getProjectionForQuery() { 2222 switch(mMode) { 2223 case MODE_JOIN_CONTACT: 2224 case MODE_STREQUENT: 2225 case MODE_FREQUENT: 2226 case MODE_STARRED: 2227 case MODE_DEFAULT: 2228 case MODE_CUSTOM: 2229 case MODE_INSERT_OR_EDIT_CONTACT: 2230 case MODE_GROUP: 2231 case MODE_PICK_CONTACT: 2232 case MODE_PICK_OR_CREATE_CONTACT: { 2233 return mSearchMode 2234 ? CONTACTS_SUMMARY_FILTER_PROJECTION 2235 : CONTACTS_SUMMARY_PROJECTION; 2236 } 2237 case MODE_QUERY: 2238 case MODE_QUERY_PICK: 2239 case MODE_QUERY_PICK_TO_EDIT: { 2240 return CONTACTS_SUMMARY_FILTER_PROJECTION; 2241 } 2242 case MODE_LEGACY_PICK_PERSON: 2243 case MODE_LEGACY_PICK_OR_CREATE_PERSON: { 2244 return LEGACY_PEOPLE_PROJECTION ; 2245 } 2246 case MODE_QUERY_PICK_PHONE: 2247 case MODE_PICK_PHONE: { 2248 return PHONES_PROJECTION; 2249 } 2250 case MODE_LEGACY_PICK_PHONE: { 2251 return LEGACY_PHONES_PROJECTION; 2252 } 2253 case MODE_PICK_POSTAL: { 2254 return POSTALS_PROJECTION; 2255 } 2256 case MODE_LEGACY_PICK_POSTAL: { 2257 return LEGACY_POSTALS_PROJECTION; 2258 } 2259 case MODE_QUERY_PICK_TO_VIEW: { 2260 if (mQueryMode == QUERY_MODE_MAILTO) { 2261 return CONTACTS_SUMMARY_PROJECTION_FROM_EMAIL; 2262 } else if (mQueryMode == QUERY_MODE_TEL) { 2263 return PHONES_PROJECTION; 2264 } 2265 break; 2266 } 2267 } 2268 2269 // Default to normal aggregate projection 2270 return CONTACTS_SUMMARY_PROJECTION; 2271 } 2272 loadContactPhoto(Uri selectedUri, BitmapFactory.Options options)2273 private Bitmap loadContactPhoto(Uri selectedUri, BitmapFactory.Options options) { 2274 Uri contactUri = null; 2275 if (Contacts.CONTENT_ITEM_TYPE.equals(getContentResolver().getType(selectedUri))) { 2276 // TODO we should have a "photo" directory under the lookup URI itself 2277 contactUri = Contacts.lookupContact(getContentResolver(), selectedUri); 2278 } else { 2279 2280 Cursor cursor = getContentResolver().query(selectedUri, 2281 new String[] { Data.CONTACT_ID }, null, null, null); 2282 try { 2283 if (cursor != null && cursor.moveToFirst()) { 2284 final long contactId = cursor.getLong(0); 2285 contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId); 2286 } 2287 } finally { 2288 if (cursor != null) cursor.close(); 2289 } 2290 } 2291 2292 Cursor cursor = null; 2293 Bitmap bm = null; 2294 2295 try { 2296 Uri photoUri = Uri.withAppendedPath(contactUri, Contacts.Photo.CONTENT_DIRECTORY); 2297 cursor = getContentResolver().query(photoUri, new String[] {Photo.PHOTO}, 2298 null, null, null); 2299 if (cursor != null && cursor.moveToFirst()) { 2300 bm = ContactsUtils.loadContactPhoto(cursor, 0, options); 2301 } 2302 } finally { 2303 if (cursor != null) { 2304 cursor.close(); 2305 } 2306 } 2307 2308 if (bm == null) { 2309 final int[] fallbacks = { 2310 R.drawable.ic_contact_picture, 2311 R.drawable.ic_contact_picture_2, 2312 R.drawable.ic_contact_picture_3 2313 }; 2314 bm = BitmapFactory.decodeResource(getResources(), 2315 fallbacks[new Random().nextInt(fallbacks.length)]); 2316 } 2317 2318 return bm; 2319 } 2320 2321 /** 2322 * Return the selection arguments for a default query based on the 2323 * {@link #mDisplayOnlyPhones} flag. 2324 */ getContactSelection()2325 private String getContactSelection() { 2326 if (mDisplayOnlyPhones) { 2327 return CLAUSE_ONLY_VISIBLE + " AND " + CLAUSE_ONLY_PHONES; 2328 } else { 2329 return CLAUSE_ONLY_VISIBLE; 2330 } 2331 } 2332 getContactFilterUri(String filter)2333 private Uri getContactFilterUri(String filter) { 2334 Uri baseUri; 2335 if (!TextUtils.isEmpty(filter)) { 2336 baseUri = Uri.withAppendedPath(Contacts.CONTENT_FILTER_URI, Uri.encode(filter)); 2337 } else { 2338 baseUri = Contacts.CONTENT_URI; 2339 } 2340 2341 if (mAdapter.getDisplaySectionHeadersEnabled()) { 2342 return buildSectionIndexerUri(baseUri); 2343 } else { 2344 return baseUri; 2345 } 2346 } 2347 getPeopleFilterUri(String filter)2348 private Uri getPeopleFilterUri(String filter) { 2349 if (!TextUtils.isEmpty(filter)) { 2350 return Uri.withAppendedPath(People.CONTENT_FILTER_URI, Uri.encode(filter)); 2351 } else { 2352 return People.CONTENT_URI; 2353 } 2354 } 2355 buildSectionIndexerUri(Uri uri)2356 private static Uri buildSectionIndexerUri(Uri uri) { 2357 return uri.buildUpon() 2358 .appendQueryParameter(ContactCounts.ADDRESS_BOOK_INDEX_EXTRAS, "true").build(); 2359 } 2360 getJoinSuggestionsUri(String filter)2361 private Uri getJoinSuggestionsUri(String filter) { 2362 Builder builder = Contacts.CONTENT_URI.buildUpon(); 2363 builder.appendEncodedPath(String.valueOf(mQueryAggregateId)); 2364 builder.appendEncodedPath(AggregationSuggestions.CONTENT_DIRECTORY); 2365 if (!TextUtils.isEmpty(filter)) { 2366 builder.appendEncodedPath(Uri.encode(filter)); 2367 } 2368 builder.appendQueryParameter("limit", String.valueOf(MAX_SUGGESTIONS)); 2369 return builder.build(); 2370 } 2371 getSortOrder(String[] projectionType)2372 private String getSortOrder(String[] projectionType) { 2373 if (mSortOrder == ContactsContract.Preferences.SORT_ORDER_PRIMARY) { 2374 return Contacts.SORT_KEY_PRIMARY; 2375 } else { 2376 return Contacts.SORT_KEY_ALTERNATIVE; 2377 } 2378 } 2379 startQuery()2380 void startQuery() { 2381 // Set the proper empty string 2382 setEmptyText(); 2383 2384 if (mSearchResultsMode) { 2385 TextView foundContactsText = (TextView)findViewById(R.id.search_results_found); 2386 foundContactsText.setText(R.string.search_results_searching); 2387 } 2388 2389 mAdapter.setLoading(true); 2390 2391 // Cancel any pending queries 2392 mQueryHandler.cancelOperation(QUERY_TOKEN); 2393 mQueryHandler.setLoadingJoinSuggestions(false); 2394 2395 mSortOrder = mContactsPrefs.getSortOrder(); 2396 mDisplayOrder = mContactsPrefs.getDisplayOrder(); 2397 2398 // When sort order and display order contradict each other, we want to 2399 // highlight the part of the name used for sorting. 2400 mHighlightWhenScrolling = false; 2401 if (mSortOrder == ContactsContract.Preferences.SORT_ORDER_PRIMARY && 2402 mDisplayOrder == ContactsContract.Preferences.DISPLAY_ORDER_ALTERNATIVE) { 2403 mHighlightWhenScrolling = true; 2404 } else if (mSortOrder == ContactsContract.Preferences.SORT_ORDER_ALTERNATIVE && 2405 mDisplayOrder == ContactsContract.Preferences.DISPLAY_ORDER_PRIMARY) { 2406 mHighlightWhenScrolling = true; 2407 } 2408 2409 String[] projection = getProjectionForQuery(); 2410 if (mSearchMode && TextUtils.isEmpty(getTextFilter())) { 2411 mAdapter.changeCursor(new MatrixCursor(projection)); 2412 return; 2413 } 2414 2415 String callingPackage = getCallingPackage(); 2416 Uri uri = getUriToQuery(); 2417 if (!TextUtils.isEmpty(callingPackage)) { 2418 uri = uri.buildUpon() 2419 .appendQueryParameter(ContactsContract.REQUESTING_PACKAGE_PARAM_KEY, 2420 callingPackage) 2421 .build(); 2422 } 2423 2424 // Kick off the new query 2425 switch (mMode) { 2426 case MODE_GROUP: 2427 case MODE_DEFAULT: 2428 case MODE_CUSTOM: 2429 case MODE_PICK_CONTACT: 2430 case MODE_PICK_OR_CREATE_CONTACT: 2431 case MODE_INSERT_OR_EDIT_CONTACT: 2432 mQueryHandler.startQuery(QUERY_TOKEN, null, uri, projection, getContactSelection(), 2433 null, getSortOrder(projection)); 2434 break; 2435 2436 case MODE_LEGACY_PICK_PERSON: 2437 case MODE_LEGACY_PICK_OR_CREATE_PERSON: { 2438 mQueryHandler.startQuery(QUERY_TOKEN, null, uri, projection, null, null, 2439 People.DISPLAY_NAME); 2440 break; 2441 } 2442 case MODE_PICK_POSTAL: 2443 case MODE_QUERY: 2444 case MODE_QUERY_PICK: 2445 case MODE_QUERY_PICK_PHONE: 2446 case MODE_QUERY_PICK_TO_VIEW: 2447 case MODE_QUERY_PICK_TO_EDIT: { 2448 mQueryHandler.startQuery(QUERY_TOKEN, null, uri, projection, null, null, 2449 getSortOrder(projection)); 2450 break; 2451 } 2452 2453 case MODE_STARRED: 2454 mQueryHandler.startQuery(QUERY_TOKEN, null, uri, 2455 projection, Contacts.STARRED + "=1", null, 2456 getSortOrder(projection)); 2457 break; 2458 2459 case MODE_FREQUENT: 2460 mQueryHandler.startQuery(QUERY_TOKEN, null, uri, 2461 projection, 2462 Contacts.TIMES_CONTACTED + " > 0", null, 2463 Contacts.TIMES_CONTACTED + " DESC, " 2464 + getSortOrder(projection)); 2465 break; 2466 2467 case MODE_STREQUENT: 2468 mQueryHandler.startQuery(QUERY_TOKEN, null, uri, projection, null, null, null); 2469 break; 2470 2471 case MODE_PICK_PHONE: 2472 mQueryHandler.startQuery(QUERY_TOKEN, null, uri, 2473 projection, CLAUSE_ONLY_VISIBLE, null, getSortOrder(projection)); 2474 break; 2475 2476 case MODE_LEGACY_PICK_PHONE: 2477 mQueryHandler.startQuery(QUERY_TOKEN, null, uri, 2478 projection, null, null, Phones.DISPLAY_NAME); 2479 break; 2480 2481 case MODE_LEGACY_PICK_POSTAL: 2482 mQueryHandler.startQuery(QUERY_TOKEN, null, uri, 2483 projection, 2484 ContactMethods.KIND + "=" + android.provider.Contacts.KIND_POSTAL, null, 2485 ContactMethods.DISPLAY_NAME); 2486 break; 2487 2488 case MODE_JOIN_CONTACT: 2489 mQueryHandler.setLoadingJoinSuggestions(true); 2490 mQueryHandler.startQuery(QUERY_TOKEN, null, uri, projection, 2491 null, null, null); 2492 break; 2493 } 2494 } 2495 2496 /** 2497 * Called from a background thread to do the filter and return the resulting cursor. 2498 * 2499 * @param filter the text that was entered to filter on 2500 * @return a cursor with the results of the filter 2501 */ doFilter(String filter)2502 Cursor doFilter(String filter) { 2503 String[] projection = getProjectionForQuery(); 2504 if (mSearchMode && TextUtils.isEmpty(getTextFilter())) { 2505 return new MatrixCursor(projection); 2506 } 2507 2508 final ContentResolver resolver = getContentResolver(); 2509 switch (mMode) { 2510 case MODE_DEFAULT: 2511 case MODE_CUSTOM: 2512 case MODE_PICK_CONTACT: 2513 case MODE_PICK_OR_CREATE_CONTACT: 2514 case MODE_INSERT_OR_EDIT_CONTACT: { 2515 return resolver.query(getContactFilterUri(filter), projection, 2516 getContactSelection(), null, getSortOrder(projection)); 2517 } 2518 2519 case MODE_LEGACY_PICK_PERSON: 2520 case MODE_LEGACY_PICK_OR_CREATE_PERSON: { 2521 return resolver.query(getPeopleFilterUri(filter), projection, null, null, 2522 People.DISPLAY_NAME); 2523 } 2524 2525 case MODE_STARRED: { 2526 return resolver.query(getContactFilterUri(filter), projection, 2527 Contacts.STARRED + "=1", null, 2528 getSortOrder(projection)); 2529 } 2530 2531 case MODE_FREQUENT: { 2532 return resolver.query(getContactFilterUri(filter), projection, 2533 Contacts.TIMES_CONTACTED + " > 0", null, 2534 Contacts.TIMES_CONTACTED + " DESC, " 2535 + getSortOrder(projection)); 2536 } 2537 2538 case MODE_STREQUENT: { 2539 Uri uri; 2540 if (!TextUtils.isEmpty(filter)) { 2541 uri = Uri.withAppendedPath(Contacts.CONTENT_STREQUENT_FILTER_URI, 2542 Uri.encode(filter)); 2543 } else { 2544 uri = Contacts.CONTENT_STREQUENT_URI; 2545 } 2546 return resolver.query(uri, projection, null, null, null); 2547 } 2548 2549 case MODE_PICK_PHONE: { 2550 Uri uri = getUriToQuery(); 2551 if (!TextUtils.isEmpty(filter)) { 2552 uri = Uri.withAppendedPath(Phone.CONTENT_FILTER_URI, Uri.encode(filter)); 2553 } 2554 return resolver.query(uri, projection, CLAUSE_ONLY_VISIBLE, null, 2555 getSortOrder(projection)); 2556 } 2557 2558 case MODE_LEGACY_PICK_PHONE: { 2559 //TODO: Support filtering here (bug 2092503) 2560 break; 2561 } 2562 2563 case MODE_JOIN_CONTACT: { 2564 2565 // We are on a background thread. Run queries one after the other synchronously 2566 Cursor cursor = resolver.query(getJoinSuggestionsUri(filter), projection, null, 2567 null, null); 2568 mAdapter.setSuggestionsCursor(cursor); 2569 mJoinModeShowAllContacts = false; 2570 return resolver.query(getContactFilterUri(filter), projection, 2571 Contacts._ID + " != " + mQueryAggregateId + " AND " + CLAUSE_ONLY_VISIBLE, 2572 null, getSortOrder(projection)); 2573 } 2574 } 2575 throw new UnsupportedOperationException("filtering not allowed in mode " + mMode); 2576 } 2577 getShowAllContactsLabelCursor(String[] projection)2578 private Cursor getShowAllContactsLabelCursor(String[] projection) { 2579 MatrixCursor matrixCursor = new MatrixCursor(projection); 2580 Object[] row = new Object[projection.length]; 2581 // The only columns we care about is the id 2582 row[SUMMARY_ID_COLUMN_INDEX] = JOIN_MODE_SHOW_ALL_CONTACTS_ID; 2583 matrixCursor.addRow(row); 2584 return matrixCursor; 2585 } 2586 2587 /** 2588 * Calls the currently selected list item. 2589 * @return true if the call was initiated, false otherwise 2590 */ callSelection()2591 boolean callSelection() { 2592 ListView list = getListView(); 2593 if (list.hasFocus()) { 2594 Cursor cursor = (Cursor) list.getSelectedItem(); 2595 return callContact(cursor); 2596 } 2597 return false; 2598 } 2599 callContact(Cursor cursor)2600 boolean callContact(Cursor cursor) { 2601 return callOrSmsContact(cursor, false /*call*/); 2602 } 2603 smsContact(Cursor cursor)2604 boolean smsContact(Cursor cursor) { 2605 return callOrSmsContact(cursor, true /*sms*/); 2606 } 2607 2608 /** 2609 * Calls the contact which the cursor is point to. 2610 * @return true if the call was initiated, false otherwise 2611 */ callOrSmsContact(Cursor cursor, boolean sendSms)2612 boolean callOrSmsContact(Cursor cursor, boolean sendSms) { 2613 if (cursor == null) { 2614 return false; 2615 } 2616 2617 switch (mMode) { 2618 case MODE_PICK_PHONE: 2619 case MODE_LEGACY_PICK_PHONE: 2620 case MODE_QUERY_PICK_PHONE: { 2621 String phone = cursor.getString(PHONE_NUMBER_COLUMN_INDEX); 2622 if (sendSms) { 2623 ContactsUtils.initiateSms(this, phone); 2624 } else { 2625 ContactsUtils.initiateCall(this, phone); 2626 } 2627 return true; 2628 } 2629 2630 case MODE_PICK_POSTAL: 2631 case MODE_LEGACY_PICK_POSTAL: { 2632 return false; 2633 } 2634 2635 default: { 2636 2637 boolean hasPhone = cursor.getInt(SUMMARY_HAS_PHONE_COLUMN_INDEX) != 0; 2638 if (!hasPhone) { 2639 // There is no phone number. 2640 signalError(); 2641 return false; 2642 } 2643 2644 String phone = null; 2645 Cursor phonesCursor = null; 2646 phonesCursor = queryPhoneNumbers(cursor.getLong(SUMMARY_ID_COLUMN_INDEX)); 2647 if (phonesCursor == null || phonesCursor.getCount() == 0) { 2648 // No valid number 2649 signalError(); 2650 return false; 2651 } else if (phonesCursor.getCount() == 1) { 2652 // only one number, call it. 2653 phone = phonesCursor.getString(phonesCursor.getColumnIndex(Phone.NUMBER)); 2654 } else { 2655 phonesCursor.moveToPosition(-1); 2656 while (phonesCursor.moveToNext()) { 2657 if (phonesCursor.getInt(phonesCursor. 2658 getColumnIndex(Phone.IS_SUPER_PRIMARY)) != 0) { 2659 // Found super primary, call it. 2660 phone = phonesCursor. 2661 getString(phonesCursor.getColumnIndex(Phone.NUMBER)); 2662 break; 2663 } 2664 } 2665 } 2666 2667 if (phone == null) { 2668 // Display dialog to choose a number to call. 2669 PhoneDisambigDialog phoneDialog = new PhoneDisambigDialog( 2670 this, phonesCursor, sendSms, StickyTabs.getTab(getIntent())); 2671 phoneDialog.show(); 2672 } else { 2673 if (sendSms) { 2674 ContactsUtils.initiateSms(this, phone); 2675 } else { 2676 StickyTabs.saveTab(this, getIntent()); 2677 ContactsUtils.initiateCall(this, phone); 2678 } 2679 } 2680 } 2681 } 2682 return true; 2683 } 2684 queryPhoneNumbers(long contactId)2685 private Cursor queryPhoneNumbers(long contactId) { 2686 Uri baseUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId); 2687 Uri dataUri = Uri.withAppendedPath(baseUri, Contacts.Data.CONTENT_DIRECTORY); 2688 2689 Cursor c = getContentResolver().query(dataUri, 2690 new String[] {Phone._ID, Phone.NUMBER, Phone.IS_SUPER_PRIMARY, 2691 RawContacts.ACCOUNT_TYPE, Phone.TYPE, Phone.LABEL}, 2692 Data.MIMETYPE + "=?", new String[] {Phone.CONTENT_ITEM_TYPE}, null); 2693 if (c != null) { 2694 if (c.moveToFirst()) { 2695 return c; 2696 } 2697 c.close(); 2698 } 2699 return null; 2700 } 2701 2702 // TODO: fix PluralRules to handle zero correctly and use Resources.getQuantityText directly getQuantityText(int count, int zeroResourceId, int pluralResourceId)2703 protected String getQuantityText(int count, int zeroResourceId, int pluralResourceId) { 2704 if (count == 0) { 2705 return getString(zeroResourceId); 2706 } else { 2707 String format = getResources().getQuantityText(pluralResourceId, count).toString(); 2708 return String.format(format, count); 2709 } 2710 } 2711 2712 /** 2713 * Signal an error to the user. 2714 */ signalError()2715 void signalError() { 2716 //TODO play an error beep or something... 2717 } 2718 getItemForView(View view)2719 Cursor getItemForView(View view) { 2720 ListView listView = getListView(); 2721 int index = listView.getPositionForView(view); 2722 if (index < 0) { 2723 return null; 2724 } 2725 return (Cursor) listView.getAdapter().getItem(index); 2726 } 2727 2728 private static class QueryHandler extends AsyncQueryHandler { 2729 protected final WeakReference<ContactsListActivity> mActivity; 2730 protected boolean mLoadingJoinSuggestions = false; 2731 QueryHandler(Context context)2732 public QueryHandler(Context context) { 2733 super(context.getContentResolver()); 2734 mActivity = new WeakReference<ContactsListActivity>((ContactsListActivity) context); 2735 } 2736 setLoadingJoinSuggestions(boolean flag)2737 public void setLoadingJoinSuggestions(boolean flag) { 2738 mLoadingJoinSuggestions = flag; 2739 } 2740 2741 @Override onQueryComplete(int token, Object cookie, Cursor cursor)2742 protected void onQueryComplete(int token, Object cookie, Cursor cursor) { 2743 final ContactsListActivity activity = mActivity.get(); 2744 if (activity != null && !activity.isFinishing()) { 2745 2746 // Whenever we get a suggestions cursor, we need to immediately kick off 2747 // another query for the complete list of contacts 2748 if (cursor != null && mLoadingJoinSuggestions) { 2749 mLoadingJoinSuggestions = false; 2750 if (cursor.getCount() > 0) { 2751 activity.mAdapter.setSuggestionsCursor(cursor); 2752 } else { 2753 cursor.close(); 2754 activity.mAdapter.setSuggestionsCursor(null); 2755 } 2756 2757 if (activity.mAdapter.mSuggestionsCursorCount == 0 2758 || !activity.mJoinModeShowAllContacts) { 2759 startQuery(QUERY_TOKEN, null, activity.getContactFilterUri( 2760 activity.getTextFilter()), 2761 CONTACTS_SUMMARY_PROJECTION, 2762 Contacts._ID + " != " + activity.mQueryAggregateId 2763 + " AND " + CLAUSE_ONLY_VISIBLE, null, 2764 activity.getSortOrder(CONTACTS_SUMMARY_PROJECTION)); 2765 return; 2766 } 2767 2768 cursor = activity.getShowAllContactsLabelCursor(CONTACTS_SUMMARY_PROJECTION); 2769 } 2770 2771 activity.mAdapter.changeCursor(cursor); 2772 2773 // Now that the cursor is populated again, it's possible to restore the list state 2774 if (activity.mListState != null) { 2775 activity.mList.onRestoreInstanceState(activity.mListState); 2776 activity.mListState = null; 2777 } 2778 } else { 2779 if (cursor != null) { 2780 cursor.close(); 2781 } 2782 } 2783 } 2784 } 2785 2786 final static class ContactListItemCache { 2787 public CharArrayBuffer nameBuffer = new CharArrayBuffer(128); 2788 public CharArrayBuffer dataBuffer = new CharArrayBuffer(128); 2789 public CharArrayBuffer highlightedTextBuffer = new CharArrayBuffer(128); 2790 public TextWithHighlighting textWithHighlighting; 2791 public CharArrayBuffer phoneticNameBuffer = new CharArrayBuffer(128); 2792 } 2793 2794 final static class PinnedHeaderCache { 2795 public TextView titleView; 2796 public ColorStateList textColor; 2797 public Drawable background; 2798 } 2799 2800 private final class ContactItemListAdapter extends CursorAdapter 2801 implements SectionIndexer, OnScrollListener, PinnedHeaderListView.PinnedHeaderAdapter { 2802 private SectionIndexer mIndexer; 2803 private boolean mLoading = true; 2804 private CharSequence mUnknownNameText; 2805 private boolean mDisplayPhotos = false; 2806 private boolean mDisplayCallButton = false; 2807 private boolean mDisplayAdditionalData = true; 2808 private int mFrequentSeparatorPos = ListView.INVALID_POSITION; 2809 private boolean mDisplaySectionHeaders = true; 2810 private Cursor mSuggestionsCursor; 2811 private int mSuggestionsCursorCount; 2812 ContactItemListAdapter(Context context)2813 public ContactItemListAdapter(Context context) { 2814 super(context, null, false); 2815 2816 mUnknownNameText = context.getText(android.R.string.unknownName); 2817 switch (mMode) { 2818 case MODE_LEGACY_PICK_POSTAL: 2819 case MODE_PICK_POSTAL: 2820 case MODE_LEGACY_PICK_PHONE: 2821 case MODE_PICK_PHONE: 2822 case MODE_STREQUENT: 2823 case MODE_FREQUENT: 2824 mDisplaySectionHeaders = false; 2825 break; 2826 } 2827 2828 if (mSearchMode) { 2829 mDisplaySectionHeaders = false; 2830 } 2831 2832 // Do not display the second line of text if in a specific SEARCH query mode, usually for 2833 // matching a specific E-mail or phone number. Any contact details 2834 // shown would be identical, and columns might not even be present 2835 // in the returned cursor. 2836 if (mMode != MODE_QUERY_PICK_PHONE && mQueryMode != QUERY_MODE_NONE) { 2837 mDisplayAdditionalData = false; 2838 } 2839 2840 if ((mMode & MODE_MASK_NO_DATA) == MODE_MASK_NO_DATA) { 2841 mDisplayAdditionalData = false; 2842 } 2843 2844 if ((mMode & MODE_MASK_SHOW_CALL_BUTTON) == MODE_MASK_SHOW_CALL_BUTTON) { 2845 mDisplayCallButton = true; 2846 } 2847 2848 if ((mMode & MODE_MASK_SHOW_PHOTOS) == MODE_MASK_SHOW_PHOTOS) { 2849 mDisplayPhotos = true; 2850 } 2851 } 2852 getDisplaySectionHeadersEnabled()2853 public boolean getDisplaySectionHeadersEnabled() { 2854 return mDisplaySectionHeaders; 2855 } 2856 setSuggestionsCursor(Cursor cursor)2857 public void setSuggestionsCursor(Cursor cursor) { 2858 if (mSuggestionsCursor != null) { 2859 mSuggestionsCursor.close(); 2860 } 2861 mSuggestionsCursor = cursor; 2862 mSuggestionsCursorCount = cursor == null ? 0 : cursor.getCount(); 2863 } 2864 2865 /** 2866 * Callback on the UI thread when the content observer on the backing cursor fires. 2867 * Instead of calling requery we need to do an async query so that the requery doesn't 2868 * block the UI thread for a long time. 2869 */ 2870 @Override onContentChanged()2871 protected void onContentChanged() { 2872 CharSequence constraint = getTextFilter(); 2873 if (!TextUtils.isEmpty(constraint)) { 2874 // Reset the filter state then start an async filter operation 2875 Filter filter = getFilter(); 2876 filter.filter(constraint); 2877 } else { 2878 // Start an async query 2879 startQuery(); 2880 } 2881 } 2882 setLoading(boolean loading)2883 public void setLoading(boolean loading) { 2884 mLoading = loading; 2885 } 2886 2887 @Override isEmpty()2888 public boolean isEmpty() { 2889 if (mProviderStatus != ProviderStatus.STATUS_NORMAL) { 2890 return true; 2891 } 2892 2893 if (mSearchMode) { 2894 return TextUtils.isEmpty(getTextFilter()); 2895 } else if ((mMode & MODE_MASK_CREATE_NEW) == MODE_MASK_CREATE_NEW) { 2896 // This mode mask adds a header and we always want it to show up, even 2897 // if the list is empty, so always claim the list is not empty. 2898 return false; 2899 } else { 2900 if (mCursor == null || mLoading) { 2901 // We don't want the empty state to show when loading. 2902 return false; 2903 } else { 2904 return super.isEmpty(); 2905 } 2906 } 2907 } 2908 2909 @Override getItemViewType(int position)2910 public int getItemViewType(int position) { 2911 if (position == 0 && (mShowNumberOfContacts || (mMode & MODE_MASK_CREATE_NEW) != 0)) { 2912 return IGNORE_ITEM_VIEW_TYPE; 2913 } 2914 2915 if (isShowAllContactsItemPosition(position)) { 2916 return IGNORE_ITEM_VIEW_TYPE; 2917 } 2918 2919 if (isSearchAllContactsItemPosition(position)) { 2920 return IGNORE_ITEM_VIEW_TYPE; 2921 } 2922 2923 if (getSeparatorId(position) != 0) { 2924 // We don't want the separator view to be recycled. 2925 return IGNORE_ITEM_VIEW_TYPE; 2926 } 2927 2928 return super.getItemViewType(position); 2929 } 2930 2931 @Override getView(int position, View convertView, ViewGroup parent)2932 public View getView(int position, View convertView, ViewGroup parent) { 2933 if (!mDataValid) { 2934 throw new IllegalStateException( 2935 "this should only be called when the cursor is valid"); 2936 } 2937 2938 // handle the total contacts item 2939 if (position == 0 && mShowNumberOfContacts) { 2940 return getTotalContactCountView(parent); 2941 } 2942 2943 if (position == 0 && (mMode & MODE_MASK_CREATE_NEW) != 0) { 2944 // Add the header for creating a new contact 2945 return getLayoutInflater().inflate(R.layout.create_new_contact, parent, false); 2946 } 2947 2948 if (isShowAllContactsItemPosition(position)) { 2949 return getLayoutInflater(). 2950 inflate(R.layout.contacts_list_show_all_item, parent, false); 2951 } 2952 2953 if (isSearchAllContactsItemPosition(position)) { 2954 return getLayoutInflater(). 2955 inflate(R.layout.contacts_list_search_all_item, parent, false); 2956 } 2957 2958 // Handle the separator specially 2959 int separatorId = getSeparatorId(position); 2960 if (separatorId != 0) { 2961 TextView view = (TextView) getLayoutInflater(). 2962 inflate(R.layout.list_separator, parent, false); 2963 view.setText(separatorId); 2964 return view; 2965 } 2966 2967 boolean showingSuggestion; 2968 Cursor cursor; 2969 if (mSuggestionsCursorCount != 0 && position < mSuggestionsCursorCount + 2) { 2970 showingSuggestion = true; 2971 cursor = mSuggestionsCursor; 2972 } else { 2973 showingSuggestion = false; 2974 cursor = mCursor; 2975 } 2976 2977 int realPosition = getRealPosition(position); 2978 if (!cursor.moveToPosition(realPosition)) { 2979 throw new IllegalStateException("couldn't move cursor to position " + position); 2980 } 2981 2982 boolean newView; 2983 View v; 2984 if (convertView == null || convertView.getTag() == null) { 2985 newView = true; 2986 v = newView(mContext, cursor, parent); 2987 } else { 2988 newView = false; 2989 v = convertView; 2990 } 2991 bindView(v, mContext, cursor); 2992 bindSectionHeader(v, realPosition, mDisplaySectionHeaders && !showingSuggestion); 2993 return v; 2994 } 2995 getTotalContactCountView(ViewGroup parent)2996 private View getTotalContactCountView(ViewGroup parent) { 2997 final LayoutInflater inflater = getLayoutInflater(); 2998 View view = inflater.inflate(R.layout.total_contacts, parent, false); 2999 3000 TextView totalContacts = (TextView) view.findViewById(R.id.totalContactsText); 3001 3002 String text; 3003 int count = getRealCount(); 3004 3005 if (mSearchMode && !TextUtils.isEmpty(getTextFilter())) { 3006 text = getQuantityText(count, R.string.listFoundAllContactsZero, 3007 R.plurals.searchFoundContacts); 3008 } else { 3009 if (mDisplayOnlyPhones) { 3010 text = getQuantityText(count, R.string.listTotalPhoneContactsZero, 3011 R.plurals.listTotalPhoneContacts); 3012 } else { 3013 text = getQuantityText(count, R.string.listTotalAllContactsZero, 3014 R.plurals.listTotalAllContacts); 3015 } 3016 } 3017 totalContacts.setText(text); 3018 return view; 3019 } 3020 isShowAllContactsItemPosition(int position)3021 private boolean isShowAllContactsItemPosition(int position) { 3022 return mMode == MODE_JOIN_CONTACT && mJoinModeShowAllContacts 3023 && mSuggestionsCursorCount != 0 && position == mSuggestionsCursorCount + 2; 3024 } 3025 isSearchAllContactsItemPosition(int position)3026 private boolean isSearchAllContactsItemPosition(int position) { 3027 return mSearchMode && position == getCount() - 1; 3028 } 3029 getSeparatorId(int position)3030 private int getSeparatorId(int position) { 3031 int separatorId = 0; 3032 if (position == mFrequentSeparatorPos) { 3033 separatorId = R.string.favoritesFrquentSeparator; 3034 } 3035 if (mSuggestionsCursorCount != 0) { 3036 if (position == 0) { 3037 separatorId = R.string.separatorJoinAggregateSuggestions; 3038 } else if (position == mSuggestionsCursorCount + 1) { 3039 separatorId = R.string.separatorJoinAggregateAll; 3040 } 3041 } 3042 return separatorId; 3043 } 3044 3045 @Override newView(Context context, Cursor cursor, ViewGroup parent)3046 public View newView(Context context, Cursor cursor, ViewGroup parent) { 3047 final ContactListItemView view = new ContactListItemView(context, null); 3048 view.setOnCallButtonClickListener(ContactsListActivity.this); 3049 view.setTag(new ContactListItemCache()); 3050 return view; 3051 } 3052 3053 @Override bindView(View itemView, Context context, Cursor cursor)3054 public void bindView(View itemView, Context context, Cursor cursor) { 3055 final ContactListItemView view = (ContactListItemView)itemView; 3056 final ContactListItemCache cache = (ContactListItemCache) view.getTag(); 3057 3058 int typeColumnIndex; 3059 int dataColumnIndex; 3060 int labelColumnIndex; 3061 int defaultType; 3062 int nameColumnIndex; 3063 int phoneticNameColumnIndex; 3064 boolean displayAdditionalData = mDisplayAdditionalData; 3065 boolean highlightingEnabled = false; 3066 switch(mMode) { 3067 case MODE_PICK_PHONE: 3068 case MODE_LEGACY_PICK_PHONE: 3069 case MODE_QUERY_PICK_PHONE: { 3070 nameColumnIndex = PHONE_DISPLAY_NAME_COLUMN_INDEX; 3071 phoneticNameColumnIndex = -1; 3072 dataColumnIndex = PHONE_NUMBER_COLUMN_INDEX; 3073 typeColumnIndex = PHONE_TYPE_COLUMN_INDEX; 3074 labelColumnIndex = PHONE_LABEL_COLUMN_INDEX; 3075 defaultType = Phone.TYPE_HOME; 3076 break; 3077 } 3078 case MODE_PICK_POSTAL: 3079 case MODE_LEGACY_PICK_POSTAL: { 3080 nameColumnIndex = POSTAL_DISPLAY_NAME_COLUMN_INDEX; 3081 phoneticNameColumnIndex = -1; 3082 dataColumnIndex = POSTAL_ADDRESS_COLUMN_INDEX; 3083 typeColumnIndex = POSTAL_TYPE_COLUMN_INDEX; 3084 labelColumnIndex = POSTAL_LABEL_COLUMN_INDEX; 3085 defaultType = StructuredPostal.TYPE_HOME; 3086 break; 3087 } 3088 default: { 3089 nameColumnIndex = getSummaryDisplayNameColumnIndex(); 3090 if (mMode == MODE_LEGACY_PICK_PERSON 3091 || mMode == MODE_LEGACY_PICK_OR_CREATE_PERSON) { 3092 phoneticNameColumnIndex = -1; 3093 } else { 3094 phoneticNameColumnIndex = SUMMARY_PHONETIC_NAME_COLUMN_INDEX; 3095 } 3096 dataColumnIndex = -1; 3097 typeColumnIndex = -1; 3098 labelColumnIndex = -1; 3099 defaultType = Phone.TYPE_HOME; 3100 displayAdditionalData = false; 3101 highlightingEnabled = mHighlightWhenScrolling && mMode != MODE_STREQUENT; 3102 } 3103 } 3104 3105 // Set the name 3106 cursor.copyStringToBuffer(nameColumnIndex, cache.nameBuffer); 3107 TextView nameView = view.getNameTextView(); 3108 int size = cache.nameBuffer.sizeCopied; 3109 if (size != 0) { 3110 if (highlightingEnabled) { 3111 if (cache.textWithHighlighting == null) { 3112 cache.textWithHighlighting = 3113 mHighlightingAnimation.createTextWithHighlighting(); 3114 } 3115 buildDisplayNameWithHighlighting(nameView, cursor, cache.nameBuffer, 3116 cache.highlightedTextBuffer, cache.textWithHighlighting); 3117 } else { 3118 nameView.setText(cache.nameBuffer.data, 0, size); 3119 } 3120 } else { 3121 nameView.setText(mUnknownNameText); 3122 } 3123 3124 boolean hasPhone = cursor.getColumnCount() > SUMMARY_HAS_PHONE_COLUMN_INDEX 3125 && cursor.getInt(SUMMARY_HAS_PHONE_COLUMN_INDEX) != 0; 3126 3127 // Make the call button visible if requested. 3128 if (mDisplayCallButton && hasPhone) { 3129 int pos = cursor.getPosition(); 3130 view.showCallButton(android.R.id.button1, pos); 3131 } else { 3132 view.hideCallButton(); 3133 } 3134 3135 // Set the photo, if requested 3136 if (mDisplayPhotos) { 3137 boolean useQuickContact = (mMode & MODE_MASK_DISABLE_QUIKCCONTACT) == 0; 3138 3139 long photoId = 0; 3140 if (!cursor.isNull(SUMMARY_PHOTO_ID_COLUMN_INDEX)) { 3141 photoId = cursor.getLong(SUMMARY_PHOTO_ID_COLUMN_INDEX); 3142 } 3143 3144 ImageView viewToUse; 3145 if (useQuickContact) { 3146 // Build soft lookup reference 3147 final long contactId = cursor.getLong(SUMMARY_ID_COLUMN_INDEX); 3148 final String lookupKey = cursor.getString(SUMMARY_LOOKUP_KEY_COLUMN_INDEX); 3149 QuickContactBadge quickContact = view.getQuickContact(); 3150 quickContact.assignContactUri(Contacts.getLookupUri(contactId, lookupKey)); 3151 quickContact.setSelectedContactsAppTabIndex(StickyTabs.getTab(getIntent())); 3152 viewToUse = quickContact; 3153 } else { 3154 viewToUse = view.getPhotoView(); 3155 } 3156 3157 final int position = cursor.getPosition(); 3158 mPhotoLoader.loadPhoto(viewToUse, photoId); 3159 } 3160 3161 if ((mMode & MODE_MASK_NO_PRESENCE) == 0) { 3162 // Set the proper icon (star or presence or nothing) 3163 int serverStatus; 3164 if (!cursor.isNull(SUMMARY_PRESENCE_STATUS_COLUMN_INDEX)) { 3165 serverStatus = cursor.getInt(SUMMARY_PRESENCE_STATUS_COLUMN_INDEX); 3166 Drawable icon = ContactPresenceIconUtil.getPresenceIcon(mContext, serverStatus); 3167 if (icon != null) { 3168 view.setPresence(icon); 3169 } else { 3170 view.setPresence(null); 3171 } 3172 } else { 3173 view.setPresence(null); 3174 } 3175 } else { 3176 view.setPresence(null); 3177 } 3178 3179 if (mShowSearchSnippets) { 3180 boolean showSnippet = false; 3181 String snippetMimeType = cursor.getString(SUMMARY_SNIPPET_MIMETYPE_COLUMN_INDEX); 3182 if (Email.CONTENT_ITEM_TYPE.equals(snippetMimeType)) { 3183 String email = cursor.getString(SUMMARY_SNIPPET_DATA1_COLUMN_INDEX); 3184 if (!TextUtils.isEmpty(email)) { 3185 view.setSnippet(email); 3186 showSnippet = true; 3187 } 3188 } else if (Organization.CONTENT_ITEM_TYPE.equals(snippetMimeType)) { 3189 String company = cursor.getString(SUMMARY_SNIPPET_DATA1_COLUMN_INDEX); 3190 String title = cursor.getString(SUMMARY_SNIPPET_DATA4_COLUMN_INDEX); 3191 if (!TextUtils.isEmpty(company)) { 3192 if (!TextUtils.isEmpty(title)) { 3193 view.setSnippet(company + " / " + title); 3194 } else { 3195 view.setSnippet(company); 3196 } 3197 showSnippet = true; 3198 } else if (!TextUtils.isEmpty(title)) { 3199 view.setSnippet(title); 3200 showSnippet = true; 3201 } 3202 } else if (Nickname.CONTENT_ITEM_TYPE.equals(snippetMimeType)) { 3203 String nickname = cursor.getString(SUMMARY_SNIPPET_DATA1_COLUMN_INDEX); 3204 if (!TextUtils.isEmpty(nickname)) { 3205 view.setSnippet(nickname); 3206 showSnippet = true; 3207 } 3208 } 3209 3210 if (!showSnippet) { 3211 view.setSnippet(null); 3212 } 3213 } 3214 3215 if (!displayAdditionalData) { 3216 if (phoneticNameColumnIndex != -1) { 3217 3218 // Set the name 3219 cursor.copyStringToBuffer(phoneticNameColumnIndex, cache.phoneticNameBuffer); 3220 int phoneticNameSize = cache.phoneticNameBuffer.sizeCopied; 3221 if (phoneticNameSize != 0) { 3222 view.setLabel(cache.phoneticNameBuffer.data, phoneticNameSize); 3223 } else { 3224 view.setLabel(null); 3225 } 3226 } else { 3227 view.setLabel(null); 3228 } 3229 return; 3230 } 3231 3232 // Set the data. 3233 cursor.copyStringToBuffer(dataColumnIndex, cache.dataBuffer); 3234 3235 size = cache.dataBuffer.sizeCopied; 3236 view.setData(cache.dataBuffer.data, size); 3237 3238 // Set the label. 3239 if (!cursor.isNull(typeColumnIndex)) { 3240 final int type = cursor.getInt(typeColumnIndex); 3241 final String label = cursor.getString(labelColumnIndex); 3242 3243 if (mMode == MODE_LEGACY_PICK_POSTAL || mMode == MODE_PICK_POSTAL) { 3244 // TODO cache 3245 view.setLabel(StructuredPostal.getTypeLabel(context.getResources(), type, 3246 label)); 3247 } else { 3248 // TODO cache 3249 view.setLabel(Phone.getTypeLabel(context.getResources(), type, label)); 3250 } 3251 } else { 3252 view.setLabel(null); 3253 } 3254 } 3255 3256 /** 3257 * Computes the span of the display name that has highlighted parts and configures 3258 * the display name text view accordingly. 3259 */ buildDisplayNameWithHighlighting(TextView textView, Cursor cursor, CharArrayBuffer buffer1, CharArrayBuffer buffer2, TextWithHighlighting textWithHighlighting)3260 private void buildDisplayNameWithHighlighting(TextView textView, Cursor cursor, 3261 CharArrayBuffer buffer1, CharArrayBuffer buffer2, 3262 TextWithHighlighting textWithHighlighting) { 3263 int oppositeDisplayOrderColumnIndex; 3264 if (mDisplayOrder == ContactsContract.Preferences.DISPLAY_ORDER_PRIMARY) { 3265 oppositeDisplayOrderColumnIndex = SUMMARY_DISPLAY_NAME_ALTERNATIVE_COLUMN_INDEX; 3266 } else { 3267 oppositeDisplayOrderColumnIndex = SUMMARY_DISPLAY_NAME_PRIMARY_COLUMN_INDEX; 3268 } 3269 cursor.copyStringToBuffer(oppositeDisplayOrderColumnIndex, buffer2); 3270 3271 textWithHighlighting.setText(buffer1, buffer2); 3272 textView.setText(textWithHighlighting); 3273 } 3274 bindSectionHeader(View itemView, int position, boolean displaySectionHeaders)3275 private void bindSectionHeader(View itemView, int position, boolean displaySectionHeaders) { 3276 final ContactListItemView view = (ContactListItemView)itemView; 3277 final ContactListItemCache cache = (ContactListItemCache) view.getTag(); 3278 if (!displaySectionHeaders) { 3279 view.setSectionHeader(null); 3280 view.setDividerVisible(true); 3281 } else { 3282 final int section = getSectionForPosition(position); 3283 if (getPositionForSection(section) == position) { 3284 String title = (String)mIndexer.getSections()[section]; 3285 view.setSectionHeader(title); 3286 } else { 3287 view.setDividerVisible(false); 3288 view.setSectionHeader(null); 3289 } 3290 3291 // move the divider for the last item in a section 3292 if (getPositionForSection(section + 1) - 1 == position) { 3293 view.setDividerVisible(false); 3294 } else { 3295 view.setDividerVisible(true); 3296 } 3297 } 3298 } 3299 3300 @Override changeCursor(Cursor cursor)3301 public void changeCursor(Cursor cursor) { 3302 if (cursor != null) { 3303 setLoading(false); 3304 } 3305 3306 // Get the split between starred and frequent items, if the mode is strequent 3307 mFrequentSeparatorPos = ListView.INVALID_POSITION; 3308 int cursorCount = 0; 3309 if (cursor != null && (cursorCount = cursor.getCount()) > 0 3310 && mMode == MODE_STREQUENT) { 3311 cursor.move(-1); 3312 for (int i = 0; cursor.moveToNext(); i++) { 3313 int starred = cursor.getInt(SUMMARY_STARRED_COLUMN_INDEX); 3314 if (starred == 0) { 3315 if (i > 0) { 3316 // Only add the separator when there are starred items present 3317 mFrequentSeparatorPos = i; 3318 } 3319 break; 3320 } 3321 } 3322 } 3323 3324 if (cursor != null && mSearchResultsMode) { 3325 TextView foundContactsText = (TextView)findViewById(R.id.search_results_found); 3326 String text = getQuantityText(cursor.getCount(), R.string.listFoundAllContactsZero, 3327 R.plurals.listFoundAllContacts); 3328 foundContactsText.setText(text); 3329 } 3330 3331 super.changeCursor(cursor); 3332 // Update the indexer for the fast scroll widget 3333 updateIndexer(cursor); 3334 } 3335 updateIndexer(Cursor cursor)3336 private void updateIndexer(Cursor cursor) { 3337 if (cursor == null) { 3338 mIndexer = null; 3339 return; 3340 } 3341 3342 Bundle bundle = cursor.getExtras(); 3343 if (bundle.containsKey(ContactCounts.EXTRA_ADDRESS_BOOK_INDEX_TITLES)) { 3344 String sections[] = 3345 bundle.getStringArray(ContactCounts.EXTRA_ADDRESS_BOOK_INDEX_TITLES); 3346 int counts[] = bundle.getIntArray(ContactCounts.EXTRA_ADDRESS_BOOK_INDEX_COUNTS); 3347 mIndexer = new ContactsSectionIndexer(sections, counts); 3348 } else { 3349 mIndexer = null; 3350 } 3351 } 3352 3353 /** 3354 * Run the query on a helper thread. Beware that this code does not run 3355 * on the main UI thread! 3356 */ 3357 @Override runQueryOnBackgroundThread(CharSequence constraint)3358 public Cursor runQueryOnBackgroundThread(CharSequence constraint) { 3359 return doFilter(constraint.toString()); 3360 } 3361 getSections()3362 public Object [] getSections() { 3363 if (mIndexer == null) { 3364 return new String[] { " " }; 3365 } else { 3366 return mIndexer.getSections(); 3367 } 3368 } 3369 getPositionForSection(int sectionIndex)3370 public int getPositionForSection(int sectionIndex) { 3371 if (mIndexer == null) { 3372 return -1; 3373 } 3374 3375 return mIndexer.getPositionForSection(sectionIndex); 3376 } 3377 getSectionForPosition(int position)3378 public int getSectionForPosition(int position) { 3379 if (mIndexer == null) { 3380 return -1; 3381 } 3382 3383 return mIndexer.getSectionForPosition(position); 3384 } 3385 3386 @Override areAllItemsEnabled()3387 public boolean areAllItemsEnabled() { 3388 return mMode != MODE_STARRED 3389 && !mShowNumberOfContacts 3390 && mSuggestionsCursorCount == 0; 3391 } 3392 3393 @Override isEnabled(int position)3394 public boolean isEnabled(int position) { 3395 if (mShowNumberOfContacts) { 3396 if (position == 0) { 3397 return false; 3398 } 3399 position--; 3400 } 3401 3402 if (mSuggestionsCursorCount > 0) { 3403 return position != 0 && position != mSuggestionsCursorCount + 1; 3404 } 3405 return position != mFrequentSeparatorPos; 3406 } 3407 3408 @Override getCount()3409 public int getCount() { 3410 if (!mDataValid) { 3411 return 0; 3412 } 3413 int superCount = super.getCount(); 3414 3415 if (mShowNumberOfContacts && (mSearchMode || superCount > 0)) { 3416 // We don't want to count this header if it's the only thing visible, so that 3417 // the empty text will display. 3418 superCount++; 3419 } 3420 3421 if (mSearchMode) { 3422 // Last element in the list is the "Find 3423 superCount++; 3424 } 3425 3426 // We do not show the "Create New" button in Search mode 3427 if ((mMode & MODE_MASK_CREATE_NEW) != 0 && !mSearchMode) { 3428 // Count the "Create new contact" line 3429 superCount++; 3430 } 3431 3432 if (mSuggestionsCursorCount != 0) { 3433 // When showing suggestions, we have 2 additional list items: the "Suggestions" 3434 // and "All contacts" headers. 3435 return mSuggestionsCursorCount + superCount + 2; 3436 } 3437 else if (mFrequentSeparatorPos != ListView.INVALID_POSITION) { 3438 // When showing strequent list, we have an additional list item - the separator. 3439 return superCount + 1; 3440 } else { 3441 return superCount; 3442 } 3443 } 3444 3445 /** 3446 * Gets the actual count of contacts and excludes all the headers. 3447 */ getRealCount()3448 public int getRealCount() { 3449 return super.getCount(); 3450 } 3451 getRealPosition(int pos)3452 private int getRealPosition(int pos) { 3453 if (mShowNumberOfContacts) { 3454 pos--; 3455 } 3456 3457 if ((mMode & MODE_MASK_CREATE_NEW) != 0 && !mSearchMode) { 3458 return pos - 1; 3459 } else if (mSuggestionsCursorCount != 0) { 3460 // When showing suggestions, we have 2 additional list items: the "Suggestions" 3461 // and "All contacts" separators. 3462 if (pos < mSuggestionsCursorCount + 2) { 3463 // We are in the upper partition (Suggestions). Adjusting for the "Suggestions" 3464 // separator. 3465 return pos - 1; 3466 } else { 3467 // We are in the lower partition (All contacts). Adjusting for the size 3468 // of the upper partition plus the two separators. 3469 return pos - mSuggestionsCursorCount - 2; 3470 } 3471 } else if (mFrequentSeparatorPos == ListView.INVALID_POSITION) { 3472 // No separator, identity map 3473 return pos; 3474 } else if (pos <= mFrequentSeparatorPos) { 3475 // Before or at the separator, identity map 3476 return pos; 3477 } else { 3478 // After the separator, remove 1 from the pos to get the real underlying pos 3479 return pos - 1; 3480 } 3481 } 3482 3483 @Override getItem(int pos)3484 public Object getItem(int pos) { 3485 if (mSuggestionsCursorCount != 0 && pos <= mSuggestionsCursorCount) { 3486 mSuggestionsCursor.moveToPosition(getRealPosition(pos)); 3487 return mSuggestionsCursor; 3488 } else if (isSearchAllContactsItemPosition(pos)){ 3489 return null; 3490 } else { 3491 int realPosition = getRealPosition(pos); 3492 if (realPosition < 0) { 3493 return null; 3494 } 3495 return super.getItem(realPosition); 3496 } 3497 } 3498 3499 @Override getItemId(int pos)3500 public long getItemId(int pos) { 3501 if (mSuggestionsCursorCount != 0 && pos < mSuggestionsCursorCount + 2) { 3502 if (mSuggestionsCursor.moveToPosition(pos - 1)) { 3503 return mSuggestionsCursor.getLong(mRowIDColumn); 3504 } else { 3505 return 0; 3506 } 3507 } else if (isSearchAllContactsItemPosition(pos)) { 3508 return 0; 3509 } 3510 int realPosition = getRealPosition(pos); 3511 if (realPosition < 0) { 3512 return 0; 3513 } 3514 return super.getItemId(realPosition); 3515 } 3516 onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount)3517 public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, 3518 int totalItemCount) { 3519 if (view instanceof PinnedHeaderListView) { 3520 ((PinnedHeaderListView)view).configureHeaderView(firstVisibleItem); 3521 } 3522 } 3523 onScrollStateChanged(AbsListView view, int scrollState)3524 public void onScrollStateChanged(AbsListView view, int scrollState) { 3525 if (mHighlightWhenScrolling) { 3526 if (scrollState != OnScrollListener.SCROLL_STATE_IDLE) { 3527 mHighlightingAnimation.startHighlighting(); 3528 } else { 3529 mHighlightingAnimation.stopHighlighting(); 3530 } 3531 } 3532 3533 if (scrollState == OnScrollListener.SCROLL_STATE_FLING) { 3534 mPhotoLoader.pause(); 3535 } else if (mDisplayPhotos) { 3536 mPhotoLoader.resume(); 3537 } 3538 } 3539 3540 /** 3541 * Computes the state of the pinned header. It can be invisible, fully 3542 * visible or partially pushed up out of the view. 3543 */ getPinnedHeaderState(int position)3544 public int getPinnedHeaderState(int position) { 3545 if (mIndexer == null || mCursor == null || mCursor.getCount() == 0) { 3546 return PINNED_HEADER_GONE; 3547 } 3548 3549 int realPosition = getRealPosition(position); 3550 if (realPosition < 0) { 3551 return PINNED_HEADER_GONE; 3552 } 3553 3554 // The header should get pushed up if the top item shown 3555 // is the last item in a section for a particular letter. 3556 int section = getSectionForPosition(realPosition); 3557 int nextSectionPosition = getPositionForSection(section + 1); 3558 if (nextSectionPosition != -1 && realPosition == nextSectionPosition - 1) { 3559 return PINNED_HEADER_PUSHED_UP; 3560 } 3561 3562 return PINNED_HEADER_VISIBLE; 3563 } 3564 3565 /** 3566 * Configures the pinned header by setting the appropriate text label 3567 * and also adjusting color if necessary. The color needs to be 3568 * adjusted when the pinned header is being pushed up from the view. 3569 */ configurePinnedHeader(View header, int position, int alpha)3570 public void configurePinnedHeader(View header, int position, int alpha) { 3571 PinnedHeaderCache cache = (PinnedHeaderCache)header.getTag(); 3572 if (cache == null) { 3573 cache = new PinnedHeaderCache(); 3574 cache.titleView = (TextView)header.findViewById(R.id.header_text); 3575 cache.textColor = cache.titleView.getTextColors(); 3576 cache.background = header.getBackground(); 3577 header.setTag(cache); 3578 } 3579 3580 int realPosition = getRealPosition(position); 3581 int section = getSectionForPosition(realPosition); 3582 3583 String title = (String)mIndexer.getSections()[section]; 3584 cache.titleView.setText(title); 3585 3586 if (alpha == 255) { 3587 // Opaque: use the default background, and the original text color 3588 header.setBackgroundDrawable(cache.background); 3589 cache.titleView.setTextColor(cache.textColor); 3590 } else { 3591 // Faded: use a solid color approximation of the background, and 3592 // a translucent text color 3593 header.setBackgroundColor(Color.rgb( 3594 Color.red(mPinnedHeaderBackgroundColor) * alpha / 255, 3595 Color.green(mPinnedHeaderBackgroundColor) * alpha / 255, 3596 Color.blue(mPinnedHeaderBackgroundColor) * alpha / 255)); 3597 3598 int textColor = cache.textColor.getDefaultColor(); 3599 cache.titleView.setTextColor(Color.argb(alpha, 3600 Color.red(textColor), Color.green(textColor), Color.blue(textColor))); 3601 } 3602 } 3603 } 3604 3605 private ContactsPreferences.ChangeListener mPreferencesChangeListener = 3606 new ContactsPreferences.ChangeListener() { 3607 @Override 3608 public void onChange() { 3609 // When returning from DisplayOptions, onActivityResult ensures that we reload the list, 3610 // so we do not have to do anything here. However, ContactsPreferences requires a change 3611 // listener, otherwise it would not reload its settings. 3612 } 3613 }; 3614 } 3615