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 android.app.Activity; 20 import android.app.AlertDialog; 21 import android.app.ListActivity; 22 import android.app.SearchManager; 23 import android.content.AsyncQueryHandler; 24 import android.content.ContentResolver; 25 import android.content.ContentUris; 26 import android.content.ContentValues; 27 import android.content.Context; 28 import android.content.DialogInterface; 29 import android.content.IContentProvider; 30 import android.content.ISyncAdapter; 31 import android.content.Intent; 32 import android.content.SharedPreferences; 33 import android.content.res.Resources; 34 import android.database.CharArrayBuffer; 35 import android.database.Cursor; 36 import android.graphics.Bitmap; 37 import android.graphics.BitmapFactory; 38 import android.graphics.Canvas; 39 import android.graphics.Paint; 40 import android.graphics.Rect; 41 import android.graphics.Typeface; 42 import android.graphics.drawable.BitmapDrawable; 43 import android.graphics.drawable.Drawable; 44 import android.net.Uri; 45 import android.os.Bundle; 46 import android.os.Handler; 47 import android.os.Parcelable; 48 import android.os.RemoteException; 49 import android.preference.PreferenceManager; 50 import android.provider.Contacts; 51 import android.provider.Contacts.ContactMethods; 52 import android.provider.Contacts.Groups; 53 import android.provider.Contacts.Intents; 54 import android.provider.Contacts.People; 55 import android.provider.Contacts.Phones; 56 import android.provider.Contacts.Presence; 57 import android.provider.Contacts.Intents.Insert; 58 import android.provider.Contacts.Intents.UI; 59 import android.text.TextUtils; 60 import android.util.Log; 61 import android.util.SparseArray; 62 import android.view.ContextMenu; 63 import android.view.Gravity; 64 import android.view.KeyEvent; 65 import android.view.LayoutInflater; 66 import android.view.Menu; 67 import android.view.MenuItem; 68 import android.view.View; 69 import android.view.ViewGroup; 70 import android.view.ContextMenu.ContextMenuInfo; 71 import android.view.inputmethod.InputMethodManager; 72 import android.widget.AdapterView; 73 import android.widget.AlphabetIndexer; 74 import android.widget.Filter; 75 import android.widget.ImageView; 76 import android.widget.ListView; 77 import android.widget.ResourceCursorAdapter; 78 import android.widget.SectionIndexer; 79 import android.widget.TextView; 80 81 import java.lang.ref.SoftReference; 82 import java.lang.ref.WeakReference; 83 import java.util.ArrayList; 84 import java.util.Locale; 85 86 /** 87 * Displays a list of contacts. Usually is embedded into the ContactsActivity. 88 */ 89 public final class ContactsListActivity extends ListActivity 90 implements View.OnCreateContextMenuListener, DialogInterface.OnClickListener { 91 private static final String TAG = "ContactsListActivity"; 92 93 private static final boolean ENABLE_ACTION_ICON_OVERLAYS = false; 94 95 private static final String LIST_STATE_KEY = "liststate"; 96 private static final String FOCUS_KEY = "focused"; 97 98 static final int MENU_ITEM_VIEW_CONTACT = 1; 99 static final int MENU_ITEM_CALL = 2; 100 static final int MENU_ITEM_EDIT_BEFORE_CALL = 3; 101 static final int MENU_ITEM_SEND_SMS = 4; 102 static final int MENU_ITEM_SEND_IM = 5; 103 static final int MENU_ITEM_EDIT = 6; 104 static final int MENU_ITEM_DELETE = 7; 105 static final int MENU_ITEM_TOGGLE_STAR = 8; 106 107 public static final int MENU_SEARCH = 1; 108 public static final int MENU_DIALER = 9; 109 public static final int MENU_NEW_CONTACT = 10; 110 public static final int MENU_DISPLAY_GROUP = 11; 111 public static final int MENU_IMPORT_CONTACTS = 12; 112 public static final int MENU_EXPORT_CONTACTS = 13; 113 114 private static final int SUBACTIVITY_NEW_CONTACT = 1; 115 116 /** Mask for picker mode */ 117 static final int MODE_MASK_PICKER = 0x80000000; 118 /** Mask for no presence mode */ 119 static final int MODE_MASK_NO_PRESENCE = 0x40000000; 120 /** Mask for enabling list filtering */ 121 static final int MODE_MASK_NO_FILTER = 0x20000000; 122 /** Mask for having a "create new contact" header in the list */ 123 static final int MODE_MASK_CREATE_NEW = 0x10000000; 124 /** Mask for showing photos in the list */ 125 static final int MODE_MASK_SHOW_PHOTOS = 0x08000000; 126 127 /** Unknown mode */ 128 static final int MODE_UNKNOWN = 0; 129 /** Show members of the "Contacts" group */ 130 static final int MODE_GROUP = 5; 131 /** Show all contacts sorted alphabetically */ 132 static final int MODE_ALL_CONTACTS = 10; 133 /** Show all contacts with phone numbers, sorted alphabetically */ 134 static final int MODE_WITH_PHONES = 15; 135 /** Show all starred contacts */ 136 static final int MODE_STARRED = 20; 137 /** Show frequently contacted contacts */ 138 static final int MODE_FREQUENT = 30; 139 /** Show starred and the frequent */ 140 static final int MODE_STREQUENT = 35 | MODE_MASK_SHOW_PHOTOS; 141 /** Show all contacts and pick them when clicking */ 142 static final int MODE_PICK_CONTACT = 40 | MODE_MASK_PICKER; 143 /** Show all contacts as well as the option to create a new one */ 144 static final int MODE_PICK_OR_CREATE_CONTACT = 42 | MODE_MASK_PICKER | MODE_MASK_CREATE_NEW; 145 /** Show all contacts and pick them when clicking, and allow creating a new contact */ 146 static final int MODE_INSERT_OR_EDIT_CONTACT = 45 | MODE_MASK_PICKER | MODE_MASK_CREATE_NEW; 147 /** Show all phone numbers and pick them when clicking */ 148 static final int MODE_PICK_PHONE = 50 | MODE_MASK_PICKER | MODE_MASK_NO_PRESENCE; 149 /** Show all postal addresses and pick them when clicking */ 150 static final int MODE_PICK_POSTAL = 151 55 | MODE_MASK_PICKER | MODE_MASK_NO_PRESENCE | MODE_MASK_NO_FILTER; 152 /** Run a search query */ 153 static final int MODE_QUERY = 60 | MODE_MASK_NO_FILTER; 154 /** Run a search query in PICK mode, but that still launches to VIEW */ 155 static final int MODE_QUERY_PICK_TO_VIEW = 65 | MODE_MASK_NO_FILTER | MODE_MASK_PICKER; 156 157 static final int DEFAULT_MODE = MODE_ALL_CONTACTS; 158 159 /** 160 * The type of data to display in the main contacts list. 161 */ 162 static final String PREF_DISPLAY_TYPE = "display_system_group"; 163 164 /** Unknown display type. */ 165 static final int DISPLAY_TYPE_UNKNOWN = -1; 166 /** Display all contacts */ 167 static final int DISPLAY_TYPE_ALL = 0; 168 /** Display all contacts that have phone numbers */ 169 static final int DISPLAY_TYPE_ALL_WITH_PHONES = 1; 170 /** Display a system group */ 171 static final int DISPLAY_TYPE_SYSTEM_GROUP = 2; 172 /** Display a user group */ 173 static final int DISPLAY_TYPE_USER_GROUP = 3; 174 175 /** 176 * Info about what to display. If {@link #PREF_DISPLAY_TYPE} 177 * is {@link #DISPLAY_TYPE_SYSTEM_GROUP} then this will be the system id. 178 * If {@link #PREF_DISPLAY_TYPE} is {@link #DISPLAY_TYPE_USER_GROUP} then this will 179 * be the group name. 180 */ 181 static final String PREF_DISPLAY_INFO = "display_group"; 182 183 184 static final String NAME_COLUMN = People.DISPLAY_NAME; 185 static final String SORT_STRING = People.SORT_STRING; 186 187 static final String[] CONTACTS_PROJECTION = new String[] { 188 People._ID, // 0 189 NAME_COLUMN, // 1 190 People.NUMBER, // 2 191 People.TYPE, // 3 192 People.LABEL, // 4 193 People.STARRED, // 5 194 People.PRIMARY_PHONE_ID, // 6 195 People.PRIMARY_EMAIL_ID, // 7 196 People.PRESENCE_STATUS, // 8 197 SORT_STRING, // 9 198 }; 199 200 static final String[] SIMPLE_CONTACTS_PROJECTION = new String[] { 201 People._ID, // 0 202 NAME_COLUMN, // 1 203 }; 204 205 static final String[] STREQUENT_PROJECTION = new String[] { 206 People._ID, // 0 207 NAME_COLUMN, // 1 208 People.NUMBER, // 2 209 People.TYPE, // 3 210 People.LABEL, // 4 211 People.STARRED, // 5 212 People.PRIMARY_PHONE_ID, // 6 213 People.PRIMARY_EMAIL_ID, // 7 214 People.PRESENCE_STATUS, // 8 215 "photo_data", // 9 216 People.TIMES_CONTACTED, // 10 (not displayed, but required for the order by to work) 217 }; 218 219 static final String[] PHONES_PROJECTION = new String[] { 220 Phones._ID, // 0 221 NAME_COLUMN, // 1 222 Phones.NUMBER, // 2 223 Phones.TYPE, // 3 224 Phones.LABEL, // 4 225 Phones.STARRED, // 5 226 Phones.PERSON_ID, // 6 227 }; 228 229 static final String[] CONTACT_METHODS_PROJECTION = new String[] { 230 ContactMethods._ID, // 0 231 NAME_COLUMN, // 1 232 ContactMethods.DATA, // 2 233 ContactMethods.TYPE, // 3 234 ContactMethods.LABEL, // 4 235 ContactMethods.STARRED, // 5 236 ContactMethods.PERSON_ID, // 6 237 }; 238 239 static final int ID_COLUMN_INDEX = 0; 240 static final int NAME_COLUMN_INDEX = 1; 241 static final int NUMBER_COLUMN_INDEX = 2; 242 static final int DATA_COLUMN_INDEX = 2; 243 static final int TYPE_COLUMN_INDEX = 3; 244 static final int LABEL_COLUMN_INDEX = 4; 245 static final int STARRED_COLUMN_INDEX = 5; 246 static final int PRIMARY_PHONE_ID_COLUMN_INDEX = 6; 247 static final int PRIMARY_EMAIL_ID_COLUMN_INDEX = 7; 248 static final int SERVER_STATUS_COLUMN_INDEX = 8; 249 static final int PHOTO_COLUMN_INDEX = 9; 250 static final int SORT_STRING_INDEX = 9; 251 252 static final int PHONES_PERSON_ID_INDEX = 6; 253 static final int SIMPLE_CONTACTS_PERSON_ID_INDEX = 0; 254 255 static final int DISPLAY_GROUP_INDEX_ALL_CONTACTS = 0; 256 static final int DISPLAY_GROUP_INDEX_ALL_CONTACTS_WITH_PHONES = 1; 257 static final int DISPLAY_GROUP_INDEX_MY_CONTACTS = 2; 258 259 private static final int QUERY_TOKEN = 42; 260 261 private static final String[] GROUPS_PROJECTION = new String[] { 262 Groups.SYSTEM_ID, // 0 263 Groups.NAME, // 1 264 }; 265 private static final int GROUPS_COLUMN_INDEX_SYSTEM_ID = 0; 266 private static final int GROUPS_COLUMN_INDEX_NAME = 1; 267 268 static final String GROUP_WITH_PHONES = "android_smartgroup_phone"; 269 270 ContactItemListAdapter mAdapter; 271 272 int mMode = DEFAULT_MODE; 273 // The current display group 274 private String mDisplayInfo; 275 private int mDisplayType; 276 // The current list of display groups, during selection from menu 277 private CharSequence[] mDisplayGroups; 278 // If true position 2 in mDisplayGroups is the MyContacts group 279 private boolean mDisplayGroupsIncludesMyContacts = false; 280 281 private int mDisplayGroupOriginalSelection; 282 private int mDisplayGroupCurrentSelection; 283 284 private QueryHandler mQueryHandler; 285 private String mQuery; 286 private Uri mGroupFilterUri; 287 private Uri mGroupUri; 288 private boolean mJustCreated; 289 private boolean mSyncEnabled; 290 291 /** 292 * Cursor row index that holds reference back to {@link People#_ID}, such as 293 * {@link ContactMethods#PERSON_ID}. Used when responding to a 294 * {@link Intent#ACTION_SEARCH} in mode {@link #MODE_QUERY_PICK_TO_VIEW}. 295 */ 296 private int mQueryPersonIdIndex; 297 298 /** 299 * Used to keep track of the scroll state of the list. 300 */ 301 private Parcelable mListState = null; 302 private boolean mListHasFocus; 303 304 private String mShortcutAction; 305 private boolean mDefaultMode = false; 306 307 /** 308 * Internal query type when in mode {@link #MODE_QUERY_PICK_TO_VIEW}. 309 */ 310 private int mQueryMode = QUERY_MODE_NONE; 311 312 private static final int QUERY_MODE_NONE = -1; 313 private static final int QUERY_MODE_MAILTO = 1; 314 private static final int QUERY_MODE_TEL = 2; 315 316 /** 317 * Data to use when in mode {@link #MODE_QUERY_PICK_TO_VIEW}. Usually 318 * provided by scheme-specific part of incoming {@link Intent#getData()}. 319 */ 320 private String mQueryData; 321 322 private Handler mHandler = new Handler(); 323 324 private class ImportTypeSelectedListener implements DialogInterface.OnClickListener { 325 public static final int IMPORT_FROM_SIM = 0; 326 public static final int IMPORT_FROM_SDCARD = 1; 327 328 private int mIndex; 329 ImportTypeSelectedListener()330 public ImportTypeSelectedListener() { 331 mIndex = IMPORT_FROM_SIM; 332 } 333 onClick(DialogInterface dialog, int which)334 public void onClick(DialogInterface dialog, int which) { 335 if (which == DialogInterface.BUTTON_POSITIVE) { 336 if (mIndex == IMPORT_FROM_SIM) { 337 doImportFromSim(); 338 } else { 339 doImportFromSDCard(); 340 } 341 } else if (which == DialogInterface.BUTTON_NEGATIVE) { 342 343 } else { 344 mIndex = which; 345 } 346 } 347 } 348 349 private class DeleteClickListener implements DialogInterface.OnClickListener { 350 private Uri mUri; 351 DeleteClickListener(Uri uri)352 public DeleteClickListener(Uri uri) { 353 mUri = uri; 354 } 355 onClick(DialogInterface dialog, int which)356 public void onClick(DialogInterface dialog, int which) { 357 getContentResolver().delete(mUri, null, null); 358 } 359 } 360 361 @Override onCreate(Bundle icicle)362 protected void onCreate(Bundle icicle) { 363 super.onCreate(icicle); 364 365 // Resolve the intent 366 final Intent intent = getIntent(); 367 368 // Allow the title to be set to a custom String using an extra on the intent 369 String title = intent.getStringExtra(Contacts.Intents.UI.TITLE_EXTRA_KEY); 370 if (title != null) { 371 setTitle(title); 372 } 373 374 final String action = intent.getAction(); 375 mMode = MODE_UNKNOWN; 376 377 setContentView(R.layout.contacts_list_content); 378 379 if (UI.LIST_DEFAULT.equals(action)) { 380 mDefaultMode = true; 381 // When mDefaultMode is true the mode is set in onResume(), since the preferneces 382 // activity may change it whenever this activity isn't running 383 } else if (UI.LIST_GROUP_ACTION.equals(action)) { 384 mMode = MODE_GROUP; 385 String groupName = intent.getStringExtra(UI.GROUP_NAME_EXTRA_KEY); 386 if (TextUtils.isEmpty(groupName)) { 387 finish(); 388 return; 389 } 390 buildUserGroupUris(groupName); 391 } else if (UI.LIST_ALL_CONTACTS_ACTION.equals(action)) { 392 mMode = MODE_ALL_CONTACTS; 393 } else if (UI.LIST_STARRED_ACTION.equals(action)) { 394 mMode = MODE_STARRED; 395 } else if (UI.LIST_FREQUENT_ACTION.equals(action)) { 396 mMode = MODE_FREQUENT; 397 } else if (UI.LIST_STREQUENT_ACTION.equals(action)) { 398 mMode = MODE_STREQUENT; 399 } else if (UI.LIST_CONTACTS_WITH_PHONES_ACTION.equals(action)) { 400 mMode = MODE_WITH_PHONES; 401 } else if (Intent.ACTION_PICK.equals(action)) { 402 // XXX These should be showing the data from the URI given in 403 // the Intent. 404 final String type = intent.resolveType(this); 405 if (People.CONTENT_TYPE.equals(type)) { 406 mMode = MODE_PICK_CONTACT; 407 } else if (Phones.CONTENT_TYPE.equals(type)) { 408 mMode = MODE_PICK_PHONE; 409 } else if (ContactMethods.CONTENT_POSTAL_TYPE.equals(type)) { 410 mMode = MODE_PICK_POSTAL; 411 } 412 } else if (Intent.ACTION_CREATE_SHORTCUT.equals(action)) { 413 if (intent.getComponent().getClassName().equals("alias.DialShortcut")) { 414 mMode = MODE_PICK_PHONE; 415 mShortcutAction = Intent.ACTION_CALL; 416 setTitle(R.string.callShortcutActivityTitle); 417 } else if (intent.getComponent().getClassName().equals("alias.MessageShortcut")) { 418 mMode = MODE_PICK_PHONE; 419 mShortcutAction = Intent.ACTION_SENDTO; 420 setTitle(R.string.messageShortcutActivityTitle); 421 } else { 422 mMode = MODE_PICK_OR_CREATE_CONTACT; 423 mShortcutAction = Intent.ACTION_VIEW; 424 setTitle(R.string.shortcutActivityTitle); 425 } 426 } else if (Intent.ACTION_GET_CONTENT.equals(action)) { 427 final String type = intent.resolveType(this); 428 if (People.CONTENT_ITEM_TYPE.equals(type)) { 429 mMode = MODE_PICK_OR_CREATE_CONTACT; 430 } else if (Phones.CONTENT_ITEM_TYPE.equals(type)) { 431 mMode = MODE_PICK_PHONE; 432 } else if (ContactMethods.CONTENT_POSTAL_ITEM_TYPE.equals(type)) { 433 mMode = MODE_PICK_POSTAL; 434 } 435 } else if (Intent.ACTION_INSERT_OR_EDIT.equals(action)) { 436 mMode = MODE_INSERT_OR_EDIT_CONTACT; 437 } else if (Intent.ACTION_SEARCH.equals(action)) { 438 // See if the suggestion was clicked with a search action key (call button) 439 if ("call".equals(intent.getStringExtra(SearchManager.ACTION_MSG))) { 440 String query = intent.getStringExtra(SearchManager.QUERY); 441 if (!TextUtils.isEmpty(query)) { 442 Intent newIntent = new Intent(Intent.ACTION_CALL_PRIVILEGED, 443 Uri.fromParts("tel", query, null)); 444 startActivity(newIntent); 445 } 446 finish(); 447 return; 448 } 449 450 // See if search request has extras to specify query 451 if (intent.hasExtra(Insert.EMAIL)) { 452 mMode = MODE_QUERY_PICK_TO_VIEW; 453 mQueryMode = QUERY_MODE_MAILTO; 454 mQueryData = intent.getStringExtra(Insert.EMAIL); 455 } else if (intent.hasExtra(Insert.PHONE)) { 456 mMode = MODE_QUERY_PICK_TO_VIEW; 457 mQueryMode = QUERY_MODE_TEL; 458 mQueryData = intent.getStringExtra(Insert.PHONE); 459 } else { 460 // Otherwise handle the more normal search case 461 mMode = MODE_QUERY; 462 } 463 464 // Since this is the filter activity it receives all intents 465 // dispatched from the SearchManager for security reasons 466 // so we need to re-dispatch from here to the intended target. 467 } else if (Intents.SEARCH_SUGGESTION_CLICKED.equals(action)) { 468 // See if the suggestion was clicked with a search action key (call button) 469 Intent newIntent; 470 if ("call".equals(intent.getStringExtra(SearchManager.ACTION_MSG))) { 471 newIntent = new Intent(Intent.ACTION_CALL_PRIVILEGED, intent.getData()); 472 } else { 473 newIntent = new Intent(Intent.ACTION_VIEW, intent.getData()); 474 } 475 startActivity(newIntent); 476 finish(); 477 return; 478 } else if (Intents.SEARCH_SUGGESTION_DIAL_NUMBER_CLICKED.equals(action)) { 479 Intent newIntent = new Intent(Intent.ACTION_CALL_PRIVILEGED, intent.getData()); 480 startActivity(newIntent); 481 finish(); 482 return; 483 } else if (Intents.SEARCH_SUGGESTION_CREATE_CONTACT_CLICKED.equals(action)) { 484 String number = intent.getData().getSchemeSpecificPart(); 485 Intent newIntent = new Intent(Intent.ACTION_INSERT, People.CONTENT_URI); 486 newIntent.putExtra(Intents.Insert.PHONE, number); 487 startActivity(newIntent); 488 finish(); 489 return; 490 } 491 492 if (mMode == MODE_UNKNOWN) { 493 mMode = DEFAULT_MODE; 494 } 495 496 // Setup the UI 497 final ListView list = getListView(); 498 list.setFocusable(true); 499 list.setOnCreateContextMenuListener(this); 500 if ((mMode & MODE_MASK_NO_FILTER) != MODE_MASK_NO_FILTER) { 501 list.setTextFilterEnabled(true); 502 } 503 504 if ((mMode & MODE_MASK_CREATE_NEW) != 0) { 505 // Add the header for creating a new contact 506 final LayoutInflater inflater = getLayoutInflater(); 507 View header = inflater.inflate(android.R.layout.simple_list_item_1, list, false); 508 TextView text = (TextView) header.findViewById(android.R.id.text1); 509 text.setText(R.string.pickerNewContactHeader); 510 list.addHeaderView(header); 511 } 512 513 // Set the proper empty string 514 setEmptyText(); 515 516 mAdapter = new ContactItemListAdapter(this); 517 setListAdapter(mAdapter); 518 519 // We manually save/restore the listview state 520 list.setSaveEnabled(false); 521 522 mQueryHandler = new QueryHandler(this); 523 mJustCreated = true; 524 525 // Check to see if sync is enabled 526 final ContentResolver resolver = getContentResolver(); 527 IContentProvider provider = resolver.acquireProvider(Contacts.CONTENT_URI); 528 if (provider == null) { 529 // No contacts provider, bail. 530 finish(); 531 return; 532 } 533 534 try { 535 ISyncAdapter sa = provider.getSyncAdapter(); 536 mSyncEnabled = sa != null; 537 } catch (RemoteException e) { 538 mSyncEnabled = false; 539 } finally { 540 resolver.releaseProvider(provider); 541 } 542 } 543 setEmptyText()544 private void setEmptyText() { 545 TextView empty = (TextView) findViewById(R.id.emptyText); 546 // Center the text by default 547 int gravity = Gravity.CENTER; 548 switch (mMode) { 549 case MODE_GROUP: 550 if (Groups.GROUP_MY_CONTACTS.equals(mDisplayInfo)) { 551 if (mSyncEnabled) { 552 empty.setText(getText(R.string.noContactsHelpTextWithSync)); 553 } else { 554 empty.setText(getText(R.string.noContactsHelpText)); 555 } 556 gravity = Gravity.NO_GRAVITY; 557 } else { 558 empty.setText(getString(R.string.groupEmpty, mDisplayInfo)); 559 } 560 break; 561 562 case MODE_STARRED: 563 case MODE_STREQUENT: 564 case MODE_FREQUENT: 565 empty.setText(getText(R.string.noFavorites)); 566 break; 567 568 case MODE_WITH_PHONES: 569 empty.setText(getText(R.string.noContactsWithPhoneNumbers)); 570 break; 571 572 default: 573 empty.setText(getText(R.string.noContacts)); 574 break; 575 } 576 empty.setGravity(gravity); 577 } 578 579 /** 580 * Builds the URIs to query when displaying a user group 581 * 582 * @param groupName the group being displayed 583 */ buildUserGroupUris(String groupName)584 private void buildUserGroupUris(String groupName) { 585 mGroupFilterUri = Uri.parse("content://contacts/groups/name/" + groupName 586 + "/members/filter/"); 587 mGroupUri = Uri.parse("content://contacts/groups/name/" + groupName + "/members"); 588 } 589 590 /** 591 * Builds the URIs to query when displaying a system group 592 * 593 * @param systemId the system group's ID 594 */ buildSystemGroupUris(String systemId)595 private void buildSystemGroupUris(String systemId) { 596 mGroupFilterUri = Uri.parse("content://contacts/groups/system_id/" + systemId 597 + "/members/filter/"); 598 mGroupUri = Uri.parse("content://contacts/groups/system_id/" + systemId + "/members"); 599 } 600 601 /** 602 * Sets the mode when the request is for "default" 603 */ setDefaultMode()604 private void setDefaultMode() { 605 // Load the preferences 606 SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); 607 608 // Lookup the group to display 609 mDisplayType = prefs.getInt(PREF_DISPLAY_TYPE, DISPLAY_TYPE_UNKNOWN); 610 switch (mDisplayType) { 611 case DISPLAY_TYPE_ALL_WITH_PHONES: { 612 mMode = MODE_WITH_PHONES; 613 mDisplayInfo = null; 614 break; 615 } 616 617 case DISPLAY_TYPE_SYSTEM_GROUP: { 618 String systemId = prefs.getString( 619 PREF_DISPLAY_INFO, null); 620 if (!TextUtils.isEmpty(systemId)) { 621 // Display the selected system group 622 mMode = MODE_GROUP; 623 buildSystemGroupUris(systemId); 624 mDisplayInfo = systemId; 625 } else { 626 // No valid group is present, display everything 627 mMode = MODE_WITH_PHONES; 628 mDisplayInfo = null; 629 mDisplayType = DISPLAY_TYPE_ALL; 630 } 631 break; 632 } 633 634 case DISPLAY_TYPE_USER_GROUP: { 635 String displayGroup = prefs.getString( 636 PREF_DISPLAY_INFO, null); 637 if (!TextUtils.isEmpty(displayGroup)) { 638 // Display the selected user group 639 mMode = MODE_GROUP; 640 buildUserGroupUris(displayGroup); 641 mDisplayInfo = displayGroup; 642 } else { 643 // No valid group is present, display everything 644 mMode = MODE_WITH_PHONES; 645 mDisplayInfo = null; 646 mDisplayType = DISPLAY_TYPE_ALL; 647 } 648 break; 649 } 650 651 case DISPLAY_TYPE_ALL: { 652 mMode = MODE_ALL_CONTACTS; 653 mDisplayInfo = null; 654 break; 655 } 656 657 default: { 658 // We don't know what to display, default to My Contacts 659 mMode = MODE_GROUP; 660 mDisplayType = DISPLAY_TYPE_SYSTEM_GROUP; 661 buildSystemGroupUris(Groups.GROUP_MY_CONTACTS); 662 mDisplayInfo = Groups.GROUP_MY_CONTACTS; 663 break; 664 } 665 } 666 667 // Update the empty text view with the proper string, as the group may have changed 668 setEmptyText(); 669 } 670 671 @Override onResume()672 protected void onResume() { 673 super.onResume(); 674 675 boolean runQuery = true; 676 Activity parent = getParent(); 677 678 // Do this before setting the filter. The filter thread relies 679 // on some state that is initialized in setDefaultMode 680 if (mDefaultMode) { 681 // If we're in default mode we need to possibly reset the mode due to a change 682 // in the preferences activity while we weren't running 683 setDefaultMode(); 684 } 685 686 // See if we were invoked with a filter 687 if (parent != null && parent instanceof DialtactsActivity) { 688 String filterText = ((DialtactsActivity) parent).getAndClearFilterText(); 689 if (filterText != null && filterText.length() > 0) { 690 getListView().setFilterText(filterText); 691 // Don't start a new query since it will conflict with the filter 692 runQuery = false; 693 } else if (mJustCreated) { 694 getListView().clearTextFilter(); 695 } 696 } 697 698 if (mJustCreated && runQuery) { 699 // We need to start a query here the first time the activity is launched, as long 700 // as we aren't doing a filter. 701 startQuery(); 702 } 703 mJustCreated = false; 704 } 705 706 @Override onRestart()707 protected void onRestart() { 708 super.onRestart(); 709 710 // The cursor was killed off in onStop(), so we need to get a new one here 711 // We do not perform the query if a filter is set on the list because the 712 // filter will cause the query to happen anyway 713 if (TextUtils.isEmpty(getListView().getTextFilter())) { 714 startQuery(); 715 } else { 716 // Run the filtered query on the adapter 717 ((ContactItemListAdapter) getListAdapter()).onContentChanged(); 718 } 719 } 720 updateGroup()721 private void updateGroup() { 722 if (mDefaultMode) { 723 setDefaultMode(); 724 } 725 726 // Calling requery here may cause an ANR, so always do the async query 727 startQuery(); 728 } 729 730 @Override onSaveInstanceState(Bundle icicle)731 protected void onSaveInstanceState(Bundle icicle) { 732 super.onSaveInstanceState(icicle); 733 // Save list state in the bundle so we can restore it after the QueryHandler has run 734 icicle.putParcelable(LIST_STATE_KEY, mList.onSaveInstanceState()); 735 icicle.putBoolean(FOCUS_KEY, mList.hasFocus()); 736 } 737 738 @Override onRestoreInstanceState(Bundle icicle)739 protected void onRestoreInstanceState(Bundle icicle) { 740 super.onRestoreInstanceState(icicle); 741 // Retrieve list state. This will be applied after the QueryHandler has run 742 mListState = icicle.getParcelable(LIST_STATE_KEY); 743 mListHasFocus = icicle.getBoolean(FOCUS_KEY); 744 } 745 746 @Override onStop()747 protected void onStop() { 748 super.onStop(); 749 750 // We don't want the list to display the empty state, since when we resume it will still 751 // be there and show up while the new query is happening. After the async query finished 752 // in response to onRestart() setLoading(false) will be called. 753 mAdapter.setLoading(true); 754 mAdapter.changeCursor(null); 755 756 if (mMode == MODE_QUERY) { 757 // Make sure the search box is closed 758 SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE); 759 searchManager.stopSearch(); 760 } 761 } 762 763 @Override onCreateOptionsMenu(Menu menu)764 public boolean onCreateOptionsMenu(Menu menu) { 765 // If Contacts was invoked by another Activity simply as a way of 766 // picking a contact, don't show the options menu 767 if ((mMode & MODE_MASK_PICKER) == MODE_MASK_PICKER) { 768 return false; 769 } 770 771 // Search 772 menu.add(0, MENU_SEARCH, 0, R.string.menu_search) 773 .setIcon(android.R.drawable.ic_menu_search); 774 775 // New contact 776 menu.add(0, MENU_NEW_CONTACT, 0, R.string.menu_newContact) 777 .setIcon(android.R.drawable.ic_menu_add) 778 .setIntent(new Intent(Intents.Insert.ACTION, People.CONTENT_URI)) 779 .setAlphabeticShortcut('n'); 780 781 // Display group 782 if (mDefaultMode) { 783 menu.add(0, MENU_DISPLAY_GROUP, 0, R.string.menu_displayGroup) 784 .setIcon(com.android.internal.R.drawable.ic_menu_allfriends); 785 } 786 787 // Sync settings 788 if (mSyncEnabled) { 789 Intent syncIntent = new Intent(Intent.ACTION_VIEW); 790 syncIntent.setClass(this, ContactsGroupSyncSelector.class); 791 menu.add(0, 0, 0, R.string.syncGroupPreference) 792 .setIcon(com.android.internal.R.drawable.ic_menu_refresh) 793 .setIntent(syncIntent); 794 } 795 796 // Contacts import (SIM/SDCard) 797 menu.add(0, MENU_IMPORT_CONTACTS, 0, R.string.importFromSim) 798 .setIcon(R.drawable.ic_menu_import_contact); 799 800 if (getResources().getBoolean(R.bool.config_allow_export_to_sdcard)) { 801 menu.add(0, MENU_EXPORT_CONTACTS, 0, R.string.export_contact_list) 802 .setIcon(R.drawable.ic_menu_export_contact); 803 } 804 805 return super.onCreateOptionsMenu(menu); 806 } 807 808 /* 809 * Implements the handler for display group selection. 810 */ onClick(DialogInterface dialogInterface, int which)811 public void onClick(DialogInterface dialogInterface, int which) { 812 if (which == DialogInterface.BUTTON_POSITIVE) { 813 // The OK button was pressed 814 if (mDisplayGroupOriginalSelection != mDisplayGroupCurrentSelection) { 815 // Set the group to display 816 if (mDisplayGroupCurrentSelection == DISPLAY_GROUP_INDEX_ALL_CONTACTS) { 817 // Display all 818 mDisplayType = DISPLAY_TYPE_ALL; 819 mDisplayInfo = null; 820 } else if (mDisplayGroupCurrentSelection 821 == DISPLAY_GROUP_INDEX_ALL_CONTACTS_WITH_PHONES) { 822 // Display all with phone numbers 823 mDisplayType = DISPLAY_TYPE_ALL_WITH_PHONES; 824 mDisplayInfo = null; 825 } else if (mDisplayGroupsIncludesMyContacts && 826 mDisplayGroupCurrentSelection == DISPLAY_GROUP_INDEX_MY_CONTACTS) { 827 mDisplayType = DISPLAY_TYPE_SYSTEM_GROUP; 828 mDisplayInfo = Groups.GROUP_MY_CONTACTS; 829 } else { 830 mDisplayType = DISPLAY_TYPE_USER_GROUP; 831 mDisplayInfo = mDisplayGroups[mDisplayGroupCurrentSelection].toString(); 832 } 833 834 // Save the changes to the preferences 835 SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); 836 prefs.edit() 837 .putInt(PREF_DISPLAY_TYPE, mDisplayType) 838 .putString(PREF_DISPLAY_INFO, mDisplayInfo) 839 .commit(); 840 841 // Update the display state 842 updateGroup(); 843 } 844 } else { 845 // A list item was selected, cache the position 846 mDisplayGroupCurrentSelection = which; 847 } 848 } 849 850 @Override onOptionsItemSelected(MenuItem item)851 public boolean onOptionsItemSelected(MenuItem item) { 852 switch (item.getItemId()) { 853 case MENU_DISPLAY_GROUP: 854 AlertDialog.Builder builder = new AlertDialog.Builder(this) 855 .setTitle(R.string.select_group_title) 856 .setPositiveButton(android.R.string.ok, this) 857 .setNegativeButton(android.R.string.cancel, null); 858 859 setGroupEntries(builder); 860 861 builder.show(); 862 return true; 863 864 case MENU_SEARCH: 865 startSearch(null, false, null, false); 866 return true; 867 868 case MENU_IMPORT_CONTACTS: 869 if (getResources().getBoolean(R.bool.config_allow_import_from_sdcard)) { 870 ImportTypeSelectedListener listener = 871 new ImportTypeSelectedListener(); 872 AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this) 873 .setTitle(R.string.select_import_type_title) 874 .setPositiveButton(android.R.string.ok, listener) 875 .setNegativeButton(android.R.string.cancel, null); 876 dialogBuilder.setSingleChoiceItems(new String[] { 877 getString(R.string.import_from_sim), 878 getString(R.string.import_from_sdcard)}, 879 ImportTypeSelectedListener.IMPORT_FROM_SIM, listener); 880 dialogBuilder.show(); 881 } else { 882 doImportFromSim(); 883 } 884 return true; 885 886 case MENU_EXPORT_CONTACTS: 887 handleExportContacts(); 888 } 889 return false; 890 } 891 doImportFromSim()892 private void doImportFromSim() { 893 Intent importIntent = new Intent(Intent.ACTION_VIEW); 894 importIntent.setType("vnd.android.cursor.item/sim-contact"); 895 importIntent.setClassName("com.android.phone", "com.android.phone.SimContacts"); 896 startActivity(importIntent); 897 } 898 doImportFromSDCard()899 private void doImportFromSDCard() { 900 Intent intent = new Intent(this, ImportVCardActivity.class); 901 startActivity(intent); 902 } 903 handleExportContacts()904 private void handleExportContacts() { 905 VCardExporter exporter = new VCardExporter(ContactsListActivity.this, mHandler); 906 exporter.startExportVCardToSdCard(); 907 } 908 909 @Override onActivityResult(int requestCode, int resultCode, Intent data)910 protected void onActivityResult(int requestCode, int resultCode, 911 Intent data) { 912 switch (requestCode) { 913 case SUBACTIVITY_NEW_CONTACT: 914 if (resultCode == RESULT_OK) { 915 // Contact was created, pass it back 916 returnPickerResult(null, data.getStringExtra(Intent.EXTRA_SHORTCUT_NAME), 917 data.getData(), 0); 918 } 919 } 920 } 921 922 @Override onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo)923 public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) { 924 // If Contacts was invoked by another Activity simply as a way of 925 // picking a contact, don't show the context menu 926 if ((mMode & MODE_MASK_PICKER) == MODE_MASK_PICKER) { 927 return; 928 } 929 930 AdapterView.AdapterContextMenuInfo info; 931 try { 932 info = (AdapterView.AdapterContextMenuInfo) menuInfo; 933 } catch (ClassCastException e) { 934 Log.e(TAG, "bad menuInfo", e); 935 return; 936 } 937 938 Cursor cursor = (Cursor) getListAdapter().getItem(info.position); 939 if (cursor == null) { 940 // For some reason the requested item isn't available, do nothing 941 return; 942 } 943 long id = info.id; 944 Uri personUri = ContentUris.withAppendedId(People.CONTENT_URI, id); 945 946 // Setup the menu header 947 menu.setHeaderTitle(cursor.getString(NAME_COLUMN_INDEX)); 948 949 // View contact details 950 menu.add(0, MENU_ITEM_VIEW_CONTACT, 0, R.string.menu_viewContact) 951 .setIntent(new Intent(Intent.ACTION_VIEW, personUri)); 952 953 // Calling contact 954 long phoneId = cursor.getLong(PRIMARY_PHONE_ID_COLUMN_INDEX); 955 if (phoneId > 0) { 956 // Get the display label for the number 957 CharSequence label = cursor.getString(LABEL_COLUMN_INDEX); 958 int type = cursor.getInt(TYPE_COLUMN_INDEX); 959 label = Phones.getDisplayLabel(this, type, label); 960 Intent intent = new Intent(Intent.ACTION_CALL_PRIVILEGED, 961 ContentUris.withAppendedId(Phones.CONTENT_URI, phoneId)); 962 menu.add(0, MENU_ITEM_CALL, 0, String.format(getString(R.string.menu_callNumber), label)) 963 .setIntent(intent); 964 965 // Send SMS item 966 menu.add(0, MENU_ITEM_SEND_SMS, 0, R.string.menu_sendSMS) 967 .setIntent(new Intent(Intent.ACTION_SENDTO, 968 Uri.fromParts("sms", cursor.getString(NUMBER_COLUMN_INDEX), null))); 969 } 970 971 // Star toggling 972 int starState = cursor.getInt(STARRED_COLUMN_INDEX); 973 if (starState == 0) { 974 menu.add(0, MENU_ITEM_TOGGLE_STAR, 0, R.string.menu_addStar); 975 } else { 976 menu.add(0, MENU_ITEM_TOGGLE_STAR, 0, R.string.menu_removeStar); 977 } 978 979 // Contact editing 980 menu.add(0, MENU_ITEM_EDIT, 0, R.string.menu_editContact) 981 .setIntent(new Intent(Intent.ACTION_EDIT, personUri)); 982 menu.add(0, MENU_ITEM_DELETE, 0, R.string.menu_deleteContact); 983 } 984 985 @Override onContextItemSelected(MenuItem item)986 public boolean onContextItemSelected(MenuItem item) { 987 AdapterView.AdapterContextMenuInfo info; 988 try { 989 info = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo(); 990 } catch (ClassCastException e) { 991 Log.e(TAG, "bad menuInfo", e); 992 return false; 993 } 994 995 Cursor cursor = (Cursor) getListAdapter().getItem(info.position); 996 997 switch (item.getItemId()) { 998 case MENU_ITEM_TOGGLE_STAR: { 999 // Toggle the star 1000 ContentValues values = new ContentValues(1); 1001 values.put(People.STARRED, cursor.getInt(STARRED_COLUMN_INDEX) == 0 ? 1 : 0); 1002 Uri personUri = ContentUris.withAppendedId(People.CONTENT_URI, 1003 cursor.getInt(ID_COLUMN_INDEX)); 1004 getContentResolver().update(personUri, values, null, null); 1005 return true; 1006 } 1007 1008 case MENU_ITEM_DELETE: { 1009 // Get confirmation 1010 Uri uri = ContentUris.withAppendedId(People.CONTENT_URI, 1011 cursor.getLong(ID_COLUMN_INDEX)); 1012 //TODO make this dialog persist across screen rotations 1013 new AlertDialog.Builder(ContactsListActivity.this) 1014 .setTitle(R.string.deleteConfirmation_title) 1015 .setIcon(android.R.drawable.ic_dialog_alert) 1016 .setMessage(R.string.deleteConfirmation) 1017 .setNegativeButton(android.R.string.cancel, null) 1018 .setPositiveButton(android.R.string.ok, new DeleteClickListener(uri)) 1019 .show(); 1020 return true; 1021 } 1022 } 1023 1024 return super.onContextItemSelected(item); 1025 } 1026 1027 @Override onKeyDown(int keyCode, KeyEvent event)1028 public boolean onKeyDown(int keyCode, KeyEvent event) { 1029 switch (keyCode) { 1030 case KeyEvent.KEYCODE_CALL: { 1031 if (callSelection()) { 1032 return true; 1033 } 1034 break; 1035 } 1036 1037 case KeyEvent.KEYCODE_DEL: { 1038 Object o = getListView().getSelectedItem(); 1039 if (o != null) { 1040 Cursor cursor = (Cursor) o; 1041 Uri uri = ContentUris.withAppendedId(People.CONTENT_URI, 1042 cursor.getLong(ID_COLUMN_INDEX)); 1043 //TODO make this dialog persist across screen rotations 1044 new AlertDialog.Builder(ContactsListActivity.this) 1045 .setTitle(R.string.deleteConfirmation_title) 1046 .setIcon(android.R.drawable.ic_dialog_alert) 1047 .setMessage(R.string.deleteConfirmation) 1048 .setNegativeButton(android.R.string.cancel, null) 1049 .setPositiveButton(android.R.string.ok, new DeleteClickListener(uri)) 1050 .setCancelable(false) 1051 .show(); 1052 return true; 1053 } 1054 break; 1055 } 1056 } 1057 1058 return super.onKeyDown(keyCode, event); 1059 } 1060 1061 @Override onListItemClick(ListView l, View v, int position, long id)1062 protected void onListItemClick(ListView l, View v, int position, long id) { 1063 // Hide soft keyboard, if visible 1064 InputMethodManager inputMethodManager = (InputMethodManager) 1065 getSystemService(Context.INPUT_METHOD_SERVICE); 1066 inputMethodManager.hideSoftInputFromWindow(mList.getWindowToken(), 0); 1067 1068 if (mMode == MODE_INSERT_OR_EDIT_CONTACT) { 1069 Intent intent; 1070 if (position == 0) { 1071 // Insert 1072 intent = new Intent(Intent.ACTION_INSERT, People.CONTENT_URI); 1073 } else { 1074 // Edit 1075 intent = new Intent(Intent.ACTION_EDIT, 1076 ContentUris.withAppendedId(People.CONTENT_URI, id)); 1077 } 1078 intent.setFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT); 1079 final Bundle extras = getIntent().getExtras(); 1080 if (extras != null) { 1081 intent.putExtras(extras); 1082 } 1083 startActivity(intent); 1084 finish(); 1085 } else if (id != -1) { 1086 if ((mMode & MODE_MASK_PICKER) == 0) { 1087 Intent intent = new Intent(Intent.ACTION_VIEW, 1088 ContentUris.withAppendedId(People.CONTENT_URI, id)); 1089 startActivity(intent); 1090 } else if (mMode == MODE_QUERY_PICK_TO_VIEW) { 1091 // Started with query that should launch to view contact 1092 Cursor c = (Cursor) mAdapter.getItem(position); 1093 long personId = c.getLong(mQueryPersonIdIndex); 1094 Intent intent = new Intent(Intent.ACTION_VIEW, 1095 ContentUris.withAppendedId(People.CONTENT_URI, personId)); 1096 startActivity(intent); 1097 finish(); 1098 } else if (mMode == MODE_PICK_CONTACT 1099 || mMode == MODE_PICK_OR_CREATE_CONTACT) { 1100 Uri uri = ContentUris.withAppendedId(People.CONTENT_URI, id); 1101 if (mShortcutAction != null) { 1102 // Subtract one if we have Create Contact at the top 1103 Cursor c = (Cursor) mAdapter.getItem(position 1104 - (mMode == MODE_PICK_OR_CREATE_CONTACT? 1:0)); 1105 returnPickerResult(c, c.getString(NAME_COLUMN_INDEX), uri, id); 1106 } else { 1107 returnPickerResult(null, null, uri, id); 1108 } 1109 } else if (mMode == MODE_PICK_PHONE) { 1110 Uri uri = ContentUris.withAppendedId(Phones.CONTENT_URI, id); 1111 if (mShortcutAction != null) { 1112 Cursor c = (Cursor) mAdapter.getItem(position); 1113 returnPickerResult(c, c.getString(NAME_COLUMN_INDEX), uri, id); 1114 } else { 1115 returnPickerResult(null, null, uri, id); 1116 } 1117 } else if (mMode == MODE_PICK_POSTAL) { 1118 setResult(RESULT_OK, new Intent().setData( 1119 ContentUris.withAppendedId(ContactMethods.CONTENT_URI, id))); 1120 finish(); 1121 } 1122 } else if ((mMode & MODE_MASK_CREATE_NEW) == MODE_MASK_CREATE_NEW 1123 && position == 0) { 1124 Intent newContact = new Intent(Intents.Insert.ACTION, People.CONTENT_URI); 1125 startActivityForResult(newContact, SUBACTIVITY_NEW_CONTACT); 1126 } else { 1127 signalError(); 1128 } 1129 } 1130 returnPickerResult(Cursor c, String name, Uri uri, long id)1131 private void returnPickerResult(Cursor c, String name, Uri uri, long id) { 1132 final Intent intent = new Intent(); 1133 1134 if (mShortcutAction != null) { 1135 Intent shortcutIntent; 1136 if (Intent.ACTION_VIEW.equals(mShortcutAction)) { 1137 // This is a simple shortcut to view a contact. 1138 shortcutIntent = new Intent(mShortcutAction, uri); 1139 final Bitmap icon = People.loadContactPhoto(this, uri, 0, null); 1140 if (icon != null) { 1141 intent.putExtra(Intent.EXTRA_SHORTCUT_ICON, icon); 1142 } else { 1143 intent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE, 1144 Intent.ShortcutIconResource.fromContext(this, 1145 R.drawable.ic_launcher_shortcut_contact)); 1146 } 1147 } else { 1148 // This is a direct dial or sms shortcut. 1149 String number = c.getString(DATA_COLUMN_INDEX); 1150 int type = c.getInt(TYPE_COLUMN_INDEX); 1151 String scheme; 1152 int resid; 1153 if (Intent.ACTION_CALL.equals(mShortcutAction)) { 1154 scheme = "tel"; 1155 resid = R.drawable.badge_action_call; 1156 } else { 1157 scheme = "smsto"; 1158 resid = R.drawable.badge_action_sms; 1159 } 1160 // Make the URI a direct tel: URI so that it will always continue to work 1161 Uri phoneUri = Uri.fromParts(scheme, number, null); 1162 shortcutIntent = new Intent(mShortcutAction, phoneUri); 1163 1164 // Find the People._ID for this phone number 1165 final long personId = c.getLong(PHONES_PERSON_ID_INDEX); 1166 Uri personUri = ContentUris.withAppendedId(People.CONTENT_URI, personId); 1167 intent.putExtra(Intent.EXTRA_SHORTCUT_ICON, 1168 generatePhoneNumberIcon(personUri, type, resid)); 1169 1170 } 1171 shortcutIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); 1172 intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent); 1173 intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, name); 1174 setResult(RESULT_OK, intent); 1175 } else { 1176 setResult(RESULT_OK, intent.setData(uri)); 1177 } 1178 finish(); 1179 } 1180 1181 /** 1182 * Generates a phone number shortcut icon. Adds an overlay describing the type of the phone 1183 * number, and if there is a photo also adds the call action icon. 1184 * 1185 * @param personUri The person the phone number belongs to 1186 * @param type The type of the phone number 1187 * @param actionResId The ID for the action resource 1188 * @return The bitmap for the icon 1189 */ generatePhoneNumberIcon(Uri personUri, int type, int actionResId)1190 private Bitmap generatePhoneNumberIcon(Uri personUri, int type, int actionResId) { 1191 final Resources r = getResources(); 1192 boolean drawPhoneOverlay = true; 1193 1194 Bitmap photo = People.loadContactPhoto(this, personUri, 0, null); 1195 if (photo == null) { 1196 // If there isn't a photo use the generic phone action icon instead 1197 Bitmap phoneIcon = getPhoneActionIcon(r, actionResId); 1198 if (phoneIcon != null) { 1199 photo = phoneIcon; 1200 drawPhoneOverlay = false; 1201 } else { 1202 return null; 1203 } 1204 } 1205 1206 // Setup the drawing classes 1207 int iconSize = (int) r.getDimension(android.R.dimen.app_icon_size); 1208 Bitmap icon = Bitmap.createBitmap(iconSize, iconSize, Bitmap.Config.ARGB_8888); 1209 Canvas canvas = new Canvas(icon); 1210 1211 // Copy in the photo 1212 Paint photoPaint = new Paint(); 1213 photoPaint.setDither(true); 1214 photoPaint.setFilterBitmap(true); 1215 Rect src = new Rect(0,0, photo.getWidth(),photo.getHeight()); 1216 Rect dst = new Rect(0,0, iconSize,iconSize); 1217 canvas.drawBitmap(photo, src, dst, photoPaint); 1218 1219 // Create an overlay for the phone number type 1220 String overlay = null; 1221 switch (type) { 1222 case Phones.TYPE_HOME: 1223 overlay = "H"; 1224 break; 1225 1226 case Phones.TYPE_MOBILE: 1227 overlay = "M"; 1228 break; 1229 1230 case Phones.TYPE_WORK: 1231 overlay = "W"; 1232 break; 1233 1234 case Phones.TYPE_PAGER: 1235 overlay = "P"; 1236 break; 1237 1238 case Phones.TYPE_OTHER: 1239 overlay = "O"; 1240 break; 1241 } 1242 if (overlay != null) { 1243 Paint textPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DEV_KERN_TEXT_FLAG); 1244 textPaint.setTextSize(20.0f); 1245 textPaint.setTypeface(Typeface.DEFAULT_BOLD); 1246 textPaint.setColor(r.getColor(R.color.textColorIconOverlay)); 1247 textPaint.setShadowLayer(3f, 1, 1, r.getColor(R.color.textColorIconOverlayShadow)); 1248 canvas.drawText(overlay, 2, 16, textPaint); 1249 } 1250 1251 // Draw the phone action icon as an overlay 1252 if (ENABLE_ACTION_ICON_OVERLAYS && drawPhoneOverlay) { 1253 Bitmap phoneIcon = getPhoneActionIcon(r, actionResId); 1254 if (phoneIcon != null) { 1255 src.set(0,0, phoneIcon.getWidth(),phoneIcon.getHeight()); 1256 int iconWidth = icon.getWidth(); 1257 dst.set(iconWidth - 20, -1, iconWidth, 19); 1258 canvas.drawBitmap(phoneIcon, src, dst, photoPaint); 1259 } 1260 } 1261 1262 return icon; 1263 } 1264 1265 /** 1266 * Returns the icon for the phone call action. 1267 * 1268 * @param r The resources to load the icon from 1269 * @param resId The resource ID to load 1270 * @return the icon for the phone call action 1271 */ getPhoneActionIcon(Resources r, int resId)1272 private Bitmap getPhoneActionIcon(Resources r, int resId) { 1273 Drawable phoneIcon = r.getDrawable(resId); 1274 if (phoneIcon instanceof BitmapDrawable) { 1275 BitmapDrawable bd = (BitmapDrawable) phoneIcon; 1276 return bd.getBitmap(); 1277 } else { 1278 return null; 1279 } 1280 } 1281 getProjection()1282 String[] getProjection() { 1283 switch (mMode) { 1284 case MODE_GROUP: 1285 case MODE_ALL_CONTACTS: 1286 case MODE_WITH_PHONES: 1287 case MODE_PICK_CONTACT: 1288 case MODE_PICK_OR_CREATE_CONTACT: 1289 case MODE_QUERY: 1290 case MODE_STARRED: 1291 case MODE_FREQUENT: 1292 case MODE_INSERT_OR_EDIT_CONTACT: 1293 return CONTACTS_PROJECTION; 1294 1295 case MODE_STREQUENT: 1296 return STREQUENT_PROJECTION; 1297 1298 case MODE_PICK_PHONE: 1299 return PHONES_PROJECTION; 1300 1301 case MODE_PICK_POSTAL: 1302 return CONTACT_METHODS_PROJECTION; 1303 } 1304 return null; 1305 } 1306 getPeopleFilterUri(String filter)1307 private Uri getPeopleFilterUri(String filter) { 1308 if (!TextUtils.isEmpty(filter)) { 1309 return Uri.withAppendedPath(People.CONTENT_FILTER_URI, Uri.encode(filter)); 1310 } else { 1311 return People.CONTENT_URI; 1312 } 1313 } 1314 getSortOrder(String[] projectionType)1315 private static String getSortOrder(String[] projectionType) { 1316 if (Locale.getDefault().equals(Locale.JAPAN) && 1317 projectionType == CONTACTS_PROJECTION) { 1318 return SORT_STRING + " ASC"; 1319 } else { 1320 return NAME_COLUMN + " COLLATE LOCALIZED ASC"; 1321 } 1322 } 1323 startQuery()1324 void startQuery() { 1325 mAdapter.setLoading(true); 1326 1327 // Cancel any pending queries 1328 mQueryHandler.cancelOperation(QUERY_TOKEN); 1329 1330 // Kick off the new query 1331 switch (mMode) { 1332 case MODE_GROUP: 1333 mQueryHandler.startQuery(QUERY_TOKEN, null, 1334 mGroupUri, CONTACTS_PROJECTION, null, null, 1335 getSortOrder(CONTACTS_PROJECTION)); 1336 break; 1337 1338 case MODE_ALL_CONTACTS: 1339 case MODE_PICK_CONTACT: 1340 case MODE_PICK_OR_CREATE_CONTACT: 1341 case MODE_INSERT_OR_EDIT_CONTACT: 1342 mQueryHandler.startQuery(QUERY_TOKEN, null, People.CONTENT_URI, CONTACTS_PROJECTION, 1343 null, null, getSortOrder(CONTACTS_PROJECTION)); 1344 break; 1345 1346 case MODE_WITH_PHONES: 1347 mQueryHandler.startQuery(QUERY_TOKEN, null, People.CONTENT_URI, CONTACTS_PROJECTION, 1348 People.PRIMARY_PHONE_ID + " IS NOT NULL", null, 1349 getSortOrder(CONTACTS_PROJECTION)); 1350 break; 1351 1352 case MODE_QUERY: { 1353 mQuery = getIntent().getStringExtra(SearchManager.QUERY); 1354 mQueryHandler.startQuery(QUERY_TOKEN, null, getPeopleFilterUri(mQuery), 1355 CONTACTS_PROJECTION, null, null, 1356 getSortOrder(CONTACTS_PROJECTION)); 1357 break; 1358 } 1359 1360 case MODE_QUERY_PICK_TO_VIEW: { 1361 if (mQueryMode == QUERY_MODE_MAILTO) { 1362 // Find all contacts with the given search string as either 1363 // an E-mail or IM address. 1364 mQueryPersonIdIndex = SIMPLE_CONTACTS_PERSON_ID_INDEX; 1365 Uri uri = Uri.withAppendedPath(People.WITH_EMAIL_OR_IM_FILTER_URI, 1366 Uri.encode(mQueryData)); 1367 mQueryHandler.startQuery(QUERY_TOKEN, null, 1368 uri, SIMPLE_CONTACTS_PROJECTION, null, null, 1369 getSortOrder(CONTACTS_PROJECTION)); 1370 1371 } else if (mQueryMode == QUERY_MODE_TEL) { 1372 mQueryPersonIdIndex = PHONES_PERSON_ID_INDEX; 1373 mQueryHandler.startQuery(QUERY_TOKEN, null, 1374 Uri.withAppendedPath(Phones.CONTENT_FILTER_URL, mQueryData), 1375 PHONES_PROJECTION, null, null, 1376 getSortOrder(PHONES_PROJECTION)); 1377 } 1378 break; 1379 } 1380 1381 case MODE_STARRED: 1382 mQueryHandler.startQuery(QUERY_TOKEN, null, People.CONTENT_URI, 1383 CONTACTS_PROJECTION, 1384 People.STARRED + "=1", null, getSortOrder(CONTACTS_PROJECTION)); 1385 break; 1386 1387 case MODE_FREQUENT: 1388 mQueryHandler.startQuery(QUERY_TOKEN, null, 1389 People.CONTENT_URI, CONTACTS_PROJECTION, 1390 People.TIMES_CONTACTED + " > 0", null, 1391 People.TIMES_CONTACTED + " DESC, " + getSortOrder(CONTACTS_PROJECTION)); 1392 break; 1393 1394 case MODE_STREQUENT: 1395 mQueryHandler.startQuery(QUERY_TOKEN, null, 1396 Uri.withAppendedPath(People.CONTENT_URI, "strequent"), STREQUENT_PROJECTION, 1397 null, null, null); 1398 break; 1399 1400 case MODE_PICK_PHONE: 1401 mQueryHandler.startQuery(QUERY_TOKEN, null, Phones.CONTENT_URI, PHONES_PROJECTION, 1402 null, null, getSortOrder(PHONES_PROJECTION)); 1403 break; 1404 1405 case MODE_PICK_POSTAL: 1406 mQueryHandler.startQuery(QUERY_TOKEN, null, ContactMethods.CONTENT_URI, 1407 CONTACT_METHODS_PROJECTION, 1408 ContactMethods.KIND + "=" + Contacts.KIND_POSTAL, null, 1409 getSortOrder(CONTACT_METHODS_PROJECTION)); 1410 break; 1411 } 1412 } 1413 1414 /** 1415 * Called from a background thread to do the filter and return the resulting cursor. 1416 * 1417 * @param filter the text that was entered to filter on 1418 * @return a cursor with the results of the filter 1419 */ doFilter(String filter)1420 Cursor doFilter(String filter) { 1421 final ContentResolver resolver = getContentResolver(); 1422 1423 switch (mMode) { 1424 case MODE_GROUP: { 1425 Uri uri; 1426 if (TextUtils.isEmpty(filter)) { 1427 uri = mGroupUri; 1428 } else { 1429 uri = Uri.withAppendedPath(mGroupFilterUri, Uri.encode(filter)); 1430 } 1431 return resolver.query(uri, CONTACTS_PROJECTION, null, null, 1432 getSortOrder(CONTACTS_PROJECTION)); 1433 } 1434 1435 case MODE_ALL_CONTACTS: 1436 case MODE_PICK_CONTACT: 1437 case MODE_PICK_OR_CREATE_CONTACT: 1438 case MODE_INSERT_OR_EDIT_CONTACT: { 1439 return resolver.query(getPeopleFilterUri(filter), CONTACTS_PROJECTION, null, null, 1440 getSortOrder(CONTACTS_PROJECTION)); 1441 } 1442 1443 case MODE_WITH_PHONES: { 1444 return resolver.query(getPeopleFilterUri(filter), CONTACTS_PROJECTION, 1445 People.PRIMARY_PHONE_ID + " IS NOT NULL", null, 1446 getSortOrder(CONTACTS_PROJECTION)); 1447 } 1448 1449 case MODE_STARRED: { 1450 return resolver.query(getPeopleFilterUri(filter), CONTACTS_PROJECTION, 1451 People.STARRED + "=1", null, getSortOrder(CONTACTS_PROJECTION)); 1452 } 1453 1454 case MODE_FREQUENT: { 1455 return resolver.query(getPeopleFilterUri(filter), CONTACTS_PROJECTION, 1456 People.TIMES_CONTACTED + " > 0", null, 1457 People.TIMES_CONTACTED + " DESC, " + getSortOrder(CONTACTS_PROJECTION)); 1458 1459 } 1460 1461 case MODE_STREQUENT: { 1462 Uri uri; 1463 if (!TextUtils.isEmpty(filter)) { 1464 uri = Uri.withAppendedPath(People.CONTENT_URI, "strequent/filter/" 1465 + Uri.encode(filter)); 1466 } else { 1467 uri = Uri.withAppendedPath(People.CONTENT_URI, "strequent"); 1468 } 1469 return resolver.query(uri, STREQUENT_PROJECTION, null, null, null); 1470 } 1471 1472 case MODE_PICK_PHONE: { 1473 Uri uri; 1474 if (!TextUtils.isEmpty(filter)) { 1475 uri = Uri.withAppendedPath(Phones.CONTENT_URI, "filter_name/" 1476 + Uri.encode(filter)); 1477 } else { 1478 uri = Phones.CONTENT_URI; 1479 } 1480 return resolver.query(uri, PHONES_PROJECTION, null, null, 1481 getSortOrder(PHONES_PROJECTION)); 1482 } 1483 } 1484 throw new UnsupportedOperationException("filtering not allowed in mode " + mMode); 1485 } 1486 1487 /** 1488 * Calls the currently selected list item. 1489 * @return true if the call was initiated, false otherwise 1490 */ callSelection()1491 boolean callSelection() { 1492 ListView list = getListView(); 1493 if (list.hasFocus()) { 1494 Cursor cursor = (Cursor) list.getSelectedItem(); 1495 if (cursor != null) { 1496 long phoneId = cursor.getLong(PRIMARY_PHONE_ID_COLUMN_INDEX); 1497 if (phoneId == 0) { 1498 // There is no phone number. 1499 signalError(); 1500 return false; 1501 } 1502 Uri uri = ContentUris.withAppendedId(Phones.CONTENT_URI, phoneId); 1503 Intent intent = new Intent(Intent.ACTION_CALL_PRIVILEGED, uri); 1504 startActivity(intent); 1505 return true; 1506 } 1507 } 1508 1509 return false; 1510 } 1511 1512 /** 1513 * Signal an error to the user. 1514 */ signalError()1515 void signalError() { 1516 //TODO play an error beep or something... 1517 } 1518 getItemForView(View view)1519 Cursor getItemForView(View view) { 1520 ListView listView = getListView(); 1521 int index = listView.getPositionForView(view); 1522 if (index < 0) { 1523 return null; 1524 } 1525 return (Cursor) listView.getAdapter().getItem(index); 1526 } 1527 setGroupEntries(AlertDialog.Builder builder)1528 private void setGroupEntries(AlertDialog.Builder builder) { 1529 boolean syncEverything; 1530 // For now we only support a single account and the UI doesn't know what 1531 // the account name is, so we're using a global setting for SYNC_EVERYTHING. 1532 // Some day when we add multiple accounts to the UI this should use the per 1533 // account setting. 1534 String value = Contacts.Settings.getSetting(getContentResolver(), null, 1535 Contacts.Settings.SYNC_EVERYTHING); 1536 if (value == null) { 1537 // If nothing is set yet we default to syncing everything 1538 syncEverything = true; 1539 } else { 1540 syncEverything = !TextUtils.isEmpty(value) && !"0".equals(value); 1541 } 1542 1543 Cursor cursor; 1544 if (!syncEverything) { 1545 cursor = getContentResolver().query(Groups.CONTENT_URI, GROUPS_PROJECTION, 1546 Groups.SHOULD_SYNC + " != 0", null, Groups.DEFAULT_SORT_ORDER); 1547 } else { 1548 cursor = getContentResolver().query(Groups.CONTENT_URI, GROUPS_PROJECTION, 1549 null, null, Groups.DEFAULT_SORT_ORDER); 1550 } 1551 try { 1552 ArrayList<CharSequence> groups = new ArrayList<CharSequence>(); 1553 ArrayList<CharSequence> prefStrings = new ArrayList<CharSequence>(); 1554 1555 // Add All Contacts 1556 groups.add(DISPLAY_GROUP_INDEX_ALL_CONTACTS, getString(R.string.showAllGroups)); 1557 prefStrings.add(""); 1558 1559 // Add Contacts with phones 1560 groups.add(DISPLAY_GROUP_INDEX_ALL_CONTACTS_WITH_PHONES, 1561 getString(R.string.groupNameWithPhones)); 1562 prefStrings.add(GROUP_WITH_PHONES); 1563 1564 int currentIndex = DISPLAY_GROUP_INDEX_ALL_CONTACTS; 1565 while (cursor.moveToNext()) { 1566 String systemId = cursor.getString(GROUPS_COLUMN_INDEX_SYSTEM_ID); 1567 String name = cursor.getString(GROUPS_COLUMN_INDEX_NAME); 1568 if (cursor.isNull(GROUPS_COLUMN_INDEX_SYSTEM_ID) 1569 && !Groups.GROUP_MY_CONTACTS.equals(systemId)) { 1570 // All groups that aren't My Contacts, since that one is localized on the phone 1571 1572 // Localize the "Starred in Android" string which we get from the server side. 1573 if (Groups.GROUP_ANDROID_STARRED.equals(name)) { 1574 name = getString(R.string.starredInAndroid); 1575 } 1576 groups.add(name); 1577 if (name.equals(mDisplayInfo)) { 1578 currentIndex = groups.size() - 1; 1579 } 1580 } else { 1581 // The My Contacts group 1582 groups.add(DISPLAY_GROUP_INDEX_MY_CONTACTS, 1583 getString(R.string.groupNameMyContacts)); 1584 if (mDisplayType == DISPLAY_TYPE_SYSTEM_GROUP 1585 && Groups.GROUP_MY_CONTACTS.equals(mDisplayInfo)) { 1586 currentIndex = DISPLAY_GROUP_INDEX_MY_CONTACTS; 1587 } 1588 mDisplayGroupsIncludesMyContacts = true; 1589 } 1590 } 1591 if (mMode == MODE_ALL_CONTACTS) { 1592 currentIndex = DISPLAY_GROUP_INDEX_ALL_CONTACTS; 1593 } else if (mMode == MODE_WITH_PHONES) { 1594 currentIndex = DISPLAY_GROUP_INDEX_ALL_CONTACTS_WITH_PHONES; 1595 } 1596 mDisplayGroups = groups.toArray(new CharSequence[groups.size()]); 1597 builder.setSingleChoiceItems(mDisplayGroups, currentIndex, this); 1598 mDisplayGroupOriginalSelection = currentIndex; 1599 } finally { 1600 cursor.close(); 1601 } 1602 } 1603 1604 private static final class QueryHandler extends AsyncQueryHandler { 1605 private final WeakReference<ContactsListActivity> mActivity; 1606 QueryHandler(Context context)1607 public QueryHandler(Context context) { 1608 super(context.getContentResolver()); 1609 mActivity = new WeakReference<ContactsListActivity>((ContactsListActivity) context); 1610 } 1611 1612 @Override onQueryComplete(int token, Object cookie, Cursor cursor)1613 protected void onQueryComplete(int token, Object cookie, Cursor cursor) { 1614 final ContactsListActivity activity = mActivity.get(); 1615 if (activity != null && !activity.isFinishing()) { 1616 activity.mAdapter.setLoading(false); 1617 activity.getListView().clearTextFilter(); 1618 activity.mAdapter.changeCursor(cursor); 1619 1620 // Now that the cursor is populated again, it's possible to restore the list state 1621 if (activity.mListState != null) { 1622 activity.mList.onRestoreInstanceState(activity.mListState); 1623 if (activity.mListHasFocus) { 1624 activity.mList.requestFocus(); 1625 } 1626 activity.mListHasFocus = false; 1627 activity.mListState = null; 1628 } 1629 } else { 1630 cursor.close(); 1631 } 1632 } 1633 } 1634 1635 final static class ContactListItemCache { 1636 public TextView nameView; 1637 public CharArrayBuffer nameBuffer = new CharArrayBuffer(128); 1638 public TextView labelView; 1639 public CharArrayBuffer labelBuffer = new CharArrayBuffer(128); 1640 public TextView numberView; 1641 public CharArrayBuffer numberBuffer = new CharArrayBuffer(128); 1642 public ImageView presenceView; 1643 public ImageView photoView; 1644 } 1645 1646 private final class ContactItemListAdapter extends ResourceCursorAdapter 1647 implements SectionIndexer { 1648 private SectionIndexer mIndexer; 1649 private String mAlphabet; 1650 private boolean mLoading = true; 1651 private CharSequence mUnknownNameText; 1652 private CharSequence[] mLocalizedLabels; 1653 private boolean mDisplayPhotos = false; 1654 private SparseArray<SoftReference<Bitmap>> mBitmapCache = null; 1655 private int mFrequentSeparatorPos = ListView.INVALID_POSITION; 1656 ContactItemListAdapter(Context context)1657 public ContactItemListAdapter(Context context) { 1658 super(context, R.layout.contacts_list_item, null, false); 1659 1660 mAlphabet = context.getString(com.android.internal.R.string.fast_scroll_alphabet); 1661 1662 mUnknownNameText = context.getText(android.R.string.unknownName); 1663 switch (mMode) { 1664 case MODE_PICK_POSTAL: 1665 mLocalizedLabels = EditContactActivity.getLabelsForKind(mContext, 1666 Contacts.KIND_POSTAL); 1667 break; 1668 default: 1669 mLocalizedLabels = EditContactActivity.getLabelsForKind(mContext, 1670 Contacts.KIND_PHONE); 1671 break; 1672 } 1673 1674 if ((mMode & MODE_MASK_SHOW_PHOTOS) == MODE_MASK_SHOW_PHOTOS) { 1675 mDisplayPhotos = true; 1676 setViewResource(R.layout.contacts_list_item_photo); 1677 mBitmapCache = new SparseArray<SoftReference<Bitmap>>(); 1678 } 1679 } 1680 getNewIndexer(Cursor cursor)1681 private SectionIndexer getNewIndexer(Cursor cursor) { 1682 if (Locale.getDefault().getLanguage().equals(Locale.JAPAN.getLanguage())) { 1683 return new JapaneseContactListIndexer(cursor, SORT_STRING_INDEX); 1684 } else { 1685 return new AlphabetIndexer(cursor, NAME_COLUMN_INDEX, mAlphabet); 1686 } 1687 } 1688 1689 /** 1690 * Callback on the UI thread when the content observer on the backing cursor fires. 1691 * Instead of calling requery we need to do an async query so that the requery doesn't 1692 * block the UI thread for a long time. 1693 */ 1694 @Override onContentChanged()1695 protected void onContentChanged() { 1696 CharSequence constraint = getListView().getTextFilter(); 1697 if (!TextUtils.isEmpty(constraint)) { 1698 // Reset the filter state then start an async filter operation 1699 Filter filter = getFilter(); 1700 filter.filter(constraint); 1701 } else { 1702 // Start an async query 1703 startQuery(); 1704 } 1705 } 1706 setLoading(boolean loading)1707 public void setLoading(boolean loading) { 1708 mLoading = loading; 1709 } 1710 1711 @Override isEmpty()1712 public boolean isEmpty() { 1713 if ((mMode & MODE_MASK_CREATE_NEW) == MODE_MASK_CREATE_NEW) { 1714 // This mode mask adds a header and we always want it to show up, even 1715 // if the list is empty, so always claim the list is not empty. 1716 return false; 1717 } else { 1718 if (mLoading) { 1719 // We don't want the empty state to show when loading. 1720 return false; 1721 } else { 1722 return super.isEmpty(); 1723 } 1724 } 1725 } 1726 1727 @Override getItemViewType(int position)1728 public int getItemViewType(int position) { 1729 if (position == mFrequentSeparatorPos) { 1730 // We don't want the separator view to be recycled. 1731 return IGNORE_ITEM_VIEW_TYPE; 1732 } 1733 return super.getItemViewType(position); 1734 } 1735 1736 @Override getView(int position, View convertView, ViewGroup parent)1737 public View getView(int position, View convertView, ViewGroup parent) { 1738 if (!mDataValid) { 1739 throw new IllegalStateException( 1740 "this should only be called when the cursor is valid"); 1741 } 1742 1743 // Handle the separator specially 1744 if (position == mFrequentSeparatorPos) { 1745 LayoutInflater inflater = 1746 (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 1747 TextView view = (TextView) inflater.inflate(R.layout.list_separator, parent, false); 1748 view.setText(R.string.favoritesFrquentSeparator); 1749 return view; 1750 } 1751 1752 if (!mCursor.moveToPosition(getRealPosition(position))) { 1753 throw new IllegalStateException("couldn't move cursor to position " + position); 1754 } 1755 1756 View v; 1757 if (convertView == null) { 1758 v = newView(mContext, mCursor, parent); 1759 } else { 1760 v = convertView; 1761 } 1762 bindView(v, mContext, mCursor); 1763 return v; 1764 } 1765 1766 @Override newView(Context context, Cursor cursor, ViewGroup parent)1767 public View newView(Context context, Cursor cursor, ViewGroup parent) { 1768 final View view = super.newView(context, cursor, parent); 1769 1770 final ContactListItemCache cache = new ContactListItemCache(); 1771 cache.nameView = (TextView) view.findViewById(R.id.name); 1772 cache.labelView = (TextView) view.findViewById(R.id.label); 1773 cache.numberView = (TextView) view.findViewById(R.id.number); 1774 cache.presenceView = (ImageView) view.findViewById(R.id.presence); 1775 cache.photoView = (ImageView) view.findViewById(R.id.photo); 1776 view.setTag(cache); 1777 1778 return view; 1779 } 1780 1781 @Override bindView(View view, Context context, Cursor cursor)1782 public void bindView(View view, Context context, Cursor cursor) { 1783 final ContactListItemCache cache = (ContactListItemCache) view.getTag(); 1784 1785 // Set the name 1786 cursor.copyStringToBuffer(NAME_COLUMN_INDEX, cache.nameBuffer); 1787 int size = cache.nameBuffer.sizeCopied; 1788 if (size != 0) { 1789 cache.nameView.setText(cache.nameBuffer.data, 0, size); 1790 } else { 1791 cache.nameView.setText(mUnknownNameText); 1792 } 1793 1794 // Bail out early if using a specific SEARCH query mode, usually for 1795 // matching a specific E-mail or phone number. Any contact details 1796 // shown would be identical, and columns might not even be present 1797 // in the returned cursor. 1798 if (mQueryMode != QUERY_MODE_NONE) { 1799 cache.numberView.setVisibility(View.GONE); 1800 cache.labelView.setVisibility(View.GONE); 1801 cache.presenceView.setVisibility(View.GONE); 1802 return; 1803 } 1804 1805 // Set the phone number 1806 TextView numberView = cache.numberView; 1807 TextView labelView = cache.labelView; 1808 cursor.copyStringToBuffer(NUMBER_COLUMN_INDEX, cache.numberBuffer); 1809 size = cache.numberBuffer.sizeCopied; 1810 if (size != 0) { 1811 numberView.setText(cache.numberBuffer.data, 0, size); 1812 numberView.setVisibility(View.VISIBLE); 1813 labelView.setVisibility(View.VISIBLE); 1814 } else { 1815 numberView.setVisibility(View.GONE); 1816 labelView.setVisibility(View.GONE); 1817 } 1818 1819 // Set the label 1820 if (!cursor.isNull(TYPE_COLUMN_INDEX)) { 1821 int type = cursor.getInt(TYPE_COLUMN_INDEX); 1822 1823 if (type != People.Phones.TYPE_CUSTOM) { 1824 try { 1825 labelView.setText(mLocalizedLabels[type - 1]); 1826 } catch (ArrayIndexOutOfBoundsException e) { 1827 labelView.setText(mLocalizedLabels[People.Phones.TYPE_HOME - 1]); 1828 } 1829 } else { 1830 cursor.copyStringToBuffer(LABEL_COLUMN_INDEX, cache.labelBuffer); 1831 // Don't check size, if it's zero just don't show anything 1832 labelView.setText(cache.labelBuffer.data, 0, cache.labelBuffer.sizeCopied); 1833 } 1834 } else { 1835 // There is no label, hide the the view 1836 labelView.setVisibility(View.GONE); 1837 } 1838 1839 // Set the proper icon (star or presence or nothing) 1840 ImageView presenceView = cache.presenceView; 1841 if ((mMode & MODE_MASK_NO_PRESENCE) == 0) { 1842 int serverStatus; 1843 if (!cursor.isNull(SERVER_STATUS_COLUMN_INDEX)) { 1844 serverStatus = cursor.getInt(SERVER_STATUS_COLUMN_INDEX); 1845 presenceView.setImageResource( 1846 Presence.getPresenceIconResourceId(serverStatus)); 1847 presenceView.setVisibility(View.VISIBLE); 1848 } else { 1849 presenceView.setVisibility(View.GONE); 1850 } 1851 } else { 1852 presenceView.setVisibility(View.GONE); 1853 } 1854 1855 // Set the photo, if requested 1856 if (mDisplayPhotos) { 1857 Bitmap photo = null; 1858 1859 // Look for the cached bitmap 1860 int pos = cursor.getPosition(); 1861 SoftReference<Bitmap> ref = mBitmapCache.get(pos); 1862 if (ref != null) { 1863 photo = ref.get(); 1864 } 1865 1866 if (photo == null) { 1867 // Bitmap cache miss, decode it from the cursor 1868 if (!cursor.isNull(PHOTO_COLUMN_INDEX)) { 1869 try { 1870 byte[] photoData = cursor.getBlob(PHOTO_COLUMN_INDEX); 1871 photo = BitmapFactory.decodeByteArray(photoData, 0, 1872 photoData.length); 1873 mBitmapCache.put(pos, new SoftReference<Bitmap>(photo)); 1874 } catch (OutOfMemoryError e) { 1875 // Not enough memory for the photo, use the default one instead 1876 photo = null; 1877 } 1878 } 1879 } 1880 1881 // Bind the photo, or use the fallback no photo resource 1882 if (photo != null) { 1883 cache.photoView.setImageBitmap(photo); 1884 } else { 1885 cache.photoView.setImageResource(R.drawable.ic_contact_list_picture); 1886 } 1887 } 1888 } 1889 1890 @Override changeCursor(Cursor cursor)1891 public void changeCursor(Cursor cursor) { 1892 // Get the split between starred and frequent items, if the mode is strequent 1893 mFrequentSeparatorPos = ListView.INVALID_POSITION; 1894 if (cursor != null && cursor.getCount() > 0 && mMode == MODE_STREQUENT) { 1895 cursor.move(-1); 1896 for (int i = 0; cursor.moveToNext(); i++) { 1897 int starred = cursor.getInt(STARRED_COLUMN_INDEX); 1898 if (starred == 0) { 1899 if (i > 0) { 1900 // Only add the separator when there are starred items present 1901 mFrequentSeparatorPos = i; 1902 } 1903 break; 1904 } 1905 } 1906 } 1907 1908 super.changeCursor(cursor); 1909 1910 // Update the indexer for the fast scroll widget 1911 updateIndexer(cursor); 1912 1913 // Clear the photo bitmap cache, if there is one 1914 if (mBitmapCache != null) { 1915 mBitmapCache.clear(); 1916 } 1917 } 1918 updateIndexer(Cursor cursor)1919 private void updateIndexer(Cursor cursor) { 1920 if (mIndexer == null) { 1921 mIndexer = getNewIndexer(cursor); 1922 } else { 1923 if (Locale.getDefault().equals(Locale.JAPAN)) { 1924 if (mIndexer instanceof JapaneseContactListIndexer) { 1925 ((JapaneseContactListIndexer)mIndexer).setCursor(cursor); 1926 } else { 1927 mIndexer = getNewIndexer(cursor); 1928 } 1929 } else { 1930 if (mIndexer instanceof AlphabetIndexer) { 1931 ((AlphabetIndexer)mIndexer).setCursor(cursor); 1932 } else { 1933 mIndexer = getNewIndexer(cursor); 1934 } 1935 } 1936 } 1937 } 1938 1939 /** 1940 * Run the query on a helper thread. Beware that this code does not run 1941 * on the main UI thread! 1942 */ 1943 @Override runQueryOnBackgroundThread(CharSequence constraint)1944 public Cursor runQueryOnBackgroundThread(CharSequence constraint) { 1945 return doFilter(constraint.toString()); 1946 } 1947 getSections()1948 public Object [] getSections() { 1949 if (mMode == MODE_STREQUENT) { 1950 return new String[] { " " }; 1951 } else { 1952 return mIndexer.getSections(); 1953 } 1954 } 1955 getPositionForSection(int sectionIndex)1956 public int getPositionForSection(int sectionIndex) { 1957 if (mMode == MODE_STREQUENT) { 1958 return 0; 1959 } 1960 1961 if (mIndexer == null) { 1962 Cursor cursor = mAdapter.getCursor(); 1963 if (cursor == null) { 1964 // No cursor, the section doesn't exist so just return 0 1965 return 0; 1966 } 1967 mIndexer = getNewIndexer(cursor); 1968 } 1969 1970 return mIndexer.getPositionForSection(sectionIndex); 1971 } 1972 getSectionForPosition(int position)1973 public int getSectionForPosition(int position) { 1974 // Note: JapaneseContactListIndexer depends on the fact 1975 // this method always returns 0. If you change this, 1976 // please care it too. 1977 return 0; 1978 } 1979 1980 @Override areAllItemsEnabled()1981 public boolean areAllItemsEnabled() { 1982 return mMode != MODE_STREQUENT; 1983 } 1984 1985 @Override isEnabled(int position)1986 public boolean isEnabled(int position) { 1987 return position != mFrequentSeparatorPos; 1988 } 1989 1990 @Override getCount()1991 public int getCount() { 1992 if (mFrequentSeparatorPos != ListView.INVALID_POSITION) { 1993 return super.getCount() + 1; 1994 } else { 1995 return super.getCount(); 1996 } 1997 } 1998 getRealPosition(int pos)1999 private int getRealPosition(int pos) { 2000 if (mFrequentSeparatorPos == ListView.INVALID_POSITION) { 2001 // No separator, identity map 2002 return pos; 2003 } else if (pos <= mFrequentSeparatorPos) { 2004 // Before or at the separator, identity map 2005 return pos; 2006 } else { 2007 // After the separator, remove 1 from the pos to get the real underlying pos 2008 return pos - 1; 2009 } 2010 2011 } 2012 2013 @Override getItem(int pos)2014 public Object getItem(int pos) { 2015 return super.getItem(getRealPosition(pos)); 2016 } 2017 2018 @Override getItemId(int pos)2019 public long getItemId(int pos) { 2020 return super.getItemId(getRealPosition(pos)); 2021 } 2022 } 2023 } 2024