2 * Copyright (C) 2007 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.android.contacts;
19 import static com.android.contacts.ShowOrCreateActivity.QUERY_KIND_EMAIL_OR_IM;
21 import android.app.Activity;
22 import android.app.AlertDialog;
23 import android.app.ListActivity;
24 import android.app.SearchManager;
25 import android.content.AsyncQueryHandler;
26 import android.content.ContentResolver;
27 import android.content.ContentUris;
28 import android.content.ContentValues;
29 import android.content.Context;
30 import android.content.DialogInterface;
31 import android.content.IContentProvider;
32 import android.content.ISyncAdapter;
33 import android.content.Intent;
34 import android.content.SharedPreferences;
35 import android.database.CharArrayBuffer;
36 import android.database.Cursor;
37 import android.database.CursorWrapper;
38 import android.graphics.Bitmap;
39 import android.graphics.BitmapFactory;
40 import android.net.Uri;
41 import android.os.Bundle;
42 import android.os.Parcelable;
43 import android.os.RemoteException;
44 import android.preference.PreferenceManager;
45 import android.provider.Contacts;
46 import android.provider.Contacts.ContactMethods;
47 import android.provider.Contacts.Groups;
48 import android.provider.Contacts.Intents;
49 import android.provider.Contacts.People;
50 import android.provider.Contacts.Phones;
51 import android.provider.Contacts.Presence;
52 import android.provider.Contacts.Intents.Insert;
53 import android.provider.Contacts.Intents.UI;
54 import android.text.TextUtils;
55 import android.util.Log;
56 import android.util.SparseArray;
57 import android.view.ContextMenu;
58 import android.view.Gravity;
59 import android.view.KeyEvent;
60 import android.view.LayoutInflater;
61 import android.view.Menu;
62 import android.view.MenuItem;
63 import android.view.View;
64 import android.view.ViewGroup;
65 import android.view.ContextMenu.ContextMenuInfo;
66 import android.view.inputmethod.InputMethodManager;
67 import android.widget.AdapterView;
68 import android.widget.AlphabetIndexer;
69 import android.widget.Filter;
70 import android.widget.ImageView;
71 import android.widget.ListView;
72 import android.widget.ResourceCursorAdapter;
73 import android.widget.SectionIndexer;
74 import android.widget.TextView;
76 import java.lang.ref.SoftReference;
77 import java.lang.ref.WeakReference;
78 import java.util.ArrayList;
79 import java.util.Locale;
82 * Displays a list of contacts. Usually is embedded into the ContactsActivity.
84 public final class ContactsListActivity extends ListActivity
85 implements View.OnCreateContextMenuListener, DialogInterface.OnClickListener {
86 private static final String TAG = "ContactsListActivity";
88 private static final String LIST_STATE_KEY = "liststate";
89 private static final String FOCUS_KEY = "focused";
91 static final int MENU_ITEM_VIEW_CONTACT = 1;
92 static final int MENU_ITEM_CALL = 2;
93 static final int MENU_ITEM_EDIT_BEFORE_CALL = 3;
94 static final int MENU_ITEM_SEND_SMS = 4;
95 static final int MENU_ITEM_SEND_IM = 5;
96 static final int MENU_ITEM_EDIT = 6;
97 static final int MENU_ITEM_DELETE = 7;
98 static final int MENU_ITEM_TOGGLE_STAR = 8;
100 public static final int MENU_SEARCH = 1;
101 public static final int MENU_DIALER = 9;
102 public static final int MENU_NEW_CONTACT = 10;
103 public static final int MENU_DISPLAY_GROUP = 11;
105 private static final int SUBACTIVITY_NEW_CONTACT = 1;
107 /** Mask for picker mode */
108 static final int MODE_MASK_PICKER = 0x80000000;
109 /** Mask for no presence mode */
110 static final int MODE_MASK_NO_PRESENCE = 0x40000000;
111 /** Mask for enabling list filtering */
112 static final int MODE_MASK_NO_FILTER = 0x20000000;
113 /** Mask for having a "create new contact" header in the list */
114 static final int MODE_MASK_CREATE_NEW = 0x10000000;
115 /** Mask for showing photos in the list */
116 static final int MODE_MASK_SHOW_PHOTOS = 0x08000000;
119 static final int MODE_UNKNOWN = 0;
120 /** Show members of the "Contacts" group */
121 static final int MODE_GROUP = 5;
122 /** Show all contacts sorted alphabetically */
123 static final int MODE_ALL_CONTACTS = 10;
124 /** Show all contacts with phone numbers, sorted alphabetically */
125 static final int MODE_WITH_PHONES = 15;
126 /** Show all starred contacts */
127 static final int MODE_STARRED = 20;
128 /** Show frequently contacted contacts */
129 static final int MODE_FREQUENT = 30;
130 /** Show starred and the frequent */
131 static final int MODE_STREQUENT = 35 | MODE_MASK_SHOW_PHOTOS;
132 /** Show all contacts and pick them when clicking */
133 static final int MODE_PICK_CONTACT = 40 | MODE_MASK_PICKER;
134 /** Show all contacts as well as the option to create a new one */
135 static final int MODE_PICK_OR_CREATE_CONTACT = 42 | MODE_MASK_PICKER | MODE_MASK_CREATE_NEW;
136 /** Show all contacts and pick them when clicking, and allow creating a new contact */
137 static final int MODE_INSERT_OR_EDIT_CONTACT = 45 | MODE_MASK_PICKER | MODE_MASK_CREATE_NEW;
138 /** Show all phone numbers and pick them when clicking */
139 static final int MODE_PICK_PHONE = 50 | MODE_MASK_PICKER | MODE_MASK_NO_PRESENCE;
140 /** Show all postal addresses and pick them when clicking */
141 static final int MODE_PICK_POSTAL =
142 55 | MODE_MASK_PICKER | MODE_MASK_NO_PRESENCE | MODE_MASK_NO_FILTER;
143 /** Run a search query */
144 static final int MODE_QUERY = 60 | MODE_MASK_NO_FILTER;
145 /** Run a search query in PICK mode, but that still launches to VIEW */
146 static final int MODE_QUERY_PICK_TO_VIEW = 65 | MODE_MASK_NO_FILTER | MODE_MASK_PICKER;
148 static final int DEFAULT_MODE = MODE_ALL_CONTACTS;
151 * The type of data to display in the main contacts list.
153 static final String PREF_DISPLAY_TYPE = "display_system_group";
155 /** Unknown display type. */
156 static final int DISPLAY_TYPE_UNKNOWN = -1;
157 /** Display all contacts */
158 static final int DISPLAY_TYPE_ALL = 0;
159 /** Display all contacts that have phone numbers */
160 static final int DISPLAY_TYPE_ALL_WITH_PHONES = 1;
161 /** Display a system group */
162 static final int DISPLAY_TYPE_SYSTEM_GROUP = 2;
163 /** Display a user group */
164 static final int DISPLAY_TYPE_USER_GROUP = 3;
167 * Info about what to display. If {@link #PREF_DISPLAY_TYPE}
168 * is {@link #DISPLAY_TYPE_SYSTEM_GROUP} then this will be the system id.
169 * If {@link #PREF_DISPLAY_TYPE} is {@link #DISPLAY_TYPE_USER_GROUP} then this will
172 static final String PREF_DISPLAY_INFO = "display_group";
175 static final String NAME_COLUMN = People.DISPLAY_NAME;
176 static final String SORT_STRING = People.SORT_STRING;
178 static final String[] CONTACTS_PROJECTION = new String[] {
185 People.PRIMARY_PHONE_ID, // 6
186 People.PRIMARY_EMAIL_ID, // 7
187 People.PRESENCE_STATUS, // 8
191 static final String[] STREQUENT_PROJECTION = new String[] {
198 People.PRIMARY_PHONE_ID, // 6
199 People.PRIMARY_EMAIL_ID, // 7
200 People.PRESENCE_STATUS, // 8
202 People.TIMES_CONTACTED, // 10 (not displayed, but required for the order by to work)
205 static final String[] PHONES_PROJECTION = new String[] {
212 Phones.PERSON_ID, // 6
215 static final String[] CONTACT_METHODS_PROJECTION = new String[] {
216 ContactMethods._ID, // 0
218 ContactMethods.DATA, // 2
219 ContactMethods.TYPE, // 3
220 ContactMethods.LABEL, // 4
221 ContactMethods.STARRED, // 5
222 ContactMethods.PERSON_ID, // 6
225 static final int ID_COLUMN_INDEX = 0;
226 static final int NAME_COLUMN_INDEX = 1;
227 static final int NUMBER_COLUMN_INDEX = 2;
228 static final int DATA_COLUMN_INDEX = 2;
229 static final int TYPE_COLUMN_INDEX = 3;
230 static final int LABEL_COLUMN_INDEX = 4;
231 static final int STARRED_COLUMN_INDEX = 5;
232 static final int PRIMARY_PHONE_ID_COLUMN_INDEX = 6;
233 static final int PRIMARY_EMAIL_ID_COLUMN_INDEX = 7;
234 static final int SERVER_STATUS_COLUMN_INDEX = 8;
235 static final int PHOTO_COLUMN_INDEX = 9;
236 static final int SORT_STRING_INDEX = 9;
238 static final int PHONES_PERSON_ID_INDEX = 6;
239 static final int CONTACT_METHODS_PERSON_ID_INDEX = 6;
241 static final int DISPLAY_GROUP_INDEX_ALL_CONTACTS = 0;
242 static final int DISPLAY_GROUP_INDEX_ALL_CONTACTS_WITH_PHONES = 1;
243 static final int DISPLAY_GROUP_INDEX_MY_CONTACTS = 2;
245 private static final int QUERY_TOKEN = 42;
247 private static final String[] GROUPS_PROJECTION = new String[] {
248 Groups.SYSTEM_ID, // 0
251 private static final int GROUPS_COLUMN_INDEX_SYSTEM_ID = 0;
252 private static final int GROUPS_COLUMN_INDEX_NAME = 1;
254 static final String GROUP_WITH_PHONES = "android_smartgroup_phone";
256 ContactItemListAdapter mAdapter;
258 int mMode = DEFAULT_MODE;
259 // The current display group
260 private String mDisplayInfo;
261 private int mDisplayType;
262 // The current list of display groups, during selection from menu
263 private CharSequence[] mDisplayGroups;
264 // If true position 2 in mDisplayGroups is the MyContacts group
265 private boolean mDisplayGroupsIncludesMyContacts = false;
267 private int mDisplayGroupOriginalSelection;
268 private int mDisplayGroupCurrentSelection;
270 private QueryHandler mQueryHandler;
271 private String mQuery;
272 private Uri mGroupFilterUri;
273 private Uri mGroupUri;
274 private boolean mJustCreated;
275 private boolean mSyncEnabled;
278 * Cursor row index that holds reference back to {@link People#_ID}, such as
279 * {@link ContactMethods#PERSON_ID}. Used when responding to a
280 * {@link Intent#ACTION_SEARCH} in mode {@link #MODE_QUERY_PICK_TO_VIEW}.
282 private int mQueryPersonIdIndex;
285 * Used to keep track of the scroll state of the list.
287 private Parcelable mListState = null;
288 private boolean mListHasFocus;
290 private boolean mCreateShortcut;
291 private boolean mDefaultMode = false;
294 * Internal query type when in mode {@link #MODE_QUERY_PICK_TO_VIEW}.
296 private int mQueryMode = QUERY_MODE_NONE;
298 private static final int QUERY_MODE_NONE = -1;
299 private static final int QUERY_MODE_MAILTO = 1;
300 private static final int QUERY_MODE_TEL = 2;
303 * Data to use when in mode {@link #MODE_QUERY_PICK_TO_VIEW}. Usually
304 * provided by scheme-specific part of incoming {@link Intent#getData()}.
306 private String mQueryData;
308 private class DeleteClickListener implements DialogInterface.OnClickListener {
311 public DeleteClickListener(Uri uri) {
315 public void onClick(DialogInterface dialog, int which) {
316 getContentResolver().delete(mUri, null, null);
321 protected void onCreate(Bundle icicle) {
322 super.onCreate(icicle);
324 // Resolve the intent
325 final Intent intent = getIntent();
327 // Allow the title to be set to a custom String using an extra on the intent
328 String title = intent.getStringExtra(Contacts.Intents.UI.TITLE_EXTRA_KEY);
333 final String action = intent.getAction();
334 mMode = MODE_UNKNOWN;
336 setContentView(R.layout.contacts_list_content);
338 if (UI.LIST_DEFAULT.equals(action)) {
340 // When mDefaultMode is true the mode is set in onResume(), since the preferneces
341 // activity may change it whenever this activity isn't running
342 } else if (UI.LIST_GROUP_ACTION.equals(action)) {
344 String groupName = intent.getStringExtra(UI.GROUP_NAME_EXTRA_KEY);
345 if (TextUtils.isEmpty(groupName)) {
349 buildUserGroupUris(groupName);
350 } else if (UI.LIST_ALL_CONTACTS_ACTION.equals(action)) {
351 mMode = MODE_ALL_CONTACTS;
352 } else if (UI.LIST_STARRED_ACTION.equals(action)) {
353 mMode = MODE_STARRED;
354 } else if (UI.LIST_FREQUENT_ACTION.equals(action)) {
355 mMode = MODE_FREQUENT;
356 } else if (UI.LIST_STREQUENT_ACTION.equals(action)) {
357 mMode = MODE_STREQUENT;
358 } else if (UI.LIST_CONTACTS_WITH_PHONES_ACTION.equals(action)) {
359 mMode = MODE_WITH_PHONES;
360 } else if (Intent.ACTION_PICK.equals(action)) {
361 // XXX These should be showing the data from the URI given in
363 final String type = intent.resolveType(this);
364 if (People.CONTENT_TYPE.equals(type)) {
365 mMode = MODE_PICK_CONTACT;
366 } else if (Phones.CONTENT_TYPE.equals(type)) {
367 mMode = MODE_PICK_PHONE;
368 } else if (ContactMethods.CONTENT_POSTAL_TYPE.equals(type)) {
369 mMode = MODE_PICK_POSTAL;
371 } else if (Intent.ACTION_CREATE_SHORTCUT.equals(action)) {
372 mMode = MODE_PICK_OR_CREATE_CONTACT;
373 mCreateShortcut = true;
374 } else if (Intent.ACTION_GET_CONTENT.equals(action)) {
375 final String type = intent.resolveType(this);
376 if (People.CONTENT_ITEM_TYPE.equals(type)) {
377 mMode = MODE_PICK_OR_CREATE_CONTACT;
378 } else if (Phones.CONTENT_ITEM_TYPE.equals(type)) {
379 mMode = MODE_PICK_PHONE;
380 } else if (ContactMethods.CONTENT_POSTAL_ITEM_TYPE.equals(type)) {
381 mMode = MODE_PICK_POSTAL;
383 } else if (Intent.ACTION_INSERT_OR_EDIT.equals(action)) {
384 mMode = MODE_INSERT_OR_EDIT_CONTACT;
385 } else if (Intent.ACTION_SEARCH.equals(action)) {
386 // See if the suggestion was clicked with a search action key (call button)
387 if ("call".equals(intent.getStringExtra(SearchManager.ACTION_MSG))) {
388 String query = intent.getStringExtra(SearchManager.QUERY);
389 if (!TextUtils.isEmpty(query)) {
390 Intent newIntent = new Intent(Intent.ACTION_CALL_PRIVILEGED,
391 Uri.fromParts("tel", query, null));
392 startActivity(newIntent);
398 // See if search request has extras to specify query
399 if (intent.hasExtra(Insert.EMAIL)) {
400 mMode = MODE_QUERY_PICK_TO_VIEW;
401 mQueryMode = QUERY_MODE_MAILTO;
402 mQueryData = intent.getStringExtra(Insert.EMAIL);
403 } else if (intent.hasExtra(Insert.PHONE)) {
404 mMode = MODE_QUERY_PICK_TO_VIEW;
405 mQueryMode = QUERY_MODE_TEL;
406 mQueryData = intent.getStringExtra(Insert.PHONE);
408 // Otherwise handle the more normal search case
412 // Since this is the filter activity it receives all intents
413 // dispatched from the SearchManager for security reasons
414 // so we need to re-dispatch from here to the intended target.
415 } else if (Intents.SEARCH_SUGGESTION_CLICKED.equals(action)) {
416 // See if the suggestion was clicked with a search action key (call button)
418 if ("call".equals(intent.getStringExtra(SearchManager.ACTION_MSG))) {
419 newIntent = new Intent(Intent.ACTION_CALL_PRIVILEGED, intent.getData());
421 newIntent = new Intent(Intent.ACTION_VIEW, intent.getData());
423 startActivity(newIntent);
426 } else if (Intents.SEARCH_SUGGESTION_DIAL_NUMBER_CLICKED.equals(action)) {
427 Intent newIntent = new Intent(Intent.ACTION_CALL_PRIVILEGED, intent.getData());
428 startActivity(newIntent);
431 } else if (Intents.SEARCH_SUGGESTION_CREATE_CONTACT_CLICKED.equals(action)) {
432 String number = intent.getData().getSchemeSpecificPart();
433 Intent newIntent = new Intent(Intent.ACTION_INSERT, People.CONTENT_URI);
434 newIntent.putExtra(Intents.Insert.PHONE, number);
435 startActivity(newIntent);
440 if (mMode == MODE_UNKNOWN) {
441 mMode = DEFAULT_MODE;
445 final ListView list = getListView();
446 list.setFocusable(true);
447 list.setOnCreateContextMenuListener(this);
448 if ((mMode & MODE_MASK_NO_FILTER) != MODE_MASK_NO_FILTER) {
449 list.setTextFilterEnabled(true);
452 if ((mMode & MODE_MASK_CREATE_NEW) != 0) {
453 // Add the header for creating a new contact
454 final LayoutInflater inflater = getLayoutInflater();
455 View header = inflater.inflate(android.R.layout.simple_list_item_1, list, false);
456 TextView text = (TextView) header.findViewById(android.R.id.text1);
457 text.setText(R.string.pickerNewContactHeader);
458 list.addHeaderView(header);
461 // Set the proper empty string
464 mAdapter = new ContactItemListAdapter(this);
465 setListAdapter(mAdapter);
467 // We manually save/restore the listview state
468 list.setSaveEnabled(false);
470 mQueryHandler = new QueryHandler(this);
473 // Check to see if sync is enabled
474 final ContentResolver resolver = getContentResolver();
475 IContentProvider provider = resolver.acquireProvider(Contacts.CONTENT_URI);
476 if (provider == null) {
477 // No contacts provider, bail.
483 ISyncAdapter sa = provider.getSyncAdapter();
484 mSyncEnabled = sa != null;
485 } catch (RemoteException e) {
486 mSyncEnabled = false;
488 resolver.releaseProvider(provider);
492 private void setEmptyText() {
493 TextView empty = (TextView) findViewById(R.id.emptyText);
494 // Center the text by default
495 int gravity = Gravity.CENTER;
498 if (Groups.GROUP_MY_CONTACTS.equals(mDisplayInfo)) {
500 empty.setText(getText(R.string.noContactsHelpTextWithSync));
502 empty.setText(getText(R.string.noContactsHelpText));
504 gravity = Gravity.NO_GRAVITY;
506 empty.setText(getString(R.string.groupEmpty, mDisplayInfo));
513 empty.setText(getText(R.string.noFavorites));
516 case MODE_WITH_PHONES:
517 empty.setText(getText(R.string.noContactsWithPhoneNumbers));
521 empty.setText(getText(R.string.noContacts));
524 empty.setGravity(gravity);
528 * Builds the URIs to query when displaying a user group
530 * @param groupName the group being displayed
532 private void buildUserGroupUris(String groupName) {
533 mGroupFilterUri = Uri.parse("content://contacts/groups/name/" + groupName
534 + "/members/filter/");
535 mGroupUri = Uri.parse("content://contacts/groups/name/" + groupName + "/members");
539 * Builds the URIs to query when displaying a system group
541 * @param systemId the system group's ID
543 private void buildSystemGroupUris(String systemId) {
544 mGroupFilterUri = Uri.parse("content://contacts/groups/system_id/" + systemId
545 + "/members/filter/");
546 mGroupUri = Uri.parse("content://contacts/groups/system_id/" + systemId + "/members");
550 * Sets the mode when the request is for "default"
552 private void setDefaultMode() {
553 // Load the preferences
554 SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
556 // Lookup the group to display
557 mDisplayType = prefs.getInt(PREF_DISPLAY_TYPE, DISPLAY_TYPE_UNKNOWN);
558 switch (mDisplayType) {
559 case DISPLAY_TYPE_ALL_WITH_PHONES: {
560 mMode = MODE_WITH_PHONES;
565 case DISPLAY_TYPE_SYSTEM_GROUP: {
566 String systemId = prefs.getString(
567 PREF_DISPLAY_INFO, null);
568 if (!TextUtils.isEmpty(systemId)) {
569 // Display the selected system group
571 buildSystemGroupUris(systemId);
572 mDisplayInfo = systemId;
574 // No valid group is present, display everything
575 mMode = MODE_WITH_PHONES;
577 mDisplayType = DISPLAY_TYPE_ALL;
582 case DISPLAY_TYPE_USER_GROUP: {
583 String displayGroup = prefs.getString(
584 PREF_DISPLAY_INFO, null);
585 if (!TextUtils.isEmpty(displayGroup)) {
586 // Display the selected user group
588 buildUserGroupUris(displayGroup);
589 mDisplayInfo = displayGroup;
591 // No valid group is present, display everything
592 mMode = MODE_WITH_PHONES;
594 mDisplayType = DISPLAY_TYPE_ALL;
599 case DISPLAY_TYPE_ALL: {
600 mMode = MODE_ALL_CONTACTS;
606 // We don't know what to display, default to My Contacts
608 mDisplayType = DISPLAY_TYPE_SYSTEM_GROUP;
609 buildSystemGroupUris(Groups.GROUP_MY_CONTACTS);
610 mDisplayInfo = Groups.GROUP_MY_CONTACTS;
615 // Update the empty text view with the proper string, as the group may have changed
620 protected void onResume() {
623 boolean runQuery = true;
624 Activity parent = getParent();
626 // Do this before setting the filter. The filter thread relies
627 // on some state that is initialized in setDefaultMode
629 // If we're in default mode we need to possibly reset the mode due to a change
630 // in the preferences activity while we weren't running
634 // See if we were invoked with a filter
635 if (parent != null && parent instanceof DialtactsActivity) {
636 String filterText = ((DialtactsActivity) parent).getAndClearFilterText();
637 if (filterText != null && filterText.length() > 0) {
638 getListView().setFilterText(filterText);
639 // Don't start a new query since it will conflict with the filter
641 } else if (mJustCreated) {
642 getListView().clearTextFilter();
646 if (mJustCreated && runQuery) {
647 // We need to start a query here the first time the activity is launched, as long
648 // as we aren't doing a filter.
651 mJustCreated = false;
655 protected void onRestart() {
658 // The cursor was killed off in onStop(), so we need to get a new one here
662 private void updateGroup() {
667 // Calling requery here may cause an ANR, so always do the async query
672 protected void onSaveInstanceState(Bundle icicle) {
673 super.onSaveInstanceState(icicle);
674 // Save list state in the bundle so we can restore it after the QueryHandler has run
675 icicle.putParcelable(LIST_STATE_KEY, mList.onSaveInstanceState());
676 icicle.putBoolean(FOCUS_KEY, mList.hasFocus());
680 protected void onRestoreInstanceState(Bundle icicle) {
681 super.onRestoreInstanceState(icicle);
682 // Retrieve list state. This will be applied after the QueryHandler has run
683 mListState = icicle.getParcelable(LIST_STATE_KEY);
684 mListHasFocus = icicle.getBoolean(FOCUS_KEY);
688 protected void onStop() {
691 // We don't want the list to display the empty state, since when we resume it will still
692 // be there and show up while the new query is happening. After the async query finished
693 // in response to onRestart() setLoading(false) will be called.
694 mAdapter.setLoading(true);
695 mAdapter.changeCursor(null);
697 if (mMode == MODE_QUERY) {
698 // Make sure the search box is closed
699 SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE);
700 searchManager.stopSearch();
705 public boolean onCreateOptionsMenu(Menu menu) {
706 // If Contacts was invoked by another Activity simply as a way of
707 // picking a contact, don't show the options menu
708 if ((mMode & MODE_MASK_PICKER) == MODE_MASK_PICKER) {
713 menu.add(0, MENU_SEARCH, 0, R.string.menu_search)
714 .setIcon(android.R.drawable.ic_menu_search);
717 menu.add(0, MENU_NEW_CONTACT, 0, R.string.menu_newContact)
718 .setIcon(android.R.drawable.ic_menu_add)
719 .setIntent(new Intent(Intents.Insert.ACTION, People.CONTENT_URI))
720 .setAlphabeticShortcut('n');
724 menu.add(0, MENU_DISPLAY_GROUP, 0, R.string.menu_displayGroup)
725 .setIcon(com.android.internal.R.drawable.ic_menu_allfriends);
730 Intent syncIntent = new Intent(Intent.ACTION_VIEW);
731 syncIntent.setClass(this, ContactsGroupSyncSelector.class);
732 menu.add(0, 0, 0, R.string.syncGroupPreference)
733 .setIcon(com.android.internal.R.drawable.ic_menu_refresh)
734 .setIntent(syncIntent);
738 Intent importIntent = new Intent(Intent.ACTION_VIEW);
739 importIntent.setType("vnd.android.cursor.item/sim-contact");
740 importIntent.setClassName("com.android.phone", "com.android.phone.SimContacts");
741 menu.add(0, 0, 0, R.string.importFromSim)
742 .setIcon(R.drawable.ic_menu_import_contact)
743 .setIntent(importIntent);
745 return super.onCreateOptionsMenu(menu);
749 * Implements the handler for display group selection.
751 public void onClick(DialogInterface dialogInterface, int which) {
752 if (which == DialogInterface.BUTTON_POSITIVE) {
753 // The OK button was pressed
754 if (mDisplayGroupOriginalSelection != mDisplayGroupCurrentSelection) {
755 // Set the group to display
756 if (mDisplayGroupCurrentSelection == DISPLAY_GROUP_INDEX_ALL_CONTACTS) {
758 mDisplayType = DISPLAY_TYPE_ALL;
760 } else if (mDisplayGroupCurrentSelection
761 == DISPLAY_GROUP_INDEX_ALL_CONTACTS_WITH_PHONES) {
762 // Display all with phone numbers
763 mDisplayType = DISPLAY_TYPE_ALL_WITH_PHONES;
765 } else if (mDisplayGroupsIncludesMyContacts &&
766 mDisplayGroupCurrentSelection == DISPLAY_GROUP_INDEX_MY_CONTACTS) {
767 mDisplayType = DISPLAY_TYPE_SYSTEM_GROUP;
768 mDisplayInfo = Groups.GROUP_MY_CONTACTS;
770 mDisplayType = DISPLAY_TYPE_USER_GROUP;
771 mDisplayInfo = mDisplayGroups[mDisplayGroupCurrentSelection].toString();
774 // Save the changes to the preferences
775 SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
777 .putInt(PREF_DISPLAY_TYPE, mDisplayType)
778 .putString(PREF_DISPLAY_INFO, mDisplayInfo)
781 // Update the display state
785 // A list item was selected, cache the position
786 mDisplayGroupCurrentSelection = which;
791 public boolean onOptionsItemSelected(MenuItem item) {
792 switch (item.getItemId()) {
793 case MENU_DISPLAY_GROUP:
794 AlertDialog.Builder builder = new AlertDialog.Builder(this)
795 .setTitle(R.string.select_group_title)
796 .setPositiveButton(android.R.string.ok, this)
797 .setNegativeButton(android.R.string.cancel, null);
799 setGroupEntries(builder);
805 startSearch(null, false, null, false);
812 protected void onActivityResult(int requestCode, int resultCode,
814 switch (requestCode) {
815 case SUBACTIVITY_NEW_CONTACT:
816 if (resultCode == RESULT_OK) {
817 // Contact was created, pass it back
818 returnPickerResult(data.getStringExtra(Intent.EXTRA_SHORTCUT_NAME),
825 public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) {
826 // If Contacts was invoked by another Activity simply as a way of
827 // picking a contact, don't show the context menu
828 if ((mMode & MODE_MASK_PICKER) == MODE_MASK_PICKER) {
832 AdapterView.AdapterContextMenuInfo info;
834 info = (AdapterView.AdapterContextMenuInfo) menuInfo;
835 } catch (ClassCastException e) {
836 Log.e(TAG, "bad menuInfo", e);
840 Cursor cursor = (Cursor) getListAdapter().getItem(info.position);
841 if (cursor == null) {
842 // For some reason the requested item isn't available, do nothing
846 Uri personUri = ContentUris.withAppendedId(People.CONTENT_URI, id);
848 // Setup the menu header
849 menu.setHeaderTitle(cursor.getString(NAME_COLUMN_INDEX));
851 // View contact details
852 menu.add(0, MENU_ITEM_VIEW_CONTACT, 0, R.string.menu_viewContact)
853 .setIntent(new Intent(Intent.ACTION_VIEW, personUri));
856 long phoneId = cursor.getLong(PRIMARY_PHONE_ID_COLUMN_INDEX);
858 // Get the display label for the number
859 CharSequence label = cursor.getString(LABEL_COLUMN_INDEX);
860 int type = cursor.getInt(TYPE_COLUMN_INDEX);
861 label = Phones.getDisplayLabel(this, type, label);
862 Intent intent = new Intent(Intent.ACTION_CALL_PRIVILEGED,
863 ContentUris.withAppendedId(Phones.CONTENT_URI, phoneId));
864 menu.add(0, MENU_ITEM_CALL, 0, String.format(getString(R.string.menu_callNumber), label))
868 menu.add(0, MENU_ITEM_SEND_SMS, 0, R.string.menu_sendSMS)
869 .setIntent(new Intent(Intent.ACTION_SENDTO,
870 Uri.fromParts("sms", cursor.getString(NUMBER_COLUMN_INDEX), null)));
874 int starState = cursor.getInt(STARRED_COLUMN_INDEX);
875 if (starState == 0) {
876 menu.add(0, MENU_ITEM_TOGGLE_STAR, 0, R.string.menu_addStar);
878 menu.add(0, MENU_ITEM_TOGGLE_STAR, 0, R.string.menu_removeStar);
882 menu.add(0, MENU_ITEM_EDIT, 0, R.string.menu_editContact)
883 .setIntent(new Intent(Intent.ACTION_EDIT, personUri));
884 menu.add(0, MENU_ITEM_DELETE, 0, R.string.menu_deleteContact);
888 public boolean onContextItemSelected(MenuItem item) {
889 AdapterView.AdapterContextMenuInfo info;
891 info = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo();
892 } catch (ClassCastException e) {
893 Log.e(TAG, "bad menuInfo", e);
897 Cursor cursor = (Cursor) getListAdapter().getItem(info.position);
899 switch (item.getItemId()) {
900 case MENU_ITEM_TOGGLE_STAR: {
902 ContentValues values = new ContentValues(1);
903 values.put(People.STARRED, cursor.getInt(STARRED_COLUMN_INDEX) == 0 ? 1 : 0);
904 Uri personUri = ContentUris.withAppendedId(People.CONTENT_URI,
905 cursor.getInt(ID_COLUMN_INDEX));
906 getContentResolver().update(personUri, values, null, null);
910 case MENU_ITEM_DELETE: {
912 Uri uri = ContentUris.withAppendedId(People.CONTENT_URI,
913 cursor.getLong(ID_COLUMN_INDEX));
914 //TODO make this dialog persist across screen rotations
915 new AlertDialog.Builder(ContactsListActivity.this)
916 .setTitle(R.string.deleteConfirmation_title)
917 .setIcon(android.R.drawable.ic_dialog_alert)
918 .setMessage(R.string.deleteConfirmation)
919 .setNegativeButton(android.R.string.cancel, null)
920 .setPositiveButton(android.R.string.ok, new DeleteClickListener(uri))
926 return super.onContextItemSelected(item);
930 public boolean onKeyDown(int keyCode, KeyEvent event) {
932 case KeyEvent.KEYCODE_CALL: {
933 if (callSelection()) {
939 case KeyEvent.KEYCODE_DEL: {
940 Object o = getListView().getSelectedItem();
942 Cursor cursor = (Cursor) o;
943 Uri uri = ContentUris.withAppendedId(People.CONTENT_URI,
944 cursor.getLong(ID_COLUMN_INDEX));
945 //TODO make this dialog persist across screen rotations
946 new AlertDialog.Builder(ContactsListActivity.this)
947 .setTitle(R.string.deleteConfirmation_title)
948 .setIcon(android.R.drawable.ic_dialog_alert)
949 .setMessage(R.string.deleteConfirmation)
950 .setNegativeButton(android.R.string.cancel, null)
951 .setPositiveButton(android.R.string.ok, new DeleteClickListener(uri))
952 .setCancelable(false)
960 return super.onKeyDown(keyCode, event);
964 protected void onListItemClick(ListView l, View v, int position, long id) {
965 // Hide soft keyboard, if visible
966 InputMethodManager inputMethodManager = (InputMethodManager)
967 getSystemService(Context.INPUT_METHOD_SERVICE);
968 inputMethodManager.hideSoftInputFromWindow(mList.getWindowToken(), 0);
970 if (mMode == MODE_INSERT_OR_EDIT_CONTACT) {
974 intent = new Intent(Intent.ACTION_INSERT, People.CONTENT_URI);
977 intent = new Intent(Intent.ACTION_EDIT,
978 ContentUris.withAppendedId(People.CONTENT_URI, id));
980 intent.setFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
981 final Bundle extras = getIntent().getExtras();
982 if (extras != null) {
983 intent.putExtras(extras);
985 startActivity(intent);
987 } else if (id != -1) {
988 if ((mMode & MODE_MASK_PICKER) == 0) {
989 Intent intent = new Intent(Intent.ACTION_VIEW,
990 ContentUris.withAppendedId(People.CONTENT_URI, id));
991 startActivity(intent);
992 } else if (mMode == MODE_QUERY_PICK_TO_VIEW) {
993 // Started with query that should launch to view contact
994 Cursor c = (Cursor) mAdapter.getItem(position);
995 long personId = c.getLong(mQueryPersonIdIndex);
996 Intent intent = new Intent(Intent.ACTION_VIEW,
997 ContentUris.withAppendedId(People.CONTENT_URI, personId));
998 startActivity(intent);
1000 } else if (mMode == MODE_PICK_CONTACT
1001 || mMode == MODE_PICK_OR_CREATE_CONTACT) {
1002 Uri uri = ContentUris.withAppendedId(People.CONTENT_URI, id);
1003 if (mCreateShortcut) {
1004 // Subtract one if we have Create Contact at the top
1005 Cursor c = (Cursor) mAdapter.getItem(position
1006 - (mMode == MODE_PICK_OR_CREATE_CONTACT? 1:0));
1007 returnPickerResult(c.getString(NAME_COLUMN_INDEX), uri);
1009 returnPickerResult(null, uri);
1011 } else if (mMode == MODE_PICK_PHONE) {
1012 setResult(RESULT_OK, new Intent().setData(
1013 ContentUris.withAppendedId(Phones.CONTENT_URI, id)));
1015 } else if (mMode == MODE_PICK_POSTAL) {
1016 setResult(RESULT_OK, new Intent().setData(
1017 ContentUris.withAppendedId(ContactMethods.CONTENT_URI, id)));
1020 } else if ((mMode & MODE_MASK_CREATE_NEW) == MODE_MASK_CREATE_NEW
1022 Intent newContact = new Intent(Intents.Insert.ACTION, People.CONTENT_URI);
1023 startActivityForResult(newContact, SUBACTIVITY_NEW_CONTACT);
1029 private void returnPickerResult(String name, Uri uri) {
1030 final Intent intent = new Intent();
1032 if (mCreateShortcut) {
1033 Intent shortcutIntent = new Intent(Intent.ACTION_VIEW, uri);
1034 shortcutIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
1035 intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent);
1036 intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, name);
1037 final Bitmap icon = People.loadContactPhoto(this, uri, 0, null);
1039 intent.putExtra(Intent.EXTRA_SHORTCUT_ICON, icon);
1041 intent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE,
1042 Intent.ShortcutIconResource.fromContext(this,
1043 R.drawable.ic_launcher_shortcut_contact));
1045 setResult(RESULT_OK, intent);
1047 setResult(RESULT_OK, intent.setData(uri));
1052 String[] getProjection() {
1055 case MODE_ALL_CONTACTS:
1056 case MODE_WITH_PHONES:
1057 case MODE_PICK_CONTACT:
1058 case MODE_PICK_OR_CREATE_CONTACT:
1062 case MODE_INSERT_OR_EDIT_CONTACT:
1063 return CONTACTS_PROJECTION;
1065 case MODE_STREQUENT:
1066 return STREQUENT_PROJECTION;
1068 case MODE_PICK_PHONE:
1069 return PHONES_PROJECTION;
1071 case MODE_PICK_POSTAL:
1072 return CONTACT_METHODS_PROJECTION;
1077 private Uri getPeopleFilterUri(String filter) {
1078 if (!TextUtils.isEmpty(filter)) {
1079 return Uri.withAppendedPath(People.CONTENT_FILTER_URI, Uri.encode(filter));
1081 return People.CONTENT_URI;
1085 private static String getSortOrder(String[] projectionType) {
1086 if (Locale.getDefault().equals(Locale.JAPAN) &&
1087 projectionType == CONTACTS_PROJECTION) {
1088 return SORT_STRING + " ASC";
1090 return NAME_COLUMN + " COLLATE LOCALIZED ASC";
1095 mAdapter.setLoading(true);
1097 // Cancel any pending queries
1098 mQueryHandler.cancelOperation(QUERY_TOKEN);
1100 // Kick off the new query
1103 mQueryHandler.startQuery(QUERY_TOKEN, null,
1104 mGroupUri, CONTACTS_PROJECTION, null, null,
1105 getSortOrder(CONTACTS_PROJECTION));
1108 case MODE_ALL_CONTACTS:
1109 case MODE_PICK_CONTACT:
1110 case MODE_PICK_OR_CREATE_CONTACT:
1111 case MODE_INSERT_OR_EDIT_CONTACT:
1112 mQueryHandler.startQuery(QUERY_TOKEN, null, People.CONTENT_URI, CONTACTS_PROJECTION,
1113 null, null, getSortOrder(CONTACTS_PROJECTION));
1116 case MODE_WITH_PHONES:
1117 mQueryHandler.startQuery(QUERY_TOKEN, null, People.CONTENT_URI, CONTACTS_PROJECTION,
1118 People.PRIMARY_PHONE_ID + " IS NOT NULL", null,
1119 getSortOrder(CONTACTS_PROJECTION));
1123 mQuery = getIntent().getStringExtra(SearchManager.QUERY);
1124 mQueryHandler.startQuery(QUERY_TOKEN, null, getPeopleFilterUri(mQuery),
1125 CONTACTS_PROJECTION, null, null,
1126 getSortOrder(CONTACTS_PROJECTION));
1130 case MODE_QUERY_PICK_TO_VIEW: {
1131 if (mQueryMode == QUERY_MODE_MAILTO) {
1132 mQueryPersonIdIndex = CONTACT_METHODS_PERSON_ID_INDEX;
1133 mQueryHandler.startQuery(QUERY_TOKEN, null,
1134 ContactMethods.CONTENT_URI, CONTACT_METHODS_PROJECTION,
1135 QUERY_KIND_EMAIL_OR_IM + " AND " + ContactMethods.DATA + "=?",
1136 new String[] { mQueryData },
1137 getSortOrder(CONTACT_METHODS_PROJECTION));
1139 } else if (mQueryMode == QUERY_MODE_TEL) {
1140 mQueryPersonIdIndex = PHONES_PERSON_ID_INDEX;
1141 mQueryHandler.startQuery(QUERY_TOKEN, null,
1142 Uri.withAppendedPath(Phones.CONTENT_FILTER_URL, mQueryData),
1143 PHONES_PROJECTION, null, null,
1144 getSortOrder(PHONES_PROJECTION));
1150 mQueryHandler.startQuery(QUERY_TOKEN, null, People.CONTENT_URI,
1151 CONTACTS_PROJECTION,
1152 People.STARRED + "=1", null, getSortOrder(CONTACTS_PROJECTION));
1156 mQueryHandler.startQuery(QUERY_TOKEN, null,
1157 People.CONTENT_URI, CONTACTS_PROJECTION,
1158 People.TIMES_CONTACTED + " > 0", null,
1159 People.TIMES_CONTACTED + " DESC, " + getSortOrder(CONTACTS_PROJECTION));
1162 case MODE_STREQUENT:
1163 mQueryHandler.startQuery(QUERY_TOKEN, null,
1164 Uri.withAppendedPath(People.CONTENT_URI, "strequent"), STREQUENT_PROJECTION,
1168 case MODE_PICK_PHONE:
1169 mQueryHandler.startQuery(QUERY_TOKEN, null, Phones.CONTENT_URI, PHONES_PROJECTION,
1170 null, null, getSortOrder(PHONES_PROJECTION));
1173 case MODE_PICK_POSTAL:
1174 mQueryHandler.startQuery(QUERY_TOKEN, null, ContactMethods.CONTENT_URI,
1175 CONTACT_METHODS_PROJECTION,
1176 ContactMethods.KIND + "=" + Contacts.KIND_POSTAL, null,
1177 getSortOrder(CONTACT_METHODS_PROJECTION));
1183 * Called from a background thread to do the filter and return the resulting cursor.
1185 * @param filter the text that was entered to filter on
1186 * @return a cursor with the results of the filter
1188 Cursor doFilter(String filter) {
1189 final ContentResolver resolver = getContentResolver();
1194 if (TextUtils.isEmpty(filter)) {
1197 uri = Uri.withAppendedPath(mGroupFilterUri, Uri.encode(filter));
1199 return resolver.query(uri, CONTACTS_PROJECTION, null, null,
1200 getSortOrder(CONTACTS_PROJECTION));
1203 case MODE_ALL_CONTACTS:
1204 case MODE_PICK_CONTACT:
1205 case MODE_PICK_OR_CREATE_CONTACT:
1206 case MODE_INSERT_OR_EDIT_CONTACT: {
1207 return resolver.query(getPeopleFilterUri(filter), CONTACTS_PROJECTION, null, null,
1208 getSortOrder(CONTACTS_PROJECTION));
1211 case MODE_WITH_PHONES: {
1212 return resolver.query(getPeopleFilterUri(filter), CONTACTS_PROJECTION,
1213 People.PRIMARY_PHONE_ID + " IS NOT NULL", null,
1214 getSortOrder(CONTACTS_PROJECTION));
1217 case MODE_STARRED: {
1218 return resolver.query(getPeopleFilterUri(filter), CONTACTS_PROJECTION,
1219 People.STARRED + "=1", null, getSortOrder(CONTACTS_PROJECTION));
1222 case MODE_FREQUENT: {
1223 return resolver.query(getPeopleFilterUri(filter), CONTACTS_PROJECTION,
1224 People.TIMES_CONTACTED + " > 0", null,
1225 People.TIMES_CONTACTED + " DESC, " + getSortOrder(CONTACTS_PROJECTION));
1229 case MODE_STREQUENT: {
1231 if (!TextUtils.isEmpty(filter)) {
1232 uri = Uri.withAppendedPath(People.CONTENT_URI, "strequent/filter/"
1233 + Uri.encode(filter));
1235 uri = Uri.withAppendedPath(People.CONTENT_URI, "strequent");
1237 return resolver.query(uri, STREQUENT_PROJECTION, null, null, null);
1240 case MODE_PICK_PHONE: {
1242 if (!TextUtils.isEmpty(filter)) {
1243 uri = Uri.withAppendedPath(Phones.CONTENT_URI, "filter_name/"
1244 + Uri.encode(filter));
1246 uri = Phones.CONTENT_URI;
1248 return resolver.query(uri, PHONES_PROJECTION, null, null,
1249 getSortOrder(PHONES_PROJECTION));
1252 throw new UnsupportedOperationException("filtering not allowed in mode " + mMode);
1256 * Calls the currently selected list item.
1257 * @return true if the call was initiated, false otherwise
1259 boolean callSelection() {
1260 ListView list = getListView();
1261 if (list.hasFocus()) {
1262 Cursor cursor = (Cursor) list.getSelectedItem();
1263 if (cursor != null) {
1264 long phoneId = cursor.getLong(PRIMARY_PHONE_ID_COLUMN_INDEX);
1266 // There is no phone number.
1270 Uri uri = ContentUris.withAppendedId(Phones.CONTENT_URI, phoneId);
1271 Intent intent = new Intent(Intent.ACTION_CALL_PRIVILEGED, uri);
1272 startActivity(intent);
1281 * Signal an error to the user.
1283 void signalError() {
1284 //TODO play an error beep or something...
1287 Cursor getItemForView(View view) {
1288 ListView listView = getListView();
1289 int index = listView.getPositionForView(view);
1293 return (Cursor) listView.getAdapter().getItem(index);
1296 private void setGroupEntries(AlertDialog.Builder builder) {
1297 boolean syncEverything;
1298 // For now we only support a single account and the UI doesn't know what
1299 // the account name is, so we're using a global setting for SYNC_EVERYTHING.
1300 // Some day when we add multiple accounts to the UI this should use the per
1302 String value = Contacts.Settings.getSetting(getContentResolver(), null,
1303 Contacts.Settings.SYNC_EVERYTHING);
1304 if (value == null) {
1305 // If nothing is set yet we default to syncing everything
1306 syncEverything = true;
1308 syncEverything = !TextUtils.isEmpty(value) && !"0".equals(value);
1312 if (!syncEverything) {
1313 cursor = getContentResolver().query(Groups.CONTENT_URI, GROUPS_PROJECTION,
1314 Groups.SHOULD_SYNC + " != 0", null, Groups.DEFAULT_SORT_ORDER);
1316 cursor = getContentResolver().query(Groups.CONTENT_URI, GROUPS_PROJECTION,
1317 null, null, Groups.DEFAULT_SORT_ORDER);
1320 ArrayList<CharSequence> groups = new ArrayList<CharSequence>();
1321 ArrayList<CharSequence> prefStrings = new ArrayList<CharSequence>();
1324 groups.add(DISPLAY_GROUP_INDEX_ALL_CONTACTS, getString(R.string.showAllGroups));
1325 prefStrings.add("");
1327 // Add Contacts with phones
1328 groups.add(DISPLAY_GROUP_INDEX_ALL_CONTACTS_WITH_PHONES,
1329 getString(R.string.groupNameWithPhones));
1330 prefStrings.add(GROUP_WITH_PHONES);
1332 int currentIndex = DISPLAY_GROUP_INDEX_ALL_CONTACTS;
1333 while (cursor.moveToNext()) {
1334 String systemId = cursor.getString(GROUPS_COLUMN_INDEX_SYSTEM_ID);
1335 String name = cursor.getString(GROUPS_COLUMN_INDEX_NAME);
1336 if (cursor.isNull(GROUPS_COLUMN_INDEX_SYSTEM_ID)
1337 && !Groups.GROUP_MY_CONTACTS.equals(systemId)) {
1338 // All groups that aren't My Contacts, since that one is localized on the phone
1340 if (name.equals(mDisplayInfo)) {
1341 currentIndex = groups.size() - 1;
1344 // The My Contacts group
1345 groups.add(DISPLAY_GROUP_INDEX_MY_CONTACTS,
1346 getString(R.string.groupNameMyContacts));
1347 if (mDisplayType == DISPLAY_TYPE_SYSTEM_GROUP
1348 && Groups.GROUP_MY_CONTACTS.equals(mDisplayInfo)) {
1349 currentIndex = DISPLAY_GROUP_INDEX_MY_CONTACTS;
1351 mDisplayGroupsIncludesMyContacts = true;
1354 if (mMode == MODE_ALL_CONTACTS) {
1355 currentIndex = DISPLAY_GROUP_INDEX_ALL_CONTACTS;
1356 } else if (mMode == MODE_WITH_PHONES) {
1357 currentIndex = DISPLAY_GROUP_INDEX_ALL_CONTACTS_WITH_PHONES;
1359 mDisplayGroups = groups.toArray(new CharSequence[groups.size()]);
1360 builder.setSingleChoiceItems(mDisplayGroups, currentIndex, this);
1361 mDisplayGroupOriginalSelection = currentIndex;
1367 private static final class QueryHandler extends AsyncQueryHandler {
1368 private final WeakReference<ContactsListActivity> mActivity;
1370 public QueryHandler(Context context) {
1371 super(context.getContentResolver());
1372 mActivity = new WeakReference<ContactsListActivity>((ContactsListActivity) context);
1376 protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
1377 final ContactsListActivity activity = mActivity.get();
1378 if (activity != null && !activity.isFinishing()) {
1379 activity.mAdapter.setLoading(false);
1380 activity.getListView().clearTextFilter();
1381 activity.mAdapter.changeCursor(cursor);
1383 // Now that the cursor is populated again, it's possible to restore the list state
1384 if (activity.mListState != null) {
1385 activity.mList.onRestoreInstanceState(activity.mListState);
1386 if (activity.mListHasFocus) {
1387 activity.mList.requestFocus();
1389 activity.mListHasFocus = false;
1390 activity.mListState = null;
1398 final static class ContactListItemCache {
1399 public TextView nameView;
1400 public CharArrayBuffer nameBuffer = new CharArrayBuffer(128);
1401 public TextView labelView;
1402 public CharArrayBuffer labelBuffer = new CharArrayBuffer(128);
1403 public TextView numberView;
1404 public CharArrayBuffer numberBuffer = new CharArrayBuffer(128);
1405 public ImageView presenceView;
1406 public ImageView photoView;
1409 private final class ContactItemListAdapter extends ResourceCursorAdapter
1410 implements SectionIndexer {
1411 private SectionIndexer mIndexer;
1412 private String mAlphabet;
1413 private boolean mLoading = true;
1414 private CharSequence mUnknownNameText;
1415 private CharSequence[] mLocalizedLabels;
1416 private boolean mDisplayPhotos = false;
1417 private SparseArray<SoftReference<Bitmap>> mBitmapCache = null;
1418 private int mFrequentSeparatorPos = ListView.INVALID_POSITION;
1420 public ContactItemListAdapter(Context context) {
1421 super(context, R.layout.contacts_list_item, null, false);
1423 mAlphabet = context.getString(com.android.internal.R.string.fast_scroll_alphabet);
1425 mUnknownNameText = context.getText(android.R.string.unknownName);
1427 case MODE_PICK_POSTAL:
1428 mLocalizedLabels = EditContactActivity.getLabelsForKind(mContext,
1429 Contacts.KIND_POSTAL);
1432 mLocalizedLabels = EditContactActivity.getLabelsForKind(mContext,
1433 Contacts.KIND_PHONE);
1437 if ((mMode & MODE_MASK_SHOW_PHOTOS) == MODE_MASK_SHOW_PHOTOS) {
1438 mDisplayPhotos = true;
1439 setViewResource(R.layout.contacts_list_item_photo);
1440 mBitmapCache = new SparseArray<SoftReference<Bitmap>>();
1444 private SectionIndexer getNewIndexer(Cursor cursor) {
1445 if (Locale.getDefault().equals(Locale.JAPAN)) {
1446 return new JapaneseContactListIndexer(cursor, SORT_STRING_INDEX);
1448 return new AlphabetIndexer(cursor, NAME_COLUMN_INDEX, mAlphabet);
1453 * Callback on the UI thread when the content observer on the backing cursor fires.
1454 * Instead of calling requery we need to do an async query so that the requery doesn't
1455 * block the UI thread for a long time.
1458 protected void onContentChanged() {
1459 CharSequence constraint = getListView().getTextFilter();
1460 if (!TextUtils.isEmpty(constraint)) {
1461 // Reset the filter state then start an async filter operation
1462 Filter filter = getFilter();
1463 filter.filter(constraint);
1465 // Start an async query
1470 public void setLoading(boolean loading) {
1475 public boolean isEmpty() {
1476 if ((mMode & MODE_MASK_CREATE_NEW) == MODE_MASK_CREATE_NEW) {
1477 // This mode mask adds a header and we always want it to show up, even
1478 // if the list is empty, so always claim the list is not empty.
1482 // We don't want the empty state to show when loading.
1485 return super.isEmpty();
1491 public int getItemViewType(int position) {
1492 if (position == mFrequentSeparatorPos) {
1493 // We don't want the separator view to be recycled.
1494 return IGNORE_ITEM_VIEW_TYPE;
1496 return super.getItemViewType(position);
1500 public View getView(int position, View convertView, ViewGroup parent) {
1502 throw new IllegalStateException(
1503 "this should only be called when the cursor is valid");
1506 // Handle the separator specially
1507 if (position == mFrequentSeparatorPos) {
1508 LayoutInflater inflater =
1509 (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
1510 TextView view = (TextView) inflater.inflate(R.layout.list_separator, parent, false);
1511 view.setText(R.string.favoritesFrquentSeparator);
1515 if (!mCursor.moveToPosition(getRealPosition(position))) {
1516 throw new IllegalStateException("couldn't move cursor to position " + position);
1520 if (convertView == null) {
1521 v = newView(mContext, mCursor, parent);
1525 bindView(v, mContext, mCursor);
1530 public View newView(Context context, Cursor cursor, ViewGroup parent) {
1531 final View view = super.newView(context, cursor, parent);
1533 final ContactListItemCache cache = new ContactListItemCache();
1534 cache.nameView = (TextView) view.findViewById(R.id.name);
1535 cache.labelView = (TextView) view.findViewById(R.id.label);
1536 cache.numberView = (TextView) view.findViewById(R.id.number);
1537 cache.presenceView = (ImageView) view.findViewById(R.id.presence);
1538 cache.photoView = (ImageView) view.findViewById(R.id.photo);
1545 public void bindView(View view, Context context, Cursor cursor) {
1546 final ContactListItemCache cache = (ContactListItemCache) view.getTag();
1549 cursor.copyStringToBuffer(NAME_COLUMN_INDEX, cache.nameBuffer);
1550 int size = cache.nameBuffer.sizeCopied;
1552 cache.nameView.setText(cache.nameBuffer.data, 0, size);
1554 cache.nameView.setText(mUnknownNameText);
1557 // Set the phone number
1558 TextView numberView = cache.numberView;
1559 TextView labelView = cache.labelView;
1560 cursor.copyStringToBuffer(NUMBER_COLUMN_INDEX, cache.numberBuffer);
1561 size = cache.numberBuffer.sizeCopied;
1563 numberView.setText(cache.numberBuffer.data, 0, size);
1564 numberView.setVisibility(View.VISIBLE);
1565 labelView.setVisibility(View.VISIBLE);
1567 numberView.setVisibility(View.GONE);
1568 labelView.setVisibility(View.GONE);
1572 if (!cursor.isNull(TYPE_COLUMN_INDEX)) {
1573 int type = cursor.getInt(TYPE_COLUMN_INDEX);
1575 if (type != People.Phones.TYPE_CUSTOM) {
1577 labelView.setText(mLocalizedLabels[type - 1]);
1578 } catch (ArrayIndexOutOfBoundsException e) {
1579 labelView.setText(mLocalizedLabels[People.Phones.TYPE_HOME - 1]);
1582 cursor.copyStringToBuffer(LABEL_COLUMN_INDEX, cache.labelBuffer);
1583 // Don't check size, if it's zero just don't show anything
1584 labelView.setText(cache.labelBuffer.data, 0, cache.labelBuffer.sizeCopied);
1587 // There is no label, hide the the view
1588 labelView.setVisibility(View.GONE);
1591 // Set the proper icon (star or presence or nothing)
1592 ImageView presenceView = cache.presenceView;
1593 if ((mMode & MODE_MASK_NO_PRESENCE) == 0) {
1595 if (!cursor.isNull(SERVER_STATUS_COLUMN_INDEX)) {
1596 serverStatus = cursor.getInt(SERVER_STATUS_COLUMN_INDEX);
1597 presenceView.setImageResource(
1598 Presence.getPresenceIconResourceId(serverStatus));
1599 presenceView.setVisibility(View.VISIBLE);
1601 presenceView.setVisibility(View.GONE);
1604 presenceView.setVisibility(View.GONE);
1607 // Set the photo, if requested
1608 if (mDisplayPhotos) {
1609 Bitmap photo = null;
1611 // Look for the cached bitmap
1612 int pos = cursor.getPosition();
1613 SoftReference<Bitmap> ref = mBitmapCache.get(pos);
1618 if (photo == null) {
1619 // Bitmap cache miss, decode it from the cursor
1620 if (!cursor.isNull(PHOTO_COLUMN_INDEX)) {
1622 byte[] photoData = cursor.getBlob(PHOTO_COLUMN_INDEX);
1623 photo = BitmapFactory.decodeByteArray(photoData, 0,
1625 mBitmapCache.put(pos, new SoftReference<Bitmap>(photo));
1626 } catch (OutOfMemoryError e) {
1627 // Not enough memory for the photo, use the default one instead
1633 // Bind the photo, or use the fallback no photo resource
1634 if (photo != null) {
1635 cache.photoView.setImageBitmap(photo);
1637 cache.photoView.setImageResource(R.drawable.ic_contact_list_picture);
1643 public void changeCursor(Cursor cursor) {
1644 // Get the split between starred and frequent items, if the mode is strequent
1645 mFrequentSeparatorPos = ListView.INVALID_POSITION;
1646 if (cursor != null && cursor.getCount() > 0 && mMode == MODE_STREQUENT) {
1648 for (int i = 0; cursor.moveToNext(); i++) {
1649 int starred = cursor.getInt(STARRED_COLUMN_INDEX);
1652 // Only add the separator when there are starred items present
1653 mFrequentSeparatorPos = i;
1660 super.changeCursor(cursor);
1662 // Update the indexer for the fast scroll widget
1663 updateIndexer(cursor);
1665 // Clear the photo bitmap cache, if there is one
1666 if (mBitmapCache != null) {
1667 mBitmapCache.clear();
1671 private void updateIndexer(Cursor cursor) {
1672 if (mIndexer == null) {
1673 mIndexer = getNewIndexer(cursor);
1675 if (Locale.getDefault().equals(Locale.JAPAN)) {
1676 if (mIndexer instanceof JapaneseContactListIndexer) {
1677 ((JapaneseContactListIndexer)mIndexer).setCursor(cursor);
1679 mIndexer = getNewIndexer(cursor);
1682 if (mIndexer instanceof AlphabetIndexer) {
1683 ((AlphabetIndexer)mIndexer).setCursor(cursor);
1685 mIndexer = getNewIndexer(cursor);
1692 * Run the query on a helper thread. Beware that this code does not run
1693 * on the main UI thread!
1696 public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
1697 return doFilter(constraint.toString());
1700 public Object [] getSections() {
1701 if (mMode == MODE_STREQUENT) {
1702 return new String[] { " " };
1704 return mIndexer.getSections();
1708 public int getPositionForSection(int sectionIndex) {
1709 if (mMode == MODE_STREQUENT) {
1713 if (mIndexer == null) {
1714 Cursor cursor = mAdapter.getCursor();
1715 if (cursor == null) {
1716 // No cursor, the section doesn't exist so just return 0
1719 mIndexer = getNewIndexer(cursor);
1722 return mIndexer.getPositionForSection(sectionIndex);
1725 public int getSectionForPosition(int position) {
1726 // Note: JapaneseContactListIndexer depends on the fact
1727 // this method always returns 0. If you change this,
1728 // please care it too.
1733 public boolean areAllItemsEnabled() {
1734 return mMode != MODE_STREQUENT;
1738 public boolean isEnabled(int position) {
1739 return position != mFrequentSeparatorPos;
1743 public int getCount() {
1744 if (mFrequentSeparatorPos != ListView.INVALID_POSITION) {
1745 return super.getCount() + 1;
1747 return super.getCount();
1751 private int getRealPosition(int pos) {
1752 if (mFrequentSeparatorPos == ListView.INVALID_POSITION) {
1753 // No separator, identity map
1755 } else if (pos <= mFrequentSeparatorPos) {
1756 // Before or at the separator, identity map
1759 // After the separator, remove 1 from the pos to get the real underlying pos
1766 public Object getItem(int pos) {
1767 return super.getItem(getRealPosition(pos));
1771 public long getItemId(int pos) {
1772 return super.getItemId(getRealPosition(pos));