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.model.ContactsSource; 20 import com.android.contacts.model.Sources; 21 import com.android.contacts.ui.DisplayGroupsActivity; 22 import com.android.contacts.ui.DisplayGroupsActivity.Prefs; 23 import com.android.contacts.util.AccountSelectionUtil; 24 import com.android.contacts.util.Constants; 25 26 import android.accounts.Account; 27 import android.app.Activity; 28 import android.app.AlertDialog; 29 import android.app.Dialog; 30 import android.app.ListActivity; 31 import android.app.SearchManager; 32 import android.content.AsyncQueryHandler; 33 import android.content.ContentResolver; 34 import android.content.ContentUris; 35 import android.content.ContentValues; 36 import android.content.Context; 37 import android.content.DialogInterface; 38 import android.content.Intent; 39 import android.content.SharedPreferences; 40 import android.content.UriMatcher; 41 import android.content.res.Resources; 42 import android.database.CharArrayBuffer; 43 import android.database.Cursor; 44 import android.database.MatrixCursor; 45 import android.graphics.Bitmap; 46 import android.graphics.BitmapFactory; 47 import android.graphics.Canvas; 48 import android.graphics.Paint; 49 import android.graphics.Rect; 50 import android.graphics.Typeface; 51 import android.graphics.drawable.BitmapDrawable; 52 import android.graphics.drawable.Drawable; 53 import android.net.Uri; 54 import android.net.Uri.Builder; 55 import android.os.Bundle; 56 import android.os.Handler; 57 import android.os.Message; 58 import android.os.Parcelable; 59 import android.preference.PreferenceManager; 60 import android.provider.ContactsContract; 61 import android.provider.Settings; 62 import android.provider.Contacts.ContactMethods; 63 import android.provider.Contacts.People; 64 import android.provider.Contacts.PeopleColumns; 65 import android.provider.Contacts.Phones; 66 import android.provider.ContactsContract.Contacts; 67 import android.provider.ContactsContract.Data; 68 import android.provider.ContactsContract.Intents; 69 import android.provider.ContactsContract.Presence; 70 import android.provider.ContactsContract.RawContacts; 71 import android.provider.ContactsContract.CommonDataKinds.Email; 72 import android.provider.ContactsContract.CommonDataKinds.Phone; 73 import android.provider.ContactsContract.CommonDataKinds.Photo; 74 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal; 75 import android.provider.ContactsContract.Contacts.AggregationSuggestions; 76 import android.provider.ContactsContract.Intents.Insert; 77 import android.provider.ContactsContract.Intents.UI; 78 import android.telephony.TelephonyManager; 79 import android.text.TextUtils; 80 import android.util.DisplayMetrics; 81 import android.util.Log; 82 import android.view.ContextMenu; 83 import android.view.ContextThemeWrapper; 84 import android.view.Gravity; 85 import android.view.KeyEvent; 86 import android.view.LayoutInflater; 87 import android.view.Menu; 88 import android.view.MenuInflater; 89 import android.view.MenuItem; 90 import android.view.View; 91 import android.view.ViewGroup; 92 import android.view.ContextMenu.ContextMenuInfo; 93 import android.view.inputmethod.InputMethodManager; 94 import android.widget.AbsListView; 95 import android.widget.AdapterView; 96 import android.widget.AlphabetIndexer; 97 import android.widget.ArrayAdapter; 98 import android.widget.Filter; 99 import android.widget.ImageView; 100 import android.widget.ListView; 101 import android.widget.QuickContactBadge; 102 import android.widget.ResourceCursorAdapter; 103 import android.widget.SectionIndexer; 104 import android.widget.TextView; 105 import android.widget.AbsListView.OnScrollListener; 106 107 import java.lang.ref.SoftReference; 108 import java.lang.ref.WeakReference; 109 import java.util.ArrayList; 110 import java.util.HashMap; 111 import java.util.HashSet; 112 import java.util.List; 113 import java.util.Locale; 114 import java.util.concurrent.ExecutorService; 115 import java.util.concurrent.Executors; 116 117 /*TODO(emillar) I commented most of the code that deals with modes and filtering. It should be 118 * brought back in as we add back that functionality. 119 */ 120 121 122 /** 123 * Displays a list of contacts. Usually is embedded into the ContactsActivity. 124 */ 125 @SuppressWarnings("deprecation") 126 public class ContactsListActivity extends ListActivity implements 127 View.OnCreateContextMenuListener, View.OnClickListener { 128 129 public static class JoinContactActivity extends ContactsListActivity { 130 131 } 132 133 private static final String TAG = "ContactsListActivity"; 134 135 private static final boolean ENABLE_ACTION_ICON_OVERLAYS = true; 136 137 private static final String LIST_STATE_KEY = "liststate"; 138 private static final String FOCUS_KEY = "focused"; 139 140 static final int MENU_ITEM_VIEW_CONTACT = 1; 141 static final int MENU_ITEM_CALL = 2; 142 static final int MENU_ITEM_EDIT_BEFORE_CALL = 3; 143 static final int MENU_ITEM_SEND_SMS = 4; 144 static final int MENU_ITEM_SEND_IM = 5; 145 static final int MENU_ITEM_EDIT = 6; 146 static final int MENU_ITEM_DELETE = 7; 147 static final int MENU_ITEM_TOGGLE_STAR = 8; 148 149 private static final int SUBACTIVITY_NEW_CONTACT = 1; 150 private static final int SUBACTIVITY_VIEW_CONTACT = 2; 151 private static final int SUBACTIVITY_DISPLAY_GROUP = 3; 152 153 /** 154 * The action for the join contact activity. 155 * <p> 156 * Input: extra field {@link #EXTRA_AGGREGATE_ID} is the aggregate ID. 157 * 158 * TODO: move to {@link ContactsContract}. 159 */ 160 public static final String JOIN_AGGREGATE = 161 "com.android.contacts.action.JOIN_AGGREGATE"; 162 163 /** 164 * Used with {@link #JOIN_AGGREGATE} to give it the target for aggregation. 165 * <p> 166 * Type: LONG 167 */ 168 public static final String EXTRA_AGGREGATE_ID = 169 "com.android.contacts.action.AGGREGATE_ID"; 170 171 /** 172 * Used with {@link #JOIN_AGGREGATE} to give it the name of the aggregation target. 173 * <p> 174 * Type: STRING 175 */ 176 @Deprecated 177 public static final String EXTRA_AGGREGATE_NAME = 178 "com.android.contacts.action.AGGREGATE_NAME"; 179 180 public static final String AUTHORITIES_FILTER_KEY = "authorities"; 181 182 /** Mask for picker mode */ 183 static final int MODE_MASK_PICKER = 0x80000000; 184 /** Mask for no presence mode */ 185 static final int MODE_MASK_NO_PRESENCE = 0x40000000; 186 /** Mask for enabling list filtering */ 187 static final int MODE_MASK_NO_FILTER = 0x20000000; 188 /** Mask for having a "create new contact" header in the list */ 189 static final int MODE_MASK_CREATE_NEW = 0x10000000; 190 /** Mask for showing photos in the list */ 191 static final int MODE_MASK_SHOW_PHOTOS = 0x08000000; 192 /** Mask for hiding additional information e.g. primary phone number in the list */ 193 static final int MODE_MASK_NO_DATA = 0x04000000; 194 /** Mask for showing a call button in the list */ 195 static final int MODE_MASK_SHOW_CALL_BUTTON = 0x02000000; 196 /** Mask to disable quickcontact (images will show as normal images) */ 197 static final int MODE_MASK_DISABLE_QUIKCCONTACT = 0x01000000; 198 /** Mask to show the total number of contacts at the top */ 199 static final int MODE_MASK_SHOW_NUMBER_OF_CONTACTS = 0x00800000; 200 201 /** Unknown mode */ 202 static final int MODE_UNKNOWN = 0; 203 /** Default mode */ 204 static final int MODE_DEFAULT = 4 | MODE_MASK_SHOW_PHOTOS | MODE_MASK_SHOW_NUMBER_OF_CONTACTS; 205 /** Custom mode */ 206 static final int MODE_CUSTOM = 8; 207 /** Show all starred contacts */ 208 static final int MODE_STARRED = 20 | MODE_MASK_SHOW_PHOTOS; 209 /** Show frequently contacted contacts */ 210 static final int MODE_FREQUENT = 30 | MODE_MASK_SHOW_PHOTOS; 211 /** Show starred and the frequent */ 212 static final int MODE_STREQUENT = 35 | MODE_MASK_SHOW_PHOTOS | MODE_MASK_SHOW_CALL_BUTTON; 213 /** Show all contacts and pick them when clicking */ 214 static final int MODE_PICK_CONTACT = 40 | MODE_MASK_PICKER | MODE_MASK_SHOW_PHOTOS 215 | MODE_MASK_DISABLE_QUIKCCONTACT; 216 /** Show all contacts as well as the option to create a new one */ 217 static final int MODE_PICK_OR_CREATE_CONTACT = 42 | MODE_MASK_PICKER | MODE_MASK_CREATE_NEW 218 | MODE_MASK_SHOW_PHOTOS | MODE_MASK_DISABLE_QUIKCCONTACT; 219 /** Show all people through the legacy provider and pick them when clicking */ 220 static final int MODE_LEGACY_PICK_PERSON = 43 | MODE_MASK_PICKER | MODE_MASK_SHOW_PHOTOS 221 | MODE_MASK_DISABLE_QUIKCCONTACT; 222 /** Show all people through the legacy provider as well as the option to create a new one */ 223 static final int MODE_LEGACY_PICK_OR_CREATE_PERSON = 44 | MODE_MASK_PICKER 224 | MODE_MASK_CREATE_NEW | MODE_MASK_SHOW_PHOTOS | MODE_MASK_DISABLE_QUIKCCONTACT; 225 /** Show all contacts and pick them when clicking, and allow creating a new contact */ 226 static final int MODE_INSERT_OR_EDIT_CONTACT = 45 | MODE_MASK_PICKER | MODE_MASK_CREATE_NEW; 227 /** Show all phone numbers and pick them when clicking */ 228 static final int MODE_PICK_PHONE = 50 | MODE_MASK_PICKER | MODE_MASK_NO_PRESENCE; 229 /** Show all phone numbers through the legacy provider and pick them when clicking */ 230 static final int MODE_LEGACY_PICK_PHONE = 231 51 | MODE_MASK_PICKER | MODE_MASK_NO_PRESENCE | MODE_MASK_NO_FILTER; 232 /** Show all postal addresses and pick them when clicking */ 233 static final int MODE_PICK_POSTAL = 234 55 | MODE_MASK_PICKER | MODE_MASK_NO_PRESENCE | MODE_MASK_NO_FILTER; 235 /** Show all postal addresses and pick them when clicking */ 236 static final int MODE_LEGACY_PICK_POSTAL = 237 56 | MODE_MASK_PICKER | MODE_MASK_NO_PRESENCE | MODE_MASK_NO_FILTER; 238 static final int MODE_GROUP = 57 | MODE_MASK_SHOW_PHOTOS; 239 /** Run a search query */ 240 static final int MODE_QUERY = 60 | MODE_MASK_NO_FILTER | MODE_MASK_SHOW_NUMBER_OF_CONTACTS; 241 /** Run a search query in PICK mode, but that still launches to VIEW */ 242 static final int MODE_QUERY_PICK_TO_VIEW = 65 | MODE_MASK_NO_FILTER | MODE_MASK_PICKER; 243 244 /** Show join suggestions followed by an A-Z list */ 245 static final int MODE_JOIN_CONTACT = 70 | MODE_MASK_PICKER | MODE_MASK_NO_PRESENCE 246 | MODE_MASK_NO_DATA | MODE_MASK_SHOW_PHOTOS | MODE_MASK_DISABLE_QUIKCCONTACT; 247 248 /** Maximum number of suggestions shown for joining aggregates */ 249 static final int MAX_SUGGESTIONS = 4; 250 251 static final String NAME_COLUMN = Contacts.DISPLAY_NAME; 252 //static final String SORT_STRING = People.SORT_STRING; 253 254 static final String[] CONTACTS_SUMMARY_PROJECTION = new String[] { 255 Contacts._ID, // 0 256 Contacts.DISPLAY_NAME, // 1 257 Contacts.STARRED, //2 258 Contacts.TIMES_CONTACTED, //3 259 Contacts.CONTACT_PRESENCE, //4 260 Contacts.PHOTO_ID, //5 261 Contacts.LOOKUP_KEY, //6 262 Contacts.HAS_PHONE_NUMBER, //7 263 }; 264 static final String[] CONTACTS_SUMMARY_PROJECTION_FROM_EMAIL = new String[] { 265 Contacts._ID, // 0 266 Contacts.DISPLAY_NAME, // 1 267 Contacts.STARRED, //2 268 Contacts.TIMES_CONTACTED, //3 269 Contacts.CONTACT_PRESENCE, //4 270 Contacts.PHOTO_ID, //5 271 Contacts.LOOKUP_KEY, //6 272 // email lookup doesn't included HAS_PHONE_NUMBER OR LOOKUP_KEY in projection 273 }; 274 static final String[] LEGACY_PEOPLE_PROJECTION = new String[] { 275 People._ID, // 0 276 People.DISPLAY_NAME, // 1 277 People.STARRED, //2 278 PeopleColumns.TIMES_CONTACTED, //3 279 People.PRESENCE_STATUS, //4 280 }; 281 static final int SUMMARY_ID_COLUMN_INDEX = 0; 282 static final int SUMMARY_NAME_COLUMN_INDEX = 1; 283 static final int SUMMARY_STARRED_COLUMN_INDEX = 2; 284 static final int SUMMARY_TIMES_CONTACTED_COLUMN_INDEX = 3; 285 static final int SUMMARY_PRESENCE_STATUS_COLUMN_INDEX = 4; 286 static final int SUMMARY_PHOTO_ID_COLUMN_INDEX = 5; 287 static final int SUMMARY_LOOKUP_KEY = 6; 288 static final int SUMMARY_HAS_PHONE_COLUMN_INDEX = 7; 289 290 static final String[] PHONES_PROJECTION = new String[] { 291 Phone._ID, //0 292 Phone.TYPE, //1 293 Phone.LABEL, //2 294 Phone.NUMBER, //3 295 Phone.DISPLAY_NAME, // 4 296 Phone.CONTACT_ID, // 5 297 }; 298 static final String[] LEGACY_PHONES_PROJECTION = new String[] { 299 Phones._ID, //0 300 Phones.TYPE, //1 301 Phones.LABEL, //2 302 Phones.NUMBER, //3 303 People.DISPLAY_NAME, // 4 304 }; 305 static final int PHONE_ID_COLUMN_INDEX = 0; 306 static final int PHONE_TYPE_COLUMN_INDEX = 1; 307 static final int PHONE_LABEL_COLUMN_INDEX = 2; 308 static final int PHONE_NUMBER_COLUMN_INDEX = 3; 309 static final int PHONE_DISPLAY_NAME_COLUMN_INDEX = 4; 310 static final int PHONE_CONTACT_ID_COLUMN_INDEX = 5; 311 312 static final String[] POSTALS_PROJECTION = new String[] { 313 StructuredPostal._ID, //0 314 StructuredPostal.TYPE, //1 315 StructuredPostal.LABEL, //2 316 StructuredPostal.DATA, //3 317 StructuredPostal.DISPLAY_NAME, // 4 318 }; 319 static final String[] LEGACY_POSTALS_PROJECTION = new String[] { 320 ContactMethods._ID, //0 321 ContactMethods.TYPE, //1 322 ContactMethods.LABEL, //2 323 ContactMethods.DATA, //3 324 People.DISPLAY_NAME, // 4 325 }; 326 static final String[] RAW_CONTACTS_PROJECTION = new String[] { 327 RawContacts._ID, //0 328 RawContacts.CONTACT_ID, //1 329 RawContacts.ACCOUNT_TYPE, //2 330 }; 331 332 static final int POSTAL_ID_COLUMN_INDEX = 0; 333 static final int POSTAL_TYPE_COLUMN_INDEX = 1; 334 static final int POSTAL_LABEL_COLUMN_INDEX = 2; 335 static final int POSTAL_ADDRESS_COLUMN_INDEX = 3; 336 static final int POSTAL_DISPLAY_NAME_COLUMN_INDEX = 4; 337 338 private static final int QUERY_TOKEN = 42; 339 340 static final String KEY_PICKER_MODE = "picker_mode"; 341 342 private ContactItemListAdapter mAdapter; 343 344 int mMode = MODE_DEFAULT; 345 346 private QueryHandler mQueryHandler; 347 private boolean mJustCreated; 348 private boolean mSyncEnabled; 349 private Uri mSelectedContactUri; 350 351 // private boolean mDisplayAll; 352 private boolean mDisplayOnlyPhones; 353 354 private Uri mGroupUri; 355 356 private long mQueryAggregateId; 357 358 private ArrayList<Long> mWritableRawContactIds = new ArrayList<Long>(); 359 private int mWritableSourcesCnt; 360 private int mReadOnlySourcesCnt; 361 362 /** 363 * Used to keep track of the scroll state of the list. 364 */ 365 private Parcelable mListState = null; 366 private boolean mListHasFocus; 367 368 private String mShortcutAction; 369 370 private int mScrollState; 371 372 /** 373 * Internal query type when in mode {@link #MODE_QUERY_PICK_TO_VIEW}. 374 */ 375 private int mQueryMode = QUERY_MODE_NONE; 376 377 private static final int QUERY_MODE_NONE = -1; 378 private static final int QUERY_MODE_MAILTO = 1; 379 private static final int QUERY_MODE_TEL = 2; 380 381 /** 382 * Data to use when in mode {@link #MODE_QUERY_PICK_TO_VIEW}. Usually 383 * provided by scheme-specific part of incoming {@link Intent#getData()}. 384 */ 385 private String mQueryData; 386 387 private static final String CLAUSE_ONLY_VISIBLE = Contacts.IN_VISIBLE_GROUP + "=1"; 388 private static final String CLAUSE_ONLY_PHONES = Contacts.HAS_PHONE_NUMBER + "=1"; 389 390 /** 391 * In the {@link #MODE_JOIN} determines whether we display a list item with the label 392 * "Show all contacts" or actually show all contacts 393 */ 394 private boolean mJoinModeShowAllContacts; 395 396 /** 397 * The ID of the special item described above. 398 */ 399 private static final long JOIN_MODE_SHOW_ALL_CONTACTS_ID = -2; 400 401 // Uri matcher for contact id 402 private static final int CONTACTS_ID = 1001; 403 private static final UriMatcher sContactsIdMatcher; 404 405 private static ExecutorService sImageFetchThreadPool; 406 407 static { 408 sContactsIdMatcher = new UriMatcher(UriMatcher.NO_MATCH); sContactsIdMatcher.addURI(ContactsContract.AUTHORITY, "contacts/#", CONTACTS_ID)409 sContactsIdMatcher.addURI(ContactsContract.AUTHORITY, "contacts/#", CONTACTS_ID); 410 } 411 412 private class DeleteClickListener implements DialogInterface.OnClickListener { onClick(DialogInterface dialog, int which)413 public void onClick(DialogInterface dialog, int which) { 414 getContentResolver().delete(mSelectedContactUri, null, null); 415 } 416 } 417 418 @Override onCreate(Bundle icicle)419 protected void onCreate(Bundle icicle) { 420 super.onCreate(icicle); 421 422 // Resolve the intent 423 final Intent intent = getIntent(); 424 425 // Allow the title to be set to a custom String using an extra on the intent 426 String title = intent.getStringExtra(UI.TITLE_EXTRA_KEY); 427 if (title != null) { 428 setTitle(title); 429 } 430 431 final String action = intent.getAction(); 432 mMode = MODE_UNKNOWN; 433 434 Log.i(TAG, "Called with action: " + action); 435 if (UI.LIST_DEFAULT.equals(action)) { 436 mMode = MODE_DEFAULT; 437 // When mDefaultMode is true the mode is set in onResume(), since the preferneces 438 // activity may change it whenever this activity isn't running 439 } else if (UI.LIST_GROUP_ACTION.equals(action)) { 440 mMode = MODE_GROUP; 441 String groupName = intent.getStringExtra(UI.GROUP_NAME_EXTRA_KEY); 442 if (TextUtils.isEmpty(groupName)) { 443 finish(); 444 return; 445 } 446 buildUserGroupUri(groupName); 447 } else if (UI.LIST_ALL_CONTACTS_ACTION.equals(action)) { 448 mMode = MODE_CUSTOM; 449 mDisplayOnlyPhones = false; 450 } else if (UI.LIST_STARRED_ACTION.equals(action)) { 451 mMode = MODE_STARRED; 452 } else if (UI.LIST_FREQUENT_ACTION.equals(action)) { 453 mMode = MODE_FREQUENT; 454 } else if (UI.LIST_STREQUENT_ACTION.equals(action)) { 455 mMode = MODE_STREQUENT; 456 } else if (UI.LIST_CONTACTS_WITH_PHONES_ACTION.equals(action)) { 457 mMode = MODE_CUSTOM; 458 mDisplayOnlyPhones = true; 459 } else if (Intent.ACTION_PICK.equals(action)) { 460 // XXX These should be showing the data from the URI given in 461 // the Intent. 462 final String type = intent.resolveType(this); 463 if (Contacts.CONTENT_TYPE.equals(type)) { 464 mMode = MODE_PICK_CONTACT; 465 } else if (People.CONTENT_TYPE.equals(type)) { 466 mMode = MODE_LEGACY_PICK_PERSON; 467 } else if (Phone.CONTENT_TYPE.equals(type)) { 468 mMode = MODE_PICK_PHONE; 469 } else if (Phones.CONTENT_TYPE.equals(type)) { 470 mMode = MODE_LEGACY_PICK_PHONE; 471 } else if (StructuredPostal.CONTENT_TYPE.equals(type)) { 472 mMode = MODE_PICK_POSTAL; 473 } else if (ContactMethods.CONTENT_POSTAL_TYPE.equals(type)) { 474 mMode = MODE_LEGACY_PICK_POSTAL; 475 } 476 } else if (Intent.ACTION_CREATE_SHORTCUT.equals(action)) { 477 if (intent.getComponent().getClassName().equals("alias.DialShortcut")) { 478 mMode = MODE_PICK_PHONE; 479 mShortcutAction = Intent.ACTION_CALL; 480 setTitle(R.string.callShortcutActivityTitle); 481 } else if (intent.getComponent().getClassName().equals("alias.MessageShortcut")) { 482 mMode = MODE_PICK_PHONE; 483 mShortcutAction = Intent.ACTION_SENDTO; 484 setTitle(R.string.messageShortcutActivityTitle); 485 } else { 486 mMode = MODE_PICK_OR_CREATE_CONTACT; 487 mShortcutAction = Intent.ACTION_VIEW; 488 setTitle(R.string.shortcutActivityTitle); 489 } 490 } else if (Intent.ACTION_GET_CONTENT.equals(action)) { 491 final String type = intent.resolveType(this); 492 if (Contacts.CONTENT_ITEM_TYPE.equals(type)) { 493 mMode = MODE_PICK_OR_CREATE_CONTACT; 494 } else if (Phone.CONTENT_ITEM_TYPE.equals(type)) { 495 mMode = MODE_PICK_PHONE; 496 } else if (Phones.CONTENT_ITEM_TYPE.equals(type)) { 497 mMode = MODE_LEGACY_PICK_PHONE; 498 } else if (StructuredPostal.CONTENT_ITEM_TYPE.equals(type)) { 499 mMode = MODE_PICK_POSTAL; 500 } else if (ContactMethods.CONTENT_POSTAL_ITEM_TYPE.equals(type)) { 501 mMode = MODE_LEGACY_PICK_POSTAL; 502 } else if (People.CONTENT_ITEM_TYPE.equals(type)) { 503 mMode = MODE_LEGACY_PICK_OR_CREATE_PERSON; 504 } 505 506 } else if (Intent.ACTION_INSERT_OR_EDIT.equals(action)) { 507 mMode = MODE_INSERT_OR_EDIT_CONTACT; 508 } else if (Intent.ACTION_SEARCH.equals(action)) { 509 // See if the suggestion was clicked with a search action key (call button) 510 if ("call".equals(intent.getStringExtra(SearchManager.ACTION_MSG))) { 511 String query = intent.getStringExtra(SearchManager.QUERY); 512 if (!TextUtils.isEmpty(query)) { 513 Intent newIntent = new Intent(Intent.ACTION_CALL_PRIVILEGED, 514 Uri.fromParts("tel", query, null)); 515 startActivity(newIntent); 516 } 517 finish(); 518 return; 519 } 520 521 // See if search request has extras to specify query 522 if (intent.hasExtra(Insert.EMAIL)) { 523 mMode = MODE_QUERY_PICK_TO_VIEW; 524 mQueryMode = QUERY_MODE_MAILTO; 525 mQueryData = intent.getStringExtra(Insert.EMAIL); 526 } else if (intent.hasExtra(Insert.PHONE)) { 527 mMode = MODE_QUERY_PICK_TO_VIEW; 528 mQueryMode = QUERY_MODE_TEL; 529 mQueryData = intent.getStringExtra(Insert.PHONE); 530 } else { 531 // Otherwise handle the more normal search case 532 mMode = MODE_QUERY; 533 mQueryData = getIntent().getStringExtra(SearchManager.QUERY); 534 } 535 536 // Since this is the filter activity it receives all intents 537 // dispatched from the SearchManager for security reasons 538 // so we need to re-dispatch from here to the intended target. 539 } else if (Intents.SEARCH_SUGGESTION_CLICKED.equals(action)) { 540 Uri data = intent.getData(); 541 Uri telUri = null; 542 if (sContactsIdMatcher.match(data) == CONTACTS_ID) { 543 long contactId = Long.valueOf(data.getLastPathSegment()); 544 final Cursor cursor = queryPhoneNumbers(contactId); 545 if (cursor != null) { 546 if (cursor.getCount() == 1 && cursor.moveToFirst()) { 547 int phoneNumberIndex = cursor.getColumnIndex(Phone.NUMBER); 548 String phoneNumber = cursor.getString(phoneNumberIndex); 549 telUri = Uri.parse("tel:" + phoneNumber); 550 } 551 cursor.close(); 552 } 553 } 554 // See if the suggestion was clicked with a search action key (call button) 555 Intent newIntent; 556 if ("call".equals(intent.getStringExtra(SearchManager.ACTION_MSG)) && telUri != null) { 557 newIntent = new Intent(Intent.ACTION_CALL_PRIVILEGED, telUri); 558 } else { 559 newIntent = new Intent(Intent.ACTION_VIEW, data); 560 } 561 startActivity(newIntent); 562 finish(); 563 return; 564 } else if (Intents.SEARCH_SUGGESTION_DIAL_NUMBER_CLICKED.equals(action)) { 565 Intent newIntent = new Intent(Intent.ACTION_CALL_PRIVILEGED, intent.getData()); 566 startActivity(newIntent); 567 finish(); 568 return; 569 } else if (Intents.SEARCH_SUGGESTION_CREATE_CONTACT_CLICKED.equals(action)) { 570 // TODO actually support this in EditContactActivity. 571 String number = intent.getData().getSchemeSpecificPart(); 572 Intent newIntent = new Intent(Intent.ACTION_INSERT, Contacts.CONTENT_URI); 573 newIntent.putExtra(Intents.Insert.PHONE, number); 574 startActivity(newIntent); 575 finish(); 576 return; 577 } 578 579 if (JOIN_AGGREGATE.equals(action)) { 580 mMode = MODE_JOIN_CONTACT; 581 mQueryAggregateId = intent.getLongExtra(EXTRA_AGGREGATE_ID, -1); 582 if (mQueryAggregateId == -1) { 583 Log.e(TAG, "Intent " + action + " is missing required extra: " 584 + EXTRA_AGGREGATE_ID); 585 setResult(RESULT_CANCELED); 586 finish(); 587 } 588 } 589 590 if (mMode == MODE_UNKNOWN) { 591 mMode = MODE_DEFAULT; 592 } 593 594 if (mMode == MODE_JOIN_CONTACT) { 595 setContentView(R.layout.contacts_list_content_join); 596 TextView blurbView = (TextView)findViewById(R.id.join_contact_blurb); 597 598 String blurb = getString(R.string.blurbJoinContactDataWith, 599 getContactDisplayName(mQueryAggregateId)); 600 blurbView.setText(blurb); 601 mJoinModeShowAllContacts = true; 602 } else { 603 setContentView(R.layout.contacts_list_content); 604 } 605 606 // Setup the UI 607 final ListView list = getListView(); 608 609 // Tell list view to not show dividers. We'll do it ourself so that we can *not* show 610 // them when an A-Z headers is visible. 611 list.setDividerHeight(0); 612 list.setFocusable(true); 613 list.setOnCreateContextMenuListener(this); 614 if ((mMode & MODE_MASK_NO_FILTER) != MODE_MASK_NO_FILTER) { 615 list.setTextFilterEnabled(true); 616 } 617 618 if ((mMode & MODE_MASK_CREATE_NEW) != 0) { 619 // Add the header for creating a new contact 620 final LayoutInflater inflater = getLayoutInflater(); 621 View header = inflater.inflate(R.layout.create_new_contact, list, false); 622 list.addHeaderView(header); 623 } 624 625 // Set the proper empty string 626 setEmptyText(); 627 628 mAdapter = new ContactItemListAdapter(this); 629 setListAdapter(mAdapter); 630 getListView().setOnScrollListener(mAdapter); 631 632 // We manually save/restore the listview state 633 list.setSaveEnabled(false); 634 635 mQueryHandler = new QueryHandler(this); 636 mJustCreated = true; 637 638 // TODO(jham) redesign this 639 mSyncEnabled = true; 640 // // Check to see if sync is enabled 641 // final ContentResolver resolver = getContentResolver(); 642 // IContentProvider provider = resolver.acquireProvider(Contacts.CONTENT_URI); 643 // if (provider == null) { 644 // // No contacts provider, bail. 645 // finish(); 646 // return; 647 // } 648 // 649 // try { 650 // ISyncAdapter sa = provider.getSyncAdapter(); 651 // mSyncEnabled = sa != null; 652 // } catch (RemoteException e) { 653 // mSyncEnabled = false; 654 // } finally { 655 // resolver.releaseProvider(provider); 656 // } 657 } 658 getContactDisplayName(long contactId)659 private String getContactDisplayName(long contactId) { 660 String contactName = null; 661 Cursor c = getContentResolver().query( 662 ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId), 663 new String[] {Contacts.DISPLAY_NAME}, null, null, null); 664 try { 665 if (c != null && c.moveToFirst()) { 666 contactName = c.getString(0); 667 } 668 } finally { 669 if (c != null) { 670 c.close(); 671 } 672 } 673 674 if (contactName == null) { 675 contactName = ""; 676 } 677 678 return contactName; 679 } 680 681 private int[] mLocation = new int[2]; 682 private Rect mRect = new Rect(); 683 684 /** {@inheritDoc} */ onClick(View v)685 public void onClick(View v) { 686 if (v.getId() == R.id.call_button) { 687 final int position = (Integer) v.getTag(); 688 Cursor c = mAdapter.getCursor(); 689 if (c != null) { 690 c.moveToPosition(position); 691 callContact(c); 692 } 693 } 694 } 695 setEmptyText()696 private void setEmptyText() { 697 if (mMode == MODE_JOIN_CONTACT) { 698 return; 699 } 700 701 TextView empty = (TextView) findViewById(R.id.emptyText); 702 int gravity = Gravity.NO_GRAVITY; 703 704 if (mDisplayOnlyPhones) { 705 empty.setText(getText(R.string.noContactsWithPhoneNumbers)); 706 gravity = Gravity.CENTER; 707 } else if (mMode == MODE_STREQUENT || mMode == MODE_STARRED) { 708 empty.setText(getText(R.string.noFavoritesHelpText)); 709 } else if (mMode == MODE_QUERY) { 710 empty.setText(getText(R.string.noMatchingContacts)); 711 } else { 712 boolean hasSim = ((TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE)) 713 .hasIccCard(); 714 715 if (hasSim) { 716 if (mSyncEnabled) { 717 empty.setText(getText(R.string.noContactsHelpTextWithSync)); 718 } else { 719 empty.setText(getText(R.string.noContactsHelpText)); 720 } 721 } else { 722 if (mSyncEnabled) { 723 empty.setText(getText(R.string.noContactsNoSimHelpTextWithSync)); 724 } else { 725 empty.setText(getText(R.string.noContactsNoSimHelpText)); 726 } 727 } 728 } 729 empty.setGravity(gravity); 730 } 731 buildUserGroupUri(String group)732 private void buildUserGroupUri(String group) { 733 mGroupUri = Uri.withAppendedPath(Contacts.CONTENT_GROUP_URI, group); 734 } 735 736 /** 737 * Sets the mode when the request is for "default" 738 */ setDefaultMode()739 private void setDefaultMode() { 740 // Load the preferences 741 SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); 742 743 mDisplayOnlyPhones = prefs.getBoolean(Prefs.DISPLAY_ONLY_PHONES, 744 Prefs.DISPLAY_ONLY_PHONES_DEFAULT); 745 746 // Update the empty text view with the proper string, as the group may have changed 747 setEmptyText(); 748 } 749 750 @Override onResume()751 protected void onResume() { 752 super.onResume(); 753 754 // Force cache to reload so we don't show stale photos. 755 if (mAdapter.mBitmapCache != null) { 756 mAdapter.mBitmapCache.clear(); 757 } 758 759 mScrollState = OnScrollListener.SCROLL_STATE_IDLE; 760 boolean runQuery = true; 761 Activity parent = getParent(); 762 763 // Do this before setting the filter. The filter thread relies 764 // on some state that is initialized in setDefaultMode 765 if (mMode == MODE_DEFAULT) { 766 // If we're in default mode we need to possibly reset the mode due to a change 767 // in the preferences activity while we weren't running 768 setDefaultMode(); 769 } 770 771 // See if we were invoked with a filter 772 if (parent != null && parent instanceof DialtactsActivity) { 773 String filterText = ((DialtactsActivity) parent).getAndClearFilterText(); 774 if (filterText != null && filterText.length() > 0) { 775 getListView().setFilterText(filterText); 776 // Don't start a new query since it will conflict with the filter 777 runQuery = false; 778 } else if (mJustCreated) { 779 getListView().clearTextFilter(); 780 } 781 } 782 783 if (mJustCreated && runQuery) { 784 // We need to start a query here the first time the activity is launched, as long 785 // as we aren't doing a filter. 786 startQuery(); 787 } 788 mJustCreated = false; 789 } 790 791 @Override onRestart()792 protected void onRestart() { 793 super.onRestart(); 794 795 // The cursor was killed off in onStop(), so we need to get a new one here 796 // We do not perform the query if a filter is set on the list because the 797 // filter will cause the query to happen anyway 798 if (TextUtils.isEmpty(getListView().getTextFilter())) { 799 startQuery(); 800 } else { 801 // Run the filtered query on the adapter 802 ((ContactItemListAdapter) getListAdapter()).onContentChanged(); 803 } 804 } 805 806 @Override onSaveInstanceState(Bundle icicle)807 protected void onSaveInstanceState(Bundle icicle) { 808 super.onSaveInstanceState(icicle); 809 // Save list state in the bundle so we can restore it after the QueryHandler has run 810 icicle.putParcelable(LIST_STATE_KEY, mList.onSaveInstanceState()); 811 icicle.putBoolean(FOCUS_KEY, mList.hasFocus()); 812 } 813 814 @Override onRestoreInstanceState(Bundle icicle)815 protected void onRestoreInstanceState(Bundle icicle) { 816 super.onRestoreInstanceState(icicle); 817 // Retrieve list state. This will be applied after the QueryHandler has run 818 mListState = icicle.getParcelable(LIST_STATE_KEY); 819 mListHasFocus = icicle.getBoolean(FOCUS_KEY); 820 } 821 822 @Override onStop()823 protected void onStop() { 824 super.onStop(); 825 826 // We don't want the list to display the empty state, since when we resume it will still 827 // be there and show up while the new query is happening. After the async query finished 828 // in response to onRestart() setLoading(false) will be called. 829 mAdapter.setLoading(true); 830 mAdapter.setSuggestionsCursor(null); 831 mAdapter.changeCursor(null); 832 mAdapter.clearImageFetching(); 833 834 if (mMode == MODE_QUERY) { 835 // Make sure the search box is closed 836 SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE); 837 searchManager.stopSearch(); 838 } 839 } 840 841 @Override onCreateOptionsMenu(Menu menu)842 public boolean onCreateOptionsMenu(Menu menu) { 843 super.onCreateOptionsMenu(menu); 844 845 // If Contacts was invoked by another Activity simply as a way of 846 // picking a contact, don't show the options menu 847 if ((mMode & MODE_MASK_PICKER) == MODE_MASK_PICKER) { 848 return false; 849 } 850 851 MenuInflater inflater = getMenuInflater(); 852 inflater.inflate(R.menu.list, menu); 853 return true; 854 } 855 856 @Override onPrepareOptionsMenu(Menu menu)857 public boolean onPrepareOptionsMenu(Menu menu) { 858 final boolean defaultMode = (mMode == MODE_DEFAULT); 859 menu.findItem(R.id.menu_display_groups).setVisible(defaultMode); 860 return true; 861 } 862 863 @Override onOptionsItemSelected(MenuItem item)864 public boolean onOptionsItemSelected(MenuItem item) { 865 switch (item.getItemId()) { 866 case R.id.menu_display_groups: { 867 final Intent intent = new Intent(this, DisplayGroupsActivity.class); 868 startActivityForResult(intent, SUBACTIVITY_DISPLAY_GROUP); 869 return true; 870 } 871 case R.id.menu_search: { 872 startSearch(null, false, null, false); 873 return true; 874 } 875 case R.id.menu_add: { 876 final Intent intent = new Intent(Intent.ACTION_INSERT, Contacts.CONTENT_URI); 877 startActivity(intent); 878 return true; 879 } 880 case R.id.menu_import_export: { 881 displayImportExportDialog(); 882 return true; 883 } 884 case R.id.menu_accounts: { 885 final Intent intent = new Intent(Settings.ACTION_SYNC_SETTINGS); 886 intent.putExtra(AUTHORITIES_FILTER_KEY, new String[] { 887 ContactsContract.AUTHORITY 888 }); 889 startActivity(intent); 890 return true; 891 } 892 } 893 return false; 894 } 895 896 @Override onCreateDialog(int id)897 protected Dialog onCreateDialog(int id) { 898 switch (id) { 899 case R.string.import_from_sim: 900 case R.string.import_from_sdcard: { 901 return AccountSelectionUtil.getSelectAccountDialog(this, id); 902 } 903 case R.id.dialog_sdcard_not_found: { 904 return new AlertDialog.Builder(this) 905 .setTitle(R.string.no_sdcard_title) 906 .setIcon(android.R.drawable.ic_dialog_alert) 907 .setMessage(R.string.no_sdcard_message) 908 .setPositiveButton(android.R.string.ok, null).create(); 909 } 910 case R.id.dialog_delete_contact_confirmation: { 911 return new AlertDialog.Builder(this) 912 .setTitle(R.string.deleteConfirmation_title) 913 .setIcon(android.R.drawable.ic_dialog_alert) 914 .setMessage(R.string.deleteConfirmation) 915 .setNegativeButton(android.R.string.cancel, null) 916 .setPositiveButton(android.R.string.ok, 917 new DeleteClickListener()).create(); 918 } 919 case R.id.dialog_readonly_contact_hide_confirmation: { 920 return new AlertDialog.Builder(this) 921 .setTitle(R.string.deleteConfirmation_title) 922 .setIcon(android.R.drawable.ic_dialog_alert) 923 .setMessage(R.string.readOnlyContactWarning) 924 .setNegativeButton(android.R.string.cancel, null) 925 .setPositiveButton(android.R.string.ok, 926 new DeleteClickListener()).create(); 927 } 928 case R.id.dialog_readonly_contact_delete_confirmation: { 929 return new AlertDialog.Builder(this) 930 .setTitle(R.string.deleteConfirmation_title) 931 .setIcon(android.R.drawable.ic_dialog_alert) 932 .setMessage(R.string.readOnlyContactDeleteConfirmation) 933 .setNegativeButton(android.R.string.cancel, null) 934 .setPositiveButton(android.R.string.ok, 935 new DeleteClickListener()).create(); 936 } 937 case R.id.dialog_multiple_contact_delete_confirmation: { 938 return new AlertDialog.Builder(this) 939 .setTitle(R.string.deleteConfirmation_title) 940 .setIcon(android.R.drawable.ic_dialog_alert) 941 .setMessage(R.string.multipleContactDeleteConfirmation) 942 .setNegativeButton(android.R.string.cancel, null) 943 .setPositiveButton(android.R.string.ok, 944 new DeleteClickListener()).create(); 945 } 946 } 947 return super.onCreateDialog(id); 948 } 949 950 /** 951 * Create a {@link Dialog} that allows the user to pick from a bulk import 952 * or bulk export task across all contacts. 953 */ displayImportExportDialog()954 private void displayImportExportDialog() { 955 // Wrap our context to inflate list items using correct theme 956 final Context dialogContext = new ContextThemeWrapper(this, android.R.style.Theme_Light); 957 final Resources res = dialogContext.getResources(); 958 final LayoutInflater dialogInflater = (LayoutInflater)dialogContext 959 .getSystemService(Context.LAYOUT_INFLATER_SERVICE); 960 961 // Adapter that shows a list of string resources 962 final ArrayAdapter<Integer> adapter = new ArrayAdapter<Integer>(this, 963 android.R.layout.simple_list_item_1) { 964 @Override 965 public View getView(int position, View convertView, ViewGroup parent) { 966 if (convertView == null) { 967 convertView = dialogInflater.inflate(android.R.layout.simple_list_item_1, 968 parent, false); 969 } 970 971 final int resId = this.getItem(position); 972 ((TextView)convertView).setText(resId); 973 return convertView; 974 } 975 }; 976 977 if (TelephonyManager.getDefault().hasIccCard()) { 978 adapter.add(R.string.import_from_sim); 979 } 980 if (res.getBoolean(R.bool.config_allow_import_from_sdcard)) { 981 adapter.add(R.string.import_from_sdcard); 982 } 983 if (res.getBoolean(R.bool.config_allow_export_to_sdcard)) { 984 adapter.add(R.string.export_to_sdcard); 985 } 986 987 final DialogInterface.OnClickListener clickListener = 988 new DialogInterface.OnClickListener() { 989 public void onClick(DialogInterface dialog, int which) { 990 dialog.dismiss(); 991 992 final int resId = adapter.getItem(which); 993 switch (resId) { 994 case R.string.import_from_sim: 995 case R.string.import_from_sdcard: { 996 handleImportRequest(resId); 997 break; 998 } 999 case R.string.export_to_sdcard: { 1000 Context context = ContactsListActivity.this; 1001 Intent exportIntent = new Intent(context, ExportVCardActivity.class); 1002 context.startActivity(exportIntent); 1003 break; 1004 } 1005 default: { 1006 Log.e(TAG, "Unexpected resource: " + 1007 getResources().getResourceEntryName(resId)); 1008 } 1009 } 1010 } 1011 }; 1012 1013 new AlertDialog.Builder(this) 1014 .setTitle(R.string.dialog_import_export) 1015 .setNegativeButton(android.R.string.cancel, null) 1016 .setSingleChoiceItems(adapter, -1, clickListener) 1017 .show(); 1018 } 1019 handleImportRequest(int resId)1020 private void handleImportRequest(int resId) { 1021 // There's three possibilities: 1022 // - more than one accounts -> ask the user 1023 // - just one account -> use the account without asking the user 1024 // - no account -> use phone-local storage without asking the user 1025 final Sources sources = Sources.getInstance(this); 1026 final List<Account> accountList = sources.getAccounts(true); 1027 final int size = accountList.size(); 1028 if (size > 1) { 1029 showDialog(resId); 1030 return; 1031 } 1032 1033 AccountSelectionUtil.doImport(this, resId, (size == 1 ? accountList.get(0) : null)); 1034 } 1035 1036 @Override onActivityResult(int requestCode, int resultCode, Intent data)1037 protected void onActivityResult(int requestCode, int resultCode, 1038 Intent data) { 1039 switch (requestCode) { 1040 case SUBACTIVITY_NEW_CONTACT: 1041 if (resultCode == RESULT_OK) { 1042 returnPickerResult(null, data.getStringExtra(Intent.EXTRA_SHORTCUT_NAME), 1043 data.getData(), 0); 1044 } 1045 break; 1046 1047 case SUBACTIVITY_VIEW_CONTACT: 1048 if (resultCode == RESULT_OK) { 1049 mAdapter.notifyDataSetChanged(); 1050 } 1051 break; 1052 1053 case SUBACTIVITY_DISPLAY_GROUP: 1054 // Mark as just created so we re-run the view query 1055 mJustCreated = true; 1056 break; 1057 } 1058 } 1059 1060 @Override onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo)1061 public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) { 1062 // If Contacts was invoked by another Activity simply as a way of 1063 // picking a contact, don't show the context menu 1064 if ((mMode & MODE_MASK_PICKER) == MODE_MASK_PICKER) { 1065 return; 1066 } 1067 1068 AdapterView.AdapterContextMenuInfo info; 1069 try { 1070 info = (AdapterView.AdapterContextMenuInfo) menuInfo; 1071 } catch (ClassCastException e) { 1072 Log.e(TAG, "bad menuInfo", e); 1073 return; 1074 } 1075 1076 Cursor cursor = (Cursor) getListAdapter().getItem(info.position); 1077 if (cursor == null) { 1078 // For some reason the requested item isn't available, do nothing 1079 return; 1080 } 1081 long id = info.id; 1082 Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, id); 1083 long rawContactId = ContactsUtils.queryForRawContactId(getContentResolver(), id); 1084 Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId); 1085 1086 // Setup the menu header 1087 menu.setHeaderTitle(cursor.getString(SUMMARY_NAME_COLUMN_INDEX)); 1088 1089 // View contact details 1090 menu.add(0, MENU_ITEM_VIEW_CONTACT, 0, R.string.menu_viewContact) 1091 .setIntent(new Intent(Intent.ACTION_VIEW, contactUri)); 1092 1093 if (cursor.getInt(SUMMARY_HAS_PHONE_COLUMN_INDEX) != 0) { 1094 // Calling contact 1095 menu.add(0, MENU_ITEM_CALL, 0, 1096 getString(R.string.menu_call)); 1097 // Send SMS item 1098 menu.add(0, MENU_ITEM_SEND_SMS, 0, getString(R.string.menu_sendSMS)); 1099 } 1100 1101 // Star toggling 1102 int starState = cursor.getInt(SUMMARY_STARRED_COLUMN_INDEX); 1103 if (starState == 0) { 1104 menu.add(0, MENU_ITEM_TOGGLE_STAR, 0, R.string.menu_addStar); 1105 } else { 1106 menu.add(0, MENU_ITEM_TOGGLE_STAR, 0, R.string.menu_removeStar); 1107 } 1108 1109 // Contact editing 1110 menu.add(0, MENU_ITEM_EDIT, 0, R.string.menu_editContact) 1111 .setIntent(new Intent(Intent.ACTION_EDIT, rawContactUri)); 1112 menu.add(0, MENU_ITEM_DELETE, 0, R.string.menu_deleteContact); 1113 } 1114 1115 @Override onContextItemSelected(MenuItem item)1116 public boolean onContextItemSelected(MenuItem item) { 1117 AdapterView.AdapterContextMenuInfo info; 1118 try { 1119 info = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo(); 1120 } catch (ClassCastException e) { 1121 Log.e(TAG, "bad menuInfo", e); 1122 return false; 1123 } 1124 1125 Cursor cursor = (Cursor) getListAdapter().getItem(info.position); 1126 1127 switch (item.getItemId()) { 1128 case MENU_ITEM_TOGGLE_STAR: { 1129 // Toggle the star 1130 ContentValues values = new ContentValues(1); 1131 values.put(Contacts.STARRED, cursor.getInt(SUMMARY_STARRED_COLUMN_INDEX) == 0 ? 1 : 0); 1132 final Uri selectedUri = this.getContactUri(info.position); 1133 getContentResolver().update(selectedUri, values, null, null); 1134 return true; 1135 } 1136 1137 case MENU_ITEM_CALL: { 1138 callContact(cursor); 1139 return true; 1140 } 1141 1142 case MENU_ITEM_SEND_SMS: { 1143 smsContact(cursor); 1144 return true; 1145 } 1146 1147 case MENU_ITEM_DELETE: { 1148 mSelectedContactUri = getContactUri(info.position); 1149 doContactDelete(); 1150 return true; 1151 } 1152 } 1153 1154 return super.onContextItemSelected(item); 1155 } 1156 1157 @Override onKeyDown(int keyCode, KeyEvent event)1158 public boolean onKeyDown(int keyCode, KeyEvent event) { 1159 switch (keyCode) { 1160 case KeyEvent.KEYCODE_CALL: { 1161 if (callSelection()) { 1162 return true; 1163 } 1164 break; 1165 } 1166 case KeyEvent.KEYCODE_DEL: { 1167 final int position = getListView().getSelectedItemPosition(); 1168 if (position != ListView.INVALID_POSITION) { 1169 mSelectedContactUri = getContactUri(position); 1170 doContactDelete(); 1171 return true; 1172 } 1173 break; 1174 } 1175 } 1176 1177 return super.onKeyDown(keyCode, event); 1178 } 1179 1180 /** 1181 * Prompt the user before deleting the given {@link Contacts} entry. 1182 */ doContactDelete()1183 protected void doContactDelete() { 1184 mReadOnlySourcesCnt = 0; 1185 mWritableSourcesCnt = 0; 1186 mWritableRawContactIds.clear(); 1187 1188 if (mSelectedContactUri != null) { 1189 Cursor c = getContentResolver().query(RawContacts.CONTENT_URI, RAW_CONTACTS_PROJECTION, 1190 RawContacts.CONTACT_ID + "=" + ContentUris.parseId(mSelectedContactUri), null, 1191 null); 1192 Sources sources = Sources.getInstance(ContactsListActivity.this); 1193 if (c != null) { 1194 while (c.moveToNext()) { 1195 final String accountType = c.getString(2); 1196 final long rawContactId = c.getLong(0); 1197 ContactsSource contactsSource = sources.getInflatedSource(accountType, 1198 ContactsSource.LEVEL_SUMMARY); 1199 if (contactsSource != null && contactsSource.readOnly) { 1200 mReadOnlySourcesCnt += 1; 1201 } else { 1202 mWritableSourcesCnt += 1; 1203 mWritableRawContactIds.add(rawContactId); 1204 } 1205 } 1206 } 1207 c.close(); 1208 if (mReadOnlySourcesCnt > 0 && mWritableSourcesCnt > 0) { 1209 showDialog(R.id.dialog_readonly_contact_delete_confirmation); 1210 } else if (mReadOnlySourcesCnt > 0 && mWritableSourcesCnt == 0) { 1211 showDialog(R.id.dialog_readonly_contact_hide_confirmation); 1212 } else if (mReadOnlySourcesCnt == 0 && mWritableSourcesCnt > 1) { 1213 showDialog(R.id.dialog_multiple_contact_delete_confirmation); 1214 } else { 1215 showDialog(R.id.dialog_delete_contact_confirmation); 1216 } 1217 } 1218 } 1219 1220 @Override onListItemClick(ListView l, View v, int position, long id)1221 protected void onListItemClick(ListView l, View v, int position, long id) { 1222 // Hide soft keyboard, if visible 1223 InputMethodManager inputMethodManager = (InputMethodManager) 1224 getSystemService(Context.INPUT_METHOD_SERVICE); 1225 inputMethodManager.hideSoftInputFromWindow(mList.getWindowToken(), 0); 1226 1227 if (mMode == MODE_INSERT_OR_EDIT_CONTACT) { 1228 Intent intent; 1229 if (position == 0) { 1230 intent = new Intent(Intent.ACTION_INSERT, Contacts.CONTENT_URI); 1231 } else { 1232 // Edit. adjusting position by subtracting header view count. 1233 position -= getListView().getHeaderViewsCount(); 1234 final Uri uri = getSelectedUri(position); 1235 intent = new Intent(Intent.ACTION_EDIT, uri); 1236 } 1237 intent.setFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT); 1238 Bundle extras = getIntent().getExtras(); 1239 1240 if (extras == null) { 1241 extras = new Bundle(); 1242 } 1243 intent.putExtras(extras); 1244 extras.putBoolean(KEY_PICKER_MODE, (mMode & MODE_MASK_PICKER) == MODE_MASK_PICKER); 1245 1246 startActivity(intent); 1247 finish(); 1248 } else if (id != -1) { 1249 // Subtract one if we have Create Contact at the top 1250 if ((mMode & MODE_MASK_CREATE_NEW) != 0) { 1251 position--; 1252 } 1253 final Uri uri = getSelectedUri(position); 1254 if ((mMode & MODE_MASK_PICKER) == 0) { 1255 final Intent intent = new Intent(Intent.ACTION_VIEW, uri); 1256 startActivityForResult(intent, SUBACTIVITY_VIEW_CONTACT); 1257 } else if (mMode == MODE_JOIN_CONTACT) { 1258 if (id == JOIN_MODE_SHOW_ALL_CONTACTS_ID) { 1259 mJoinModeShowAllContacts = false; 1260 startQuery(); 1261 } else { 1262 returnPickerResult(null, null, uri, id); 1263 } 1264 } else if (mMode == MODE_QUERY_PICK_TO_VIEW) { 1265 // Started with query that should launch to view contact 1266 final Intent intent = new Intent(Intent.ACTION_VIEW, uri); 1267 startActivity(intent); 1268 finish(); 1269 } else if (mMode == MODE_PICK_CONTACT 1270 || mMode == MODE_PICK_OR_CREATE_CONTACT 1271 || mMode == MODE_LEGACY_PICK_PERSON 1272 || mMode == MODE_LEGACY_PICK_OR_CREATE_PERSON) { 1273 if (mShortcutAction != null) { 1274 Cursor c = (Cursor) mAdapter.getItem(position); 1275 returnPickerResult(c, c.getString(SUMMARY_NAME_COLUMN_INDEX), uri, id); 1276 } else { 1277 returnPickerResult(null, null, uri, id); 1278 } 1279 } else if (mMode == MODE_PICK_PHONE) { 1280 if (mShortcutAction != null) { 1281 Cursor c = (Cursor) mAdapter.getItem(position); 1282 returnPickerResult(c, c.getString(PHONE_DISPLAY_NAME_COLUMN_INDEX), uri, id); 1283 } else { 1284 returnPickerResult(null, null, uri, id); 1285 } 1286 } else if (mMode == MODE_PICK_POSTAL 1287 || mMode == MODE_LEGACY_PICK_POSTAL 1288 || mMode == MODE_LEGACY_PICK_PHONE) { 1289 returnPickerResult(null, null, uri, id); 1290 } 1291 } else if ((mMode & MODE_MASK_CREATE_NEW) == MODE_MASK_CREATE_NEW 1292 && position == 0) { 1293 Intent newContact = new Intent(Intents.Insert.ACTION, Contacts.CONTENT_URI); 1294 startActivityForResult(newContact, SUBACTIVITY_NEW_CONTACT); 1295 } else { 1296 signalError(); 1297 } 1298 } 1299 1300 /** 1301 * @param uri In most cases, this should be a lookup {@link Uri}, possibly 1302 * generated through {@link Contacts#getLookupUri(long, String)}. 1303 */ returnPickerResult(Cursor c, String name, Uri uri, long id)1304 private void returnPickerResult(Cursor c, String name, Uri uri, long id) { 1305 final Intent intent = new Intent(); 1306 1307 if (mShortcutAction != null) { 1308 Intent shortcutIntent; 1309 if (Intent.ACTION_VIEW.equals(mShortcutAction)) { 1310 // This is a simple shortcut to view a contact. 1311 shortcutIntent = new Intent(mShortcutAction, uri); 1312 final Bitmap icon = loadContactPhoto(id, null); 1313 if (icon != null) { 1314 intent.putExtra(Intent.EXTRA_SHORTCUT_ICON, icon); 1315 } else { 1316 intent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, 1317 Intent.ShortcutIconResource.fromContext(this, 1318 R.drawable.ic_launcher_shortcut_contact)); 1319 } 1320 } else { 1321 // This is a direct dial or sms shortcut. 1322 String number = c.getString(PHONE_NUMBER_COLUMN_INDEX); 1323 int type = c.getInt(PHONE_TYPE_COLUMN_INDEX); 1324 String scheme; 1325 int resid; 1326 if (Intent.ACTION_CALL.equals(mShortcutAction)) { 1327 scheme = Constants.SCHEME_TEL; 1328 resid = R.drawable.badge_action_call; 1329 } else { 1330 scheme = Constants.SCHEME_SMSTO; 1331 resid = R.drawable.badge_action_sms; 1332 } 1333 1334 // Make the URI a direct tel: URI so that it will always continue to work 1335 Uri phoneUri = Uri.fromParts(scheme, number, null); 1336 shortcutIntent = new Intent(mShortcutAction, phoneUri); 1337 1338 // Find the Contacts._ID for this phone number 1339 long contactId = c.getLong(PHONE_CONTACT_ID_COLUMN_INDEX); 1340 intent.putExtra(Intent.EXTRA_SHORTCUT_ICON, 1341 generatePhoneNumberIcon(contactId, type, resid)); 1342 } 1343 shortcutIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); 1344 intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent); 1345 intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, name); 1346 setResult(RESULT_OK, intent); 1347 } else { 1348 setResult(RESULT_OK, intent.setData(uri)); 1349 } 1350 finish(); 1351 } 1352 1353 /** 1354 * Generates a phone number shortcut icon. Adds an overlay describing the type of the phone 1355 * number, and if there is a photo also adds the call action icon. 1356 * 1357 * @param contactId The person the phone number belongs to 1358 * @param type The type of the phone number 1359 * @param actionResId The ID for the action resource 1360 * @return The bitmap for the icon 1361 */ generatePhoneNumberIcon(long contactId, int type, int actionResId)1362 private Bitmap generatePhoneNumberIcon(long contactId, int type, int actionResId) { 1363 final Resources r = getResources(); 1364 boolean drawPhoneOverlay = true; 1365 final float scaleDensity = getResources().getDisplayMetrics().scaledDensity; 1366 1367 Bitmap photo = loadContactPhoto(contactId, null); 1368 if (photo == null) { 1369 // If there isn't a photo use the generic phone action icon instead 1370 Bitmap phoneIcon = getPhoneActionIcon(r, actionResId); 1371 if (phoneIcon != null) { 1372 photo = phoneIcon; 1373 drawPhoneOverlay = false; 1374 } else { 1375 return null; 1376 } 1377 } 1378 1379 // Setup the drawing classes 1380 int iconSize = (int) r.getDimension(android.R.dimen.app_icon_size); 1381 Bitmap icon = Bitmap.createBitmap(iconSize, iconSize, Bitmap.Config.ARGB_8888); 1382 Canvas canvas = new Canvas(icon); 1383 1384 // Copy in the photo 1385 Paint photoPaint = new Paint(); 1386 photoPaint.setDither(true); 1387 photoPaint.setFilterBitmap(true); 1388 Rect src = new Rect(0,0, photo.getWidth(),photo.getHeight()); 1389 Rect dst = new Rect(0,0, iconSize,iconSize); 1390 canvas.drawBitmap(photo, src, dst, photoPaint); 1391 1392 // Create an overlay for the phone number type 1393 String overlay = null; 1394 switch (type) { 1395 case Phone.TYPE_HOME: 1396 overlay = getString(R.string.type_short_home); 1397 break; 1398 1399 case Phone.TYPE_MOBILE: 1400 overlay = getString(R.string.type_short_mobile); 1401 break; 1402 1403 case Phone.TYPE_WORK: 1404 overlay = getString(R.string.type_short_work); 1405 break; 1406 1407 case Phone.TYPE_PAGER: 1408 overlay = getString(R.string.type_short_pager); 1409 break; 1410 1411 case Phone.TYPE_OTHER: 1412 overlay = getString(R.string.type_short_other); 1413 break; 1414 } 1415 if (overlay != null) { 1416 Paint textPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DEV_KERN_TEXT_FLAG); 1417 textPaint.setTextSize(20.0f * scaleDensity); 1418 textPaint.setTypeface(Typeface.DEFAULT_BOLD); 1419 textPaint.setColor(r.getColor(R.color.textColorIconOverlay)); 1420 textPaint.setShadowLayer(3f, 1, 1, r.getColor(R.color.textColorIconOverlayShadow)); 1421 canvas.drawText(overlay, 2 * scaleDensity, 16 * scaleDensity, textPaint); 1422 } 1423 1424 // Draw the phone action icon as an overlay 1425 if (ENABLE_ACTION_ICON_OVERLAYS && drawPhoneOverlay) { 1426 Bitmap phoneIcon = getPhoneActionIcon(r, actionResId); 1427 if (phoneIcon != null) { 1428 src.set(0, 0, phoneIcon.getWidth(), phoneIcon.getHeight()); 1429 int iconWidth = icon.getWidth(); 1430 dst.set(iconWidth - ((int) (20 * scaleDensity)), -1, 1431 iconWidth, ((int) (19 * scaleDensity))); 1432 canvas.drawBitmap(phoneIcon, src, dst, photoPaint); 1433 } 1434 } 1435 1436 return icon; 1437 } 1438 1439 /** 1440 * Returns the icon for the phone call action. 1441 * 1442 * @param r The resources to load the icon from 1443 * @param resId The resource ID to load 1444 * @return the icon for the phone call action 1445 */ getPhoneActionIcon(Resources r, int resId)1446 private Bitmap getPhoneActionIcon(Resources r, int resId) { 1447 Drawable phoneIcon = r.getDrawable(resId); 1448 if (phoneIcon instanceof BitmapDrawable) { 1449 BitmapDrawable bd = (BitmapDrawable) phoneIcon; 1450 return bd.getBitmap(); 1451 } else { 1452 return null; 1453 } 1454 } 1455 getUriToQuery()1456 Uri getUriToQuery() { 1457 switch(mMode) { 1458 case MODE_JOIN_CONTACT: 1459 return getJoinSuggestionsUri(null); 1460 case MODE_FREQUENT: 1461 case MODE_STARRED: 1462 case MODE_DEFAULT: 1463 case MODE_INSERT_OR_EDIT_CONTACT: 1464 case MODE_PICK_CONTACT: 1465 case MODE_PICK_OR_CREATE_CONTACT:{ 1466 return Contacts.CONTENT_URI; 1467 } 1468 case MODE_STREQUENT: { 1469 return Contacts.CONTENT_STREQUENT_URI; 1470 } 1471 case MODE_LEGACY_PICK_PERSON: 1472 case MODE_LEGACY_PICK_OR_CREATE_PERSON: { 1473 return People.CONTENT_URI; 1474 } 1475 case MODE_PICK_PHONE: { 1476 return Phone.CONTENT_URI; 1477 } 1478 case MODE_LEGACY_PICK_PHONE: { 1479 return Phones.CONTENT_URI; 1480 } 1481 case MODE_PICK_POSTAL: { 1482 return StructuredPostal.CONTENT_URI; 1483 } 1484 case MODE_LEGACY_PICK_POSTAL: { 1485 return ContactMethods.CONTENT_URI; 1486 } 1487 case MODE_QUERY_PICK_TO_VIEW: { 1488 if (mQueryMode == QUERY_MODE_MAILTO) { 1489 return Uri.withAppendedPath(Email.CONTENT_FILTER_URI, Uri.encode(mQueryData)); 1490 } else if (mQueryMode == QUERY_MODE_TEL) { 1491 return Uri.withAppendedPath(Phone.CONTENT_FILTER_URI, Uri.encode(mQueryData)); 1492 } 1493 } 1494 case MODE_QUERY: { 1495 return getContactFilterUri(mQueryData); 1496 } 1497 case MODE_GROUP: { 1498 return mGroupUri; 1499 } 1500 default: { 1501 throw new IllegalStateException("Can't generate URI: Unsupported Mode."); 1502 } 1503 } 1504 } 1505 1506 /** 1507 * Build the {@link Contacts#CONTENT_LOOKUP_URI} for the given 1508 * {@link ListView} position, using {@link #mAdapter}. 1509 */ getContactUri(int position)1510 private Uri getContactUri(int position) { 1511 if (position == ListView.INVALID_POSITION) { 1512 throw new IllegalArgumentException("Position not in list bounds"); 1513 } 1514 1515 final Cursor cursor = (Cursor)mAdapter.getItem(position); 1516 switch(mMode) { 1517 case MODE_LEGACY_PICK_PERSON: 1518 case MODE_LEGACY_PICK_OR_CREATE_PERSON: { 1519 final long personId = cursor.getLong(SUMMARY_ID_COLUMN_INDEX); 1520 return ContentUris.withAppendedId(People.CONTENT_URI, personId); 1521 } 1522 1523 default: { 1524 // Build and return soft, lookup reference 1525 final long contactId = cursor.getLong(SUMMARY_ID_COLUMN_INDEX); 1526 final String lookupKey = cursor.getString(SUMMARY_LOOKUP_KEY); 1527 return Contacts.getLookupUri(contactId, lookupKey); 1528 } 1529 } 1530 } 1531 1532 /** 1533 * Build the {@link Uri} for the given {@link ListView} position, which can 1534 * be used as result when in {@link #MODE_MASK_PICKER} mode. 1535 */ getSelectedUri(int position)1536 private Uri getSelectedUri(int position) { 1537 if (position == ListView.INVALID_POSITION) { 1538 throw new IllegalArgumentException("Position not in list bounds"); 1539 } 1540 1541 final long id = mAdapter.getItemId(position); 1542 switch(mMode) { 1543 case MODE_LEGACY_PICK_PERSON: 1544 case MODE_LEGACY_PICK_OR_CREATE_PERSON: { 1545 return ContentUris.withAppendedId(People.CONTENT_URI, id); 1546 } 1547 case MODE_PICK_PHONE: { 1548 return ContentUris.withAppendedId(Data.CONTENT_URI, id); 1549 } 1550 case MODE_LEGACY_PICK_PHONE: { 1551 return ContentUris.withAppendedId(Phones.CONTENT_URI, id); 1552 } 1553 case MODE_PICK_POSTAL: { 1554 return ContentUris.withAppendedId(Data.CONTENT_URI, id); 1555 } 1556 case MODE_LEGACY_PICK_POSTAL: { 1557 return ContentUris.withAppendedId(ContactMethods.CONTENT_URI, id); 1558 } 1559 default: { 1560 return getContactUri(position); 1561 } 1562 } 1563 } 1564 getProjectionForQuery()1565 String[] getProjectionForQuery() { 1566 switch(mMode) { 1567 case MODE_JOIN_CONTACT: 1568 case MODE_STREQUENT: 1569 case MODE_FREQUENT: 1570 case MODE_STARRED: 1571 case MODE_QUERY: 1572 case MODE_DEFAULT: 1573 case MODE_INSERT_OR_EDIT_CONTACT: 1574 case MODE_GROUP: 1575 case MODE_PICK_CONTACT: 1576 case MODE_PICK_OR_CREATE_CONTACT: { 1577 return CONTACTS_SUMMARY_PROJECTION; 1578 } 1579 case MODE_LEGACY_PICK_PERSON: 1580 case MODE_LEGACY_PICK_OR_CREATE_PERSON: { 1581 return LEGACY_PEOPLE_PROJECTION ; 1582 } 1583 case MODE_PICK_PHONE: { 1584 return PHONES_PROJECTION; 1585 } 1586 case MODE_LEGACY_PICK_PHONE: { 1587 return LEGACY_PHONES_PROJECTION; 1588 } 1589 case MODE_PICK_POSTAL: { 1590 return POSTALS_PROJECTION; 1591 } 1592 case MODE_LEGACY_PICK_POSTAL: { 1593 return LEGACY_POSTALS_PROJECTION; 1594 } 1595 case MODE_QUERY_PICK_TO_VIEW: { 1596 if (mQueryMode == QUERY_MODE_MAILTO) { 1597 return CONTACTS_SUMMARY_PROJECTION_FROM_EMAIL; 1598 } else if (mQueryMode == QUERY_MODE_TEL) { 1599 return PHONES_PROJECTION; 1600 } 1601 break; 1602 } 1603 } 1604 1605 // Default to normal aggregate projection 1606 return CONTACTS_SUMMARY_PROJECTION; 1607 } 1608 loadContactPhoto(long contactId, BitmapFactory.Options options)1609 private Bitmap loadContactPhoto(long contactId, BitmapFactory.Options options) { 1610 Cursor cursor = null; 1611 Bitmap bm = null; 1612 try { 1613 Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId); 1614 Uri photoUri = Uri.withAppendedPath(contactUri, Contacts.Photo.CONTENT_DIRECTORY); 1615 cursor = getContentResolver().query(photoUri, new String[] {Photo.PHOTO}, 1616 null, null, null); 1617 if (cursor != null && cursor.moveToFirst()) { 1618 bm = ContactsUtils.loadContactPhoto(cursor, 0, options); 1619 } 1620 } finally { 1621 if (cursor != null) { 1622 cursor.close(); 1623 } 1624 } 1625 return bm; 1626 } 1627 1628 /** 1629 * Return the selection arguments for a default query based on 1630 * {@link #mDisplayAll} and {@link #mDisplayOnlyPhones} flags. 1631 */ getContactSelection()1632 private String getContactSelection() { 1633 if (mDisplayOnlyPhones) { 1634 return CLAUSE_ONLY_VISIBLE + " AND " + CLAUSE_ONLY_PHONES; 1635 } else { 1636 return CLAUSE_ONLY_VISIBLE; 1637 } 1638 } 1639 getContactFilterUri(String filter)1640 private Uri getContactFilterUri(String filter) { 1641 if (!TextUtils.isEmpty(filter)) { 1642 return Uri.withAppendedPath(Contacts.CONTENT_FILTER_URI, Uri.encode(filter)); 1643 } else { 1644 return Contacts.CONTENT_URI; 1645 } 1646 } 1647 getPeopleFilterUri(String filter)1648 private Uri getPeopleFilterUri(String filter) { 1649 if (!TextUtils.isEmpty(filter)) { 1650 return Uri.withAppendedPath(People.CONTENT_FILTER_URI, Uri.encode(filter)); 1651 } else { 1652 return People.CONTENT_URI; 1653 } 1654 } 1655 getJoinSuggestionsUri(String filter)1656 private Uri getJoinSuggestionsUri(String filter) { 1657 Builder builder = Contacts.CONTENT_URI.buildUpon(); 1658 builder.appendEncodedPath(String.valueOf(mQueryAggregateId)); 1659 builder.appendEncodedPath(AggregationSuggestions.CONTENT_DIRECTORY); 1660 if (!TextUtils.isEmpty(filter)) { 1661 builder.appendEncodedPath(Uri.encode(filter)); 1662 } 1663 builder.appendQueryParameter("limit", String.valueOf(MAX_SUGGESTIONS)); 1664 return builder.build(); 1665 } 1666 getSortOrder(String[] projectionType)1667 private static String getSortOrder(String[] projectionType) { 1668 /* if (Locale.getDefault().equals(Locale.JAPAN) && 1669 projectionType == AGGREGATES_PRIMARY_PHONE_PROJECTION) { 1670 return SORT_STRING + " ASC"; 1671 } else { 1672 return NAME_COLUMN + " COLLATE LOCALIZED ASC"; 1673 } */ 1674 1675 return NAME_COLUMN + " COLLATE LOCALIZED ASC"; 1676 } 1677 startQuery()1678 void startQuery() { 1679 mAdapter.setLoading(true); 1680 1681 // Cancel any pending queries 1682 mQueryHandler.cancelOperation(QUERY_TOKEN); 1683 mQueryHandler.setLoadingJoinSuggestions(false); 1684 1685 String[] projection = getProjectionForQuery(); 1686 String callingPackage = getCallingPackage(); 1687 Uri uri = getUriToQuery(); 1688 if (!TextUtils.isEmpty(callingPackage)) { 1689 uri = uri.buildUpon() 1690 .appendQueryParameter(ContactsContract.REQUESTING_PACKAGE_PARAM_KEY, 1691 callingPackage) 1692 .build(); 1693 } 1694 1695 // Kick off the new query 1696 switch (mMode) { 1697 case MODE_GROUP: 1698 mQueryHandler.startQuery(QUERY_TOKEN, null, 1699 uri, projection, getContactSelection(), null, 1700 getSortOrder(projection)); 1701 break; 1702 1703 case MODE_DEFAULT: 1704 case MODE_PICK_CONTACT: 1705 case MODE_PICK_OR_CREATE_CONTACT: 1706 case MODE_INSERT_OR_EDIT_CONTACT: 1707 mQueryHandler.startQuery(QUERY_TOKEN, null, uri, 1708 projection, getContactSelection(), null, 1709 getSortOrder(projection)); 1710 break; 1711 1712 case MODE_LEGACY_PICK_PERSON: 1713 case MODE_LEGACY_PICK_OR_CREATE_PERSON: 1714 mQueryHandler.startQuery(QUERY_TOKEN, null, uri, 1715 projection, null, null, 1716 getSortOrder(projection)); 1717 break; 1718 1719 case MODE_QUERY: { 1720 mQueryHandler.startQuery(QUERY_TOKEN, null, uri, 1721 projection, null, null, 1722 getSortOrder(projection)); 1723 break; 1724 } 1725 1726 case MODE_QUERY_PICK_TO_VIEW: { 1727 mQueryHandler.startQuery(QUERY_TOKEN, null, uri, projection, null, null, 1728 getSortOrder(projection)); 1729 break; 1730 } 1731 1732 case MODE_STARRED: 1733 mQueryHandler.startQuery(QUERY_TOKEN, null, uri, 1734 projection, Contacts.STARRED + "=1", null, 1735 getSortOrder(projection)); 1736 break; 1737 1738 case MODE_FREQUENT: 1739 mQueryHandler.startQuery(QUERY_TOKEN, null, uri, 1740 projection, 1741 Contacts.TIMES_CONTACTED + " > 0", null, 1742 Contacts.TIMES_CONTACTED + " DESC, " 1743 + getSortOrder(projection)); 1744 break; 1745 1746 case MODE_STREQUENT: 1747 mQueryHandler.startQuery(QUERY_TOKEN, null, uri, projection, null, null, null); 1748 break; 1749 1750 case MODE_PICK_PHONE: 1751 case MODE_LEGACY_PICK_PHONE: 1752 mQueryHandler.startQuery(QUERY_TOKEN, null, uri, 1753 projection, null, null, getSortOrder(projection)); 1754 break; 1755 1756 case MODE_PICK_POSTAL: 1757 mQueryHandler.startQuery(QUERY_TOKEN, null, uri, 1758 projection, null, null, getSortOrder(projection)); 1759 break; 1760 1761 case MODE_LEGACY_PICK_POSTAL: 1762 mQueryHandler.startQuery(QUERY_TOKEN, null, uri, 1763 projection, 1764 ContactMethods.KIND + "=" + android.provider.Contacts.KIND_POSTAL, null, 1765 getSortOrder(projection)); 1766 break; 1767 1768 case MODE_JOIN_CONTACT: 1769 mQueryHandler.setLoadingJoinSuggestions(true); 1770 mQueryHandler.startQuery(QUERY_TOKEN, null, uri, projection, 1771 null, null, null); 1772 break; 1773 } 1774 } 1775 1776 /** 1777 * Called from a background thread to do the filter and return the resulting cursor. 1778 * 1779 * @param filter the text that was entered to filter on 1780 * @return a cursor with the results of the filter 1781 */ doFilter(String filter)1782 Cursor doFilter(String filter) { 1783 final ContentResolver resolver = getContentResolver(); 1784 1785 String[] projection = getProjectionForQuery(); 1786 1787 switch (mMode) { 1788 case MODE_DEFAULT: 1789 case MODE_PICK_CONTACT: 1790 case MODE_PICK_OR_CREATE_CONTACT: 1791 case MODE_INSERT_OR_EDIT_CONTACT: { 1792 return resolver.query(getContactFilterUri(filter), projection, 1793 getContactSelection(), null, getSortOrder(projection)); 1794 } 1795 1796 case MODE_LEGACY_PICK_PERSON: 1797 case MODE_LEGACY_PICK_OR_CREATE_PERSON: { 1798 return resolver.query(getPeopleFilterUri(filter), projection, null, null, 1799 getSortOrder(projection)); 1800 } 1801 1802 case MODE_STARRED: { 1803 return resolver.query(getContactFilterUri(filter), projection, 1804 Contacts.STARRED + "=1", null, 1805 getSortOrder(projection)); 1806 } 1807 1808 case MODE_FREQUENT: { 1809 return resolver.query(getContactFilterUri(filter), projection, 1810 Contacts.TIMES_CONTACTED + " > 0", null, 1811 Contacts.TIMES_CONTACTED + " DESC, " 1812 + getSortOrder(projection)); 1813 } 1814 1815 case MODE_STREQUENT: { 1816 Uri uri; 1817 if (!TextUtils.isEmpty(filter)) { 1818 uri = Uri.withAppendedPath(Contacts.CONTENT_STREQUENT_FILTER_URI, 1819 Uri.encode(filter)); 1820 } else { 1821 uri = Contacts.CONTENT_STREQUENT_URI; 1822 } 1823 return resolver.query(uri, projection, null, null, null); 1824 } 1825 1826 case MODE_PICK_PHONE: { 1827 Uri uri = getUriToQuery(); 1828 if (!TextUtils.isEmpty(filter)) { 1829 uri = Uri.withAppendedPath(Phone.CONTENT_FILTER_URI, Uri.encode(filter)); 1830 } 1831 return resolver.query(uri, projection, null, null, 1832 getSortOrder(projection)); 1833 } 1834 1835 case MODE_LEGACY_PICK_PHONE: { 1836 //TODO: Support filtering here (bug 2092503) 1837 break; 1838 } 1839 1840 case MODE_JOIN_CONTACT: { 1841 1842 // We are on a background thread. Run queries one after the other synchronously 1843 Cursor cursor = resolver.query(getJoinSuggestionsUri(filter), projection, null, 1844 null, null); 1845 mAdapter.setSuggestionsCursor(cursor); 1846 mJoinModeShowAllContacts = false; 1847 return resolver.query(getContactFilterUri(filter), projection, 1848 Contacts._ID + " != " + mQueryAggregateId + " AND " + CLAUSE_ONLY_VISIBLE, 1849 null, getSortOrder(projection)); 1850 } 1851 } 1852 throw new UnsupportedOperationException("filtering not allowed in mode " + mMode); 1853 } 1854 getShowAllContactsLabelCursor(String[] projection)1855 private Cursor getShowAllContactsLabelCursor(String[] projection) { 1856 MatrixCursor matrixCursor = new MatrixCursor(projection); 1857 Object[] row = new Object[projection.length]; 1858 // The only columns we care about is the id 1859 row[SUMMARY_ID_COLUMN_INDEX] = JOIN_MODE_SHOW_ALL_CONTACTS_ID; 1860 matrixCursor.addRow(row); 1861 return matrixCursor; 1862 } 1863 1864 /** 1865 * Calls the currently selected list item. 1866 * @return true if the call was initiated, false otherwise 1867 */ callSelection()1868 boolean callSelection() { 1869 ListView list = getListView(); 1870 if (list.hasFocus()) { 1871 Cursor cursor = (Cursor) list.getSelectedItem(); 1872 return callContact(cursor); 1873 } 1874 return false; 1875 } 1876 callContact(Cursor cursor)1877 boolean callContact(Cursor cursor) { 1878 return callOrSmsContact(cursor, false /*call*/); 1879 } 1880 smsContact(Cursor cursor)1881 boolean smsContact(Cursor cursor) { 1882 return callOrSmsContact(cursor, true /*sms*/); 1883 } 1884 1885 /** 1886 * Calls the contact which the cursor is point to. 1887 * @return true if the call was initiated, false otherwise 1888 */ callOrSmsContact(Cursor cursor, boolean sendSms)1889 boolean callOrSmsContact(Cursor cursor, boolean sendSms) { 1890 if (cursor != null) { 1891 boolean hasPhone = cursor.getInt(SUMMARY_HAS_PHONE_COLUMN_INDEX) != 0; 1892 if (!hasPhone) { 1893 // There is no phone number. 1894 signalError(); 1895 return false; 1896 } 1897 1898 String phone = null; 1899 Cursor phonesCursor = null; 1900 phonesCursor = queryPhoneNumbers(cursor.getLong(SUMMARY_ID_COLUMN_INDEX)); 1901 if (phonesCursor == null || phonesCursor.getCount() == 0) { 1902 // No valid number 1903 signalError(); 1904 return false; 1905 } else if (phonesCursor.getCount() == 1) { 1906 // only one number, call it. 1907 phone = phonesCursor.getString(phonesCursor.getColumnIndex(Phone.NUMBER)); 1908 } else { 1909 phonesCursor.moveToPosition(-1); 1910 while (phonesCursor.moveToNext()) { 1911 if (phonesCursor.getInt(phonesCursor. 1912 getColumnIndex(Phone.IS_SUPER_PRIMARY)) != 0) { 1913 // Found super primary, call it. 1914 phone = phonesCursor. 1915 getString(phonesCursor.getColumnIndex(Phone.NUMBER)); 1916 break; 1917 } 1918 } 1919 } 1920 1921 if (phone == null) { 1922 // Display dialog to choose a number to call. 1923 PhoneDisambigDialog phoneDialog = new PhoneDisambigDialog( 1924 this, phonesCursor, sendSms); 1925 phoneDialog.show(); 1926 } else { 1927 if (sendSms) { 1928 ContactsUtils.initiateSms(this, phone); 1929 } else { 1930 ContactsUtils.initiateCall(this, phone); 1931 } 1932 } 1933 return true; 1934 } 1935 1936 return false; 1937 } 1938 queryPhoneNumbers(long contactId)1939 private Cursor queryPhoneNumbers(long contactId) { 1940 Uri baseUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId); 1941 Uri dataUri = Uri.withAppendedPath(baseUri, Contacts.Data.CONTENT_DIRECTORY); 1942 1943 Cursor c = getContentResolver().query(dataUri, 1944 new String[] {Phone._ID, Phone.NUMBER, Phone.IS_SUPER_PRIMARY}, 1945 Data.MIMETYPE + "=?", new String[] {Phone.CONTENT_ITEM_TYPE}, null); 1946 if (c != null && c.moveToFirst()) { 1947 return c; 1948 } 1949 return null; 1950 } 1951 1952 /** 1953 * Signal an error to the user. 1954 */ signalError()1955 void signalError() { 1956 //TODO play an error beep or something... 1957 } 1958 getItemForView(View view)1959 Cursor getItemForView(View view) { 1960 ListView listView = getListView(); 1961 int index = listView.getPositionForView(view); 1962 if (index < 0) { 1963 return null; 1964 } 1965 return (Cursor) listView.getAdapter().getItem(index); 1966 } 1967 1968 private static class QueryHandler extends AsyncQueryHandler { 1969 protected final WeakReference<ContactsListActivity> mActivity; 1970 protected boolean mLoadingJoinSuggestions = false; 1971 QueryHandler(Context context)1972 public QueryHandler(Context context) { 1973 super(context.getContentResolver()); 1974 mActivity = new WeakReference<ContactsListActivity>((ContactsListActivity) context); 1975 } 1976 setLoadingJoinSuggestions(boolean flag)1977 public void setLoadingJoinSuggestions(boolean flag) { 1978 mLoadingJoinSuggestions = flag; 1979 } 1980 1981 @Override onQueryComplete(int token, Object cookie, Cursor cursor)1982 protected void onQueryComplete(int token, Object cookie, Cursor cursor) { 1983 final ContactsListActivity activity = mActivity.get(); 1984 if (activity != null && !activity.isFinishing()) { 1985 1986 // Whenever we get a suggestions cursor, we need to immediately kick off 1987 // another query for the complete list of contacts 1988 if (cursor != null && mLoadingJoinSuggestions) { 1989 mLoadingJoinSuggestions = false; 1990 if (cursor.getCount() > 0) { 1991 activity.mAdapter.setSuggestionsCursor(cursor); 1992 } else { 1993 cursor.close(); 1994 activity.mAdapter.setSuggestionsCursor(null); 1995 } 1996 1997 if (activity.mAdapter.mSuggestionsCursorCount == 0 1998 || !activity.mJoinModeShowAllContacts) { 1999 startQuery(QUERY_TOKEN, null, activity.getContactFilterUri( 2000 activity.mQueryData), 2001 CONTACTS_SUMMARY_PROJECTION, 2002 Contacts._ID + " != " + activity.mQueryAggregateId 2003 + " AND " + CLAUSE_ONLY_VISIBLE, null, 2004 getSortOrder(CONTACTS_SUMMARY_PROJECTION)); 2005 return; 2006 } 2007 2008 cursor = activity.getShowAllContactsLabelCursor(CONTACTS_SUMMARY_PROJECTION); 2009 } 2010 2011 activity.mAdapter.setLoading(false); 2012 activity.getListView().clearTextFilter(); 2013 activity.mAdapter.changeCursor(cursor); 2014 2015 // Now that the cursor is populated again, it's possible to restore the list state 2016 if (activity.mListState != null) { 2017 activity.mList.onRestoreInstanceState(activity.mListState); 2018 if (activity.mListHasFocus) { 2019 activity.mList.requestFocus(); 2020 } 2021 activity.mListHasFocus = false; 2022 activity.mListState = null; 2023 } 2024 } else { 2025 cursor.close(); 2026 } 2027 } 2028 } 2029 2030 final static class ContactListItemCache { 2031 public View header; 2032 public TextView headerText; 2033 public View divider; 2034 public TextView nameView; 2035 public View callView; 2036 public ImageView callButton; 2037 public CharArrayBuffer nameBuffer = new CharArrayBuffer(128); 2038 public TextView labelView; 2039 public CharArrayBuffer labelBuffer = new CharArrayBuffer(128); 2040 public TextView dataView; 2041 public CharArrayBuffer dataBuffer = new CharArrayBuffer(128); 2042 public ImageView presenceView; 2043 public QuickContactBadge photoView; 2044 public ImageView nonQuickContactPhotoView; 2045 } 2046 2047 final static class PhotoInfo { 2048 public int position; 2049 public long photoId; 2050 PhotoInfo(int position, long photoId)2051 public PhotoInfo(int position, long photoId) { 2052 this.position = position; 2053 this.photoId = photoId; 2054 } 2055 public QuickContactBadge photoView; 2056 } 2057 2058 private final class ContactItemListAdapter extends ResourceCursorAdapter 2059 implements SectionIndexer, OnScrollListener { 2060 private SectionIndexer mIndexer; 2061 private String mAlphabet; 2062 private boolean mLoading = true; 2063 private CharSequence mUnknownNameText; 2064 private boolean mDisplayPhotos = false; 2065 private boolean mDisplayCallButton = false; 2066 private boolean mDisplayAdditionalData = true; 2067 private HashMap<Long, SoftReference<Bitmap>> mBitmapCache = null; 2068 private HashSet<ImageView> mItemsMissingImages = null; 2069 private int mFrequentSeparatorPos = ListView.INVALID_POSITION; 2070 private boolean mDisplaySectionHeaders = true; 2071 private int[] mSectionPositions; 2072 private Cursor mSuggestionsCursor; 2073 private int mSuggestionsCursorCount; 2074 private ImageFetchHandler mHandler; 2075 private ImageDbFetcher mImageFetcher; 2076 private static final int FETCH_IMAGE_MSG = 1; 2077 ContactItemListAdapter(Context context)2078 public ContactItemListAdapter(Context context) { 2079 super(context, R.layout.contacts_list_item, null, false); 2080 2081 mHandler = new ImageFetchHandler(); 2082 mAlphabet = context.getString(com.android.internal.R.string.fast_scroll_alphabet); 2083 2084 mUnknownNameText = context.getText(android.R.string.unknownName); 2085 switch (mMode) { 2086 case MODE_LEGACY_PICK_POSTAL: 2087 case MODE_PICK_POSTAL: 2088 mDisplaySectionHeaders = false; 2089 break; 2090 case MODE_LEGACY_PICK_PHONE: 2091 case MODE_PICK_PHONE: 2092 mDisplaySectionHeaders = false; 2093 break; 2094 default: 2095 break; 2096 } 2097 2098 // Do not display the second line of text if in a specific SEARCH query mode, usually for 2099 // matching a specific E-mail or phone number. Any contact details 2100 // shown would be identical, and columns might not even be present 2101 // in the returned cursor. 2102 if (mQueryMode != QUERY_MODE_NONE) { 2103 mDisplayAdditionalData = false; 2104 } 2105 2106 if ((mMode & MODE_MASK_NO_DATA) == MODE_MASK_NO_DATA) { 2107 mDisplayAdditionalData = false; 2108 } 2109 2110 if ((mMode & MODE_MASK_SHOW_CALL_BUTTON) == MODE_MASK_SHOW_CALL_BUTTON) { 2111 mDisplayCallButton = true; 2112 } 2113 2114 if ((mMode & MODE_MASK_SHOW_PHOTOS) == MODE_MASK_SHOW_PHOTOS) { 2115 mDisplayPhotos = true; 2116 setViewResource(R.layout.contacts_list_item_photo); 2117 mBitmapCache = new HashMap<Long, SoftReference<Bitmap>>(); 2118 mItemsMissingImages = new HashSet<ImageView>(); 2119 } 2120 2121 if (mMode == MODE_STREQUENT || mMode == MODE_FREQUENT) { 2122 mDisplaySectionHeaders = false; 2123 } 2124 } 2125 2126 private class ImageFetchHandler extends Handler { 2127 2128 @Override handleMessage(Message message)2129 public void handleMessage(Message message) { 2130 if (ContactsListActivity.this.isFinishing()) { 2131 return; 2132 } 2133 switch(message.what) { 2134 case FETCH_IMAGE_MSG: { 2135 final ImageView imageView = (ImageView) message.obj; 2136 if (imageView == null) { 2137 break; 2138 } 2139 2140 final PhotoInfo info = (PhotoInfo)imageView.getTag(); 2141 if (info == null) { 2142 break; 2143 } 2144 2145 final long photoId = info.photoId; 2146 if (photoId == 0) { 2147 break; 2148 } 2149 2150 SoftReference<Bitmap> photoRef = mBitmapCache.get(photoId); 2151 if (photoRef == null) { 2152 break; 2153 } 2154 Bitmap photo = photoRef.get(); 2155 if (photo == null) { 2156 mBitmapCache.remove(photoId); 2157 break; 2158 } 2159 2160 // Make sure the photoId on this image view has not changed 2161 // while we were loading the image. 2162 synchronized (imageView) { 2163 final PhotoInfo updatedInfo = (PhotoInfo)imageView.getTag(); 2164 long currentPhotoId = updatedInfo.photoId; 2165 if (currentPhotoId == photoId) { 2166 imageView.setImageBitmap(photo); 2167 mItemsMissingImages.remove(imageView); 2168 } 2169 } 2170 break; 2171 } 2172 } 2173 } 2174 clearImageFecthing()2175 public void clearImageFecthing() { 2176 removeMessages(FETCH_IMAGE_MSG); 2177 } 2178 } 2179 2180 private class ImageDbFetcher implements Runnable { 2181 long mPhotoId; 2182 private ImageView mImageView; 2183 ImageDbFetcher(long photoId, ImageView imageView)2184 public ImageDbFetcher(long photoId, ImageView imageView) { 2185 this.mPhotoId = photoId; 2186 this.mImageView = imageView; 2187 } 2188 run()2189 public void run() { 2190 if (ContactsListActivity.this.isFinishing()) { 2191 return; 2192 } 2193 2194 if (Thread.currentThread().interrupted()) { 2195 // shutdown has been called. 2196 return; 2197 } 2198 Bitmap photo = null; 2199 try { 2200 photo = ContactsUtils.loadContactPhoto(mContext, mPhotoId, null); 2201 } catch (OutOfMemoryError e) { 2202 // Not enough memory for the photo, do nothing. 2203 } 2204 2205 if (photo == null) { 2206 return; 2207 } 2208 2209 mBitmapCache.put(mPhotoId, new SoftReference<Bitmap>(photo)); 2210 2211 if (Thread.currentThread().interrupted()) { 2212 // shutdown has been called. 2213 return; 2214 } 2215 2216 // Update must happen on UI thread 2217 Message msg = new Message(); 2218 msg.what = FETCH_IMAGE_MSG; 2219 msg.obj = mImageView; 2220 mHandler.sendMessage(msg); 2221 } 2222 } 2223 setSuggestionsCursor(Cursor cursor)2224 public void setSuggestionsCursor(Cursor cursor) { 2225 if (mSuggestionsCursor != null) { 2226 mSuggestionsCursor.close(); 2227 } 2228 mSuggestionsCursor = cursor; 2229 mSuggestionsCursorCount = cursor == null ? 0 : cursor.getCount(); 2230 } 2231 getNewIndexer(Cursor cursor)2232 private SectionIndexer getNewIndexer(Cursor cursor) { 2233 /* if (Locale.getDefault().getLanguage().equals(Locale.JAPAN.getLanguage())) { 2234 return new JapaneseContactListIndexer(cursor, SORT_STRING_INDEX); 2235 } else { */ 2236 return new AlphabetIndexer(cursor, SUMMARY_NAME_COLUMN_INDEX, mAlphabet); 2237 /* } */ 2238 } 2239 2240 /** 2241 * Callback on the UI thread when the content observer on the backing cursor fires. 2242 * Instead of calling requery we need to do an async query so that the requery doesn't 2243 * block the UI thread for a long time. 2244 */ 2245 @Override onContentChanged()2246 protected void onContentChanged() { 2247 CharSequence constraint = getListView().getTextFilter(); 2248 if (!TextUtils.isEmpty(constraint)) { 2249 // Reset the filter state then start an async filter operation 2250 Filter filter = getFilter(); 2251 filter.filter(constraint); 2252 } else { 2253 // Start an async query 2254 startQuery(); 2255 } 2256 } 2257 setLoading(boolean loading)2258 public void setLoading(boolean loading) { 2259 mLoading = loading; 2260 } 2261 2262 @Override isEmpty()2263 public boolean isEmpty() { 2264 if ((mMode & MODE_MASK_CREATE_NEW) == MODE_MASK_CREATE_NEW) { 2265 // This mode mask adds a header and we always want it to show up, even 2266 // if the list is empty, so always claim the list is not empty. 2267 return false; 2268 } else { 2269 if (mLoading) { 2270 // We don't want the empty state to show when loading. 2271 return false; 2272 } else { 2273 return super.isEmpty(); 2274 } 2275 } 2276 } 2277 2278 @Override getItemViewType(int position)2279 public int getItemViewType(int position) { 2280 if (position == 0 && (mMode & MODE_MASK_SHOW_NUMBER_OF_CONTACTS) != 0) { 2281 return IGNORE_ITEM_VIEW_TYPE; 2282 } 2283 if (isShowAllContactsItemPosition(position)) { 2284 return IGNORE_ITEM_VIEW_TYPE; 2285 } 2286 if (getSeparatorId(position) != 0) { 2287 // We don't want the separator view to be recycled. 2288 return IGNORE_ITEM_VIEW_TYPE; 2289 } 2290 return super.getItemViewType(position); 2291 } 2292 2293 @Override getView(int position, View convertView, ViewGroup parent)2294 public View getView(int position, View convertView, ViewGroup parent) { 2295 if (!mDataValid) { 2296 throw new IllegalStateException( 2297 "this should only be called when the cursor is valid"); 2298 } 2299 2300 // handle the total contacts item 2301 if (position == 0 && (mMode & MODE_MASK_SHOW_NUMBER_OF_CONTACTS) != 0) { 2302 return getTotalContactCountView(parent); 2303 } 2304 2305 if (isShowAllContactsItemPosition(position)) { 2306 LayoutInflater inflater = 2307 (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 2308 return inflater.inflate(R.layout.contacts_list_show_all_item, parent, false); 2309 } 2310 2311 // Handle the separator specially 2312 int separatorId = getSeparatorId(position); 2313 if (separatorId != 0) { 2314 LayoutInflater inflater = 2315 (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 2316 TextView view = (TextView) inflater.inflate(R.layout.list_separator, parent, false); 2317 view.setText(separatorId); 2318 return view; 2319 } 2320 2321 boolean showingSuggestion; 2322 Cursor cursor; 2323 if (mSuggestionsCursorCount != 0 && position < mSuggestionsCursorCount + 2) { 2324 showingSuggestion = true; 2325 cursor = mSuggestionsCursor; 2326 } else { 2327 showingSuggestion = false; 2328 cursor = mCursor; 2329 } 2330 2331 int realPosition = getRealPosition(position); 2332 if (!cursor.moveToPosition(realPosition)) { 2333 throw new IllegalStateException("couldn't move cursor to position " + position); 2334 } 2335 2336 View v; 2337 if (convertView == null) { 2338 v = newView(mContext, cursor, parent); 2339 } else { 2340 v = convertView; 2341 } 2342 bindView(v, mContext, cursor); 2343 bindSectionHeader(v, realPosition, mDisplaySectionHeaders && !showingSuggestion); 2344 return v; 2345 } 2346 getTotalContactCountView(ViewGroup parent)2347 private View getTotalContactCountView(ViewGroup parent) { 2348 final LayoutInflater inflater = getLayoutInflater(); 2349 TextView totalContacts = (TextView) inflater.inflate(R.layout.total_contacts, 2350 parent, false); 2351 2352 String text; 2353 int count = getRealCount(); 2354 2355 if (mMode == MODE_QUERY || !TextUtils.isEmpty(getListView().getTextFilter())) { 2356 text = getQuantityText(count, R.string.listFoundAllContactsZero, 2357 R.plurals.listFoundAllContacts); 2358 } else { 2359 if (mDisplayOnlyPhones) { 2360 text = getQuantityText(count, R.string.listTotalPhoneContactsZero, 2361 R.plurals.listTotalPhoneContacts); 2362 } else { 2363 text = getQuantityText(count, R.string.listTotalAllContactsZero, 2364 R.plurals.listTotalAllContacts); 2365 } 2366 } 2367 totalContacts.setText(text); 2368 return totalContacts; 2369 } 2370 2371 // TODO: fix PluralRules to handle zero correctly and use Resources.getQuantityText directly getQuantityText(int count, int zeroResourceId, int pluralResourceId)2372 private String getQuantityText(int count, int zeroResourceId, int pluralResourceId) { 2373 if (count == 0) { 2374 return getString(zeroResourceId); 2375 } else { 2376 String format = getResources().getQuantityText(pluralResourceId, count).toString(); 2377 return String.format(format, count); 2378 } 2379 } 2380 isShowAllContactsItemPosition(int position)2381 private boolean isShowAllContactsItemPosition(int position) { 2382 return mMode == MODE_JOIN_CONTACT && mJoinModeShowAllContacts 2383 && mSuggestionsCursorCount != 0 && position == mSuggestionsCursorCount + 2; 2384 } 2385 getSeparatorId(int position)2386 private int getSeparatorId(int position) { 2387 int separatorId = 0; 2388 if (position == mFrequentSeparatorPos) { 2389 separatorId = R.string.favoritesFrquentSeparator; 2390 } 2391 if (mSuggestionsCursorCount != 0) { 2392 if (position == 0) { 2393 separatorId = R.string.separatorJoinAggregateSuggestions; 2394 } else if (position == mSuggestionsCursorCount + 1) { 2395 separatorId = R.string.separatorJoinAggregateAll; 2396 } 2397 } 2398 return separatorId; 2399 } 2400 2401 @Override newView(Context context, Cursor cursor, ViewGroup parent)2402 public View newView(Context context, Cursor cursor, ViewGroup parent) { 2403 final View view = super.newView(context, cursor, parent); 2404 2405 final ContactListItemCache cache = new ContactListItemCache(); 2406 cache.header = view.findViewById(R.id.header); 2407 cache.headerText = (TextView)view.findViewById(R.id.header_text); 2408 cache.divider = view.findViewById(R.id.list_divider); 2409 cache.nameView = (TextView) view.findViewById(R.id.name); 2410 cache.callView = view.findViewById(R.id.call_view); 2411 cache.callButton = (ImageView) view.findViewById(R.id.call_button); 2412 if (cache.callButton != null) { 2413 cache.callButton.setOnClickListener(ContactsListActivity.this); 2414 } 2415 cache.labelView = (TextView) view.findViewById(R.id.label); 2416 cache.dataView = (TextView) view.findViewById(R.id.data); 2417 cache.presenceView = (ImageView) view.findViewById(R.id.presence); 2418 cache.photoView = (QuickContactBadge) view.findViewById(R.id.photo); 2419 cache.nonQuickContactPhotoView = (ImageView) view.findViewById(R.id.noQuickContactPhoto); 2420 view.setTag(cache); 2421 2422 return view; 2423 } 2424 2425 @Override bindView(View view, Context context, Cursor cursor)2426 public void bindView(View view, Context context, Cursor cursor) { 2427 final ContactListItemCache cache = (ContactListItemCache) view.getTag(); 2428 2429 TextView dataView = cache.dataView; 2430 TextView labelView = cache.labelView; 2431 int typeColumnIndex; 2432 int dataColumnIndex; 2433 int labelColumnIndex; 2434 int defaultType; 2435 int nameColumnIndex; 2436 boolean displayAdditionalData = mDisplayAdditionalData; 2437 switch(mMode) { 2438 case MODE_PICK_PHONE: 2439 case MODE_LEGACY_PICK_PHONE: { 2440 nameColumnIndex = PHONE_DISPLAY_NAME_COLUMN_INDEX; 2441 dataColumnIndex = PHONE_NUMBER_COLUMN_INDEX; 2442 typeColumnIndex = PHONE_TYPE_COLUMN_INDEX; 2443 labelColumnIndex = PHONE_LABEL_COLUMN_INDEX; 2444 defaultType = Phone.TYPE_HOME; 2445 break; 2446 } 2447 case MODE_PICK_POSTAL: 2448 case MODE_LEGACY_PICK_POSTAL: { 2449 nameColumnIndex = POSTAL_DISPLAY_NAME_COLUMN_INDEX; 2450 dataColumnIndex = POSTAL_ADDRESS_COLUMN_INDEX; 2451 typeColumnIndex = POSTAL_TYPE_COLUMN_INDEX; 2452 labelColumnIndex = POSTAL_LABEL_COLUMN_INDEX; 2453 defaultType = StructuredPostal.TYPE_HOME; 2454 break; 2455 } 2456 default: { 2457 nameColumnIndex = SUMMARY_NAME_COLUMN_INDEX; 2458 dataColumnIndex = -1; 2459 typeColumnIndex = -1; 2460 labelColumnIndex = -1; 2461 defaultType = Phone.TYPE_HOME; 2462 displayAdditionalData = false; 2463 } 2464 } 2465 2466 // Set the name 2467 cursor.copyStringToBuffer(nameColumnIndex, cache.nameBuffer); 2468 int size = cache.nameBuffer.sizeCopied; 2469 if (size != 0) { 2470 cache.nameView.setText(cache.nameBuffer.data, 0, size); 2471 } else { 2472 cache.nameView.setText(mUnknownNameText); 2473 } 2474 2475 // Make the call button visible if requested. 2476 if (mDisplayCallButton) { 2477 int pos = cursor.getPosition(); 2478 cache.callView.setVisibility(View.VISIBLE); 2479 cache.callButton.setTag(pos); 2480 } else { 2481 cache.callView.setVisibility(View.GONE); 2482 } 2483 2484 // Set the photo, if requested 2485 if (mDisplayPhotos) { 2486 boolean useQuickContact = (mMode & MODE_MASK_DISABLE_QUIKCCONTACT) == 0; 2487 2488 long photoId = 0; 2489 if (!cursor.isNull(SUMMARY_PHOTO_ID_COLUMN_INDEX)) { 2490 photoId = cursor.getLong(SUMMARY_PHOTO_ID_COLUMN_INDEX); 2491 } 2492 2493 ImageView viewToUse; 2494 if (useQuickContact) { 2495 viewToUse = cache.photoView; 2496 // Build soft lookup reference 2497 final long contactId = cursor.getLong(SUMMARY_ID_COLUMN_INDEX); 2498 final String lookupKey = cursor.getString(SUMMARY_LOOKUP_KEY); 2499 cache.photoView.assignContactUri(Contacts.getLookupUri(contactId, lookupKey)); 2500 cache.photoView.setVisibility(View.VISIBLE); 2501 cache.nonQuickContactPhotoView.setVisibility(View.INVISIBLE); 2502 } else { 2503 viewToUse = cache.nonQuickContactPhotoView; 2504 cache.photoView.setVisibility(View.INVISIBLE); 2505 cache.nonQuickContactPhotoView.setVisibility(View.VISIBLE); 2506 } 2507 2508 2509 final int position = cursor.getPosition(); 2510 viewToUse.setTag(new PhotoInfo(position, photoId)); 2511 2512 if (photoId == 0) { 2513 viewToUse.setImageResource(R.drawable.ic_contact_list_picture); 2514 } else { 2515 2516 Bitmap photo = null; 2517 2518 // Look for the cached bitmap 2519 SoftReference<Bitmap> ref = mBitmapCache.get(photoId); 2520 if (ref != null) { 2521 photo = ref.get(); 2522 if (photo == null) { 2523 mBitmapCache.remove(photoId); 2524 } 2525 } 2526 2527 // Bind the photo, or use the fallback no photo resource 2528 if (photo != null) { 2529 viewToUse.setImageBitmap(photo); 2530 } else { 2531 // Cache miss 2532 viewToUse.setImageResource(R.drawable.ic_contact_list_picture); 2533 2534 // Add it to a set of images that are populated asynchronously. 2535 mItemsMissingImages.add(viewToUse); 2536 2537 if (mScrollState != OnScrollListener.SCROLL_STATE_FLING) { 2538 2539 // Scrolling is idle or slow, go get the image right now. 2540 sendFetchImageMessage(viewToUse); 2541 } 2542 } 2543 } 2544 } 2545 2546 ImageView presenceView = cache.presenceView; 2547 if ((mMode & MODE_MASK_NO_PRESENCE) == 0) { 2548 // Set the proper icon (star or presence or nothing) 2549 int serverStatus; 2550 if (!cursor.isNull(SUMMARY_PRESENCE_STATUS_COLUMN_INDEX)) { 2551 serverStatus = cursor.getInt(SUMMARY_PRESENCE_STATUS_COLUMN_INDEX); 2552 presenceView.setImageResource( 2553 Presence.getPresenceIconResourceId(serverStatus)); 2554 presenceView.setVisibility(View.VISIBLE); 2555 } else { 2556 presenceView.setVisibility(View.GONE); 2557 } 2558 } else { 2559 presenceView.setVisibility(View.GONE); 2560 } 2561 2562 if (!displayAdditionalData) { 2563 cache.dataView.setVisibility(View.GONE); 2564 cache.labelView.setVisibility(View.GONE); 2565 return; 2566 } 2567 2568 // Set the data. 2569 cursor.copyStringToBuffer(dataColumnIndex, cache.dataBuffer); 2570 2571 size = cache.dataBuffer.sizeCopied; 2572 if (size != 0) { 2573 dataView.setText(cache.dataBuffer.data, 0, size); 2574 dataView.setVisibility(View.VISIBLE); 2575 } else { 2576 dataView.setVisibility(View.GONE); 2577 } 2578 2579 // Set the label. 2580 if (!cursor.isNull(typeColumnIndex)) { 2581 labelView.setVisibility(View.VISIBLE); 2582 2583 final int type = cursor.getInt(typeColumnIndex); 2584 final String label = cursor.getString(labelColumnIndex); 2585 2586 if (mMode == MODE_LEGACY_PICK_POSTAL || mMode == MODE_PICK_POSTAL) { 2587 labelView.setText(StructuredPostal.getTypeLabel(context.getResources(), type, 2588 label)); 2589 } else { 2590 labelView.setText(Phone.getTypeLabel(context.getResources(), type, label)); 2591 } 2592 } else { 2593 // There is no label, hide the the view 2594 labelView.setVisibility(View.GONE); 2595 } 2596 } 2597 bindSectionHeader(View view, int position, boolean displaySectionHeaders)2598 private void bindSectionHeader(View view, int position, boolean displaySectionHeaders) { 2599 final ContactListItemCache cache = (ContactListItemCache) view.getTag(); 2600 if (!displaySectionHeaders) { 2601 cache.header.setVisibility(View.GONE); 2602 cache.divider.setVisibility(View.VISIBLE); 2603 } else { 2604 final int section = getSectionForPosition(position); 2605 if (getPositionForSection(section) == position) { 2606 String title = mIndexer.getSections()[section].toString().trim(); 2607 if (!TextUtils.isEmpty(title)) { 2608 cache.headerText.setText(title); 2609 cache.header.setVisibility(View.VISIBLE); 2610 } else { 2611 cache.header.setVisibility(View.GONE); 2612 } 2613 } else { 2614 cache.header.setVisibility(View.GONE); 2615 } 2616 2617 // move the divider for the last item in a section 2618 if (getPositionForSection(section + 1) - 1 == position) { 2619 cache.divider.setVisibility(View.GONE); 2620 } else { 2621 cache.divider.setVisibility(View.VISIBLE); 2622 } 2623 } 2624 } 2625 2626 @Override changeCursor(Cursor cursor)2627 public void changeCursor(Cursor cursor) { 2628 2629 // Get the split between starred and frequent items, if the mode is strequent 2630 mFrequentSeparatorPos = ListView.INVALID_POSITION; 2631 int cursorCount = 0; 2632 if (cursor != null && (cursorCount = cursor.getCount()) > 0 2633 && mMode == MODE_STREQUENT) { 2634 cursor.move(-1); 2635 for (int i = 0; cursor.moveToNext(); i++) { 2636 int starred = cursor.getInt(SUMMARY_STARRED_COLUMN_INDEX); 2637 if (starred == 0) { 2638 if (i > 0) { 2639 // Only add the separator when there are starred items present 2640 mFrequentSeparatorPos = i; 2641 } 2642 break; 2643 } 2644 } 2645 } 2646 2647 super.changeCursor(cursor); 2648 // Update the indexer for the fast scroll widget 2649 updateIndexer(cursor); 2650 } 2651 updateIndexer(Cursor cursor)2652 private void updateIndexer(Cursor cursor) { 2653 if (mIndexer == null) { 2654 mIndexer = getNewIndexer(cursor); 2655 } else { 2656 if (Locale.getDefault().equals(Locale.JAPAN)) { 2657 if (mIndexer instanceof JapaneseContactListIndexer) { 2658 ((JapaneseContactListIndexer)mIndexer).setCursor(cursor); 2659 } else { 2660 mIndexer = getNewIndexer(cursor); 2661 } 2662 } else { 2663 if (mIndexer instanceof AlphabetIndexer) { 2664 ((AlphabetIndexer)mIndexer).setCursor(cursor); 2665 } else { 2666 mIndexer = getNewIndexer(cursor); 2667 } 2668 } 2669 } 2670 2671 int sectionCount = mIndexer.getSections().length; 2672 if (mSectionPositions == null || mSectionPositions.length != sectionCount) { 2673 mSectionPositions = new int[sectionCount]; 2674 } 2675 for (int i = 0; i < sectionCount; i++) { 2676 mSectionPositions[i] = ListView.INVALID_POSITION; 2677 } 2678 } 2679 2680 /** 2681 * Run the query on a helper thread. Beware that this code does not run 2682 * on the main UI thread! 2683 */ 2684 @Override runQueryOnBackgroundThread(CharSequence constraint)2685 public Cursor runQueryOnBackgroundThread(CharSequence constraint) { 2686 return doFilter(constraint.toString()); 2687 } 2688 getSections()2689 public Object [] getSections() { 2690 if (mMode == MODE_STARRED) { 2691 return new String[] { " " }; 2692 } else { 2693 return mIndexer.getSections(); 2694 } 2695 } 2696 getPositionForSection(int sectionIndex)2697 public int getPositionForSection(int sectionIndex) { 2698 if (mMode == MODE_STARRED) { 2699 return -1; 2700 } 2701 2702 if (sectionIndex < 0 || sectionIndex >= mSectionPositions.length) { 2703 return -1; 2704 } 2705 2706 if (mIndexer == null) { 2707 Cursor cursor = mAdapter.getCursor(); 2708 if (cursor == null) { 2709 // No cursor, the section doesn't exist so just return 0 2710 return 0; 2711 } 2712 mIndexer = getNewIndexer(cursor); 2713 } 2714 2715 int position = mSectionPositions[sectionIndex]; 2716 if (position == ListView.INVALID_POSITION) { 2717 position = mSectionPositions[sectionIndex] = 2718 mIndexer.getPositionForSection(sectionIndex); 2719 } 2720 2721 return position; 2722 } 2723 getSectionForPosition(int position)2724 public int getSectionForPosition(int position) { 2725 // The current implementations of SectionIndexers (specifically the Japanese indexer) 2726 // only work in one direction: given a section they can calculate the position. 2727 // Here we are using that existing functionality to do the reverse mapping. We are 2728 // performing binary search in the mSectionPositions array, which itself is populated 2729 // lazily using the "forward" mapping supported by the indexer. 2730 2731 int start = 0; 2732 int end = mSectionPositions.length; 2733 while (start != end) { 2734 2735 // We are making the binary search slightly asymmetrical, because the 2736 // user is more likely to be scrolling the list from the top down. 2737 int pivot = start + (end - start) / 4; 2738 2739 int value = getPositionForSection(pivot); 2740 if (value <= position) { 2741 start = pivot + 1; 2742 } else { 2743 end = pivot; 2744 } 2745 } 2746 2747 // The variable "start" cannot be 0, as long as the indexer is implemented properly 2748 // and actually maps position = 0 to section = 0 2749 return start - 1; 2750 } 2751 2752 @Override areAllItemsEnabled()2753 public boolean areAllItemsEnabled() { 2754 return mMode != MODE_STARRED 2755 && (mMode & MODE_MASK_SHOW_NUMBER_OF_CONTACTS) == 0 2756 && mSuggestionsCursorCount == 0; 2757 } 2758 2759 @Override isEnabled(int position)2760 public boolean isEnabled(int position) { 2761 if ((mMode & MODE_MASK_SHOW_NUMBER_OF_CONTACTS) != 0) { 2762 if (position == 0) { 2763 return false; 2764 } 2765 position--; 2766 } 2767 2768 if (mSuggestionsCursorCount > 0) { 2769 return position != 0 && position != mSuggestionsCursorCount + 1; 2770 } 2771 return position != mFrequentSeparatorPos; 2772 } 2773 2774 @Override getCount()2775 public int getCount() { 2776 if (!mDataValid) { 2777 return 0; 2778 } 2779 int superCount = super.getCount(); 2780 if ((mMode & MODE_MASK_SHOW_NUMBER_OF_CONTACTS) != 0 && superCount > 0) { 2781 // We don't want to count this header if it's the only thing visible, so that 2782 // the empty text will display. 2783 superCount++; 2784 } 2785 if (mSuggestionsCursorCount != 0) { 2786 // When showing suggestions, we have 2 additional list items: the "Suggestions" 2787 // and "All contacts" headers. 2788 return mSuggestionsCursorCount + superCount + 2; 2789 } 2790 else if (mFrequentSeparatorPos != ListView.INVALID_POSITION) { 2791 // When showing strequent list, we have an additional list item - the separator. 2792 return superCount + 1; 2793 } else { 2794 return superCount; 2795 } 2796 } 2797 2798 /** 2799 * Gets the actual count of contacts and excludes all the headers. 2800 */ getRealCount()2801 public int getRealCount() { 2802 return super.getCount(); 2803 } 2804 getRealPosition(int pos)2805 private int getRealPosition(int pos) { 2806 if ((mMode & MODE_MASK_SHOW_NUMBER_OF_CONTACTS) != 0) { 2807 pos--; 2808 } 2809 if (mSuggestionsCursorCount != 0) { 2810 // When showing suggestions, we have 2 additional list items: the "Suggestions" 2811 // and "All contacts" separators. 2812 if (pos < mSuggestionsCursorCount + 2) { 2813 // We are in the upper partition (Suggestions). Adjusting for the "Suggestions" 2814 // separator. 2815 return pos - 1; 2816 } else { 2817 // We are in the lower partition (All contacts). Adjusting for the size 2818 // of the upper partition plus the two separators. 2819 return pos - mSuggestionsCursorCount - 2; 2820 } 2821 } else if (mFrequentSeparatorPos == ListView.INVALID_POSITION) { 2822 // No separator, identity map 2823 return pos; 2824 } else if (pos <= mFrequentSeparatorPos) { 2825 // Before or at the separator, identity map 2826 return pos; 2827 } else { 2828 // After the separator, remove 1 from the pos to get the real underlying pos 2829 return pos - 1; 2830 } 2831 } 2832 2833 @Override getItem(int pos)2834 public Object getItem(int pos) { 2835 if (mSuggestionsCursorCount != 0 && pos <= mSuggestionsCursorCount) { 2836 mSuggestionsCursor.moveToPosition(getRealPosition(pos)); 2837 return mSuggestionsCursor; 2838 } else { 2839 return super.getItem(getRealPosition(pos)); 2840 } 2841 } 2842 2843 @Override getItemId(int pos)2844 public long getItemId(int pos) { 2845 if (mSuggestionsCursorCount != 0 && pos < mSuggestionsCursorCount + 2) { 2846 if (mSuggestionsCursor.moveToPosition(pos - 1)) { 2847 return mSuggestionsCursor.getLong(mRowIDColumn); 2848 } else { 2849 return 0; 2850 } 2851 } 2852 return super.getItemId(getRealPosition(pos)); 2853 } 2854 onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount)2855 public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, 2856 int totalItemCount) { 2857 // no op 2858 } 2859 onScrollStateChanged(AbsListView view, int scrollState)2860 public void onScrollStateChanged(AbsListView view, int scrollState) { 2861 mScrollState = scrollState; 2862 if (scrollState == OnScrollListener.SCROLL_STATE_FLING) { 2863 // If we are in a fling, stop loading images. 2864 clearImageFetching(); 2865 } else if (mDisplayPhotos) { 2866 processMissingImageItems(view); 2867 } 2868 } 2869 processMissingImageItems(AbsListView view)2870 private void processMissingImageItems(AbsListView view) { 2871 for (ImageView iv : mItemsMissingImages) { 2872 sendFetchImageMessage(iv); 2873 } 2874 } 2875 sendFetchImageMessage(ImageView view)2876 private void sendFetchImageMessage(ImageView view) { 2877 final PhotoInfo info = (PhotoInfo) view.getTag(); 2878 if (info == null) { 2879 return; 2880 } 2881 final long photoId = info.photoId; 2882 if (photoId == 0) { 2883 return; 2884 } 2885 mImageFetcher = new ImageDbFetcher(photoId, view); 2886 synchronized (ContactsListActivity.this) { 2887 // can't sync on sImageFetchThreadPool. 2888 if (sImageFetchThreadPool == null) { 2889 // Don't use more than 3 threads at a time to update. The thread pool will be 2890 // shared by all contact items. 2891 sImageFetchThreadPool = Executors.newFixedThreadPool(3); 2892 } 2893 sImageFetchThreadPool.execute(mImageFetcher); 2894 } 2895 } 2896 2897 2898 /** 2899 * Stop the image fetching for ALL contacts, if one is in progress we'll 2900 * not query the database. 2901 * 2902 * TODO: move this method to ContactsListActivity, it does not apply to the current 2903 * contact. 2904 */ clearImageFetching()2905 public void clearImageFetching() { 2906 synchronized (ContactsListActivity.this) { 2907 if (sImageFetchThreadPool != null) { 2908 sImageFetchThreadPool.shutdownNow(); 2909 sImageFetchThreadPool = null; 2910 } 2911 } 2912 2913 mHandler.clearImageFecthing(); 2914 } 2915 } 2916 } 2917