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 com.android.contacts.TextHighlightingAnimation.TextWithHighlighting;
20 import com.android.contacts.model.ContactsSource;
21 import com.android.contacts.model.Sources;
22 import com.android.contacts.ui.ContactsPreferences;
23 import com.android.contacts.ui.ContactsPreferencesActivity;
24 import com.android.contacts.ui.ContactsPreferencesActivity.Prefs;
25 import com.android.contacts.util.AccountSelectionUtil;
26 import com.android.contacts.util.Constants;
28 import android.accounts.Account;
29 import android.app.Activity;
30 import android.app.AlertDialog;
31 import android.app.Dialog;
32 import android.app.ListActivity;
33 import android.app.SearchManager;
34 import android.content.AsyncQueryHandler;
35 import android.content.ContentResolver;
36 import android.content.ContentUris;
37 import android.content.ContentValues;
38 import android.content.Context;
39 import android.content.DialogInterface;
40 import android.content.Intent;
41 import android.content.SharedPreferences;
42 import android.content.UriMatcher;
43 import android.content.res.ColorStateList;
44 import android.content.res.Resources;
45 import android.database.CharArrayBuffer;
46 import android.database.Cursor;
47 import android.database.MatrixCursor;
48 import android.graphics.Bitmap;
49 import android.graphics.BitmapFactory;
50 import android.graphics.Canvas;
51 import android.graphics.Color;
52 import android.graphics.Paint;
53 import android.graphics.Rect;
54 import android.graphics.Typeface;
55 import android.graphics.drawable.BitmapDrawable;
56 import android.graphics.drawable.Drawable;
57 import android.net.Uri;
58 import android.net.Uri.Builder;
59 import android.os.Bundle;
60 import android.os.Parcelable;
61 import android.preference.PreferenceManager;
62 import android.provider.ContactsContract;
63 import android.provider.Settings;
64 import android.provider.Contacts.ContactMethods;
65 import android.provider.Contacts.People;
66 import android.provider.Contacts.PeopleColumns;
67 import android.provider.Contacts.Phones;
68 import android.provider.ContactsContract.ContactCounts;
69 import android.provider.ContactsContract.Contacts;
70 import android.provider.ContactsContract.Data;
71 import android.provider.ContactsContract.Intents;
72 import android.provider.ContactsContract.Presence;
73 import android.provider.ContactsContract.RawContacts;
74 import android.provider.ContactsContract.SearchSnippetColumns;
75 import android.provider.ContactsContract.CommonDataKinds.Email;
76 import android.provider.ContactsContract.CommonDataKinds.Nickname;
77 import android.provider.ContactsContract.CommonDataKinds.Organization;
78 import android.provider.ContactsContract.CommonDataKinds.Phone;
79 import android.provider.ContactsContract.CommonDataKinds.Photo;
80 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
81 import android.provider.ContactsContract.Contacts.AggregationSuggestions;
82 import android.provider.ContactsContract.Intents.Insert;
83 import android.provider.ContactsContract.Intents.UI;
84 import android.telephony.TelephonyManager;
85 import android.text.Editable;
86 import android.text.TextUtils;
87 import android.text.TextWatcher;
88 import android.util.Log;
89 import android.view.ContextMenu;
90 import android.view.ContextThemeWrapper;
91 import android.view.KeyEvent;
92 import android.view.LayoutInflater;
93 import android.view.Menu;
94 import android.view.MenuInflater;
95 import android.view.MenuItem;
96 import android.view.MotionEvent;
97 import android.view.View;
98 import android.view.ViewGroup;
99 import android.view.ContextMenu.ContextMenuInfo;
100 import android.view.View.OnFocusChangeListener;
101 import android.view.View.OnTouchListener;
102 import android.view.inputmethod.EditorInfo;
103 import android.view.inputmethod.InputMethodManager;
104 import android.widget.AbsListView;
105 import android.widget.AdapterView;
106 import android.widget.ArrayAdapter;
107 import android.widget.Filter;
108 import android.widget.ImageView;
109 import android.widget.ListView;
110 import android.widget.QuickContactBadge;
111 import android.widget.ResourceCursorAdapter;
112 import android.widget.SectionIndexer;
113 import android.widget.TextView;
114 import android.widget.Toast;
115 import android.widget.AbsListView.OnScrollListener;
117 import java.lang.ref.WeakReference;
118 import java.util.ArrayList;
119 import java.util.List;
120 import java.util.Random;
123 * Displays a list of contacts. Usually is embedded into the ContactsActivity.
125 @SuppressWarnings("deprecation")
126 public class ContactsListActivity extends ListActivity implements View.OnCreateContextMenuListener,
127 View.OnClickListener, View.OnKeyListener, TextWatcher, TextView.OnEditorActionListener,
128 OnFocusChangeListener, OnTouchListener {
130 public static class JoinContactActivity extends ContactsListActivity {
134 public static class ContactsSearchActivity extends ContactsListActivity {
138 private static final String TAG = "ContactsListActivity";
140 private static final boolean ENABLE_ACTION_ICON_OVERLAYS = true;
142 private static final String LIST_STATE_KEY = "liststate";
143 private static final String SHORTCUT_ACTION_KEY = "shortcutAction";
145 static final int MENU_ITEM_VIEW_CONTACT = 1;
146 static final int MENU_ITEM_CALL = 2;
147 static final int MENU_ITEM_EDIT_BEFORE_CALL = 3;
148 static final int MENU_ITEM_SEND_SMS = 4;
149 static final int MENU_ITEM_SEND_IM = 5;
150 static final int MENU_ITEM_EDIT = 6;
151 static final int MENU_ITEM_DELETE = 7;
152 static final int MENU_ITEM_TOGGLE_STAR = 8;
154 private static final int SUBACTIVITY_NEW_CONTACT = 1;
155 private static final int SUBACTIVITY_VIEW_CONTACT = 2;
156 private static final int SUBACTIVITY_DISPLAY_GROUP = 3;
157 private static final int SUBACTIVITY_SEARCH = 4;
158 private static final int SUBACTIVITY_FILTER = 5;
160 private static final int TEXT_HIGHLIGHTING_ANIMATION_DURATION = 350;
163 * The action for the join contact activity.
165 * Input: extra field {@link #EXTRA_AGGREGATE_ID} is the aggregate ID.
167 * TODO: move to {@link ContactsContract}.
169 public static final String JOIN_AGGREGATE =
170 "com.android.contacts.action.JOIN_AGGREGATE";
173 * Used with {@link #JOIN_AGGREGATE} to give it the target for aggregation.
177 public static final String EXTRA_AGGREGATE_ID =
178 "com.android.contacts.action.AGGREGATE_ID";
181 * Used with {@link #JOIN_AGGREGATE} to give it the name of the aggregation target.
186 public static final String EXTRA_AGGREGATE_NAME =
187 "com.android.contacts.action.AGGREGATE_NAME";
189 public static final String AUTHORITIES_FILTER_KEY = "authorities";
191 private static final Uri CONTACTS_CONTENT_URI_WITH_LETTER_COUNTS =
192 buildSectionIndexerUri(Contacts.CONTENT_URI);
194 /** Mask for picker mode */
195 static final int MODE_MASK_PICKER = 0x80000000;
196 /** Mask for no presence mode */
197 static final int MODE_MASK_NO_PRESENCE = 0x40000000;
198 /** Mask for enabling list filtering */
199 static final int MODE_MASK_NO_FILTER = 0x20000000;
200 /** Mask for having a "create new contact" header in the list */
201 static final int MODE_MASK_CREATE_NEW = 0x10000000;
202 /** Mask for showing photos in the list */
203 static final int MODE_MASK_SHOW_PHOTOS = 0x08000000;
204 /** Mask for hiding additional information e.g. primary phone number in the list */
205 static final int MODE_MASK_NO_DATA = 0x04000000;
206 /** Mask for showing a call button in the list */
207 static final int MODE_MASK_SHOW_CALL_BUTTON = 0x02000000;
208 /** Mask to disable quickcontact (images will show as normal images) */
209 static final int MODE_MASK_DISABLE_QUIKCCONTACT = 0x01000000;
210 /** Mask to show the total number of contacts at the top */
211 static final int MODE_MASK_SHOW_NUMBER_OF_CONTACTS = 0x00800000;
214 static final int MODE_UNKNOWN = 0;
216 static final int MODE_DEFAULT = 4 | MODE_MASK_SHOW_PHOTOS | MODE_MASK_SHOW_NUMBER_OF_CONTACTS;
218 static final int MODE_CUSTOM = 8;
219 /** Show all starred contacts */
220 static final int MODE_STARRED = 20 | MODE_MASK_SHOW_PHOTOS;
221 /** Show frequently contacted contacts */
222 static final int MODE_FREQUENT = 30 | MODE_MASK_SHOW_PHOTOS;
223 /** Show starred and the frequent */
224 static final int MODE_STREQUENT = 35 | MODE_MASK_SHOW_PHOTOS | MODE_MASK_SHOW_CALL_BUTTON;
225 /** Show all contacts and pick them when clicking */
226 static final int MODE_PICK_CONTACT = 40 | MODE_MASK_PICKER | MODE_MASK_SHOW_PHOTOS
227 | MODE_MASK_DISABLE_QUIKCCONTACT;
228 /** Show all contacts as well as the option to create a new one */
229 static final int MODE_PICK_OR_CREATE_CONTACT = 42 | MODE_MASK_PICKER | MODE_MASK_CREATE_NEW
230 | MODE_MASK_SHOW_PHOTOS | MODE_MASK_DISABLE_QUIKCCONTACT;
231 /** Show all people through the legacy provider and pick them when clicking */
232 static final int MODE_LEGACY_PICK_PERSON = 43 | MODE_MASK_PICKER
233 | MODE_MASK_DISABLE_QUIKCCONTACT;
234 /** Show all people through the legacy provider as well as the option to create a new one */
235 static final int MODE_LEGACY_PICK_OR_CREATE_PERSON = 44 | MODE_MASK_PICKER
236 | MODE_MASK_CREATE_NEW | MODE_MASK_DISABLE_QUIKCCONTACT;
237 /** Show all contacts and pick them when clicking, and allow creating a new contact */
238 static final int MODE_INSERT_OR_EDIT_CONTACT = 45 | MODE_MASK_PICKER | MODE_MASK_CREATE_NEW
239 | MODE_MASK_SHOW_PHOTOS | MODE_MASK_DISABLE_QUIKCCONTACT;
240 /** Show all phone numbers and pick them when clicking */
241 static final int MODE_PICK_PHONE = 50 | MODE_MASK_PICKER | MODE_MASK_NO_PRESENCE;
242 /** Show all phone numbers through the legacy provider and pick them when clicking */
243 static final int MODE_LEGACY_PICK_PHONE =
244 51 | MODE_MASK_PICKER | MODE_MASK_NO_PRESENCE | MODE_MASK_NO_FILTER;
245 /** Show all postal addresses and pick them when clicking */
246 static final int MODE_PICK_POSTAL =
247 55 | MODE_MASK_PICKER | MODE_MASK_NO_PRESENCE | MODE_MASK_NO_FILTER;
248 /** Show all postal addresses and pick them when clicking */
249 static final int MODE_LEGACY_PICK_POSTAL =
250 56 | MODE_MASK_PICKER | MODE_MASK_NO_PRESENCE | MODE_MASK_NO_FILTER;
251 static final int MODE_GROUP = 57 | MODE_MASK_SHOW_PHOTOS;
252 /** Run a search query */
253 static final int MODE_QUERY = 60 | MODE_MASK_SHOW_PHOTOS | MODE_MASK_NO_FILTER
254 | MODE_MASK_SHOW_NUMBER_OF_CONTACTS;
255 /** Run a search query in PICK mode, but that still launches to VIEW */
256 static final int MODE_QUERY_PICK_TO_VIEW = 65 | MODE_MASK_SHOW_PHOTOS | MODE_MASK_PICKER
257 | MODE_MASK_SHOW_NUMBER_OF_CONTACTS;
259 /** Show join suggestions followed by an A-Z list */
260 static final int MODE_JOIN_CONTACT = 70 | MODE_MASK_PICKER | MODE_MASK_NO_PRESENCE
261 | MODE_MASK_NO_DATA | MODE_MASK_SHOW_PHOTOS | MODE_MASK_DISABLE_QUIKCCONTACT;
263 /** Run a search query in a PICK mode */
264 static final int MODE_QUERY_PICK = 75 | MODE_MASK_SHOW_PHOTOS | MODE_MASK_NO_FILTER
265 | MODE_MASK_PICKER | MODE_MASK_DISABLE_QUIKCCONTACT | MODE_MASK_SHOW_NUMBER_OF_CONTACTS;
267 /** Run a search query in a PICK_PHONE mode */
268 static final int MODE_QUERY_PICK_PHONE = 80 | MODE_MASK_NO_FILTER | MODE_MASK_PICKER
269 | MODE_MASK_SHOW_NUMBER_OF_CONTACTS;
271 /** Run a search query in PICK mode, but that still launches to EDIT */
272 static final int MODE_QUERY_PICK_TO_EDIT = 85 | MODE_MASK_NO_FILTER | MODE_MASK_SHOW_PHOTOS
273 | MODE_MASK_PICKER | MODE_MASK_SHOW_NUMBER_OF_CONTACTS;
276 * An action used to do perform search while in a contact picker. It is initiated
277 * by the ContactListActivity itself.
279 private static final String ACTION_SEARCH_INTERNAL = "com.android.contacts.INTERNAL_SEARCH";
281 /** Maximum number of suggestions shown for joining aggregates */
282 static final int MAX_SUGGESTIONS = 4;
284 static final String[] CONTACTS_SUMMARY_PROJECTION = new String[] {
286 Contacts.DISPLAY_NAME_PRIMARY, // 1
287 Contacts.DISPLAY_NAME_ALTERNATIVE, // 2
288 Contacts.SORT_KEY_PRIMARY, // 3
289 Contacts.STARRED, // 4
290 Contacts.TIMES_CONTACTED, // 5
291 Contacts.CONTACT_PRESENCE, // 6
292 Contacts.PHOTO_ID, // 7
293 Contacts.LOOKUP_KEY, // 8
294 Contacts.HAS_PHONE_NUMBER, // 9
296 static final String[] CONTACTS_SUMMARY_PROJECTION_FROM_EMAIL = new String[] {
298 Contacts.DISPLAY_NAME_PRIMARY, // 1
299 Contacts.DISPLAY_NAME_ALTERNATIVE, // 2
300 Contacts.SORT_KEY_PRIMARY, // 3
301 Contacts.STARRED, // 4
302 Contacts.TIMES_CONTACTED, // 5
303 Contacts.CONTACT_PRESENCE, // 6
304 Contacts.PHOTO_ID, // 7
305 Contacts.LOOKUP_KEY, // 8
306 // email lookup doesn't included HAS_PHONE_NUMBER in projection
309 static final String[] CONTACTS_SUMMARY_FILTER_PROJECTION = new String[] {
311 Contacts.DISPLAY_NAME_PRIMARY, // 1
312 Contacts.DISPLAY_NAME_ALTERNATIVE, // 2
313 Contacts.SORT_KEY_PRIMARY, // 3
314 Contacts.STARRED, // 4
315 Contacts.TIMES_CONTACTED, // 5
316 Contacts.CONTACT_PRESENCE, // 6
317 Contacts.PHOTO_ID, // 7
318 Contacts.LOOKUP_KEY, // 8
319 Contacts.HAS_PHONE_NUMBER, // 9
320 SearchSnippetColumns.SNIPPET_MIMETYPE, // 10
321 SearchSnippetColumns.SNIPPET_DATA1, // 11
322 SearchSnippetColumns.SNIPPET_DATA4, // 12
325 static final String[] LEGACY_PEOPLE_PROJECTION = new String[] {
327 People.DISPLAY_NAME, // 1
328 People.DISPLAY_NAME, // 2
329 People.DISPLAY_NAME, // 3
331 PeopleColumns.TIMES_CONTACTED, // 5
332 People.PRESENCE_STATUS, // 6
334 static final int SUMMARY_ID_COLUMN_INDEX = 0;
335 static final int SUMMARY_DISPLAY_NAME_PRIMARY_COLUMN_INDEX = 1;
336 static final int SUMMARY_DISPLAY_NAME_ALTERNATIVE_COLUMN_INDEX = 2;
337 static final int SUMMARY_SORT_KEY_PRIMARY_COLUMN_INDEX = 3;
338 static final int SUMMARY_STARRED_COLUMN_INDEX = 4;
339 static final int SUMMARY_TIMES_CONTACTED_COLUMN_INDEX = 5;
340 static final int SUMMARY_PRESENCE_STATUS_COLUMN_INDEX = 6;
341 static final int SUMMARY_PHOTO_ID_COLUMN_INDEX = 7;
342 static final int SUMMARY_LOOKUP_KEY_COLUMN_INDEX = 8;
343 static final int SUMMARY_HAS_PHONE_COLUMN_INDEX = 9;
344 static final int SUMMARY_SNIPPET_MIMETYPE_COLUMN_INDEX = 10;
345 static final int SUMMARY_SNIPPET_DATA1_COLUMN_INDEX = 11;
346 static final int SUMMARY_SNIPPET_DATA4_COLUMN_INDEX = 12;
348 static final String[] PHONES_PROJECTION = new String[] {
353 Phone.DISPLAY_NAME, // 4
354 Phone.CONTACT_ID, // 5
356 static final String[] LEGACY_PHONES_PROJECTION = new String[] {
361 People.DISPLAY_NAME, // 4
363 static final int PHONE_ID_COLUMN_INDEX = 0;
364 static final int PHONE_TYPE_COLUMN_INDEX = 1;
365 static final int PHONE_LABEL_COLUMN_INDEX = 2;
366 static final int PHONE_NUMBER_COLUMN_INDEX = 3;
367 static final int PHONE_DISPLAY_NAME_COLUMN_INDEX = 4;
368 static final int PHONE_CONTACT_ID_COLUMN_INDEX = 5;
370 static final String[] POSTALS_PROJECTION = new String[] {
371 StructuredPostal._ID, //0
372 StructuredPostal.TYPE, //1
373 StructuredPostal.LABEL, //2
374 StructuredPostal.DATA, //3
375 StructuredPostal.DISPLAY_NAME, // 4
377 static final String[] LEGACY_POSTALS_PROJECTION = new String[] {
378 ContactMethods._ID, //0
379 ContactMethods.TYPE, //1
380 ContactMethods.LABEL, //2
381 ContactMethods.DATA, //3
382 People.DISPLAY_NAME, // 4
384 static final String[] RAW_CONTACTS_PROJECTION = new String[] {
386 RawContacts.CONTACT_ID, //1
387 RawContacts.ACCOUNT_TYPE, //2
390 static final int POSTAL_ID_COLUMN_INDEX = 0;
391 static final int POSTAL_TYPE_COLUMN_INDEX = 1;
392 static final int POSTAL_LABEL_COLUMN_INDEX = 2;
393 static final int POSTAL_ADDRESS_COLUMN_INDEX = 3;
394 static final int POSTAL_DISPLAY_NAME_COLUMN_INDEX = 4;
396 private static final int QUERY_TOKEN = 42;
398 static final String KEY_PICKER_MODE = "picker_mode";
400 private ContactItemListAdapter mAdapter;
402 int mMode = MODE_DEFAULT;
404 private QueryHandler mQueryHandler;
405 private boolean mJustCreated;
406 private boolean mSyncEnabled;
407 private Uri mSelectedContactUri;
409 // private boolean mDisplayAll;
410 private boolean mDisplayOnlyPhones;
412 private Uri mGroupUri;
414 private long mQueryAggregateId;
416 private ArrayList<Long> mWritableRawContactIds = new ArrayList<Long>();
417 private int mWritableSourcesCnt;
418 private int mReadOnlySourcesCnt;
421 * Used to keep track of the scroll state of the list.
423 private Parcelable mListState = null;
425 private String mShortcutAction;
428 * Internal query type when in mode {@link #MODE_QUERY_PICK_TO_VIEW}.
430 private int mQueryMode = QUERY_MODE_NONE;
432 private static final int QUERY_MODE_NONE = -1;
433 private static final int QUERY_MODE_MAILTO = 1;
434 private static final int QUERY_MODE_TEL = 2;
436 private boolean mSearchMode;
437 private boolean mShowNumberOfContacts;
439 private boolean mShowSearchSnippets;
441 private String mInitialFilter;
443 private static final String CLAUSE_ONLY_VISIBLE = Contacts.IN_VISIBLE_GROUP + "=1";
444 private static final String CLAUSE_ONLY_PHONES = Contacts.HAS_PHONE_NUMBER + "=1";
447 * In the {@link #MODE_JOIN_CONTACT} determines whether we display a list item with the label
448 * "Show all contacts" or actually show all contacts
450 private boolean mJoinModeShowAllContacts;
453 * The ID of the special item described above.
455 private static final long JOIN_MODE_SHOW_ALL_CONTACTS_ID = -2;
457 // Uri matcher for contact id
458 private static final int CONTACTS_ID = 1001;
459 private static final UriMatcher sContactsIdMatcher;
461 private ContactPhotoLoader mPhotoLoader;
463 final String[] sLookupProjection = new String[] {
468 sContactsIdMatcher = new UriMatcher(UriMatcher.NO_MATCH);
469 sContactsIdMatcher.addURI(ContactsContract.AUTHORITY, "contacts/#", CONTACTS_ID);
472 private class DeleteClickListener implements DialogInterface.OnClickListener {
473 public void onClick(DialogInterface dialog, int which) {
474 getContentResolver().delete(mSelectedContactUri, null, null);
479 * A {@link TextHighlightingAnimation} that redraws just the contact display name in a
482 private static class NameHighlightingAnimation extends TextHighlightingAnimation {
483 private final ListView mListView;
485 private NameHighlightingAnimation(ListView listView, int duration) {
487 this.mListView = listView;
491 * Redraws all visible items of the list corresponding to contacts
494 protected void invalidate() {
495 int childCount = mListView.getChildCount();
496 for (int i = 0; i < childCount; i++) {
497 View listItem = mListView.getChildAt(i);
498 Object tag = listItem.getTag();
499 if (tag instanceof ContactListItemCache) {
500 ((ContactListItemCache)tag).nameView.invalidate();
506 protected void onAnimationStarted() {
507 mListView.setScrollingCacheEnabled(false);
511 protected void onAnimationEnded() {
512 mListView.setScrollingCacheEnabled(true);
516 // The size of a home screen shortcut icon.
517 private int mIconSize;
518 private ContactsPreferences mContactsPrefs;
519 private int mDisplayOrder;
520 private int mSortOrder;
521 private boolean mHighlightWhenScrolling;
522 private TextHighlightingAnimation mHighlightingAnimation;
523 private SearchEditText mSearchEditText;
526 * An approximation of the background color of the pinned header. This color
527 * is used when the pinned header is being pushed up. At that point the header
528 * "fades away". Rather than computing a faded bitmap based on the 9-patch
529 * normally used for the background, we will use a solid color, which will
530 * provide better performance and reduced complexity.
532 private int mPinnedHeaderBackgroundColor;
535 protected void onCreate(Bundle icicle) {
536 super.onCreate(icicle);
538 mIconSize = getResources().getDimensionPixelSize(android.R.dimen.app_icon_size);
539 mContactsPrefs = new ContactsPreferences(this);
540 mPhotoLoader = new ContactPhotoLoader(this, R.drawable.ic_contact_list_picture);
542 // Resolve the intent
543 final Intent intent = getIntent();
545 // Allow the title to be set to a custom String using an extra on the intent
546 String title = intent.getStringExtra(UI.TITLE_EXTRA_KEY);
551 String action = intent.getAction();
552 String component = intent.getComponent().getClassName();
554 // When we get a FILTER_CONTACTS_ACTION, it represents search in the context
555 // of some other action. Let's retrieve the original action to provide proper
556 // context for the search queries.
557 if (UI.FILTER_CONTACTS_ACTION.equals(action)) {
559 mShowSearchSnippets = true;
560 Bundle extras = intent.getExtras();
561 if (extras != null) {
562 mInitialFilter = extras.getString(UI.FILTER_TEXT_EXTRA_KEY);
563 String originalAction =
564 extras.getString(ContactsSearchManager.ORIGINAL_ACTION_EXTRA_KEY);
565 if (originalAction != null) {
566 action = originalAction;
568 String originalComponent =
569 extras.getString(ContactsSearchManager.ORIGINAL_COMPONENT_EXTRA_KEY);
570 if (originalComponent != null) {
571 component = originalComponent;
574 mInitialFilter = null;
578 Log.i(TAG, "Called with action: " + action);
579 mMode = MODE_UNKNOWN;
580 if (UI.LIST_DEFAULT.equals(action) || UI.FILTER_CONTACTS_ACTION.equals(action)) {
581 mMode = MODE_DEFAULT;
582 // When mDefaultMode is true the mode is set in onResume(), since the preferneces
583 // activity may change it whenever this activity isn't running
584 } else if (UI.LIST_GROUP_ACTION.equals(action)) {
586 String groupName = intent.getStringExtra(UI.GROUP_NAME_EXTRA_KEY);
587 if (TextUtils.isEmpty(groupName)) {
591 buildUserGroupUri(groupName);
592 } else if (UI.LIST_ALL_CONTACTS_ACTION.equals(action)) {
594 mDisplayOnlyPhones = false;
595 } else if (UI.LIST_STARRED_ACTION.equals(action)) {
596 mMode = mSearchMode ? MODE_DEFAULT : MODE_STARRED;
597 } else if (UI.LIST_FREQUENT_ACTION.equals(action)) {
598 mMode = mSearchMode ? MODE_DEFAULT : MODE_FREQUENT;
599 } else if (UI.LIST_STREQUENT_ACTION.equals(action)) {
600 mMode = mSearchMode ? MODE_DEFAULT : MODE_STREQUENT;
601 } else if (UI.LIST_CONTACTS_WITH_PHONES_ACTION.equals(action)) {
603 mDisplayOnlyPhones = true;
604 } else if (Intent.ACTION_PICK.equals(action)) {
605 // XXX These should be showing the data from the URI given in
607 final String type = intent.resolveType(this);
608 if (Contacts.CONTENT_TYPE.equals(type)) {
609 mMode = MODE_PICK_CONTACT;
610 } else if (People.CONTENT_TYPE.equals(type)) {
611 mMode = MODE_LEGACY_PICK_PERSON;
612 } else if (Phone.CONTENT_TYPE.equals(type)) {
613 mMode = MODE_PICK_PHONE;
614 } else if (Phones.CONTENT_TYPE.equals(type)) {
615 mMode = MODE_LEGACY_PICK_PHONE;
616 } else if (StructuredPostal.CONTENT_TYPE.equals(type)) {
617 mMode = MODE_PICK_POSTAL;
618 } else if (ContactMethods.CONTENT_POSTAL_TYPE.equals(type)) {
619 mMode = MODE_LEGACY_PICK_POSTAL;
621 } else if (Intent.ACTION_CREATE_SHORTCUT.equals(action)) {
622 if (component.equals("alias.DialShortcut")) {
623 mMode = MODE_PICK_PHONE;
624 mShortcutAction = Intent.ACTION_CALL;
625 setTitle(R.string.callShortcutActivityTitle);
626 } else if (component.equals("alias.MessageShortcut")) {
627 mMode = MODE_PICK_PHONE;
628 mShortcutAction = Intent.ACTION_SENDTO;
629 setTitle(R.string.messageShortcutActivityTitle);
630 } else if (mSearchMode) {
631 mMode = MODE_PICK_CONTACT;
632 mShortcutAction = Intent.ACTION_VIEW;
633 setTitle(R.string.shortcutActivityTitle);
635 mMode = MODE_PICK_OR_CREATE_CONTACT;
636 mShortcutAction = Intent.ACTION_VIEW;
637 setTitle(R.string.shortcutActivityTitle);
639 } else if (Intent.ACTION_GET_CONTENT.equals(action)) {
640 final String type = intent.resolveType(this);
641 if (Contacts.CONTENT_ITEM_TYPE.equals(type)) {
643 mMode = MODE_PICK_CONTACT;
645 mMode = MODE_PICK_OR_CREATE_CONTACT;
647 } else if (Phone.CONTENT_ITEM_TYPE.equals(type)) {
648 mMode = MODE_PICK_PHONE;
649 } else if (Phones.CONTENT_ITEM_TYPE.equals(type)) {
650 mMode = MODE_LEGACY_PICK_PHONE;
651 } else if (StructuredPostal.CONTENT_ITEM_TYPE.equals(type)) {
652 mMode = MODE_PICK_POSTAL;
653 } else if (ContactMethods.CONTENT_POSTAL_ITEM_TYPE.equals(type)) {
654 mMode = MODE_LEGACY_PICK_POSTAL;
655 } else if (People.CONTENT_ITEM_TYPE.equals(type)) {
657 mMode = MODE_LEGACY_PICK_PERSON;
659 mMode = MODE_LEGACY_PICK_OR_CREATE_PERSON;
663 } else if (Intent.ACTION_INSERT_OR_EDIT.equals(action)) {
664 mMode = MODE_INSERT_OR_EDIT_CONTACT;
665 } else if (Intent.ACTION_SEARCH.equals(action)) {
666 // See if the suggestion was clicked with a search action key (call button)
667 if ("call".equals(intent.getStringExtra(SearchManager.ACTION_MSG))) {
668 String query = intent.getStringExtra(SearchManager.QUERY);
669 if (!TextUtils.isEmpty(query)) {
670 Intent newIntent = new Intent(Intent.ACTION_CALL_PRIVILEGED,
671 Uri.fromParts("tel", query, null));
672 startActivity(newIntent);
678 // See if search request has extras to specify query
679 if (intent.hasExtra(Insert.EMAIL)) {
680 mMode = MODE_QUERY_PICK_TO_VIEW;
681 mQueryMode = QUERY_MODE_MAILTO;
682 mInitialFilter = intent.getStringExtra(Insert.EMAIL);
683 } else if (intent.hasExtra(Insert.PHONE)) {
684 mMode = MODE_QUERY_PICK_TO_VIEW;
685 mQueryMode = QUERY_MODE_TEL;
686 mInitialFilter = intent.getStringExtra(Insert.PHONE);
688 // Otherwise handle the more normal search case
690 mShowSearchSnippets = true;
691 mInitialFilter = getIntent().getStringExtra(SearchManager.QUERY);
693 } else if (ACTION_SEARCH_INTERNAL.equals(action)) {
694 String originalAction = null;
695 Bundle extras = intent.getExtras();
696 if (extras != null) {
697 originalAction = extras.getString(ContactsSearchManager.ORIGINAL_ACTION_EXTRA_KEY);
699 mShortcutAction = intent.getStringExtra(SHORTCUT_ACTION_KEY);
701 if (Intent.ACTION_INSERT_OR_EDIT.equals(originalAction)) {
702 mMode = MODE_QUERY_PICK_TO_EDIT;
703 mShowSearchSnippets = true;
704 mInitialFilter = getIntent().getStringExtra(SearchManager.QUERY);
705 } else if (mShortcutAction != null && intent.hasExtra(Insert.PHONE)) {
706 mMode = MODE_QUERY_PICK_PHONE;
707 mQueryMode = QUERY_MODE_TEL;
708 mInitialFilter = intent.getStringExtra(Insert.PHONE);
710 mMode = MODE_QUERY_PICK;
711 mQueryMode = QUERY_MODE_NONE;
712 mShowSearchSnippets = true;
713 mInitialFilter = getIntent().getStringExtra(SearchManager.QUERY);
715 // Since this is the filter activity it receives all intents
716 // dispatched from the SearchManager for security reasons
717 // so we need to re-dispatch from here to the intended target.
718 } else if (Intents.SEARCH_SUGGESTION_CLICKED.equals(action)) {
719 Uri data = intent.getData();
721 if (sContactsIdMatcher.match(data) == CONTACTS_ID) {
722 long contactId = Long.valueOf(data.getLastPathSegment());
723 final Cursor cursor = queryPhoneNumbers(contactId);
724 if (cursor != null) {
725 if (cursor.getCount() == 1 && cursor.moveToFirst()) {
726 int phoneNumberIndex = cursor.getColumnIndex(Phone.NUMBER);
727 String phoneNumber = cursor.getString(phoneNumberIndex);
728 telUri = Uri.parse("tel:" + phoneNumber);
733 // See if the suggestion was clicked with a search action key (call button)
735 if ("call".equals(intent.getStringExtra(SearchManager.ACTION_MSG)) && telUri != null) {
736 newIntent = new Intent(Intent.ACTION_CALL_PRIVILEGED, telUri);
738 newIntent = new Intent(Intent.ACTION_VIEW, data);
740 startActivity(newIntent);
743 } else if (Intents.SEARCH_SUGGESTION_DIAL_NUMBER_CLICKED.equals(action)) {
744 Intent newIntent = new Intent(Intent.ACTION_CALL_PRIVILEGED, intent.getData());
745 startActivity(newIntent);
748 } else if (Intents.SEARCH_SUGGESTION_CREATE_CONTACT_CLICKED.equals(action)) {
749 // TODO actually support this in EditContactActivity.
750 String number = intent.getData().getSchemeSpecificPart();
751 Intent newIntent = new Intent(Intent.ACTION_INSERT, Contacts.CONTENT_URI);
752 newIntent.putExtra(Intents.Insert.PHONE, number);
753 startActivity(newIntent);
758 if (JOIN_AGGREGATE.equals(action)) {
760 mMode = MODE_PICK_CONTACT;
762 mMode = MODE_JOIN_CONTACT;
763 mQueryAggregateId = intent.getLongExtra(EXTRA_AGGREGATE_ID, -1);
764 if (mQueryAggregateId == -1) {
765 Log.e(TAG, "Intent " + action + " is missing required extra: "
766 + EXTRA_AGGREGATE_ID);
767 setResult(RESULT_CANCELED);
773 if (mMode == MODE_UNKNOWN) {
774 mMode = MODE_DEFAULT;
777 if ((mMode & MODE_MASK_SHOW_NUMBER_OF_CONTACTS) != 0 || mSearchMode) {
778 mShowNumberOfContacts = true;
781 if (mMode == MODE_JOIN_CONTACT) {
782 setContentView(R.layout.contacts_list_content_join);
783 TextView blurbView = (TextView)findViewById(R.id.join_contact_blurb);
785 String blurb = getString(R.string.blurbJoinContactDataWith,
786 getContactDisplayName(mQueryAggregateId));
787 blurbView.setText(blurb);
788 mJoinModeShowAllContacts = true;
789 } else if (mSearchMode) {
790 setContentView(R.layout.contacts_search_content);
792 setContentView(R.layout.contacts_list_content);
800 mQueryHandler = new QueryHandler(this);
804 // // Check to see if sync is enabled
805 // final ContentResolver resolver = getContentResolver();
806 // IContentProvider provider = resolver.acquireProvider(Contacts.CONTENT_URI);
807 // if (provider == null) {
808 // // No contacts provider, bail.
814 // ISyncAdapter sa = provider.getSyncAdapter();
815 // mSyncEnabled = sa != null;
816 // } catch (RemoteException e) {
817 // mSyncEnabled = false;
819 // resolver.releaseProvider(provider);
823 private void setupListView() {
824 final ListView list = getListView();
825 final LayoutInflater inflater = getLayoutInflater();
827 mHighlightingAnimation =
828 new NameHighlightingAnimation(list, TEXT_HIGHLIGHTING_ANIMATION_DURATION);
830 // Tell list view to not show dividers. We'll do it ourself so that we can *not* show
831 // them when an A-Z headers is visible.
832 list.setDividerHeight(0);
833 list.setOnCreateContextMenuListener(this);
835 mAdapter = new ContactItemListAdapter(this);
836 setListAdapter(mAdapter);
838 if (list instanceof PinnedHeaderListView && mAdapter.getDisplaySectionHeadersEnabled()) {
839 mPinnedHeaderBackgroundColor =
840 getResources().getColor(R.color.pinned_header_background);
841 PinnedHeaderListView pinnedHeaderList = (PinnedHeaderListView)list;
842 View pinnedHeader = inflater.inflate(R.layout.list_section, list, false);
843 pinnedHeaderList.setPinnedHeaderView(pinnedHeader);
846 list.setOnScrollListener(mAdapter);
847 list.setOnKeyListener(this);
848 list.setOnFocusChangeListener(this);
849 list.setOnTouchListener(this);
851 // We manually save/restore the listview state
852 list.setSaveEnabled(false);
856 * Configures search UI.
858 private void setupSearchView() {
859 mSearchEditText = (SearchEditText)findViewById(R.id.search_src_text);
860 mSearchEditText.addTextChangedListener(this);
861 mSearchEditText.setOnEditorActionListener(this);
862 mSearchEditText.setText(mInitialFilter);
865 private String getContactDisplayName(long contactId) {
866 String contactName = null;
867 Cursor c = getContentResolver().query(
868 ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId),
869 new String[] {Contacts.DISPLAY_NAME}, null, null, null);
871 if (c != null && c.moveToFirst()) {
872 contactName = c.getString(0);
880 if (contactName == null) {
887 private int getSummaryDisplayNameColumnIndex() {
888 if (mDisplayOrder == ContactsContract.Preferences.DISPLAY_ORDER_PRIMARY) {
889 return SUMMARY_DISPLAY_NAME_PRIMARY_COLUMN_INDEX;
891 return SUMMARY_DISPLAY_NAME_ALTERNATIVE_COLUMN_INDEX;
896 public void onClick(View v) {
899 case R.id.call_button: {
900 final int position = (Integer)v.getTag();
901 Cursor c = mAdapter.getCursor();
903 c.moveToPosition(position);
911 private void setEmptyText() {
912 if (mMode == MODE_JOIN_CONTACT || mSearchMode) {
916 TextView empty = (TextView) findViewById(R.id.emptyText);
917 if (mDisplayOnlyPhones) {
918 empty.setText(getText(R.string.noContactsWithPhoneNumbers));
919 } else if (mMode == MODE_STREQUENT || mMode == MODE_STARRED) {
920 empty.setText(getText(R.string.noFavoritesHelpText));
921 } else if (mMode == MODE_QUERY || mMode == MODE_QUERY_PICK
922 || mMode == MODE_QUERY_PICK_PHONE || mMode == MODE_QUERY_PICK_TO_VIEW
923 || mMode == MODE_QUERY_PICK_TO_EDIT) {
924 empty.setText(getText(R.string.noMatchingContacts));
926 boolean hasSim = ((TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE))
931 empty.setText(getText(R.string.noContactsHelpTextWithSync));
933 empty.setText(getText(R.string.noContactsHelpText));
937 empty.setText(getText(R.string.noContactsNoSimHelpTextWithSync));
939 empty.setText(getText(R.string.noContactsNoSimHelpText));
945 private void buildUserGroupUri(String group) {
946 mGroupUri = Uri.withAppendedPath(Contacts.CONTENT_GROUP_URI, group);
950 * Sets the mode when the request is for "default"
952 private void setDefaultMode() {
953 // Load the preferences
954 SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
956 mDisplayOnlyPhones = prefs.getBoolean(Prefs.DISPLAY_ONLY_PHONES,
957 Prefs.DISPLAY_ONLY_PHONES_DEFAULT);
961 protected void onPause() {
967 protected void onResume() {
969 mPhotoLoader.resume();
971 Activity parent = getParent();
973 // Do this before setting the filter. The filter thread relies
974 // on some state that is initialized in setDefaultMode
975 if (mMode == MODE_DEFAULT) {
976 // If we're in default mode we need to possibly reset the mode due to a change
977 // in the preferences activity while we weren't running
981 // See if we were invoked with a filter
983 mSearchEditText.requestFocus();
987 // We need to start a query here the first time the activity is launched, as long
988 // as we aren't doing a filter.
991 mJustCreated = false;
994 private String getTextFilter() {
995 if (mSearchEditText != null) {
996 return mSearchEditText.getText().toString();
1002 protected void onRestart() {
1005 // The cursor was killed off in onStop(), so we need to get a new one here
1006 // We do not perform the query if a filter is set on the list because the
1007 // filter will cause the query to happen anyway
1008 if (TextUtils.isEmpty(getTextFilter())) {
1011 // Run the filtered query on the adapter
1012 ((ContactItemListAdapter) getListAdapter()).onContentChanged();
1017 protected void onSaveInstanceState(Bundle icicle) {
1018 super.onSaveInstanceState(icicle);
1019 // Save list state in the bundle so we can restore it after the QueryHandler has run
1020 if (mList != null) {
1021 icicle.putParcelable(LIST_STATE_KEY, mList.onSaveInstanceState());
1026 protected void onRestoreInstanceState(Bundle icicle) {
1027 super.onRestoreInstanceState(icicle);
1028 // Retrieve list state. This will be applied after the QueryHandler has run
1029 mListState = icicle.getParcelable(LIST_STATE_KEY);
1033 protected void onStop() {
1036 mAdapter.setSuggestionsCursor(null);
1037 mAdapter.changeCursor(null);
1039 if (mMode == MODE_QUERY) {
1040 // Make sure the search box is closed
1041 SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE);
1042 searchManager.stopSearch();
1047 public boolean onCreateOptionsMenu(Menu menu) {
1048 super.onCreateOptionsMenu(menu);
1050 // If Contacts was invoked by another Activity simply as a way of
1051 // picking a contact, don't show the options menu
1052 if ((mMode & MODE_MASK_PICKER) == MODE_MASK_PICKER) {
1056 MenuInflater inflater = getMenuInflater();
1057 inflater.inflate(R.menu.list, menu);
1062 public boolean onPrepareOptionsMenu(Menu menu) {
1063 final boolean defaultMode = (mMode == MODE_DEFAULT);
1064 menu.findItem(R.id.menu_display_groups).setVisible(defaultMode);
1069 public boolean onOptionsItemSelected(MenuItem item) {
1070 switch (item.getItemId()) {
1071 case R.id.menu_display_groups: {
1072 final Intent intent = new Intent(this, ContactsPreferencesActivity.class);
1073 startActivityForResult(intent, SUBACTIVITY_DISPLAY_GROUP);
1076 case R.id.menu_search: {
1077 onSearchRequested();
1080 case R.id.menu_add: {
1081 final Intent intent = new Intent(Intent.ACTION_INSERT, Contacts.CONTENT_URI);
1082 startActivity(intent);
1085 case R.id.menu_import_export: {
1086 displayImportExportDialog();
1089 case R.id.menu_accounts: {
1090 final Intent intent = new Intent(Settings.ACTION_SYNC_SETTINGS);
1091 intent.putExtra(AUTHORITIES_FILTER_KEY, new String[] {
1092 ContactsContract.AUTHORITY
1094 startActivity(intent);
1102 public void startSearch(String initialQuery, boolean selectInitialQuery, Bundle appSearchData,
1103 boolean globalSearch) {
1105 super.startSearch(initialQuery, selectInitialQuery, appSearchData, globalSearch);
1107 if (!mSearchMode && (mMode & MODE_MASK_NO_FILTER) == 0) {
1108 if ((mMode & MODE_MASK_PICKER) != 0) {
1109 ContactsSearchManager.startSearchForResult(this, initialQuery,
1110 SUBACTIVITY_FILTER);
1112 ContactsSearchManager.startSearch(this, initialQuery);
1119 * Performs filtering of the list based on the search query entered in the
1122 protected void onSearchTextChanged() {
1123 // Set the proper empty string
1126 Filter filter = mAdapter.getFilter();
1127 filter.filter(getTextFilter());
1131 * Starts a new activity that will run a search query and display search results.
1133 private void doSearch() {
1134 String query = getTextFilter();
1135 if (TextUtils.isEmpty(query)) {
1139 Intent intent = new Intent(this, ContactsListActivity.class);
1140 Intent originalIntent = getIntent();
1141 Bundle originalExtras = originalIntent.getExtras();
1142 if (originalExtras != null) {
1143 intent.putExtras(originalExtras);
1146 intent.putExtra(SearchManager.QUERY, query);
1147 if ((mMode & MODE_MASK_PICKER) != 0) {
1148 intent.setAction(ACTION_SEARCH_INTERNAL);
1149 intent.putExtra(SHORTCUT_ACTION_KEY, mShortcutAction);
1150 if (mShortcutAction != null) {
1151 if (Intent.ACTION_CALL.equals(mShortcutAction)
1152 || Intent.ACTION_SENDTO.equals(mShortcutAction)) {
1153 intent.putExtra(Insert.PHONE, query);
1156 switch (mQueryMode) {
1157 case QUERY_MODE_MAILTO:
1158 intent.putExtra(Insert.EMAIL, query);
1160 case QUERY_MODE_TEL:
1161 intent.putExtra(Insert.PHONE, query);
1165 startActivityForResult(intent, SUBACTIVITY_SEARCH);
1167 intent.setAction(Intent.ACTION_SEARCH);
1168 startActivity(intent);
1173 protected Dialog onCreateDialog(int id) {
1175 case R.string.import_from_sim:
1176 case R.string.import_from_sdcard: {
1177 return AccountSelectionUtil.getSelectAccountDialog(this, id);
1179 case R.id.dialog_sdcard_not_found: {
1180 return new AlertDialog.Builder(this)
1181 .setTitle(R.string.no_sdcard_title)
1182 .setIcon(android.R.drawable.ic_dialog_alert)
1183 .setMessage(R.string.no_sdcard_message)
1184 .setPositiveButton(android.R.string.ok, null).create();
1186 case R.id.dialog_delete_contact_confirmation: {
1187 return new AlertDialog.Builder(this)
1188 .setTitle(R.string.deleteConfirmation_title)
1189 .setIcon(android.R.drawable.ic_dialog_alert)
1190 .setMessage(R.string.deleteConfirmation)
1191 .setNegativeButton(android.R.string.cancel, null)
1192 .setPositiveButton(android.R.string.ok,
1193 new DeleteClickListener()).create();
1195 case R.id.dialog_readonly_contact_hide_confirmation: {
1196 return new AlertDialog.Builder(this)
1197 .setTitle(R.string.deleteConfirmation_title)
1198 .setIcon(android.R.drawable.ic_dialog_alert)
1199 .setMessage(R.string.readOnlyContactWarning)
1200 .setNegativeButton(android.R.string.cancel, null)
1201 .setPositiveButton(android.R.string.ok,
1202 new DeleteClickListener()).create();
1204 case R.id.dialog_readonly_contact_delete_confirmation: {
1205 return new AlertDialog.Builder(this)
1206 .setTitle(R.string.deleteConfirmation_title)
1207 .setIcon(android.R.drawable.ic_dialog_alert)
1208 .setMessage(R.string.readOnlyContactDeleteConfirmation)
1209 .setNegativeButton(android.R.string.cancel, null)
1210 .setPositiveButton(android.R.string.ok,
1211 new DeleteClickListener()).create();
1213 case R.id.dialog_multiple_contact_delete_confirmation: {
1214 return new AlertDialog.Builder(this)
1215 .setTitle(R.string.deleteConfirmation_title)
1216 .setIcon(android.R.drawable.ic_dialog_alert)
1217 .setMessage(R.string.multipleContactDeleteConfirmation)
1218 .setNegativeButton(android.R.string.cancel, null)
1219 .setPositiveButton(android.R.string.ok,
1220 new DeleteClickListener()).create();
1222 case R.id.dialog_share_confirmation: {
1223 return new AlertDialog.Builder(this)
1224 .setTitle(R.string.confirm_share_visible_contacts_title)
1225 .setMessage(getString(R.string.confirm_share_visible_contacts_message))
1226 .setPositiveButton(android.R.string.ok,
1227 new DialogInterface.OnClickListener() {
1228 public void onClick(DialogInterface dialog, int which) {
1229 if (which == DialogInterface.BUTTON_POSITIVE) {
1230 doShareVisibleContacts();
1236 return super.onCreateDialog(id);
1240 * Create a {@link Dialog} that allows the user to pick from a bulk import
1241 * or bulk export task across all contacts.
1243 private void displayImportExportDialog() {
1244 // Wrap our context to inflate list items using correct theme
1245 final Context dialogContext = new ContextThemeWrapper(this, android.R.style.Theme_Light);
1246 final Resources res = dialogContext.getResources();
1247 final LayoutInflater dialogInflater = (LayoutInflater)dialogContext
1248 .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
1250 // Adapter that shows a list of string resources
1251 final ArrayAdapter<Integer> adapter = new ArrayAdapter<Integer>(this,
1252 android.R.layout.simple_list_item_1) {
1254 public View getView(int position, View convertView, ViewGroup parent) {
1255 if (convertView == null) {
1256 convertView = dialogInflater.inflate(android.R.layout.simple_list_item_1,
1260 final int resId = this.getItem(position);
1261 ((TextView)convertView).setText(resId);
1266 if (TelephonyManager.getDefault().hasIccCard()) {
1267 adapter.add(R.string.import_from_sim);
1269 if (res.getBoolean(R.bool.config_allow_import_from_sdcard)) {
1270 adapter.add(R.string.import_from_sdcard);
1272 if (res.getBoolean(R.bool.config_allow_export_to_sdcard)) {
1273 adapter.add(R.string.export_to_sdcard);
1275 if (res.getBoolean(R.bool.config_allow_share_visible_contacts)) {
1276 adapter.add(R.string.share_visible_contacts);
1279 final DialogInterface.OnClickListener clickListener =
1280 new DialogInterface.OnClickListener() {
1281 public void onClick(DialogInterface dialog, int which) {
1284 final int resId = adapter.getItem(which);
1286 case R.string.import_from_sim:
1287 case R.string.import_from_sdcard: {
1288 handleImportRequest(resId);
1291 case R.string.export_to_sdcard: {
1292 Context context = ContactsListActivity.this;
1293 Intent exportIntent = new Intent(context, ExportVCardActivity.class);
1294 context.startActivity(exportIntent);
1297 case R.string.share_visible_contacts: {
1298 showDialog(R.id.dialog_share_confirmation);
1302 Log.e(TAG, "Unexpected resource: " +
1303 getResources().getResourceEntryName(resId));
1309 new AlertDialog.Builder(this)
1310 .setTitle(R.string.dialog_import_export)
1311 .setNegativeButton(android.R.string.cancel, null)
1312 .setSingleChoiceItems(adapter, -1, clickListener)
1316 private void doShareVisibleContacts() {
1317 final Cursor cursor = getContentResolver().query(Contacts.CONTENT_URI,
1318 sLookupProjection, getContactSelection(), null, null);
1320 if (!cursor.moveToFirst()) {
1321 Toast.makeText(this, R.string.share_error, Toast.LENGTH_SHORT).show();
1325 ArrayList<Uri> uriList = new ArrayList<Uri>();
1326 for (;!cursor.isAfterLast(); cursor.moveToNext()) {
1327 uriList.add(Uri.withAppendedPath(
1328 Contacts.CONTENT_VCARD_URI,
1329 cursor.getString(0)));
1332 final Intent intent = new Intent(Intent.ACTION_SEND_MULTIPLE);
1333 intent.setType(Contacts.CONTENT_VCARD_TYPE);
1334 intent.putExtra(Intent.EXTRA_STREAM, uriList);
1335 startActivity(intent);
1341 private void handleImportRequest(int resId) {
1342 // There's three possibilities:
1343 // - more than one accounts -> ask the user
1344 // - just one account -> use the account without asking the user
1345 // - no account -> use phone-local storage without asking the user
1346 final Sources sources = Sources.getInstance(this);
1347 final List<Account> accountList = sources.getAccounts(true);
1348 final int size = accountList.size();
1354 AccountSelectionUtil.doImport(this, resId, (size == 1 ? accountList.get(0) : null));
1358 protected void onActivityResult(int requestCode, int resultCode, Intent data) {
1359 switch (requestCode) {
1360 case SUBACTIVITY_NEW_CONTACT:
1361 if (resultCode == RESULT_OK) {
1362 returnPickerResult(null, data.getStringExtra(Intent.EXTRA_SHORTCUT_NAME),
1367 case SUBACTIVITY_VIEW_CONTACT:
1368 if (resultCode == RESULT_OK) {
1369 mAdapter.notifyDataSetChanged();
1373 case SUBACTIVITY_DISPLAY_GROUP:
1374 // Mark as just created so we re-run the view query
1375 mJustCreated = true;
1378 case SUBACTIVITY_FILTER:
1379 case SUBACTIVITY_SEARCH:
1380 // Pass through results of filter or search UI
1381 if (resultCode == RESULT_OK) {
1382 setResult(RESULT_OK, data);
1390 public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) {
1391 // If Contacts was invoked by another Activity simply as a way of
1392 // picking a contact, don't show the context menu
1393 if ((mMode & MODE_MASK_PICKER) == MODE_MASK_PICKER) {
1397 AdapterView.AdapterContextMenuInfo info;
1399 info = (AdapterView.AdapterContextMenuInfo) menuInfo;
1400 } catch (ClassCastException e) {
1401 Log.e(TAG, "bad menuInfo", e);
1405 Cursor cursor = (Cursor) getListAdapter().getItem(info.position);
1406 if (cursor == null) {
1407 // For some reason the requested item isn't available, do nothing
1411 Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, id);
1412 long rawContactId = ContactsUtils.queryForRawContactId(getContentResolver(), id);
1413 Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
1415 // Setup the menu header
1416 menu.setHeaderTitle(cursor.getString(getSummaryDisplayNameColumnIndex()));
1418 // View contact details
1419 menu.add(0, MENU_ITEM_VIEW_CONTACT, 0, R.string.menu_viewContact)
1420 .setIntent(new Intent(Intent.ACTION_VIEW, contactUri));
1422 if (cursor.getInt(SUMMARY_HAS_PHONE_COLUMN_INDEX) != 0) {
1424 menu.add(0, MENU_ITEM_CALL, 0,
1425 getString(R.string.menu_call));
1427 menu.add(0, MENU_ITEM_SEND_SMS, 0, getString(R.string.menu_sendSMS));
1431 int starState = cursor.getInt(SUMMARY_STARRED_COLUMN_INDEX);
1432 if (starState == 0) {
1433 menu.add(0, MENU_ITEM_TOGGLE_STAR, 0, R.string.menu_addStar);
1435 menu.add(0, MENU_ITEM_TOGGLE_STAR, 0, R.string.menu_removeStar);
1439 menu.add(0, MENU_ITEM_EDIT, 0, R.string.menu_editContact)
1440 .setIntent(new Intent(Intent.ACTION_EDIT, rawContactUri));
1441 menu.add(0, MENU_ITEM_DELETE, 0, R.string.menu_deleteContact);
1445 public boolean onContextItemSelected(MenuItem item) {
1446 AdapterView.AdapterContextMenuInfo info;
1448 info = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo();
1449 } catch (ClassCastException e) {
1450 Log.e(TAG, "bad menuInfo", e);
1454 Cursor cursor = (Cursor) getListAdapter().getItem(info.position);
1456 switch (item.getItemId()) {
1457 case MENU_ITEM_TOGGLE_STAR: {
1459 ContentValues values = new ContentValues(1);
1460 values.put(Contacts.STARRED, cursor.getInt(SUMMARY_STARRED_COLUMN_INDEX) == 0 ? 1 : 0);
1461 final Uri selectedUri = this.getContactUri(info.position);
1462 getContentResolver().update(selectedUri, values, null, null);
1466 case MENU_ITEM_CALL: {
1467 callContact(cursor);
1471 case MENU_ITEM_SEND_SMS: {
1476 case MENU_ITEM_DELETE: {
1477 mSelectedContactUri = getContactUri(info.position);
1483 return super.onContextItemSelected(item);
1488 * Event handler for the use case where the user starts typing without
1489 * bringing up the search UI first.
1491 public boolean onKey(View v, int keyCode, KeyEvent event) {
1492 if (!mSearchMode && (mMode & MODE_MASK_NO_FILTER) == 0) {
1493 int unicodeChar = event.getUnicodeChar();
1494 if (unicodeChar != 0) {
1495 startSearch(new String(new int[]{unicodeChar}, 0, 1), false, null, false);
1503 * Event handler for search UI.
1505 public void afterTextChanged(Editable s) {
1506 onSearchTextChanged();
1509 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
1512 public void onTextChanged(CharSequence s, int start, int before, int count) {
1516 * Event handler for search UI.
1518 public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
1519 if (actionId == EditorInfo.IME_ACTION_DONE) {
1521 if (TextUtils.isEmpty(getTextFilter())) {
1530 public boolean onKeyDown(int keyCode, KeyEvent event) {
1532 case KeyEvent.KEYCODE_CALL: {
1533 if (callSelection()) {
1539 case KeyEvent.KEYCODE_DEL: {
1540 final int position = getListView().getSelectedItemPosition();
1541 if (position != ListView.INVALID_POSITION) {
1542 mSelectedContactUri = getContactUri(position);
1550 return super.onKeyDown(keyCode, event);
1554 * Prompt the user before deleting the given {@link Contacts} entry.
1556 protected void doContactDelete() {
1557 mReadOnlySourcesCnt = 0;
1558 mWritableSourcesCnt = 0;
1559 mWritableRawContactIds.clear();
1561 if (mSelectedContactUri != null) {
1562 Sources sources = Sources.getInstance(ContactsListActivity.this);
1563 Cursor c = getContentResolver().query(RawContacts.CONTENT_URI, RAW_CONTACTS_PROJECTION,
1564 RawContacts.CONTACT_ID + "=" + ContentUris.parseId(mSelectedContactUri), null,
1568 while (c.moveToNext()) {
1569 final String accountType = c.getString(2);
1570 final long rawContactId = c.getLong(0);
1571 ContactsSource contactsSource = sources.getInflatedSource(accountType,
1572 ContactsSource.LEVEL_SUMMARY);
1573 if (contactsSource != null && contactsSource.readOnly) {
1574 mReadOnlySourcesCnt += 1;
1576 mWritableSourcesCnt += 1;
1577 mWritableRawContactIds.add(rawContactId);
1585 if (mReadOnlySourcesCnt > 0 && mWritableSourcesCnt > 0) {
1586 showDialog(R.id.dialog_readonly_contact_delete_confirmation);
1587 } else if (mReadOnlySourcesCnt > 0 && mWritableSourcesCnt == 0) {
1588 showDialog(R.id.dialog_readonly_contact_hide_confirmation);
1589 } else if (mReadOnlySourcesCnt == 0 && mWritableSourcesCnt > 1) {
1590 showDialog(R.id.dialog_multiple_contact_delete_confirmation);
1592 showDialog(R.id.dialog_delete_contact_confirmation);
1598 * Dismisses the soft keyboard when the list takes focus.
1600 public void onFocusChange(View view, boolean hasFocus) {
1601 if (view == getListView() && hasFocus) {
1607 * Dismisses the soft keyboard when the list takes focus.
1609 public boolean onTouch(View view, MotionEvent event) {
1610 if (view == getListView()) {
1617 * Dismisses the search UI along with the keyboard if the filter text is empty.
1619 public boolean onKeyPreIme(int keyCode, KeyEvent event) {
1620 if (mSearchMode && keyCode == KeyEvent.KEYCODE_BACK && TextUtils.isEmpty(getTextFilter())) {
1629 protected void onListItemClick(ListView l, View v, int position, long id) {
1632 if (mSearchMode && mAdapter.isSearchAllContactsItemPosition(position)) {
1634 } else if (mMode == MODE_INSERT_OR_EDIT_CONTACT || mMode == MODE_QUERY_PICK_TO_EDIT) {
1636 if (position == 0 && !mSearchMode && mMode != MODE_QUERY_PICK_TO_EDIT) {
1637 intent = new Intent(Intent.ACTION_INSERT, Contacts.CONTENT_URI);
1639 intent = new Intent(Intent.ACTION_EDIT, getSelectedUri(position));
1641 intent.setFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
1642 Bundle extras = getIntent().getExtras();
1643 if (extras != null) {
1644 intent.putExtras(extras);
1646 intent.putExtra(KEY_PICKER_MODE, (mMode & MODE_MASK_PICKER) == MODE_MASK_PICKER);
1648 startActivity(intent);
1650 } else if ((mMode & MODE_MASK_CREATE_NEW) == MODE_MASK_CREATE_NEW
1652 Intent newContact = new Intent(Intents.Insert.ACTION, Contacts.CONTENT_URI);
1653 startActivityForResult(newContact, SUBACTIVITY_NEW_CONTACT);
1654 } else if (mMode == MODE_JOIN_CONTACT && id == JOIN_MODE_SHOW_ALL_CONTACTS_ID) {
1655 mJoinModeShowAllContacts = false;
1657 } else if (id > 0) {
1658 final Uri uri = getSelectedUri(position);
1659 if ((mMode & MODE_MASK_PICKER) == 0) {
1660 final Intent intent = new Intent(Intent.ACTION_VIEW, uri);
1661 startActivityForResult(intent, SUBACTIVITY_VIEW_CONTACT);
1662 } else if (mMode == MODE_JOIN_CONTACT) {
1663 returnPickerResult(null, null, uri);
1664 } else if (mMode == MODE_QUERY_PICK_TO_VIEW) {
1665 // Started with query that should launch to view contact
1666 final Intent intent = new Intent(Intent.ACTION_VIEW, uri);
1667 startActivity(intent);
1669 } else if (mMode == MODE_PICK_PHONE || mMode == MODE_QUERY_PICK_PHONE) {
1670 Cursor c = (Cursor) mAdapter.getItem(position);
1671 returnPickerResult(c, c.getString(PHONE_DISPLAY_NAME_COLUMN_INDEX), uri);
1672 } else if ((mMode & MODE_MASK_PICKER) != 0) {
1673 Cursor c = (Cursor) mAdapter.getItem(position);
1674 returnPickerResult(c, c.getString(getSummaryDisplayNameColumnIndex()), uri);
1675 } else if (mMode == MODE_PICK_POSTAL
1676 || mMode == MODE_LEGACY_PICK_POSTAL
1677 || mMode == MODE_LEGACY_PICK_PHONE) {
1678 returnPickerResult(null, null, uri);
1685 private void hideSoftKeyboard() {
1686 // Hide soft keyboard, if visible
1687 InputMethodManager inputMethodManager = (InputMethodManager)
1688 getSystemService(Context.INPUT_METHOD_SERVICE);
1689 inputMethodManager.hideSoftInputFromWindow(mList.getWindowToken(), 0);
1693 * @param selectedUri In most cases, this should be a lookup {@link Uri}, possibly
1694 * generated through {@link Contacts#getLookupUri(long, String)}.
1696 private void returnPickerResult(Cursor c, String name, Uri selectedUri) {
1697 final Intent intent = new Intent();
1699 if (mShortcutAction != null) {
1700 Intent shortcutIntent;
1701 if (Intent.ACTION_VIEW.equals(mShortcutAction)) {
1702 // This is a simple shortcut to view a contact.
1703 shortcutIntent = new Intent(ContactsContract.QuickContact.ACTION_QUICK_CONTACT);
1704 shortcutIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
1705 Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
1707 shortcutIntent.setData(selectedUri);
1708 shortcutIntent.putExtra(ContactsContract.QuickContact.EXTRA_MODE,
1709 ContactsContract.QuickContact.MODE_LARGE);
1710 shortcutIntent.putExtra(ContactsContract.QuickContact.EXTRA_EXCLUDE_MIMES,
1713 final Bitmap icon = framePhoto(loadContactPhoto(selectedUri, null));
1715 intent.putExtra(Intent.EXTRA_SHORTCUT_ICON, scaleToAppIconSize(icon));
1717 intent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE,
1718 Intent.ShortcutIconResource.fromContext(this,
1719 R.drawable.ic_launcher_shortcut_contact));
1722 // This is a direct dial or sms shortcut.
1723 String number = c.getString(PHONE_NUMBER_COLUMN_INDEX);
1724 int type = c.getInt(PHONE_TYPE_COLUMN_INDEX);
1727 if (Intent.ACTION_CALL.equals(mShortcutAction)) {
1728 scheme = Constants.SCHEME_TEL;
1729 resid = R.drawable.badge_action_call;
1731 scheme = Constants.SCHEME_SMSTO;
1732 resid = R.drawable.badge_action_sms;
1735 // Make the URI a direct tel: URI so that it will always continue to work
1736 Uri phoneUri = Uri.fromParts(scheme, number, null);
1737 shortcutIntent = new Intent(mShortcutAction, phoneUri);
1739 intent.putExtra(Intent.EXTRA_SHORTCUT_ICON,
1740 generatePhoneNumberIcon(selectedUri, type, resid));
1742 shortcutIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
1743 intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent);
1744 intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, name);
1745 setResult(RESULT_OK, intent);
1747 intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, name);
1748 setResult(RESULT_OK, intent.setData(selectedUri));
1753 private Bitmap framePhoto(Bitmap photo) {
1754 final Resources r = getResources();
1755 final Drawable frame = r.getDrawable(com.android.internal.R.drawable.quickcontact_badge);
1757 final int width = r.getDimensionPixelSize(R.dimen.contact_shortcut_frame_width);
1758 final int height = r.getDimensionPixelSize(R.dimen.contact_shortcut_frame_height);
1760 frame.setBounds(0, 0, width, height);
1762 final Rect padding = new Rect();
1763 frame.getPadding(padding);
1765 final Rect source = new Rect(0, 0, photo.getWidth(), photo.getHeight());
1766 final Rect destination = new Rect(padding.left, padding.top,
1767 width - padding.right, height - padding.bottom);
1769 final int d = Math.max(width, height);
1770 final Bitmap b = Bitmap.createBitmap(d, d, Bitmap.Config.ARGB_8888);
1771 final Canvas c = new Canvas(b);
1773 c.translate((d - width) / 2.0f, (d - height) / 2.0f);
1775 c.drawBitmap(photo, source, destination, new Paint(Paint.FILTER_BITMAP_FLAG));
1781 * Generates a phone number shortcut icon. Adds an overlay describing the type of the phone
1782 * number, and if there is a photo also adds the call action icon.
1784 * @param lookupUri The person the phone number belongs to
1785 * @param type The type of the phone number
1786 * @param actionResId The ID for the action resource
1787 * @return The bitmap for the icon
1789 private Bitmap generatePhoneNumberIcon(Uri lookupUri, int type, int actionResId) {
1790 final Resources r = getResources();
1791 boolean drawPhoneOverlay = true;
1792 final float scaleDensity = getResources().getDisplayMetrics().scaledDensity;
1794 Bitmap photo = loadContactPhoto(lookupUri, null);
1795 if (photo == null) {
1796 // If there isn't a photo use the generic phone action icon instead
1797 Bitmap phoneIcon = getPhoneActionIcon(r, actionResId);
1798 if (phoneIcon != null) {
1800 drawPhoneOverlay = false;
1806 // Setup the drawing classes
1807 Bitmap icon = createShortcutBitmap();
1808 Canvas canvas = new Canvas(icon);
1810 // Copy in the photo
1811 Paint photoPaint = new Paint();
1812 photoPaint.setDither(true);
1813 photoPaint.setFilterBitmap(true);
1814 Rect src = new Rect(0,0, photo.getWidth(),photo.getHeight());
1815 Rect dst = new Rect(0,0, mIconSize, mIconSize);
1816 canvas.drawBitmap(photo, src, dst, photoPaint);
1818 // Create an overlay for the phone number type
1819 String overlay = null;
1821 case Phone.TYPE_HOME:
1822 overlay = getString(R.string.type_short_home);
1825 case Phone.TYPE_MOBILE:
1826 overlay = getString(R.string.type_short_mobile);
1829 case Phone.TYPE_WORK:
1830 overlay = getString(R.string.type_short_work);
1833 case Phone.TYPE_PAGER:
1834 overlay = getString(R.string.type_short_pager);
1837 case Phone.TYPE_OTHER:
1838 overlay = getString(R.string.type_short_other);
1841 if (overlay != null) {
1842 Paint textPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DEV_KERN_TEXT_FLAG);
1843 textPaint.setTextSize(20.0f * scaleDensity);
1844 textPaint.setTypeface(Typeface.DEFAULT_BOLD);
1845 textPaint.setColor(r.getColor(R.color.textColorIconOverlay));
1846 textPaint.setShadowLayer(3f, 1, 1, r.getColor(R.color.textColorIconOverlayShadow));
1847 canvas.drawText(overlay, 2 * scaleDensity, 16 * scaleDensity, textPaint);
1850 // Draw the phone action icon as an overlay
1851 if (ENABLE_ACTION_ICON_OVERLAYS && drawPhoneOverlay) {
1852 Bitmap phoneIcon = getPhoneActionIcon(r, actionResId);
1853 if (phoneIcon != null) {
1854 src.set(0, 0, phoneIcon.getWidth(), phoneIcon.getHeight());
1855 int iconWidth = icon.getWidth();
1856 dst.set(iconWidth - ((int) (20 * scaleDensity)), -1,
1857 iconWidth, ((int) (19 * scaleDensity)));
1858 canvas.drawBitmap(phoneIcon, src, dst, photoPaint);
1865 private Bitmap scaleToAppIconSize(Bitmap photo) {
1866 // Setup the drawing classes
1867 Bitmap icon = createShortcutBitmap();
1868 Canvas canvas = new Canvas(icon);
1870 // Copy in the photo
1871 Paint photoPaint = new Paint();
1872 photoPaint.setDither(true);
1873 photoPaint.setFilterBitmap(true);
1874 Rect src = new Rect(0,0, photo.getWidth(),photo.getHeight());
1875 Rect dst = new Rect(0,0, mIconSize, mIconSize);
1876 canvas.drawBitmap(photo, src, dst, photoPaint);
1881 private Bitmap createShortcutBitmap() {
1882 return Bitmap.createBitmap(mIconSize, mIconSize, Bitmap.Config.ARGB_8888);
1886 * Returns the icon for the phone call action.
1888 * @param r The resources to load the icon from
1889 * @param resId The resource ID to load
1890 * @return the icon for the phone call action
1892 private Bitmap getPhoneActionIcon(Resources r, int resId) {
1893 Drawable phoneIcon = r.getDrawable(resId);
1894 if (phoneIcon instanceof BitmapDrawable) {
1895 BitmapDrawable bd = (BitmapDrawable) phoneIcon;
1896 return bd.getBitmap();
1902 private Uri getUriToQuery() {
1904 case MODE_JOIN_CONTACT:
1905 return getJoinSuggestionsUri(null);
1908 return Contacts.CONTENT_URI;
1911 case MODE_INSERT_OR_EDIT_CONTACT:
1912 case MODE_PICK_CONTACT:
1913 case MODE_PICK_OR_CREATE_CONTACT:{
1914 return CONTACTS_CONTENT_URI_WITH_LETTER_COUNTS;
1916 case MODE_STREQUENT: {
1917 return Contacts.CONTENT_STREQUENT_URI;
1919 case MODE_LEGACY_PICK_PERSON:
1920 case MODE_LEGACY_PICK_OR_CREATE_PERSON: {
1921 return People.CONTENT_URI;
1923 case MODE_PICK_PHONE: {
1924 return buildSectionIndexerUri(Phone.CONTENT_URI);
1926 case MODE_LEGACY_PICK_PHONE: {
1927 return Phones.CONTENT_URI;
1929 case MODE_PICK_POSTAL: {
1930 return buildSectionIndexerUri(StructuredPostal.CONTENT_URI);
1932 case MODE_LEGACY_PICK_POSTAL: {
1933 return ContactMethods.CONTENT_URI;
1935 case MODE_QUERY_PICK_TO_VIEW: {
1936 if (mQueryMode == QUERY_MODE_MAILTO) {
1937 return Uri.withAppendedPath(Email.CONTENT_FILTER_URI,
1938 Uri.encode(mInitialFilter));
1939 } else if (mQueryMode == QUERY_MODE_TEL) {
1940 return Uri.withAppendedPath(Phone.CONTENT_FILTER_URI,
1941 Uri.encode(mInitialFilter));
1943 return CONTACTS_CONTENT_URI_WITH_LETTER_COUNTS;
1946 case MODE_QUERY_PICK:
1947 case MODE_QUERY_PICK_TO_EDIT: {
1948 return getContactFilterUri(mInitialFilter);
1950 case MODE_QUERY_PICK_PHONE: {
1951 return Uri.withAppendedPath(Phone.CONTENT_FILTER_URI,
1952 Uri.encode(mInitialFilter));
1958 throw new IllegalStateException("Can't generate URI: Unsupported Mode.");
1964 * Build the {@link Contacts#CONTENT_LOOKUP_URI} for the given
1965 * {@link ListView} position, using {@link #mAdapter}.
1967 private Uri getContactUri(int position) {
1968 if (position == ListView.INVALID_POSITION) {
1969 throw new IllegalArgumentException("Position not in list bounds");
1972 final Cursor cursor = (Cursor)mAdapter.getItem(position);
1974 case MODE_LEGACY_PICK_PERSON:
1975 case MODE_LEGACY_PICK_OR_CREATE_PERSON: {
1976 final long personId = cursor.getLong(SUMMARY_ID_COLUMN_INDEX);
1977 return ContentUris.withAppendedId(People.CONTENT_URI, personId);
1981 // Build and return soft, lookup reference
1982 final long contactId = cursor.getLong(SUMMARY_ID_COLUMN_INDEX);
1983 final String lookupKey = cursor.getString(SUMMARY_LOOKUP_KEY_COLUMN_INDEX);
1984 return Contacts.getLookupUri(contactId, lookupKey);
1990 * Build the {@link Uri} for the given {@link ListView} position, which can
1991 * be used as result when in {@link #MODE_MASK_PICKER} mode.
1993 private Uri getSelectedUri(int position) {
1994 if (position == ListView.INVALID_POSITION) {
1995 throw new IllegalArgumentException("Position not in list bounds");
1998 final long id = mAdapter.getItemId(position);
2000 case MODE_LEGACY_PICK_PERSON:
2001 case MODE_LEGACY_PICK_OR_CREATE_PERSON: {
2002 return ContentUris.withAppendedId(People.CONTENT_URI, id);
2004 case MODE_PICK_PHONE:
2005 case MODE_QUERY_PICK_PHONE: {
2006 return ContentUris.withAppendedId(Data.CONTENT_URI, id);
2008 case MODE_LEGACY_PICK_PHONE: {
2009 return ContentUris.withAppendedId(Phones.CONTENT_URI, id);
2011 case MODE_PICK_POSTAL: {
2012 return ContentUris.withAppendedId(Data.CONTENT_URI, id);
2014 case MODE_LEGACY_PICK_POSTAL: {
2015 return ContentUris.withAppendedId(ContactMethods.CONTENT_URI, id);
2018 return getContactUri(position);
2023 String[] getProjectionForQuery() {
2025 case MODE_JOIN_CONTACT:
2026 case MODE_STREQUENT:
2030 case MODE_INSERT_OR_EDIT_CONTACT:
2032 case MODE_PICK_CONTACT:
2033 case MODE_PICK_OR_CREATE_CONTACT: {
2035 ? CONTACTS_SUMMARY_FILTER_PROJECTION
2036 : CONTACTS_SUMMARY_PROJECTION;
2039 case MODE_QUERY_PICK:
2040 case MODE_QUERY_PICK_TO_EDIT: {
2041 return CONTACTS_SUMMARY_FILTER_PROJECTION;
2043 case MODE_LEGACY_PICK_PERSON:
2044 case MODE_LEGACY_PICK_OR_CREATE_PERSON: {
2045 return LEGACY_PEOPLE_PROJECTION ;
2047 case MODE_QUERY_PICK_PHONE:
2048 case MODE_PICK_PHONE: {
2049 return PHONES_PROJECTION;
2051 case MODE_LEGACY_PICK_PHONE: {
2052 return LEGACY_PHONES_PROJECTION;
2054 case MODE_PICK_POSTAL: {
2055 return POSTALS_PROJECTION;
2057 case MODE_LEGACY_PICK_POSTAL: {
2058 return LEGACY_POSTALS_PROJECTION;
2060 case MODE_QUERY_PICK_TO_VIEW: {
2061 if (mQueryMode == QUERY_MODE_MAILTO) {
2062 return CONTACTS_SUMMARY_PROJECTION_FROM_EMAIL;
2063 } else if (mQueryMode == QUERY_MODE_TEL) {
2064 return PHONES_PROJECTION;
2070 // Default to normal aggregate projection
2071 return CONTACTS_SUMMARY_PROJECTION;
2074 private Bitmap loadContactPhoto(Uri selectedUri, BitmapFactory.Options options) {
2075 Uri contactUri = null;
2076 if (Contacts.CONTENT_ITEM_TYPE.equals(getContentResolver().getType(selectedUri))) {
2077 // TODO we should have a "photo" directory under the lookup URI itself
2078 contactUri = Contacts.lookupContact(getContentResolver(), selectedUri);
2081 Cursor cursor = getContentResolver().query(selectedUri,
2082 new String[] { Data.CONTACT_ID }, null, null, null);
2084 if (cursor != null && cursor.moveToFirst()) {
2085 final long contactId = cursor.getLong(0);
2086 contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
2089 if (cursor != null) cursor.close();
2093 Cursor cursor = null;
2097 Uri photoUri = Uri.withAppendedPath(contactUri, Contacts.Photo.CONTENT_DIRECTORY);
2098 cursor = getContentResolver().query(photoUri, new String[] {Photo.PHOTO},
2100 if (cursor != null && cursor.moveToFirst()) {
2101 bm = ContactsUtils.loadContactPhoto(cursor, 0, options);
2104 if (cursor != null) {
2110 final int[] fallbacks = {
2111 R.drawable.ic_contact_picture,
2112 R.drawable.ic_contact_picture_2,
2113 R.drawable.ic_contact_picture_3
2115 bm = BitmapFactory.decodeResource(getResources(),
2116 fallbacks[new Random().nextInt(fallbacks.length)]);
2123 * Return the selection arguments for a default query based on the
2124 * {@link #mDisplayOnlyPhones} flag.
2126 private String getContactSelection() {
2127 if (mDisplayOnlyPhones) {
2128 return CLAUSE_ONLY_VISIBLE + " AND " + CLAUSE_ONLY_PHONES;
2130 return CLAUSE_ONLY_VISIBLE;
2134 private Uri getContactFilterUri(String filter) {
2136 if (!TextUtils.isEmpty(filter)) {
2137 baseUri = Uri.withAppendedPath(Contacts.CONTENT_FILTER_URI, Uri.encode(filter));
2139 baseUri = Contacts.CONTENT_URI;
2142 if (mAdapter.getDisplaySectionHeadersEnabled()) {
2143 return buildSectionIndexerUri(baseUri);
2149 private Uri getPeopleFilterUri(String filter) {
2150 if (!TextUtils.isEmpty(filter)) {
2151 return Uri.withAppendedPath(People.CONTENT_FILTER_URI, Uri.encode(filter));
2153 return People.CONTENT_URI;
2157 private static Uri buildSectionIndexerUri(Uri uri) {
2158 return uri.buildUpon()
2159 .appendQueryParameter(ContactCounts.ADDRESS_BOOK_INDEX_EXTRAS, "true").build();
2162 private Uri getJoinSuggestionsUri(String filter) {
2163 Builder builder = Contacts.CONTENT_URI.buildUpon();
2164 builder.appendEncodedPath(String.valueOf(mQueryAggregateId));
2165 builder.appendEncodedPath(AggregationSuggestions.CONTENT_DIRECTORY);
2166 if (!TextUtils.isEmpty(filter)) {
2167 builder.appendEncodedPath(Uri.encode(filter));
2169 builder.appendQueryParameter("limit", String.valueOf(MAX_SUGGESTIONS));
2170 return builder.build();
2173 private String getSortOrder(String[] projectionType) {
2174 if (mSortOrder == ContactsContract.Preferences.SORT_ORDER_PRIMARY) {
2175 return Contacts.SORT_KEY_PRIMARY;
2177 return Contacts.SORT_KEY_ALTERNATIVE;
2182 // Set the proper empty string
2185 mAdapter.setLoading(true);
2187 // Cancel any pending queries
2188 mQueryHandler.cancelOperation(QUERY_TOKEN);
2189 mQueryHandler.setLoadingJoinSuggestions(false);
2191 mSortOrder = mContactsPrefs.getSortOrder();
2192 mDisplayOrder = mContactsPrefs.getDisplayOrder();
2194 // When sort order and display order contradict each other, we want to
2195 // highlight the part of the name used for sorting.
2196 mHighlightWhenScrolling = false;
2197 if (mSortOrder == ContactsContract.Preferences.SORT_ORDER_PRIMARY &&
2198 mDisplayOrder == ContactsContract.Preferences.DISPLAY_ORDER_ALTERNATIVE) {
2199 mHighlightWhenScrolling = true;
2200 } else if (mSortOrder == ContactsContract.Preferences.SORT_ORDER_ALTERNATIVE &&
2201 mDisplayOrder == ContactsContract.Preferences.DISPLAY_ORDER_PRIMARY) {
2202 mHighlightWhenScrolling = true;
2205 String[] projection = getProjectionForQuery();
2206 if (mSearchMode && TextUtils.isEmpty(getTextFilter())) {
2207 mAdapter.changeCursor(new MatrixCursor(projection));
2211 String callingPackage = getCallingPackage();
2212 Uri uri = getUriToQuery();
2213 if (!TextUtils.isEmpty(callingPackage)) {
2214 uri = uri.buildUpon()
2215 .appendQueryParameter(ContactsContract.REQUESTING_PACKAGE_PARAM_KEY,
2220 // Kick off the new query
2224 case MODE_PICK_CONTACT:
2225 case MODE_PICK_OR_CREATE_CONTACT:
2226 case MODE_INSERT_OR_EDIT_CONTACT:
2227 mQueryHandler.startQuery(QUERY_TOKEN, null, uri, projection, getContactSelection(),
2228 null, getSortOrder(projection));
2231 case MODE_LEGACY_PICK_PERSON:
2232 case MODE_LEGACY_PICK_OR_CREATE_PERSON:
2233 case MODE_PICK_POSTAL:
2235 case MODE_QUERY_PICK:
2236 case MODE_QUERY_PICK_PHONE:
2237 case MODE_QUERY_PICK_TO_VIEW:
2238 case MODE_QUERY_PICK_TO_EDIT: {
2239 mQueryHandler.startQuery(QUERY_TOKEN, null, uri, projection, null, null,
2240 getSortOrder(projection));
2245 mQueryHandler.startQuery(QUERY_TOKEN, null, uri,
2246 projection, Contacts.STARRED + "=1", null,
2247 getSortOrder(projection));
2251 mQueryHandler.startQuery(QUERY_TOKEN, null, uri,
2253 Contacts.TIMES_CONTACTED + " > 0", null,
2254 Contacts.TIMES_CONTACTED + " DESC, "
2255 + getSortOrder(projection));
2258 case MODE_STREQUENT:
2259 mQueryHandler.startQuery(QUERY_TOKEN, null, uri, projection, null, null, null);
2262 case MODE_PICK_PHONE:
2263 case MODE_LEGACY_PICK_PHONE:
2264 mQueryHandler.startQuery(QUERY_TOKEN, null, uri,
2265 projection, CLAUSE_ONLY_VISIBLE, null, getSortOrder(projection));
2268 case MODE_LEGACY_PICK_POSTAL:
2269 mQueryHandler.startQuery(QUERY_TOKEN, null, uri,
2271 ContactMethods.KIND + "=" + android.provider.Contacts.KIND_POSTAL, null,
2272 getSortOrder(projection));
2275 case MODE_JOIN_CONTACT:
2276 mQueryHandler.setLoadingJoinSuggestions(true);
2277 mQueryHandler.startQuery(QUERY_TOKEN, null, uri, projection,
2284 * Called from a background thread to do the filter and return the resulting cursor.
2286 * @param filter the text that was entered to filter on
2287 * @return a cursor with the results of the filter
2289 Cursor doFilter(String filter) {
2290 String[] projection = getProjectionForQuery();
2291 if (mSearchMode && TextUtils.isEmpty(getTextFilter())) {
2292 return new MatrixCursor(projection);
2295 final ContentResolver resolver = getContentResolver();
2298 case MODE_PICK_CONTACT:
2299 case MODE_PICK_OR_CREATE_CONTACT:
2300 case MODE_INSERT_OR_EDIT_CONTACT: {
2301 return resolver.query(getContactFilterUri(filter), projection,
2302 getContactSelection(), null, getSortOrder(projection));
2305 case MODE_LEGACY_PICK_PERSON:
2306 case MODE_LEGACY_PICK_OR_CREATE_PERSON: {
2307 return resolver.query(getPeopleFilterUri(filter), projection, null, null,
2308 getSortOrder(projection));
2311 case MODE_STARRED: {
2312 return resolver.query(getContactFilterUri(filter), projection,
2313 Contacts.STARRED + "=1", null,
2314 getSortOrder(projection));
2317 case MODE_FREQUENT: {
2318 return resolver.query(getContactFilterUri(filter), projection,
2319 Contacts.TIMES_CONTACTED + " > 0", null,
2320 Contacts.TIMES_CONTACTED + " DESC, "
2321 + getSortOrder(projection));
2324 case MODE_STREQUENT: {
2326 if (!TextUtils.isEmpty(filter)) {
2327 uri = Uri.withAppendedPath(Contacts.CONTENT_STREQUENT_FILTER_URI,
2328 Uri.encode(filter));
2330 uri = Contacts.CONTENT_STREQUENT_URI;
2332 return resolver.query(uri, projection, null, null, null);
2335 case MODE_PICK_PHONE: {
2336 Uri uri = getUriToQuery();
2337 if (!TextUtils.isEmpty(filter)) {
2338 uri = Uri.withAppendedPath(Phone.CONTENT_FILTER_URI, Uri.encode(filter));
2340 return resolver.query(uri, projection, CLAUSE_ONLY_VISIBLE, null,
2341 getSortOrder(projection));
2344 case MODE_LEGACY_PICK_PHONE: {
2345 //TODO: Support filtering here (bug 2092503)
2349 case MODE_JOIN_CONTACT: {
2351 // We are on a background thread. Run queries one after the other synchronously
2352 Cursor cursor = resolver.query(getJoinSuggestionsUri(filter), projection, null,
2354 mAdapter.setSuggestionsCursor(cursor);
2355 mJoinModeShowAllContacts = false;
2356 return resolver.query(getContactFilterUri(filter), projection,
2357 Contacts._ID + " != " + mQueryAggregateId + " AND " + CLAUSE_ONLY_VISIBLE,
2358 null, getSortOrder(projection));
2361 throw new UnsupportedOperationException("filtering not allowed in mode " + mMode);
2364 private Cursor getShowAllContactsLabelCursor(String[] projection) {
2365 MatrixCursor matrixCursor = new MatrixCursor(projection);
2366 Object[] row = new Object[projection.length];
2367 // The only columns we care about is the id
2368 row[SUMMARY_ID_COLUMN_INDEX] = JOIN_MODE_SHOW_ALL_CONTACTS_ID;
2369 matrixCursor.addRow(row);
2370 return matrixCursor;
2374 * Calls the currently selected list item.
2375 * @return true if the call was initiated, false otherwise
2377 boolean callSelection() {
2378 ListView list = getListView();
2379 if (list.hasFocus()) {
2380 Cursor cursor = (Cursor) list.getSelectedItem();
2381 return callContact(cursor);
2386 boolean callContact(Cursor cursor) {
2387 return callOrSmsContact(cursor, false /*call*/);
2390 boolean smsContact(Cursor cursor) {
2391 return callOrSmsContact(cursor, true /*sms*/);
2395 * Calls the contact which the cursor is point to.
2396 * @return true if the call was initiated, false otherwise
2398 boolean callOrSmsContact(Cursor cursor, boolean sendSms) {
2399 if (cursor != null) {
2400 boolean hasPhone = cursor.getInt(SUMMARY_HAS_PHONE_COLUMN_INDEX) != 0;
2402 // There is no phone number.
2407 String phone = null;
2408 Cursor phonesCursor = null;
2409 phonesCursor = queryPhoneNumbers(cursor.getLong(SUMMARY_ID_COLUMN_INDEX));
2410 if (phonesCursor == null || phonesCursor.getCount() == 0) {
2414 } else if (phonesCursor.getCount() == 1) {
2415 // only one number, call it.
2416 phone = phonesCursor.getString(phonesCursor.getColumnIndex(Phone.NUMBER));
2418 phonesCursor.moveToPosition(-1);
2419 while (phonesCursor.moveToNext()) {
2420 if (phonesCursor.getInt(phonesCursor.
2421 getColumnIndex(Phone.IS_SUPER_PRIMARY)) != 0) {
2422 // Found super primary, call it.
2423 phone = phonesCursor.
2424 getString(phonesCursor.getColumnIndex(Phone.NUMBER));
2430 if (phone == null) {
2431 // Display dialog to choose a number to call.
2432 PhoneDisambigDialog phoneDialog = new PhoneDisambigDialog(
2433 this, phonesCursor, sendSms);
2437 ContactsUtils.initiateSms(this, phone);
2439 ContactsUtils.initiateCall(this, phone);
2448 private Cursor queryPhoneNumbers(long contactId) {
2449 Uri baseUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
2450 Uri dataUri = Uri.withAppendedPath(baseUri, Contacts.Data.CONTENT_DIRECTORY);
2452 Cursor c = getContentResolver().query(dataUri,
2453 new String[] {Phone._ID, Phone.NUMBER, Phone.IS_SUPER_PRIMARY,
2454 RawContacts.ACCOUNT_TYPE, Phone.TYPE, Phone.LABEL},
2455 Data.MIMETYPE + "=?", new String[] {Phone.CONTENT_ITEM_TYPE}, null);
2456 if (c != null && c.moveToFirst()) {
2463 * Signal an error to the user.
2465 void signalError() {
2466 //TODO play an error beep or something...
2469 Cursor getItemForView(View view) {
2470 ListView listView = getListView();
2471 int index = listView.getPositionForView(view);
2475 return (Cursor) listView.getAdapter().getItem(index);
2478 private static class QueryHandler extends AsyncQueryHandler {
2479 protected final WeakReference<ContactsListActivity> mActivity;
2480 protected boolean mLoadingJoinSuggestions = false;
2482 public QueryHandler(Context context) {
2483 super(context.getContentResolver());
2484 mActivity = new WeakReference<ContactsListActivity>((ContactsListActivity) context);
2487 public void setLoadingJoinSuggestions(boolean flag) {
2488 mLoadingJoinSuggestions = flag;
2492 protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
2493 final ContactsListActivity activity = mActivity.get();
2494 if (activity != null && !activity.isFinishing()) {
2496 // Whenever we get a suggestions cursor, we need to immediately kick off
2497 // another query for the complete list of contacts
2498 if (cursor != null && mLoadingJoinSuggestions) {
2499 mLoadingJoinSuggestions = false;
2500 if (cursor.getCount() > 0) {
2501 activity.mAdapter.setSuggestionsCursor(cursor);
2504 activity.mAdapter.setSuggestionsCursor(null);
2507 if (activity.mAdapter.mSuggestionsCursorCount == 0
2508 || !activity.mJoinModeShowAllContacts) {
2509 startQuery(QUERY_TOKEN, null, activity.getContactFilterUri(
2510 activity.getTextFilter()),
2511 CONTACTS_SUMMARY_PROJECTION,
2512 Contacts._ID + " != " + activity.mQueryAggregateId
2513 + " AND " + CLAUSE_ONLY_VISIBLE, null,
2514 activity.getSortOrder(CONTACTS_SUMMARY_PROJECTION));
2518 cursor = activity.getShowAllContactsLabelCursor(CONTACTS_SUMMARY_PROJECTION);
2521 activity.mAdapter.changeCursor(cursor);
2523 // Now that the cursor is populated again, it's possible to restore the list state
2524 if (activity.mListState != null) {
2525 activity.mList.onRestoreInstanceState(activity.mListState);
2526 activity.mListState = null;
2529 if (cursor != null) {
2536 final static class ContactListItemCache {
2538 public TextView headerText;
2539 public View divider;
2540 public TextView nameView;
2541 public View callView;
2542 public ImageView callButton;
2543 public CharArrayBuffer nameBuffer = new CharArrayBuffer(128);
2544 public TextView labelView;
2545 public TextView dataView;
2546 public TextView snippetView;
2547 public CharArrayBuffer dataBuffer = new CharArrayBuffer(128);
2548 public ImageView presenceView;
2549 public QuickContactBadge photoView;
2550 public ImageView nonQuickContactPhotoView;
2551 public CharArrayBuffer highlightedTextBuffer = new CharArrayBuffer(128);
2552 public TextWithHighlighting textWithHighlighting;
2555 final static class PinnedHeaderCache {
2556 public TextView titleView;
2557 public ColorStateList textColor;
2558 public Drawable background;
2561 private final class ContactItemListAdapter extends ResourceCursorAdapter
2562 implements SectionIndexer, OnScrollListener, PinnedHeaderListView.PinnedHeaderAdapter {
2563 private SectionIndexer mIndexer;
2564 private boolean mLoading = true;
2565 private CharSequence mUnknownNameText;
2566 private boolean mDisplayPhotos = false;
2567 private boolean mDisplayCallButton = false;
2568 private boolean mDisplayAdditionalData = true;
2569 private int mFrequentSeparatorPos = ListView.INVALID_POSITION;
2570 private boolean mDisplaySectionHeaders = true;
2571 private Cursor mSuggestionsCursor;
2572 private int mSuggestionsCursorCount;
2574 public ContactItemListAdapter(Context context) {
2575 super(context, R.layout.contacts_list_item, null, false);
2577 mUnknownNameText = context.getText(android.R.string.unknownName);
2579 case MODE_LEGACY_PICK_POSTAL:
2580 case MODE_PICK_POSTAL:
2581 case MODE_LEGACY_PICK_PHONE:
2582 case MODE_PICK_PHONE:
2583 case MODE_STREQUENT:
2585 mDisplaySectionHeaders = false;
2590 mDisplaySectionHeaders = false;
2593 // Do not display the second line of text if in a specific SEARCH query mode, usually for
2594 // matching a specific E-mail or phone number. Any contact details
2595 // shown would be identical, and columns might not even be present
2596 // in the returned cursor.
2597 if (mMode != MODE_QUERY_PICK_PHONE && mQueryMode != QUERY_MODE_NONE) {
2598 mDisplayAdditionalData = false;
2601 if ((mMode & MODE_MASK_NO_DATA) == MODE_MASK_NO_DATA) {
2602 mDisplayAdditionalData = false;
2605 if ((mMode & MODE_MASK_SHOW_CALL_BUTTON) == MODE_MASK_SHOW_CALL_BUTTON) {
2606 mDisplayCallButton = true;
2609 if ((mMode & MODE_MASK_SHOW_PHOTOS) == MODE_MASK_SHOW_PHOTOS) {
2610 mDisplayPhotos = true;
2611 if (mShowSearchSnippets) {
2612 setViewResource(R.layout.contacts_list_item_photo_and_snippet);
2614 setViewResource(R.layout.contacts_list_item_photo);
2619 public boolean getDisplaySectionHeadersEnabled() {
2620 return mDisplaySectionHeaders;
2623 public void setSuggestionsCursor(Cursor cursor) {
2624 if (mSuggestionsCursor != null) {
2625 mSuggestionsCursor.close();
2627 mSuggestionsCursor = cursor;
2628 mSuggestionsCursorCount = cursor == null ? 0 : cursor.getCount();
2632 * Callback on the UI thread when the content observer on the backing cursor fires.
2633 * Instead of calling requery we need to do an async query so that the requery doesn't
2634 * block the UI thread for a long time.
2637 protected void onContentChanged() {
2638 CharSequence constraint = getTextFilter();
2639 if (!TextUtils.isEmpty(constraint)) {
2640 // Reset the filter state then start an async filter operation
2641 Filter filter = getFilter();
2642 filter.filter(constraint);
2644 // Start an async query
2649 public void setLoading(boolean loading) {
2654 public boolean isEmpty() {
2656 return TextUtils.isEmpty(getTextFilter());
2657 } else if ((mMode & MODE_MASK_CREATE_NEW) == MODE_MASK_CREATE_NEW) {
2658 // This mode mask adds a header and we always want it to show up, even
2659 // if the list is empty, so always claim the list is not empty.
2662 if (mCursor == null || mLoading) {
2663 // We don't want the empty state to show when loading.
2666 return super.isEmpty();
2672 public int getItemViewType(int position) {
2673 if (position == 0 && (mShowNumberOfContacts || (mMode & MODE_MASK_CREATE_NEW) != 0)) {
2674 return IGNORE_ITEM_VIEW_TYPE;
2677 if (isShowAllContactsItemPosition(position)) {
2678 return IGNORE_ITEM_VIEW_TYPE;
2681 if (isSearchAllContactsItemPosition(position)) {
2682 return IGNORE_ITEM_VIEW_TYPE;
2685 if (getSeparatorId(position) != 0) {
2686 // We don't want the separator view to be recycled.
2687 return IGNORE_ITEM_VIEW_TYPE;
2690 return super.getItemViewType(position);
2694 public View getView(int position, View convertView, ViewGroup parent) {
2696 throw new IllegalStateException(
2697 "this should only be called when the cursor is valid");
2700 // handle the total contacts item
2701 if (position == 0 && mShowNumberOfContacts) {
2702 return getTotalContactCountView(parent);
2705 if (position == 0 && (mMode & MODE_MASK_CREATE_NEW) != 0) {
2706 // Add the header for creating a new contact
2707 return getLayoutInflater().inflate(R.layout.create_new_contact, parent, false);
2710 if (isShowAllContactsItemPosition(position)) {
2711 return getLayoutInflater().
2712 inflate(R.layout.contacts_list_show_all_item, parent, false);
2715 if (isSearchAllContactsItemPosition(position)) {
2716 return getLayoutInflater().
2717 inflate(R.layout.contacts_list_search_all_item, parent, false);
2720 // Handle the separator specially
2721 int separatorId = getSeparatorId(position);
2722 if (separatorId != 0) {
2723 TextView view = (TextView) getLayoutInflater().
2724 inflate(R.layout.list_separator, parent, false);
2725 view.setText(separatorId);
2729 boolean showingSuggestion;
2731 if (mSuggestionsCursorCount != 0 && position < mSuggestionsCursorCount + 2) {
2732 showingSuggestion = true;
2733 cursor = mSuggestionsCursor;
2735 showingSuggestion = false;
2739 int realPosition = getRealPosition(position);
2740 if (!cursor.moveToPosition(realPosition)) {
2741 throw new IllegalStateException("couldn't move cursor to position " + position);
2745 if (convertView == null || convertView.getTag() == null) {
2746 v = newView(mContext, cursor, parent);
2750 bindView(v, mContext, cursor);
2751 bindSectionHeader(v, realPosition, mDisplaySectionHeaders && !showingSuggestion);
2756 private View getTotalContactCountView(ViewGroup parent) {
2757 final LayoutInflater inflater = getLayoutInflater();
2758 View view = inflater.inflate(R.layout.total_contacts, parent, false);
2760 TextView totalContacts = (TextView) view.findViewById(R.id.totalContactsText);
2763 int count = getRealCount();
2765 if (mMode == MODE_QUERY || mMode == MODE_QUERY_PICK || mMode == MODE_QUERY_PICK_PHONE
2766 || mMode == MODE_QUERY_PICK_TO_EDIT) {
2767 text = getQuantityText(count, R.string.listFoundAllContactsZero,
2768 R.plurals.listFoundAllContacts);
2769 } else if (mSearchMode && !TextUtils.isEmpty(getTextFilter())) {
2770 text = getQuantityText(count, R.string.listFoundAllContactsZero,
2771 R.plurals.searchFoundContacts);
2773 if (mDisplayOnlyPhones) {
2774 text = getQuantityText(count, R.string.listTotalPhoneContactsZero,
2775 R.plurals.listTotalPhoneContacts);
2777 text = getQuantityText(count, R.string.listTotalAllContactsZero,
2778 R.plurals.listTotalAllContacts);
2781 totalContacts.setText(text);
2785 // TODO: fix PluralRules to handle zero correctly and use Resources.getQuantityText directly
2786 private String getQuantityText(int count, int zeroResourceId, int pluralResourceId) {
2788 return getString(zeroResourceId);
2790 String format = getResources().getQuantityText(pluralResourceId, count).toString();
2791 return String.format(format, count);
2795 private boolean isShowAllContactsItemPosition(int position) {
2796 return mMode == MODE_JOIN_CONTACT && mJoinModeShowAllContacts
2797 && mSuggestionsCursorCount != 0 && position == mSuggestionsCursorCount + 2;
2800 private boolean isSearchAllContactsItemPosition(int position) {
2801 return mSearchMode && position == getCount() - 1;
2804 private int getSeparatorId(int position) {
2805 int separatorId = 0;
2806 if (position == mFrequentSeparatorPos) {
2807 separatorId = R.string.favoritesFrquentSeparator;
2809 if (mSuggestionsCursorCount != 0) {
2810 if (position == 0) {
2811 separatorId = R.string.separatorJoinAggregateSuggestions;
2812 } else if (position == mSuggestionsCursorCount + 1) {
2813 separatorId = R.string.separatorJoinAggregateAll;
2820 public View newView(Context context, Cursor cursor, ViewGroup parent) {
2821 final View view = super.newView(context, cursor, parent);
2823 final ContactListItemCache cache = new ContactListItemCache();
2824 cache.header = view.findViewById(R.id.header);
2825 cache.headerText = (TextView)view.findViewById(R.id.header_text);
2826 cache.divider = view.findViewById(R.id.list_divider);
2827 cache.nameView = (TextView) view.findViewById(R.id.name);
2828 cache.callView = view.findViewById(R.id.call_view);
2829 cache.callButton = (ImageView) view.findViewById(R.id.call_button);
2830 if (cache.callButton != null) {
2831 cache.callButton.setOnClickListener(ContactsListActivity.this);
2833 cache.labelView = (TextView) view.findViewById(R.id.label);
2834 cache.dataView = (TextView) view.findViewById(R.id.data);
2835 cache.presenceView = (ImageView) view.findViewById(R.id.presence);
2836 cache.photoView = (QuickContactBadge) view.findViewById(R.id.photo);
2837 if (cache.photoView != null) {
2838 cache.photoView.setExcludeMimes(new String[] {Contacts.CONTENT_ITEM_TYPE});
2840 cache.nonQuickContactPhotoView = (ImageView) view.findViewById(R.id.noQuickContactPhoto);
2841 cache.textWithHighlighting = mHighlightingAnimation.createTextWithHighlighting();
2842 cache.snippetView = (TextView)view.findViewById(R.id.snippet);
2849 public void bindView(View view, Context context, Cursor cursor) {
2850 final ContactListItemCache cache = (ContactListItemCache) view.getTag();
2852 TextView dataView = cache.dataView;
2853 TextView labelView = cache.labelView;
2854 int typeColumnIndex;
2855 int dataColumnIndex;
2856 int labelColumnIndex;
2858 int nameColumnIndex;
2859 boolean displayAdditionalData = mDisplayAdditionalData;
2860 boolean highlightingEnabled = false;
2862 case MODE_PICK_PHONE:
2863 case MODE_LEGACY_PICK_PHONE:
2864 case MODE_QUERY_PICK_PHONE: {
2865 nameColumnIndex = PHONE_DISPLAY_NAME_COLUMN_INDEX;
2866 dataColumnIndex = PHONE_NUMBER_COLUMN_INDEX;
2867 typeColumnIndex = PHONE_TYPE_COLUMN_INDEX;
2868 labelColumnIndex = PHONE_LABEL_COLUMN_INDEX;
2869 defaultType = Phone.TYPE_HOME;
2872 case MODE_PICK_POSTAL:
2873 case MODE_LEGACY_PICK_POSTAL: {
2874 nameColumnIndex = POSTAL_DISPLAY_NAME_COLUMN_INDEX;
2875 dataColumnIndex = POSTAL_ADDRESS_COLUMN_INDEX;
2876 typeColumnIndex = POSTAL_TYPE_COLUMN_INDEX;
2877 labelColumnIndex = POSTAL_LABEL_COLUMN_INDEX;
2878 defaultType = StructuredPostal.TYPE_HOME;
2882 nameColumnIndex = getSummaryDisplayNameColumnIndex();
2883 dataColumnIndex = -1;
2884 typeColumnIndex = -1;
2885 labelColumnIndex = -1;
2886 defaultType = Phone.TYPE_HOME;
2887 displayAdditionalData = false;
2888 highlightingEnabled = mHighlightWhenScrolling && mMode != MODE_STREQUENT;
2893 cursor.copyStringToBuffer(nameColumnIndex, cache.nameBuffer);
2894 int size = cache.nameBuffer.sizeCopied;
2896 if (highlightingEnabled) {
2897 buildDisplayNameWithHighlighting(cache.nameView, cursor, cache.nameBuffer,
2898 cache.highlightedTextBuffer, cache.textWithHighlighting);
2900 cache.nameView.setText(cache.nameBuffer.data, 0, size);
2903 cache.nameView.setText(mUnknownNameText);
2906 boolean hasPhone = cursor.getColumnCount() >= SUMMARY_HAS_PHONE_COLUMN_INDEX
2907 && cursor.getInt(SUMMARY_HAS_PHONE_COLUMN_INDEX) != 0;
2909 // Make the call button visible if requested.
2910 if (mDisplayCallButton && hasPhone) {
2911 int pos = cursor.getPosition();
2912 cache.callView.setVisibility(View.VISIBLE);
2913 cache.callButton.setTag(pos);
2915 cache.callView.setVisibility(View.GONE);
2918 // Set the photo, if requested
2919 if (mDisplayPhotos) {
2920 boolean useQuickContact = (mMode & MODE_MASK_DISABLE_QUIKCCONTACT) == 0;
2923 if (!cursor.isNull(SUMMARY_PHOTO_ID_COLUMN_INDEX)) {
2924 photoId = cursor.getLong(SUMMARY_PHOTO_ID_COLUMN_INDEX);
2927 ImageView viewToUse;
2928 if (useQuickContact) {
2929 viewToUse = cache.photoView;
2930 // Build soft lookup reference
2931 final long contactId = cursor.getLong(SUMMARY_ID_COLUMN_INDEX);
2932 final String lookupKey = cursor.getString(SUMMARY_LOOKUP_KEY_COLUMN_INDEX);
2933 cache.photoView.assignContactUri(Contacts.getLookupUri(contactId, lookupKey));
2934 cache.photoView.setVisibility(View.VISIBLE);
2935 cache.nonQuickContactPhotoView.setVisibility(View.INVISIBLE);
2937 viewToUse = cache.nonQuickContactPhotoView;
2938 cache.photoView.setVisibility(View.INVISIBLE);
2939 cache.nonQuickContactPhotoView.setVisibility(View.VISIBLE);
2943 final int position = cursor.getPosition();
2944 mPhotoLoader.loadPhoto(viewToUse, photoId);
2947 ImageView presenceView = cache.presenceView;
2948 if ((mMode & MODE_MASK_NO_PRESENCE) == 0) {
2949 // Set the proper icon (star or presence or nothing)
2951 if (!cursor.isNull(SUMMARY_PRESENCE_STATUS_COLUMN_INDEX)) {
2952 serverStatus = cursor.getInt(SUMMARY_PRESENCE_STATUS_COLUMN_INDEX);
2953 presenceView.setImageResource(
2954 Presence.getPresenceIconResourceId(serverStatus));
2955 presenceView.setVisibility(View.VISIBLE);
2957 presenceView.setVisibility(View.GONE);
2960 presenceView.setVisibility(View.GONE);
2963 // TODO: make sure that when mShowSearchSnippets is true, the
2964 // snippet views are available
2965 if (mShowSearchSnippets && cache.snippetView != null) {
2966 boolean showSnippet = false;
2967 String snippetMimeType = cursor.getString(SUMMARY_SNIPPET_MIMETYPE_COLUMN_INDEX);
2968 if (Email.CONTENT_ITEM_TYPE.equals(snippetMimeType)) {
2969 String email = cursor.getString(SUMMARY_SNIPPET_DATA1_COLUMN_INDEX);
2970 if (!TextUtils.isEmpty(email)) {
2971 cache.snippetView.setText(email);
2974 } else if (Organization.CONTENT_ITEM_TYPE.equals(snippetMimeType)) {
2975 String company = cursor.getString(SUMMARY_SNIPPET_DATA1_COLUMN_INDEX);
2976 String title = cursor.getString(SUMMARY_SNIPPET_DATA4_COLUMN_INDEX);
2977 if (!TextUtils.isEmpty(company)) {
2978 if (!TextUtils.isEmpty(title)) {
2979 cache.snippetView.setText(company + " / " + title);
2981 cache.snippetView.setText(company);
2984 } else if (!TextUtils.isEmpty(title)) {
2985 cache.snippetView.setText(title);
2988 } else if (Nickname.CONTENT_ITEM_TYPE.equals(snippetMimeType)) {
2989 String nickname = cursor.getString(SUMMARY_SNIPPET_DATA1_COLUMN_INDEX);
2990 if (!TextUtils.isEmpty(nickname)) {
2991 cache.snippetView.setText(nickname);
2996 cache.snippetView.setVisibility(showSnippet ? View.VISIBLE : View.GONE);
2999 if (!displayAdditionalData) {
3000 cache.dataView.setVisibility(View.GONE);
3001 cache.labelView.setVisibility(View.GONE);
3006 cursor.copyStringToBuffer(dataColumnIndex, cache.dataBuffer);
3008 size = cache.dataBuffer.sizeCopied;
3010 dataView.setText(cache.dataBuffer.data, 0, size);
3011 dataView.setVisibility(View.VISIBLE);
3013 dataView.setVisibility(View.GONE);
3017 if (!cursor.isNull(typeColumnIndex)) {
3018 labelView.setVisibility(View.VISIBLE);
3020 final int type = cursor.getInt(typeColumnIndex);
3021 final String label = cursor.getString(labelColumnIndex);
3023 if (mMode == MODE_LEGACY_PICK_POSTAL || mMode == MODE_PICK_POSTAL) {
3024 labelView.setText(StructuredPostal.getTypeLabel(context.getResources(), type,
3027 labelView.setText(Phone.getTypeLabel(context.getResources(), type, label));
3030 // There is no label, hide the the view
3031 labelView.setVisibility(View.GONE);
3036 * Computes the span of the display name that has highlighted parts and configures
3037 * the display name text view accordingly.
3039 private void buildDisplayNameWithHighlighting(TextView textView, Cursor cursor,
3040 CharArrayBuffer buffer1, CharArrayBuffer buffer2,
3041 TextWithHighlighting textWithHighlighting) {
3042 int oppositeDisplayOrderColumnIndex;
3043 if (mDisplayOrder == ContactsContract.Preferences.DISPLAY_ORDER_PRIMARY) {
3044 oppositeDisplayOrderColumnIndex = SUMMARY_DISPLAY_NAME_ALTERNATIVE_COLUMN_INDEX;
3046 oppositeDisplayOrderColumnIndex = SUMMARY_DISPLAY_NAME_PRIMARY_COLUMN_INDEX;
3048 cursor.copyStringToBuffer(oppositeDisplayOrderColumnIndex, buffer2);
3050 textWithHighlighting.setText(buffer1, buffer2);
3051 textView.setText(textWithHighlighting);
3054 private void bindSectionHeader(View view, int position, boolean displaySectionHeaders) {
3055 final ContactListItemCache cache = (ContactListItemCache) view.getTag();
3056 if (!displaySectionHeaders) {
3057 cache.header.setVisibility(View.GONE);
3058 cache.divider.setVisibility(View.VISIBLE);
3060 final int section = getSectionForPosition(position);
3061 if (getPositionForSection(section) == position) {
3062 String title = (String)mIndexer.getSections()[section];
3063 if (!TextUtils.isEmpty(title)) {
3064 cache.headerText.setText(title);
3065 cache.header.setVisibility(View.VISIBLE);
3067 cache.header.setVisibility(View.GONE);
3070 cache.header.setVisibility(View.GONE);
3073 // move the divider for the last item in a section
3074 if (getPositionForSection(section + 1) - 1 == position) {
3075 cache.divider.setVisibility(View.GONE);
3077 cache.divider.setVisibility(View.VISIBLE);
3083 public void changeCursor(Cursor cursor) {
3086 // Get the split between starred and frequent items, if the mode is strequent
3087 mFrequentSeparatorPos = ListView.INVALID_POSITION;
3088 int cursorCount = 0;
3089 if (cursor != null && (cursorCount = cursor.getCount()) > 0
3090 && mMode == MODE_STREQUENT) {
3092 for (int i = 0; cursor.moveToNext(); i++) {
3093 int starred = cursor.getInt(SUMMARY_STARRED_COLUMN_INDEX);
3096 // Only add the separator when there are starred items present
3097 mFrequentSeparatorPos = i;
3104 super.changeCursor(cursor);
3105 // Update the indexer for the fast scroll widget
3106 updateIndexer(cursor);
3109 private void updateIndexer(Cursor cursor) {
3110 if (cursor == null) {
3115 Bundle bundle = cursor.getExtras();
3116 if (bundle.containsKey(ContactCounts.EXTRA_ADDRESS_BOOK_INDEX_TITLES)) {
3118 bundle.getStringArray(ContactCounts.EXTRA_ADDRESS_BOOK_INDEX_TITLES);
3119 int counts[] = bundle.getIntArray(ContactCounts.EXTRA_ADDRESS_BOOK_INDEX_COUNTS);
3120 mIndexer = new ContactsSectionIndexer(sections, counts);
3127 * Run the query on a helper thread. Beware that this code does not run
3128 * on the main UI thread!
3131 public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
3132 return doFilter(constraint.toString());
3135 public Object [] getSections() {
3136 if (mIndexer == null) {
3137 return new String[] { " " };
3139 return mIndexer.getSections();
3143 public int getPositionForSection(int sectionIndex) {
3144 if (mIndexer == null) {
3148 return mIndexer.getPositionForSection(sectionIndex);
3151 public int getSectionForPosition(int position) {
3152 if (mIndexer == null) {
3156 return mIndexer.getSectionForPosition(position);
3160 public boolean areAllItemsEnabled() {
3161 return mMode != MODE_STARRED
3162 && !mShowNumberOfContacts
3163 && mSuggestionsCursorCount == 0;
3167 public boolean isEnabled(int position) {
3168 if (mShowNumberOfContacts) {
3169 if (position == 0) {
3175 if (mSuggestionsCursorCount > 0) {
3176 return position != 0 && position != mSuggestionsCursorCount + 1;
3178 return position != mFrequentSeparatorPos;
3182 public int getCount() {
3186 int superCount = super.getCount();
3188 if (mShowNumberOfContacts && (mSearchMode || superCount > 0)) {
3189 // We don't want to count this header if it's the only thing visible, so that
3190 // the empty text will display.
3195 // Last element in the list is the "Find
3199 // We do not show the "Create New" button in Search mode
3200 if ((mMode & MODE_MASK_CREATE_NEW) != 0 && !mSearchMode) {
3201 // Count the "Create new contact" line
3205 if (mSuggestionsCursorCount != 0) {
3206 // When showing suggestions, we have 2 additional list items: the "Suggestions"
3207 // and "All contacts" headers.
3208 return mSuggestionsCursorCount + superCount + 2;
3210 else if (mFrequentSeparatorPos != ListView.INVALID_POSITION) {
3211 // When showing strequent list, we have an additional list item - the separator.
3212 return superCount + 1;
3219 * Gets the actual count of contacts and excludes all the headers.
3221 public int getRealCount() {
3222 return super.getCount();
3225 private int getRealPosition(int pos) {
3226 if (mShowNumberOfContacts) {
3230 if ((mMode & MODE_MASK_CREATE_NEW) != 0 && !mSearchMode) {
3232 } else if (mSuggestionsCursorCount != 0) {
3233 // When showing suggestions, we have 2 additional list items: the "Suggestions"
3234 // and "All contacts" separators.
3235 if (pos < mSuggestionsCursorCount + 2) {
3236 // We are in the upper partition (Suggestions). Adjusting for the "Suggestions"
3240 // We are in the lower partition (All contacts). Adjusting for the size
3241 // of the upper partition plus the two separators.
3242 return pos - mSuggestionsCursorCount - 2;
3244 } else if (mFrequentSeparatorPos == ListView.INVALID_POSITION) {
3245 // No separator, identity map
3247 } else if (pos <= mFrequentSeparatorPos) {
3248 // Before or at the separator, identity map
3251 // After the separator, remove 1 from the pos to get the real underlying pos
3257 public Object getItem(int pos) {
3258 if (mSuggestionsCursorCount != 0 && pos <= mSuggestionsCursorCount) {
3259 mSuggestionsCursor.moveToPosition(getRealPosition(pos));
3260 return mSuggestionsCursor;
3261 } else if (isSearchAllContactsItemPosition(pos)){
3264 return super.getItem(getRealPosition(pos));
3269 public long getItemId(int pos) {
3270 if (mSuggestionsCursorCount != 0 && pos < mSuggestionsCursorCount + 2) {
3271 if (mSuggestionsCursor.moveToPosition(pos - 1)) {
3272 return mSuggestionsCursor.getLong(mRowIDColumn);
3276 } else if (isSearchAllContactsItemPosition(pos)) {
3279 return super.getItemId(getRealPosition(pos));
3282 public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
3283 int totalItemCount) {
3284 if (view instanceof PinnedHeaderListView) {
3285 ((PinnedHeaderListView)view).configureHeaderView(firstVisibleItem);
3289 public void onScrollStateChanged(AbsListView view, int scrollState) {
3290 if (mHighlightWhenScrolling) {
3291 if (scrollState != OnScrollListener.SCROLL_STATE_IDLE) {
3292 mHighlightingAnimation.startHighlighting();
3294 mHighlightingAnimation.stopHighlighting();
3298 if (scrollState == OnScrollListener.SCROLL_STATE_FLING) {
3299 mPhotoLoader.pause();
3300 } else if (mDisplayPhotos) {
3301 mPhotoLoader.resume();
3306 * Computes the state of the pinned header. It can be invisible, fully
3307 * visible or partially pushed up out of the view.
3309 public int getPinnedHeaderState(int position) {
3310 if (mIndexer == null || mCursor == null || mCursor.getCount() == 0) {
3311 return PINNED_HEADER_GONE;
3314 int realPosition = getRealPosition(position);
3315 if (realPosition < 0) {
3316 return PINNED_HEADER_GONE;
3319 // The header should get pushed up if the top item shown
3320 // is the last item in a section for a particular letter.
3321 int section = getSectionForPosition(realPosition);
3322 int nextSectionPosition = getPositionForSection(section + 1);
3323 if (nextSectionPosition != -1 && realPosition == nextSectionPosition - 1) {
3324 return PINNED_HEADER_PUSHED_UP;
3327 return PINNED_HEADER_VISIBLE;
3331 * Configures the pinned header by setting the appropriate text label
3332 * and also adjusting color if necessary. The color needs to be
3333 * adjusted when the pinned header is being pushed up from the view.
3335 public void configurePinnedHeader(View header, int position, int alpha) {
3336 PinnedHeaderCache cache = (PinnedHeaderCache)header.getTag();
3337 if (cache == null) {
3338 cache = new PinnedHeaderCache();
3339 cache.titleView = (TextView)header.findViewById(R.id.header_text);
3340 cache.textColor = cache.titleView.getTextColors();
3341 cache.background = header.getBackground();
3342 header.setTag(cache);
3345 int realPosition = getRealPosition(position);
3346 int section = getSectionForPosition(realPosition);
3348 String title = (String)mIndexer.getSections()[section];
3349 cache.titleView.setText(title);
3352 // Opaque: use the default background, and the original text color
3353 header.setBackgroundDrawable(cache.background);
3354 cache.titleView.setTextColor(cache.textColor);
3356 // Faded: use a solid color approximation of the background, and
3357 // a translucent text color
3358 header.setBackgroundColor(Color.rgb(
3359 Color.red(mPinnedHeaderBackgroundColor) * alpha / 255,
3360 Color.green(mPinnedHeaderBackgroundColor) * alpha / 255,
3361 Color.blue(mPinnedHeaderBackgroundColor) * alpha / 255));
3363 int textColor = cache.textColor.getDefaultColor();
3364 cache.titleView.setTextColor(Color.argb(alpha,
3365 Color.red(textColor), Color.green(textColor), Color.blue(textColor)));