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.Handler;
61 import android.os.Message;
62 import android.os.Parcelable;
63 import android.preference.PreferenceManager;
64 import android.provider.ContactsContract;
65 import android.provider.Settings;
66 import android.provider.Contacts.ContactMethods;
67 import android.provider.Contacts.People;
68 import android.provider.Contacts.PeopleColumns;
69 import android.provider.Contacts.Phones;
70 import android.provider.ContactsContract.Contacts;
71 import android.provider.ContactsContract.Data;
72 import android.provider.ContactsContract.Intents;
73 import android.provider.ContactsContract.Presence;
74 import android.provider.ContactsContract.RawContacts;
75 import android.provider.ContactsContract.CommonDataKinds.Email;
76 import android.provider.ContactsContract.CommonDataKinds.Phone;
77 import android.provider.ContactsContract.CommonDataKinds.Photo;
78 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
79 import android.provider.ContactsContract.Contacts.AggregationSuggestions;
80 import android.provider.ContactsContract.Intents.Insert;
81 import android.provider.ContactsContract.Intents.UI;
82 import android.telephony.TelephonyManager;
83 import android.text.Editable;
84 import android.text.TextUtils;
85 import android.text.TextWatcher;
86 import android.util.Log;
87 import android.view.ContextMenu;
88 import android.view.ContextThemeWrapper;
89 import android.view.KeyEvent;
90 import android.view.LayoutInflater;
91 import android.view.Menu;
92 import android.view.MenuInflater;
93 import android.view.MenuItem;
94 import android.view.View;
95 import android.view.ViewGroup;
96 import android.view.ViewParent;
97 import android.view.ContextMenu.ContextMenuInfo;
98 import android.view.ViewGroup.LayoutParams;
99 import android.view.inputmethod.EditorInfo;
100 import android.view.inputmethod.InputMethodManager;
101 import android.widget.AbsListView;
102 import android.widget.AdapterView;
103 import android.widget.AlphabetIndexer;
104 import android.widget.ArrayAdapter;
105 import android.widget.Filter;
106 import android.widget.ImageButton;
107 import android.widget.ImageView;
108 import android.widget.ListView;
109 import android.widget.QuickContactBadge;
110 import android.widget.ResourceCursorAdapter;
111 import android.widget.SectionIndexer;
112 import android.widget.TabHost;
113 import android.widget.TextView;
114 import android.widget.AbsListView.OnScrollListener;
116 import java.lang.ref.SoftReference;
117 import java.lang.ref.WeakReference;
118 import java.util.ArrayList;
119 import java.util.HashMap;
120 import java.util.HashSet;
121 import java.util.List;
122 import java.util.Locale;
123 import java.util.Random;
124 import java.util.concurrent.ExecutorService;
125 import java.util.concurrent.Executors;
127 /*TODO(emillar) I commented most of the code that deals with modes and filtering. It should be
128 * brought back in as we add back that functionality.
133 * Displays a list of contacts. Usually is embedded into the ContactsActivity.
135 @SuppressWarnings("deprecation")
136 public class ContactsListActivity extends ListActivity implements View.OnCreateContextMenuListener,
137 View.OnClickListener, View.OnKeyListener, TextWatcher, TextView.OnEditorActionListener {
139 public static class JoinContactActivity extends ContactsListActivity {
143 private static final String TAG = "ContactsListActivity";
145 private static final boolean ENABLE_ACTION_ICON_OVERLAYS = true;
147 private static final String LIST_STATE_KEY = "liststate";
150 * Saved state key for the flag that indicates if the UI is in the search mode.
152 private static final String SEARCH_MODE_KEY = "searchMode";
154 static final int MENU_ITEM_VIEW_CONTACT = 1;
155 static final int MENU_ITEM_CALL = 2;
156 static final int MENU_ITEM_EDIT_BEFORE_CALL = 3;
157 static final int MENU_ITEM_SEND_SMS = 4;
158 static final int MENU_ITEM_SEND_IM = 5;
159 static final int MENU_ITEM_EDIT = 6;
160 static final int MENU_ITEM_DELETE = 7;
161 static final int MENU_ITEM_TOGGLE_STAR = 8;
163 private static final int SUBACTIVITY_NEW_CONTACT = 1;
164 private static final int SUBACTIVITY_VIEW_CONTACT = 2;
165 private static final int SUBACTIVITY_DISPLAY_GROUP = 3;
166 private static final int SUBACTIVITY_SEARCH = 4;
168 private static final int TEXT_HIGHLIGHTING_ANIMATION_DURATION = 350;
171 * The action for the join contact activity.
173 * Input: extra field {@link #EXTRA_AGGREGATE_ID} is the aggregate ID.
175 * TODO: move to {@link ContactsContract}.
177 public static final String JOIN_AGGREGATE =
178 "com.android.contacts.action.JOIN_AGGREGATE";
181 * Used with {@link #JOIN_AGGREGATE} to give it the target for aggregation.
185 public static final String EXTRA_AGGREGATE_ID =
186 "com.android.contacts.action.AGGREGATE_ID";
189 * Used with {@link #JOIN_AGGREGATE} to give it the name of the aggregation target.
194 public static final String EXTRA_AGGREGATE_NAME =
195 "com.android.contacts.action.AGGREGATE_NAME";
197 public static final String AUTHORITIES_FILTER_KEY = "authorities";
199 /** Mask for picker mode */
200 static final int MODE_MASK_PICKER = 0x80000000;
201 /** Mask for no presence mode */
202 static final int MODE_MASK_NO_PRESENCE = 0x40000000;
203 /** Mask for enabling list filtering */
204 static final int MODE_MASK_NO_FILTER = 0x20000000;
205 /** Mask for having a "create new contact" header in the list */
206 static final int MODE_MASK_CREATE_NEW = 0x10000000;
207 /** Mask for showing photos in the list */
208 static final int MODE_MASK_SHOW_PHOTOS = 0x08000000;
209 /** Mask for hiding additional information e.g. primary phone number in the list */
210 static final int MODE_MASK_NO_DATA = 0x04000000;
211 /** Mask for showing a call button in the list */
212 static final int MODE_MASK_SHOW_CALL_BUTTON = 0x02000000;
213 /** Mask to disable quickcontact (images will show as normal images) */
214 static final int MODE_MASK_DISABLE_QUIKCCONTACT = 0x01000000;
215 /** Mask to show the total number of contacts at the top */
216 static final int MODE_MASK_SHOW_NUMBER_OF_CONTACTS = 0x00800000;
219 static final int MODE_UNKNOWN = 0;
221 static final int MODE_DEFAULT = 4 | MODE_MASK_SHOW_PHOTOS | MODE_MASK_SHOW_NUMBER_OF_CONTACTS;
223 static final int MODE_CUSTOM = 8;
224 /** Show all starred contacts */
225 static final int MODE_STARRED = 20 | MODE_MASK_SHOW_PHOTOS;
226 /** Show frequently contacted contacts */
227 static final int MODE_FREQUENT = 30 | MODE_MASK_SHOW_PHOTOS;
228 /** Show starred and the frequent */
229 static final int MODE_STREQUENT = 35 | MODE_MASK_SHOW_PHOTOS | MODE_MASK_SHOW_CALL_BUTTON;
230 /** Show all contacts and pick them when clicking */
231 static final int MODE_PICK_CONTACT = 40 | MODE_MASK_PICKER | MODE_MASK_SHOW_PHOTOS
232 | MODE_MASK_DISABLE_QUIKCCONTACT;
233 /** Show all contacts as well as the option to create a new one */
234 static final int MODE_PICK_OR_CREATE_CONTACT = 42 | MODE_MASK_PICKER | MODE_MASK_CREATE_NEW
235 | MODE_MASK_SHOW_PHOTOS | MODE_MASK_DISABLE_QUIKCCONTACT;
236 /** Show all people through the legacy provider and pick them when clicking */
237 static final int MODE_LEGACY_PICK_PERSON = 43 | MODE_MASK_PICKER
238 | MODE_MASK_DISABLE_QUIKCCONTACT;
239 /** Show all people through the legacy provider as well as the option to create a new one */
240 static final int MODE_LEGACY_PICK_OR_CREATE_PERSON = 44 | MODE_MASK_PICKER
241 | MODE_MASK_CREATE_NEW | MODE_MASK_DISABLE_QUIKCCONTACT;
242 /** Show all contacts and pick them when clicking, and allow creating a new contact */
243 static final int MODE_INSERT_OR_EDIT_CONTACT = 45 | MODE_MASK_PICKER | MODE_MASK_CREATE_NEW;
244 /** Show all phone numbers and pick them when clicking */
245 // TODO fix and reenable search in phone number picker
246 static final int MODE_PICK_PHONE = 50 | MODE_MASK_PICKER | MODE_MASK_NO_PRESENCE |
248 /** Show all phone numbers through the legacy provider and pick them when clicking */
249 static final int MODE_LEGACY_PICK_PHONE =
250 51 | MODE_MASK_PICKER | MODE_MASK_NO_PRESENCE | MODE_MASK_NO_FILTER;
251 /** Show all postal addresses and pick them when clicking */
252 static final int MODE_PICK_POSTAL =
253 55 | MODE_MASK_PICKER | MODE_MASK_NO_PRESENCE | MODE_MASK_NO_FILTER;
254 /** Show all postal addresses and pick them when clicking */
255 static final int MODE_LEGACY_PICK_POSTAL =
256 56 | MODE_MASK_PICKER | MODE_MASK_NO_PRESENCE | MODE_MASK_NO_FILTER;
257 static final int MODE_GROUP = 57 | MODE_MASK_SHOW_PHOTOS;
258 /** Run a search query */
259 static final int MODE_QUERY = 60 | MODE_MASK_NO_FILTER | MODE_MASK_SHOW_NUMBER_OF_CONTACTS;
260 /** Run a search query in PICK mode, but that still launches to VIEW */
261 static final int MODE_QUERY_PICK_TO_VIEW = 65 | MODE_MASK_NO_FILTER | MODE_MASK_PICKER;
263 /** Show join suggestions followed by an A-Z list */
264 static final int MODE_JOIN_CONTACT = 70 | MODE_MASK_PICKER | MODE_MASK_NO_PRESENCE
265 | MODE_MASK_NO_DATA | MODE_MASK_SHOW_PHOTOS | MODE_MASK_DISABLE_QUIKCCONTACT;
267 /** Run a search query in a PICK mode */
268 static final int MODE_QUERY_PICK = 75 | MODE_MASK_NO_FILTER | MODE_MASK_PICKER;
271 * An action used to do perform search while in a contact picker. It is initiated
272 * by the ContactListActivity itself.
274 private static final String ACTION_INTERNAL_SEARCH = "com.android.contacts.INTERNAL_SEARCH";
276 /** Maximum number of suggestions shown for joining aggregates */
277 static final int MAX_SUGGESTIONS = 4;
279 static final String[] CONTACTS_SUMMARY_PROJECTION = new String[] {
281 Contacts.DISPLAY_NAME_PRIMARY, // 1
282 Contacts.DISPLAY_NAME_ALTERNATIVE, // 2
283 Contacts.SORT_KEY_PRIMARY, // 3
284 Contacts.STARRED, // 4
285 Contacts.TIMES_CONTACTED, // 5
286 Contacts.CONTACT_PRESENCE, // 6
287 Contacts.PHOTO_ID, // 7
288 Contacts.LOOKUP_KEY, // 8
289 Contacts.HAS_PHONE_NUMBER, // 9
291 static final String[] CONTACTS_SUMMARY_PROJECTION_FROM_EMAIL = new String[] {
293 Contacts.DISPLAY_NAME_PRIMARY, // 1
294 Contacts.DISPLAY_NAME_ALTERNATIVE, // 2
295 Contacts.SORT_KEY_PRIMARY, // 3
296 Contacts.STARRED, // 4
297 Contacts.TIMES_CONTACTED, // 5
298 Contacts.CONTACT_PRESENCE, // 6
299 Contacts.PHOTO_ID, // 7
300 Contacts.LOOKUP_KEY, // 8
301 // email lookup doesn't included HAS_PHONE_NUMBER OR LOOKUP_KEY in projection
303 static final String[] LEGACY_PEOPLE_PROJECTION = new String[] {
305 People.DISPLAY_NAME, // 1
306 People.DISPLAY_NAME, // 2
307 People.DISPLAY_NAME, // 3
309 PeopleColumns.TIMES_CONTACTED, // 5
310 People.PRESENCE_STATUS, // 6
312 static final int SUMMARY_ID_COLUMN_INDEX = 0;
313 static final int SUMMARY_DISPLAY_NAME_PRIMARY_COLUMN_INDEX = 1;
314 static final int SUMMARY_DISPLAY_NAME_ALTERNATIVE_COLUMN_INDEX = 2;
315 static final int SUMMARY_SORT_KEY_PRIMARY_COLUMN_INDEX = 3;
316 static final int SUMMARY_STARRED_COLUMN_INDEX = 4;
317 static final int SUMMARY_TIMES_CONTACTED_COLUMN_INDEX = 5;
318 static final int SUMMARY_PRESENCE_STATUS_COLUMN_INDEX = 6;
319 static final int SUMMARY_PHOTO_ID_COLUMN_INDEX = 7;
320 static final int SUMMARY_LOOKUP_KEY_COLUMN_INDEX = 8;
321 static final int SUMMARY_HAS_PHONE_COLUMN_INDEX = 9;
323 static final String[] PHONES_PROJECTION = new String[] {
328 Phone.DISPLAY_NAME, // 4
329 Phone.CONTACT_ID, // 5
331 static final String[] LEGACY_PHONES_PROJECTION = new String[] {
336 People.DISPLAY_NAME, // 4
338 static final int PHONE_ID_COLUMN_INDEX = 0;
339 static final int PHONE_TYPE_COLUMN_INDEX = 1;
340 static final int PHONE_LABEL_COLUMN_INDEX = 2;
341 static final int PHONE_NUMBER_COLUMN_INDEX = 3;
342 static final int PHONE_DISPLAY_NAME_COLUMN_INDEX = 4;
343 static final int PHONE_CONTACT_ID_COLUMN_INDEX = 5;
345 static final String[] POSTALS_PROJECTION = new String[] {
346 StructuredPostal._ID, //0
347 StructuredPostal.TYPE, //1
348 StructuredPostal.LABEL, //2
349 StructuredPostal.DATA, //3
350 StructuredPostal.DISPLAY_NAME, // 4
352 static final String[] LEGACY_POSTALS_PROJECTION = new String[] {
353 ContactMethods._ID, //0
354 ContactMethods.TYPE, //1
355 ContactMethods.LABEL, //2
356 ContactMethods.DATA, //3
357 People.DISPLAY_NAME, // 4
359 static final String[] RAW_CONTACTS_PROJECTION = new String[] {
361 RawContacts.CONTACT_ID, //1
362 RawContacts.ACCOUNT_TYPE, //2
365 static final int POSTAL_ID_COLUMN_INDEX = 0;
366 static final int POSTAL_TYPE_COLUMN_INDEX = 1;
367 static final int POSTAL_LABEL_COLUMN_INDEX = 2;
368 static final int POSTAL_ADDRESS_COLUMN_INDEX = 3;
369 static final int POSTAL_DISPLAY_NAME_COLUMN_INDEX = 4;
371 private static final int QUERY_TOKEN = 42;
373 static final String KEY_PICKER_MODE = "picker_mode";
375 private ContactItemListAdapter mAdapter;
377 int mMode = MODE_DEFAULT;
379 private QueryHandler mQueryHandler;
380 private boolean mJustCreated;
381 private boolean mSyncEnabled;
382 private Uri mSelectedContactUri;
384 // private boolean mDisplayAll;
385 private boolean mDisplayOnlyPhones;
387 private Uri mGroupUri;
389 private long mQueryAggregateId;
391 private ArrayList<Long> mWritableRawContactIds = new ArrayList<Long>();
392 private int mWritableSourcesCnt;
393 private int mReadOnlySourcesCnt;
396 * Used to keep track of the scroll state of the list.
398 private Parcelable mListState = null;
400 private String mShortcutAction;
402 private int mScrollState;
405 * Internal query type when in mode {@link #MODE_QUERY_PICK_TO_VIEW}.
407 private int mQueryMode = QUERY_MODE_NONE;
409 private static final int QUERY_MODE_NONE = -1;
410 private static final int QUERY_MODE_MAILTO = 1;
411 private static final int QUERY_MODE_TEL = 2;
414 * Data to use when in mode {@link #MODE_QUERY_PICK_TO_VIEW}. Usually
415 * provided by scheme-specific part of incoming {@link Intent#getData()}.
417 private String mQueryData;
419 private static final String CLAUSE_ONLY_VISIBLE = Contacts.IN_VISIBLE_GROUP + "=1";
420 private static final String CLAUSE_ONLY_PHONES = Contacts.HAS_PHONE_NUMBER + "=1";
423 * In the {@link #MODE_JOIN_CONTACT} determines whether we display a list item with the label
424 * "Show all contacts" or actually show all contacts
426 private boolean mJoinModeShowAllContacts;
429 * The ID of the special item described above.
431 private static final long JOIN_MODE_SHOW_ALL_CONTACTS_ID = -2;
433 // Uri matcher for contact id
434 private static final int CONTACTS_ID = 1001;
435 private static final UriMatcher sContactsIdMatcher;
437 private static ExecutorService sImageFetchThreadPool;
440 sContactsIdMatcher = new UriMatcher(UriMatcher.NO_MATCH);
441 sContactsIdMatcher.addURI(ContactsContract.AUTHORITY, "contacts/#", CONTACTS_ID);
444 private class DeleteClickListener implements DialogInterface.OnClickListener {
445 public void onClick(DialogInterface dialog, int which) {
446 getContentResolver().delete(mSelectedContactUri, null, null);
451 * A {@link TextHighlightingAnimation} that redraws just the contact display name in a
454 private static class NameHighlightingAnimation extends TextHighlightingAnimation {
455 private final ListView mListView;
457 private NameHighlightingAnimation(ListView listView, int duration) {
459 this.mListView = listView;
463 * Redraws all visible items of the list corresponding to contacts
466 protected void invalidate() {
467 int childCount = mListView.getChildCount();
468 for (int i = 0; i < childCount; i++) {
469 View listItem = mListView.getChildAt(i);
470 Object tag = listItem.getTag();
471 if (tag instanceof ContactListItemCache) {
472 ((ContactListItemCache)tag).nameView.invalidate();
478 protected void onAnimationStarted() {
479 mListView.setScrollingCacheEnabled(false);
483 protected void onAnimationEnded() {
484 mListView.setScrollingCacheEnabled(true);
488 // The size of a home screen shortcut icon.
489 private int mIconSize;
490 private ContactsPreferences mContactsPrefs;
491 private int mDisplayOrder;
492 private int mSortOrder;
493 private boolean mHighlightWhenScrolling;
494 private TextHighlightingAnimation mHighlightingAnimation;
496 // If true, the activity is in the "search mode" with the search UI displayed.
497 private boolean mSearchMode;
498 private View mSearchView;
499 private SearchEditText mSearchEditText;
502 * An approximation of the background color of the pinned header. This color
503 * is used when the pinned header is being pushed up. At that point the header
504 * "fades away". Rather than computing a faded bitmap based on the 9-patch
505 * normally used for the background, we will use a solid color, which will
506 * provide better performance and reduced complexity.
508 private int mPinnedHeaderBackgroundColor;
511 protected void onCreate(Bundle icicle) {
512 super.onCreate(icicle);
514 // Resolve the intent
515 final Intent intent = getIntent();
517 mIconSize = getResources().getDimensionPixelSize(android.R.dimen.app_icon_size);
518 mContactsPrefs = new ContactsPreferences(this);
520 // Allow the title to be set to a custom String using an extra on the intent
521 String title = intent.getStringExtra(UI.TITLE_EXTRA_KEY);
526 final String action = intent.getAction();
527 mMode = MODE_UNKNOWN;
529 Log.i(TAG, "Called with action: " + action);
530 if (UI.LIST_DEFAULT.equals(action)) {
531 mMode = MODE_DEFAULT;
532 // When mDefaultMode is true the mode is set in onResume(), since the preferneces
533 // activity may change it whenever this activity isn't running
534 } else if (UI.LIST_GROUP_ACTION.equals(action)) {
536 String groupName = intent.getStringExtra(UI.GROUP_NAME_EXTRA_KEY);
537 if (TextUtils.isEmpty(groupName)) {
541 buildUserGroupUri(groupName);
542 } else if (UI.LIST_ALL_CONTACTS_ACTION.equals(action)) {
544 mDisplayOnlyPhones = false;
545 } else if (UI.LIST_STARRED_ACTION.equals(action)) {
546 mMode = MODE_STARRED;
547 } else if (UI.LIST_FREQUENT_ACTION.equals(action)) {
548 mMode = MODE_FREQUENT;
549 } else if (UI.LIST_STREQUENT_ACTION.equals(action)) {
550 mMode = MODE_STREQUENT;
551 } else if (UI.LIST_CONTACTS_WITH_PHONES_ACTION.equals(action)) {
553 mDisplayOnlyPhones = true;
554 } else if (Intent.ACTION_PICK.equals(action)) {
555 // XXX These should be showing the data from the URI given in
557 final String type = intent.resolveType(this);
558 if (Contacts.CONTENT_TYPE.equals(type)) {
559 mMode = MODE_PICK_CONTACT;
560 } else if (People.CONTENT_TYPE.equals(type)) {
561 mMode = MODE_LEGACY_PICK_PERSON;
562 } else if (Phone.CONTENT_TYPE.equals(type)) {
563 mMode = MODE_PICK_PHONE;
564 } else if (Phones.CONTENT_TYPE.equals(type)) {
565 mMode = MODE_LEGACY_PICK_PHONE;
566 } else if (StructuredPostal.CONTENT_TYPE.equals(type)) {
567 mMode = MODE_PICK_POSTAL;
568 } else if (ContactMethods.CONTENT_POSTAL_TYPE.equals(type)) {
569 mMode = MODE_LEGACY_PICK_POSTAL;
571 } else if (Intent.ACTION_CREATE_SHORTCUT.equals(action)) {
572 if (intent.getComponent().getClassName().equals("alias.DialShortcut")) {
573 mMode = MODE_PICK_PHONE;
574 mShortcutAction = Intent.ACTION_CALL;
575 setTitle(R.string.callShortcutActivityTitle);
576 } else if (intent.getComponent().getClassName().equals("alias.MessageShortcut")) {
577 mMode = MODE_PICK_PHONE;
578 mShortcutAction = Intent.ACTION_SENDTO;
579 setTitle(R.string.messageShortcutActivityTitle);
581 mMode = MODE_PICK_OR_CREATE_CONTACT;
582 mShortcutAction = Intent.ACTION_VIEW;
583 setTitle(R.string.shortcutActivityTitle);
585 } else if (Intent.ACTION_GET_CONTENT.equals(action)) {
586 final String type = intent.resolveType(this);
587 if (Contacts.CONTENT_ITEM_TYPE.equals(type)) {
588 mMode = MODE_PICK_OR_CREATE_CONTACT;
589 } else if (Phone.CONTENT_ITEM_TYPE.equals(type)) {
590 mMode = MODE_PICK_PHONE;
591 } else if (Phones.CONTENT_ITEM_TYPE.equals(type)) {
592 mMode = MODE_LEGACY_PICK_PHONE;
593 } else if (StructuredPostal.CONTENT_ITEM_TYPE.equals(type)) {
594 mMode = MODE_PICK_POSTAL;
595 } else if (ContactMethods.CONTENT_POSTAL_ITEM_TYPE.equals(type)) {
596 mMode = MODE_LEGACY_PICK_POSTAL;
597 } else if (People.CONTENT_ITEM_TYPE.equals(type)) {
598 mMode = MODE_LEGACY_PICK_OR_CREATE_PERSON;
601 } else if (Intent.ACTION_INSERT_OR_EDIT.equals(action)) {
602 mMode = MODE_INSERT_OR_EDIT_CONTACT;
603 } else if (Intent.ACTION_SEARCH.equals(action)) {
604 // See if the suggestion was clicked with a search action key (call button)
605 if ("call".equals(intent.getStringExtra(SearchManager.ACTION_MSG))) {
606 String query = intent.getStringExtra(SearchManager.QUERY);
607 if (!TextUtils.isEmpty(query)) {
608 Intent newIntent = new Intent(Intent.ACTION_CALL_PRIVILEGED,
609 Uri.fromParts("tel", query, null));
610 startActivity(newIntent);
616 // See if search request has extras to specify query
617 if (intent.hasExtra(Insert.EMAIL)) {
618 mMode = MODE_QUERY_PICK_TO_VIEW;
619 mQueryMode = QUERY_MODE_MAILTO;
620 mQueryData = intent.getStringExtra(Insert.EMAIL);
621 } else if (intent.hasExtra(Insert.PHONE)) {
622 mMode = MODE_QUERY_PICK_TO_VIEW;
623 mQueryMode = QUERY_MODE_TEL;
624 mQueryData = intent.getStringExtra(Insert.PHONE);
626 // Otherwise handle the more normal search case
628 mQueryData = getIntent().getStringExtra(SearchManager.QUERY);
630 } else if (ACTION_INTERNAL_SEARCH.equals(action)) {
631 mMode = MODE_QUERY_PICK;
632 mQueryData = getIntent().getStringExtra(SearchManager.QUERY);
634 // Since this is the filter activity it receives all intents
635 // dispatched from the SearchManager for security reasons
636 // so we need to re-dispatch from here to the intended target.
637 } else if (Intents.SEARCH_SUGGESTION_CLICKED.equals(action)) {
638 Uri data = intent.getData();
640 if (sContactsIdMatcher.match(data) == CONTACTS_ID) {
641 long contactId = Long.valueOf(data.getLastPathSegment());
642 final Cursor cursor = queryPhoneNumbers(contactId);
643 if (cursor != null) {
644 if (cursor.getCount() == 1 && cursor.moveToFirst()) {
645 int phoneNumberIndex = cursor.getColumnIndex(Phone.NUMBER);
646 String phoneNumber = cursor.getString(phoneNumberIndex);
647 telUri = Uri.parse("tel:" + phoneNumber);
652 // See if the suggestion was clicked with a search action key (call button)
654 if ("call".equals(intent.getStringExtra(SearchManager.ACTION_MSG)) && telUri != null) {
655 newIntent = new Intent(Intent.ACTION_CALL_PRIVILEGED, telUri);
657 newIntent = new Intent(Intent.ACTION_VIEW, data);
659 startActivity(newIntent);
662 } else if (Intents.SEARCH_SUGGESTION_DIAL_NUMBER_CLICKED.equals(action)) {
663 Intent newIntent = new Intent(Intent.ACTION_CALL_PRIVILEGED, intent.getData());
664 startActivity(newIntent);
667 } else if (Intents.SEARCH_SUGGESTION_CREATE_CONTACT_CLICKED.equals(action)) {
668 // TODO actually support this in EditContactActivity.
669 String number = intent.getData().getSchemeSpecificPart();
670 Intent newIntent = new Intent(Intent.ACTION_INSERT, Contacts.CONTENT_URI);
671 newIntent.putExtra(Intents.Insert.PHONE, number);
672 startActivity(newIntent);
677 if (JOIN_AGGREGATE.equals(action)) {
678 mMode = MODE_JOIN_CONTACT;
679 mQueryAggregateId = intent.getLongExtra(EXTRA_AGGREGATE_ID, -1);
680 if (mQueryAggregateId == -1) {
681 Log.e(TAG, "Intent " + action + " is missing required extra: "
682 + EXTRA_AGGREGATE_ID);
683 setResult(RESULT_CANCELED);
688 if (mMode == MODE_UNKNOWN) {
689 mMode = MODE_DEFAULT;
692 if (mMode == MODE_JOIN_CONTACT) {
693 setContentView(R.layout.contacts_list_content_join);
694 TextView blurbView = (TextView)findViewById(R.id.join_contact_blurb);
696 String blurb = getString(R.string.blurbJoinContactDataWith,
697 getContactDisplayName(mQueryAggregateId));
698 blurbView.setText(blurb);
699 mJoinModeShowAllContacts = true;
701 setContentView(R.layout.contacts_list_content);
707 mQueryHandler = new QueryHandler(this);
711 // // Check to see if sync is enabled
712 // final ContentResolver resolver = getContentResolver();
713 // IContentProvider provider = resolver.acquireProvider(Contacts.CONTENT_URI);
714 // if (provider == null) {
715 // // No contacts provider, bail.
721 // ISyncAdapter sa = provider.getSyncAdapter();
722 // mSyncEnabled = sa != null;
723 // } catch (RemoteException e) {
724 // mSyncEnabled = false;
726 // resolver.releaseProvider(provider);
730 private void setupListView() {
731 final ListView list = getListView();
732 final LayoutInflater inflater = getLayoutInflater();
734 mHighlightingAnimation =
735 new NameHighlightingAnimation(list, TEXT_HIGHLIGHTING_ANIMATION_DURATION);
737 // Tell list view to not show dividers. We'll do it ourself so that we can *not* show
738 // them when an A-Z headers is visible.
739 list.setDividerHeight(0);
740 list.setFocusable(true);
741 list.setOnCreateContextMenuListener(this);
743 if ((mMode & MODE_MASK_CREATE_NEW) != 0) {
744 // Add the header for creating a new contact
745 View header = inflater.inflate(R.layout.create_new_contact, list, false);
746 list.addHeaderView(header);
749 // Set the proper empty string
752 mAdapter = new ContactItemListAdapter(this);
753 setListAdapter(mAdapter);
755 if (list instanceof PinnedHeaderListView) {
756 mPinnedHeaderBackgroundColor =
757 getResources().getColor(R.color.pinned_header_background);
758 PinnedHeaderListView pinnedHeaderList = (PinnedHeaderListView)list;
759 View pinnedHeader = inflater.inflate(R.layout.list_section, list, false);
760 pinnedHeaderList.setPinnedHeaderView(pinnedHeader);
763 list.setOnScrollListener(mAdapter);
764 list.setOnKeyListener(this);
766 // We manually save/restore the listview state
767 list.setSaveEnabled(false);
771 * Configures search UI.
773 private void setupSearchView() {
774 if ((mMode & MODE_MASK_NO_FILTER) == 0) {
775 mSearchView = findViewById(R.id.searchView);
776 mSearchEditText = (SearchEditText)mSearchView.findViewById(R.id.search_src_text);
777 mSearchEditText.addTextChangedListener(this);
778 mSearchEditText.setOnEditorActionListener(this);
780 ImageButton searchButton = (ImageButton)mSearchView.findViewById(R.id.search_btn);
781 searchButton.setOnClickListener(this);
785 private boolean isPickerMode() {
786 return mMode == MODE_PICK_CONTACT
787 || mMode == MODE_PICK_OR_CREATE_CONTACT
788 || mMode == MODE_LEGACY_PICK_PERSON
789 || mMode == MODE_LEGACY_PICK_OR_CREATE_PERSON
790 || mMode == MODE_QUERY_PICK;
793 private String getContactDisplayName(long contactId) {
794 String contactName = null;
795 Cursor c = getContentResolver().query(
796 ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId),
797 new String[] {Contacts.DISPLAY_NAME}, null, null, null);
799 if (c != null && c.moveToFirst()) {
800 contactName = c.getString(0);
808 if (contactName == null) {
816 private int getSummaryDisplayNameColumnIndex() {
817 if (mDisplayOrder == ContactsContract.Preferences.DISPLAY_ORDER_PRIMARY) {
818 return SUMMARY_DISPLAY_NAME_PRIMARY_COLUMN_INDEX;
820 return SUMMARY_DISPLAY_NAME_ALTERNATIVE_COLUMN_INDEX;
825 public void onClick(View v) {
828 case R.id.call_button: {
829 final int position = (Integer)v.getTag();
830 Cursor c = mAdapter.getCursor();
832 c.moveToPosition(position);
837 case R.id.search_btn: {
844 private void setEmptyText() {
845 if (mMode == MODE_JOIN_CONTACT) {
849 TextView empty = (TextView) findViewById(R.id.emptyText);
852 empty.setText(getText(R.string.noMatchingFilteredContacts));
853 } else if (mDisplayOnlyPhones) {
854 empty.setText(getText(R.string.noContactsWithPhoneNumbers));
855 } else if (mMode == MODE_STREQUENT || mMode == MODE_STARRED) {
856 empty.setText(getText(R.string.noFavoritesHelpText));
857 } else if (mMode == MODE_QUERY) {
858 empty.setText(getText(R.string.noMatchingContacts));
860 boolean hasSim = ((TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE))
865 empty.setText(getText(R.string.noContactsHelpTextWithSync));
867 empty.setText(getText(R.string.noContactsHelpText));
871 empty.setText(getText(R.string.noContactsNoSimHelpTextWithSync));
873 empty.setText(getText(R.string.noContactsNoSimHelpText));
879 private void buildUserGroupUri(String group) {
880 mGroupUri = Uri.withAppendedPath(Contacts.CONTENT_GROUP_URI, group);
884 * Sets the mode when the request is for "default"
886 private void setDefaultMode() {
887 // Load the preferences
888 SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
890 mDisplayOnlyPhones = prefs.getBoolean(Prefs.DISPLAY_ONLY_PHONES,
891 Prefs.DISPLAY_ONLY_PHONES_DEFAULT);
893 // Update the empty text view with the proper string, as the group may have changed
898 protected void onResume() {
901 // Force cache to reload so we don't show stale photos.
902 if (mAdapter.mBitmapCache != null) {
903 mAdapter.mBitmapCache.clear();
906 mScrollState = OnScrollListener.SCROLL_STATE_IDLE;
907 boolean runQuery = true;
908 Activity parent = getParent();
910 // Do this before setting the filter. The filter thread relies
911 // on some state that is initialized in setDefaultMode
912 if (mMode == MODE_DEFAULT) {
913 // If we're in default mode we need to possibly reset the mode due to a change
914 // in the preferences activity while we weren't running
919 startSearchMode(false);
922 if (mJustCreated && runQuery) {
923 // We need to start a query here the first time the activity is launched, as long
924 // as we aren't doing a filter.
927 mJustCreated = false;
930 private String getTextFilter() {
931 if (mSearchEditText != null) {
932 return mSearchEditText.getText().toString();
937 private void setTextFilter(String filterText) {
938 if (mSearchEditText != null) {
939 mSearchEditText.setText(filterText);
944 protected void onRestart() {
947 // The cursor was killed off in onStop(), so we need to get a new one here
948 // We do not perform the query if a filter is set on the list because the
949 // filter will cause the query to happen anyway
950 if (TextUtils.isEmpty(getTextFilter())) {
953 // Run the filtered query on the adapter
954 ((ContactItemListAdapter) getListAdapter()).onContentChanged();
959 protected void onSaveInstanceState(Bundle icicle) {
960 super.onSaveInstanceState(icicle);
961 // Save list state in the bundle so we can restore it after the QueryHandler has run
962 icicle.putParcelable(LIST_STATE_KEY, mList.onSaveInstanceState());
963 icicle.putBoolean(SEARCH_MODE_KEY, mSearchMode);
967 protected void onRestoreInstanceState(Bundle icicle) {
968 super.onRestoreInstanceState(icicle);
969 // Retrieve list state. This will be applied after the QueryHandler has run
970 mListState = icicle.getParcelable(LIST_STATE_KEY);
971 mSearchMode = icicle.getBoolean(SEARCH_MODE_KEY);
975 protected void onStop() {
978 mAdapter.setSuggestionsCursor(null);
979 mAdapter.changeCursor(null);
980 mAdapter.clearImageFetching();
982 if (mMode == MODE_QUERY) {
983 // Make sure the search box is closed
984 SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE);
985 searchManager.stopSearch();
990 public boolean onCreateOptionsMenu(Menu menu) {
991 super.onCreateOptionsMenu(menu);
993 // If Contacts was invoked by another Activity simply as a way of
994 // picking a contact, don't show the options menu
995 if ((mMode & MODE_MASK_PICKER) == MODE_MASK_PICKER) {
999 MenuInflater inflater = getMenuInflater();
1000 inflater.inflate(R.menu.list, menu);
1005 public boolean onPrepareOptionsMenu(Menu menu) {
1006 final boolean defaultMode = (mMode == MODE_DEFAULT);
1007 menu.findItem(R.id.menu_display_groups).setVisible(defaultMode);
1012 public boolean onOptionsItemSelected(MenuItem item) {
1013 switch (item.getItemId()) {
1014 case R.id.menu_display_groups: {
1015 final Intent intent = new Intent(this, ContactsPreferencesActivity.class);
1016 startActivityForResult(intent, SUBACTIVITY_DISPLAY_GROUP);
1019 case R.id.menu_search: {
1020 startSearchMode(true);
1023 case R.id.menu_add: {
1024 final Intent intent = new Intent(Intent.ACTION_INSERT, Contacts.CONTENT_URI);
1025 startActivity(intent);
1028 case R.id.menu_import_export: {
1029 displayImportExportDialog();
1032 case R.id.menu_accounts: {
1033 final Intent intent = new Intent(Settings.ACTION_SYNC_SETTINGS);
1034 intent.putExtra(AUTHORITIES_FILTER_KEY, new String[] {
1035 ContactsContract.AUTHORITY
1037 startActivity(intent);
1045 * Displays and initializes the search UI at the top of the activity. If
1046 * this activity is part of a tab activity, also removes the tabs.
1048 * @param showKeyboard a flag indicating whether the soft keyboard should be
1049 * auto shown automatically.
1051 private void startSearchMode(boolean showKeyboard) {
1052 View tabs = findTabWidget();
1054 tabs.setVisibility(View.GONE);
1057 mList.setFocusable(false);
1058 mSearchEditText.setAutoShowKeyboard(showKeyboard);
1059 mSearchEditText.requestFocus();
1060 mSearchView.setVisibility(View.VISIBLE);
1066 * Hides the search UI and shows the tabs if they were hidden before.
1068 private void stopSearchMode() {
1070 // In case the list view owns the soft keyboard at this point, hide the keyboard
1071 InputMethodManager inputManager = (InputMethodManager)getSystemService(
1072 Context.INPUT_METHOD_SERVICE);
1073 inputManager.hideSoftInputFromWindow(mList.getWindowToken(), 0);
1075 // In case the search text view owns the soft keyboard, do the same
1076 mSearchEditText.hideKeyboard();
1077 mSearchView.setVisibility(View.GONE);
1079 View tabs = findTabWidget();
1081 tabs.setVisibility(View.VISIBLE);
1084 mSearchMode = false;
1087 // This will trigger a query
1088 setTextFilter(null);
1090 mList.setFocusable(true);
1094 * If this activity is hosted by a tab activity, the method returns the
1095 * TabWidget from the TabHost activity; otherwise it returns null.
1097 private View findTabWidget() {
1098 View start = getListView();
1099 ViewParent parent = start.getParent();
1100 while (parent != null) {
1101 if (parent instanceof TabHost) {
1102 return ((TabHost)parent).getTabWidget();
1104 parent = parent.getParent();
1110 * Performs filtering of the list based on the search query entered in the
1113 protected void onSearchTextChanged() {
1114 Filter filter = mAdapter.getFilter();
1115 filter.filter(getTextFilter());
1119 * Closes search UI if shown, otherwise follows the default "back" behavior.
1122 public void onBackPressed() {
1126 super.onBackPressed();
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, getClass());
1140 intent.putExtra(SearchManager.QUERY, query);
1141 if (isPickerMode()) {
1142 intent.setAction(ACTION_INTERNAL_SEARCH);
1143 startActivityForResult(intent, SUBACTIVITY_SEARCH);
1145 intent.setAction(Intent.ACTION_SEARCH);
1146 startActivity(intent);
1151 protected Dialog onCreateDialog(int id) {
1153 case R.string.import_from_sim:
1154 case R.string.import_from_sdcard: {
1155 return AccountSelectionUtil.getSelectAccountDialog(this, id);
1157 case R.id.dialog_sdcard_not_found: {
1158 return new AlertDialog.Builder(this)
1159 .setTitle(R.string.no_sdcard_title)
1160 .setIcon(android.R.drawable.ic_dialog_alert)
1161 .setMessage(R.string.no_sdcard_message)
1162 .setPositiveButton(android.R.string.ok, null).create();
1164 case R.id.dialog_delete_contact_confirmation: {
1165 return new AlertDialog.Builder(this)
1166 .setTitle(R.string.deleteConfirmation_title)
1167 .setIcon(android.R.drawable.ic_dialog_alert)
1168 .setMessage(R.string.deleteConfirmation)
1169 .setNegativeButton(android.R.string.cancel, null)
1170 .setPositiveButton(android.R.string.ok,
1171 new DeleteClickListener()).create();
1173 case R.id.dialog_readonly_contact_hide_confirmation: {
1174 return new AlertDialog.Builder(this)
1175 .setTitle(R.string.deleteConfirmation_title)
1176 .setIcon(android.R.drawable.ic_dialog_alert)
1177 .setMessage(R.string.readOnlyContactWarning)
1178 .setNegativeButton(android.R.string.cancel, null)
1179 .setPositiveButton(android.R.string.ok,
1180 new DeleteClickListener()).create();
1182 case R.id.dialog_readonly_contact_delete_confirmation: {
1183 return new AlertDialog.Builder(this)
1184 .setTitle(R.string.deleteConfirmation_title)
1185 .setIcon(android.R.drawable.ic_dialog_alert)
1186 .setMessage(R.string.readOnlyContactDeleteConfirmation)
1187 .setNegativeButton(android.R.string.cancel, null)
1188 .setPositiveButton(android.R.string.ok,
1189 new DeleteClickListener()).create();
1191 case R.id.dialog_multiple_contact_delete_confirmation: {
1192 return new AlertDialog.Builder(this)
1193 .setTitle(R.string.deleteConfirmation_title)
1194 .setIcon(android.R.drawable.ic_dialog_alert)
1195 .setMessage(R.string.multipleContactDeleteConfirmation)
1196 .setNegativeButton(android.R.string.cancel, null)
1197 .setPositiveButton(android.R.string.ok,
1198 new DeleteClickListener()).create();
1201 return super.onCreateDialog(id);
1205 * Create a {@link Dialog} that allows the user to pick from a bulk import
1206 * or bulk export task across all contacts.
1208 private void displayImportExportDialog() {
1209 // Wrap our context to inflate list items using correct theme
1210 final Context dialogContext = new ContextThemeWrapper(this, android.R.style.Theme_Light);
1211 final Resources res = dialogContext.getResources();
1212 final LayoutInflater dialogInflater = (LayoutInflater)dialogContext
1213 .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
1215 // Adapter that shows a list of string resources
1216 final ArrayAdapter<Integer> adapter = new ArrayAdapter<Integer>(this,
1217 android.R.layout.simple_list_item_1) {
1219 public View getView(int position, View convertView, ViewGroup parent) {
1220 if (convertView == null) {
1221 convertView = dialogInflater.inflate(android.R.layout.simple_list_item_1,
1225 final int resId = this.getItem(position);
1226 ((TextView)convertView).setText(resId);
1231 if (TelephonyManager.getDefault().hasIccCard()) {
1232 adapter.add(R.string.import_from_sim);
1234 if (res.getBoolean(R.bool.config_allow_import_from_sdcard)) {
1235 adapter.add(R.string.import_from_sdcard);
1237 if (res.getBoolean(R.bool.config_allow_export_to_sdcard)) {
1238 adapter.add(R.string.export_to_sdcard);
1241 final DialogInterface.OnClickListener clickListener =
1242 new DialogInterface.OnClickListener() {
1243 public void onClick(DialogInterface dialog, int which) {
1246 final int resId = adapter.getItem(which);
1248 case R.string.import_from_sim:
1249 case R.string.import_from_sdcard: {
1250 handleImportRequest(resId);
1253 case R.string.export_to_sdcard: {
1254 Context context = ContactsListActivity.this;
1255 Intent exportIntent = new Intent(context, ExportVCardActivity.class);
1256 context.startActivity(exportIntent);
1260 Log.e(TAG, "Unexpected resource: " +
1261 getResources().getResourceEntryName(resId));
1267 new AlertDialog.Builder(this)
1268 .setTitle(R.string.dialog_import_export)
1269 .setNegativeButton(android.R.string.cancel, null)
1270 .setSingleChoiceItems(adapter, -1, clickListener)
1274 private void handleImportRequest(int resId) {
1275 // There's three possibilities:
1276 // - more than one accounts -> ask the user
1277 // - just one account -> use the account without asking the user
1278 // - no account -> use phone-local storage without asking the user
1279 final Sources sources = Sources.getInstance(this);
1280 final List<Account> accountList = sources.getAccounts(true);
1281 final int size = accountList.size();
1287 AccountSelectionUtil.doImport(this, resId, (size == 1 ? accountList.get(0) : null));
1291 protected void onActivityResult(int requestCode, int resultCode, Intent data) {
1292 switch (requestCode) {
1293 case SUBACTIVITY_NEW_CONTACT:
1294 if (resultCode == RESULT_OK) {
1295 returnPickerResult(null, data.getStringExtra(Intent.EXTRA_SHORTCUT_NAME),
1300 case SUBACTIVITY_VIEW_CONTACT:
1301 if (resultCode == RESULT_OK) {
1302 mAdapter.notifyDataSetChanged();
1306 case SUBACTIVITY_DISPLAY_GROUP:
1307 // Mark as just created so we re-run the view query
1308 mJustCreated = true;
1311 case SUBACTIVITY_SEARCH:
1312 if (resultCode == RESULT_OK) {
1313 returnPickerResult(null, data.getStringExtra(Intent.EXTRA_SHORTCUT_NAME),
1321 public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) {
1322 // If Contacts was invoked by another Activity simply as a way of
1323 // picking a contact, don't show the context menu
1324 if ((mMode & MODE_MASK_PICKER) == MODE_MASK_PICKER) {
1328 AdapterView.AdapterContextMenuInfo info;
1330 info = (AdapterView.AdapterContextMenuInfo) menuInfo;
1331 } catch (ClassCastException e) {
1332 Log.e(TAG, "bad menuInfo", e);
1336 Cursor cursor = (Cursor) getListAdapter().getItem(info.position);
1337 if (cursor == null) {
1338 // For some reason the requested item isn't available, do nothing
1342 Uri contactUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, id);
1343 long rawContactId = ContactsUtils.queryForRawContactId(getContentResolver(), id);
1344 Uri rawContactUri = ContentUris.withAppendedId(RawContacts.CONTENT_URI, rawContactId);
1346 // Setup the menu header
1347 menu.setHeaderTitle(cursor.getString(getSummaryDisplayNameColumnIndex()));
1349 // View contact details
1350 menu.add(0, MENU_ITEM_VIEW_CONTACT, 0, R.string.menu_viewContact)
1351 .setIntent(new Intent(Intent.ACTION_VIEW, contactUri));
1353 if (cursor.getInt(SUMMARY_HAS_PHONE_COLUMN_INDEX) != 0) {
1355 menu.add(0, MENU_ITEM_CALL, 0,
1356 getString(R.string.menu_call));
1358 menu.add(0, MENU_ITEM_SEND_SMS, 0, getString(R.string.menu_sendSMS));
1362 int starState = cursor.getInt(SUMMARY_STARRED_COLUMN_INDEX);
1363 if (starState == 0) {
1364 menu.add(0, MENU_ITEM_TOGGLE_STAR, 0, R.string.menu_addStar);
1366 menu.add(0, MENU_ITEM_TOGGLE_STAR, 0, R.string.menu_removeStar);
1370 menu.add(0, MENU_ITEM_EDIT, 0, R.string.menu_editContact)
1371 .setIntent(new Intent(Intent.ACTION_EDIT, rawContactUri));
1372 menu.add(0, MENU_ITEM_DELETE, 0, R.string.menu_deleteContact);
1376 public boolean onContextItemSelected(MenuItem item) {
1377 AdapterView.AdapterContextMenuInfo info;
1379 info = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo();
1380 } catch (ClassCastException e) {
1381 Log.e(TAG, "bad menuInfo", e);
1385 Cursor cursor = (Cursor) getListAdapter().getItem(info.position);
1387 switch (item.getItemId()) {
1388 case MENU_ITEM_TOGGLE_STAR: {
1390 ContentValues values = new ContentValues(1);
1391 values.put(Contacts.STARRED, cursor.getInt(SUMMARY_STARRED_COLUMN_INDEX) == 0 ? 1 : 0);
1392 final Uri selectedUri = this.getContactUri(info.position);
1393 getContentResolver().update(selectedUri, values, null, null);
1397 case MENU_ITEM_CALL: {
1398 callContact(cursor);
1402 case MENU_ITEM_SEND_SMS: {
1407 case MENU_ITEM_DELETE: {
1408 mSelectedContactUri = getContactUri(info.position);
1414 return super.onContextItemSelected(item);
1419 * Event handler for the use case where the user starts typing without
1420 * bringing up the search UI first.
1422 public boolean onKey(View v, int keyCode, KeyEvent event) {
1423 if (!mSearchMode && (mMode & MODE_MASK_NO_FILTER) == 0) {
1424 int unicodeChar = event.getUnicodeChar();
1425 if (unicodeChar != 0) {
1426 setTextFilter(new String(new int[]{unicodeChar}, 0, 1));
1427 startSearchMode(false);
1435 * Event handler for search UI.
1437 public void afterTextChanged(Editable s) {
1438 onSearchTextChanged();
1441 public void beforeTextChanged(CharSequence s, int start, int count, int after) {
1444 public void onTextChanged(CharSequence s, int start, int before, int count) {
1448 * Event handler for search UI.
1450 public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
1451 if (actionId == EditorInfo.IME_ACTION_GO) {
1459 public boolean onKeyDown(int keyCode, KeyEvent event) {
1461 case KeyEvent.KEYCODE_CALL: {
1462 if (callSelection()) {
1468 case KeyEvent.KEYCODE_DEL: {
1469 final int position = getListView().getSelectedItemPosition();
1470 if (position != ListView.INVALID_POSITION) {
1471 mSelectedContactUri = getContactUri(position);
1478 case KeyEvent.KEYCODE_SEARCH: {
1479 if ((mMode & MODE_MASK_NO_FILTER) == 0) {
1483 startSearchMode(true);
1492 return super.onKeyDown(keyCode, event);
1496 * Prompt the user before deleting the given {@link Contacts} entry.
1498 protected void doContactDelete() {
1499 mReadOnlySourcesCnt = 0;
1500 mWritableSourcesCnt = 0;
1501 mWritableRawContactIds.clear();
1503 if (mSelectedContactUri != null) {
1504 Cursor c = getContentResolver().query(RawContacts.CONTENT_URI, RAW_CONTACTS_PROJECTION,
1505 RawContacts.CONTACT_ID + "=" + ContentUris.parseId(mSelectedContactUri), null,
1507 Sources sources = Sources.getInstance(ContactsListActivity.this);
1509 while (c.moveToNext()) {
1510 final String accountType = c.getString(2);
1511 final long rawContactId = c.getLong(0);
1512 ContactsSource contactsSource = sources.getInflatedSource(accountType,
1513 ContactsSource.LEVEL_SUMMARY);
1514 if (contactsSource != null && contactsSource.readOnly) {
1515 mReadOnlySourcesCnt += 1;
1517 mWritableSourcesCnt += 1;
1518 mWritableRawContactIds.add(rawContactId);
1523 if (mReadOnlySourcesCnt > 0 && mWritableSourcesCnt > 0) {
1524 showDialog(R.id.dialog_readonly_contact_delete_confirmation);
1525 } else if (mReadOnlySourcesCnt > 0 && mWritableSourcesCnt == 0) {
1526 showDialog(R.id.dialog_readonly_contact_hide_confirmation);
1527 } else if (mReadOnlySourcesCnt == 0 && mWritableSourcesCnt > 1) {
1528 showDialog(R.id.dialog_multiple_contact_delete_confirmation);
1530 showDialog(R.id.dialog_delete_contact_confirmation);
1536 protected void onListItemClick(ListView l, View v, int position, long id) {
1537 // Hide soft keyboard, if visible
1538 InputMethodManager inputMethodManager = (InputMethodManager)
1539 getSystemService(Context.INPUT_METHOD_SERVICE);
1540 inputMethodManager.hideSoftInputFromWindow(mList.getWindowToken(), 0);
1542 if (mMode == MODE_INSERT_OR_EDIT_CONTACT) {
1544 if (position == 0) {
1545 intent = new Intent(Intent.ACTION_INSERT, Contacts.CONTENT_URI);
1547 // Edit. adjusting position by subtracting header view count.
1548 position -= getListView().getHeaderViewsCount();
1549 final Uri uri = getSelectedUri(position);
1550 intent = new Intent(Intent.ACTION_EDIT, uri);
1552 intent.setFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
1553 Bundle extras = getIntent().getExtras();
1555 if (extras == null) {
1556 extras = new Bundle();
1558 intent.putExtras(extras);
1559 extras.putBoolean(KEY_PICKER_MODE, (mMode & MODE_MASK_PICKER) == MODE_MASK_PICKER);
1561 startActivity(intent);
1563 } else if (id != -1) {
1564 // Subtract one if we have Create Contact at the top
1565 if ((mMode & MODE_MASK_CREATE_NEW) != 0) {
1568 final Uri uri = getSelectedUri(position);
1569 if ((mMode & MODE_MASK_PICKER) == 0) {
1570 final Intent intent = new Intent(Intent.ACTION_VIEW, uri);
1571 startActivityForResult(intent, SUBACTIVITY_VIEW_CONTACT);
1572 } else if (mMode == MODE_JOIN_CONTACT) {
1573 if (id == JOIN_MODE_SHOW_ALL_CONTACTS_ID) {
1574 mJoinModeShowAllContacts = false;
1577 returnPickerResult(null, null, uri);
1579 } else if (mMode == MODE_QUERY_PICK_TO_VIEW) {
1580 // Started with query that should launch to view contact
1581 final Intent intent = new Intent(Intent.ACTION_VIEW, uri);
1582 startActivity(intent);
1584 } else if (isPickerMode()) {
1585 Cursor c = (Cursor) mAdapter.getItem(position);
1586 returnPickerResult(c, c.getString(getSummaryDisplayNameColumnIndex()), uri);
1587 } else if (mMode == MODE_PICK_PHONE) {
1588 Cursor c = (Cursor) mAdapter.getItem(position);
1589 long contactId = c.getLong(PHONE_CONTACT_ID_COLUMN_INDEX);
1590 returnPickerResult(c, c.getString(PHONE_DISPLAY_NAME_COLUMN_INDEX),
1591 ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId));
1592 } else if (mMode == MODE_PICK_POSTAL
1593 || mMode == MODE_LEGACY_PICK_POSTAL
1594 || mMode == MODE_LEGACY_PICK_PHONE) {
1595 returnPickerResult(null, null, uri);
1597 } else if ((mMode & MODE_MASK_CREATE_NEW) == MODE_MASK_CREATE_NEW
1599 Intent newContact = new Intent(Intents.Insert.ACTION, Contacts.CONTENT_URI);
1600 startActivityForResult(newContact, SUBACTIVITY_NEW_CONTACT);
1607 * @param contactUri In most cases, this should be a lookup {@link Uri}, possibly
1608 * generated through {@link Contacts#getLookupUri(long, String)}.
1610 private void returnPickerResult(Cursor c, String name, Uri contactUri) {
1611 final Intent intent = new Intent();
1613 if (mShortcutAction != null) {
1614 Intent shortcutIntent;
1615 if (Intent.ACTION_VIEW.equals(mShortcutAction)) {
1616 // This is a simple shortcut to view a contact.
1617 shortcutIntent = new Intent(ContactsContract.QuickContact.ACTION_QUICK_CONTACT);
1618 shortcutIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK |
1619 Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
1621 shortcutIntent.setData(contactUri);
1622 shortcutIntent.putExtra(ContactsContract.QuickContact.EXTRA_MODE,
1623 ContactsContract.QuickContact.MODE_LARGE);
1624 shortcutIntent.putExtra(ContactsContract.QuickContact.EXTRA_EXCLUDE_MIMES,
1627 final Bitmap icon = framePhoto(loadContactPhoto(contactUri, null));
1629 intent.putExtra(Intent.EXTRA_SHORTCUT_ICON, scaleToAppIconSize(icon));
1631 intent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE,
1632 Intent.ShortcutIconResource.fromContext(this,
1633 R.drawable.ic_launcher_shortcut_contact));
1636 // This is a direct dial or sms shortcut.
1637 String number = c.getString(PHONE_NUMBER_COLUMN_INDEX);
1638 int type = c.getInt(PHONE_TYPE_COLUMN_INDEX);
1641 if (Intent.ACTION_CALL.equals(mShortcutAction)) {
1642 scheme = Constants.SCHEME_TEL;
1643 resid = R.drawable.badge_action_call;
1645 scheme = Constants.SCHEME_SMSTO;
1646 resid = R.drawable.badge_action_sms;
1649 // Make the URI a direct tel: URI so that it will always continue to work
1650 Uri phoneUri = Uri.fromParts(scheme, number, null);
1651 shortcutIntent = new Intent(mShortcutAction, phoneUri);
1653 intent.putExtra(Intent.EXTRA_SHORTCUT_ICON,
1654 generatePhoneNumberIcon(contactUri, type, resid));
1656 shortcutIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
1657 intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent);
1658 intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, name);
1659 setResult(RESULT_OK, intent);
1661 intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, name);
1662 setResult(RESULT_OK, intent.setData(contactUri));
1667 private Bitmap framePhoto(Bitmap photo) {
1668 final Resources r = getResources();
1669 final Drawable frame = r.getDrawable(com.android.internal.R.drawable.quickcontact_badge);
1671 final int width = r.getDimensionPixelSize(R.dimen.contact_shortcut_frame_width);
1672 final int height = r.getDimensionPixelSize(R.dimen.contact_shortcut_frame_height);
1674 frame.setBounds(0, 0, width, height);
1676 final Rect padding = new Rect();
1677 frame.getPadding(padding);
1679 final Rect source = new Rect(0, 0, photo.getWidth(), photo.getHeight());
1680 final Rect destination = new Rect(padding.left, padding.top,
1681 width - padding.right, height - padding.bottom);
1683 final int d = Math.max(width, height);
1684 final Bitmap b = Bitmap.createBitmap(d, d, Bitmap.Config.ARGB_8888);
1685 final Canvas c = new Canvas(b);
1687 c.translate((d - width) / 2.0f, (d - height) / 2.0f);
1689 c.drawBitmap(photo, source, destination, new Paint(Paint.FILTER_BITMAP_FLAG));
1695 * Generates a phone number shortcut icon. Adds an overlay describing the type of the phone
1696 * number, and if there is a photo also adds the call action icon.
1698 * @param lookupUri The person the phone number belongs to
1699 * @param type The type of the phone number
1700 * @param actionResId The ID for the action resource
1701 * @return The bitmap for the icon
1703 private Bitmap generatePhoneNumberIcon(Uri lookupUri, int type, int actionResId) {
1704 final Resources r = getResources();
1705 boolean drawPhoneOverlay = true;
1706 final float scaleDensity = getResources().getDisplayMetrics().scaledDensity;
1708 Bitmap photo = loadContactPhoto(lookupUri, null);
1709 if (photo == null) {
1710 // If there isn't a photo use the generic phone action icon instead
1711 Bitmap phoneIcon = getPhoneActionIcon(r, actionResId);
1712 if (phoneIcon != null) {
1714 drawPhoneOverlay = false;
1720 // Setup the drawing classes
1721 Bitmap icon = createShortcutBitmap();
1722 Canvas canvas = new Canvas(icon);
1724 // Copy in the photo
1725 Paint photoPaint = new Paint();
1726 photoPaint.setDither(true);
1727 photoPaint.setFilterBitmap(true);
1728 Rect src = new Rect(0,0, photo.getWidth(),photo.getHeight());
1729 Rect dst = new Rect(0,0, mIconSize, mIconSize);
1730 canvas.drawBitmap(photo, src, dst, photoPaint);
1732 // Create an overlay for the phone number type
1733 String overlay = null;
1735 case Phone.TYPE_HOME:
1736 overlay = getString(R.string.type_short_home);
1739 case Phone.TYPE_MOBILE:
1740 overlay = getString(R.string.type_short_mobile);
1743 case Phone.TYPE_WORK:
1744 overlay = getString(R.string.type_short_work);
1747 case Phone.TYPE_PAGER:
1748 overlay = getString(R.string.type_short_pager);
1751 case Phone.TYPE_OTHER:
1752 overlay = getString(R.string.type_short_other);
1755 if (overlay != null) {
1756 Paint textPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DEV_KERN_TEXT_FLAG);
1757 textPaint.setTextSize(20.0f * scaleDensity);
1758 textPaint.setTypeface(Typeface.DEFAULT_BOLD);
1759 textPaint.setColor(r.getColor(R.color.textColorIconOverlay));
1760 textPaint.setShadowLayer(3f, 1, 1, r.getColor(R.color.textColorIconOverlayShadow));
1761 canvas.drawText(overlay, 2 * scaleDensity, 16 * scaleDensity, textPaint);
1764 // Draw the phone action icon as an overlay
1765 if (ENABLE_ACTION_ICON_OVERLAYS && drawPhoneOverlay) {
1766 Bitmap phoneIcon = getPhoneActionIcon(r, actionResId);
1767 if (phoneIcon != null) {
1768 src.set(0, 0, phoneIcon.getWidth(), phoneIcon.getHeight());
1769 int iconWidth = icon.getWidth();
1770 dst.set(iconWidth - ((int) (20 * scaleDensity)), -1,
1771 iconWidth, ((int) (19 * scaleDensity)));
1772 canvas.drawBitmap(phoneIcon, src, dst, photoPaint);
1779 private Bitmap scaleToAppIconSize(Bitmap photo) {
1780 // Setup the drawing classes
1781 Bitmap icon = createShortcutBitmap();
1782 Canvas canvas = new Canvas(icon);
1784 // Copy in the photo
1785 Paint photoPaint = new Paint();
1786 photoPaint.setDither(true);
1787 photoPaint.setFilterBitmap(true);
1788 Rect src = new Rect(0,0, photo.getWidth(),photo.getHeight());
1789 Rect dst = new Rect(0,0, mIconSize, mIconSize);
1790 canvas.drawBitmap(photo, src, dst, photoPaint);
1795 private Bitmap createShortcutBitmap() {
1796 return Bitmap.createBitmap(mIconSize, mIconSize, Bitmap.Config.ARGB_8888);
1800 * Returns the icon for the phone call action.
1802 * @param r The resources to load the icon from
1803 * @param resId The resource ID to load
1804 * @return the icon for the phone call action
1806 private Bitmap getPhoneActionIcon(Resources r, int resId) {
1807 Drawable phoneIcon = r.getDrawable(resId);
1808 if (phoneIcon instanceof BitmapDrawable) {
1809 BitmapDrawable bd = (BitmapDrawable) phoneIcon;
1810 return bd.getBitmap();
1816 Uri getUriToQuery() {
1818 case MODE_JOIN_CONTACT:
1819 return getJoinSuggestionsUri(null);
1823 case MODE_INSERT_OR_EDIT_CONTACT:
1824 case MODE_PICK_CONTACT:
1825 case MODE_PICK_OR_CREATE_CONTACT:{
1826 return Contacts.CONTENT_URI;
1828 case MODE_STREQUENT: {
1829 return Contacts.CONTENT_STREQUENT_URI;
1831 case MODE_LEGACY_PICK_PERSON:
1832 case MODE_LEGACY_PICK_OR_CREATE_PERSON: {
1833 return People.CONTENT_URI;
1835 case MODE_PICK_PHONE: {
1836 return Phone.CONTENT_URI;
1838 case MODE_LEGACY_PICK_PHONE: {
1839 return Phones.CONTENT_URI;
1841 case MODE_PICK_POSTAL: {
1842 return StructuredPostal.CONTENT_URI;
1844 case MODE_LEGACY_PICK_POSTAL: {
1845 return ContactMethods.CONTENT_URI;
1847 case MODE_QUERY_PICK_TO_VIEW: {
1848 if (mQueryMode == QUERY_MODE_MAILTO) {
1849 return Uri.withAppendedPath(Email.CONTENT_FILTER_URI, Uri.encode(mQueryData));
1850 } else if (mQueryMode == QUERY_MODE_TEL) {
1851 return Uri.withAppendedPath(Phone.CONTENT_FILTER_URI, Uri.encode(mQueryData));
1853 return Contacts.CONTENT_URI;
1856 case MODE_QUERY_PICK: {
1857 return getContactFilterUri(mQueryData);
1863 throw new IllegalStateException("Can't generate URI: Unsupported Mode.");
1869 * Build the {@link Contacts#CONTENT_LOOKUP_URI} for the given
1870 * {@link ListView} position, using {@link #mAdapter}.
1872 private Uri getContactUri(int position) {
1873 if (position == ListView.INVALID_POSITION) {
1874 throw new IllegalArgumentException("Position not in list bounds");
1877 final Cursor cursor = (Cursor)mAdapter.getItem(position);
1879 case MODE_LEGACY_PICK_PERSON:
1880 case MODE_LEGACY_PICK_OR_CREATE_PERSON: {
1881 final long personId = cursor.getLong(SUMMARY_ID_COLUMN_INDEX);
1882 return ContentUris.withAppendedId(People.CONTENT_URI, personId);
1886 // Build and return soft, lookup reference
1887 final long contactId = cursor.getLong(SUMMARY_ID_COLUMN_INDEX);
1888 final String lookupKey = cursor.getString(SUMMARY_LOOKUP_KEY_COLUMN_INDEX);
1889 return Contacts.getLookupUri(contactId, lookupKey);
1895 * Build the {@link Uri} for the given {@link ListView} position, which can
1896 * be used as result when in {@link #MODE_MASK_PICKER} mode.
1898 private Uri getSelectedUri(int position) {
1899 if (position == ListView.INVALID_POSITION) {
1900 throw new IllegalArgumentException("Position not in list bounds");
1903 final long id = mAdapter.getItemId(position);
1905 case MODE_LEGACY_PICK_PERSON:
1906 case MODE_LEGACY_PICK_OR_CREATE_PERSON: {
1907 return ContentUris.withAppendedId(People.CONTENT_URI, id);
1909 case MODE_PICK_PHONE: {
1910 return ContentUris.withAppendedId(Data.CONTENT_URI, id);
1912 case MODE_LEGACY_PICK_PHONE: {
1913 return ContentUris.withAppendedId(Phones.CONTENT_URI, id);
1915 case MODE_PICK_POSTAL: {
1916 return ContentUris.withAppendedId(Data.CONTENT_URI, id);
1918 case MODE_LEGACY_PICK_POSTAL: {
1919 return ContentUris.withAppendedId(ContactMethods.CONTENT_URI, id);
1922 return getContactUri(position);
1927 String[] getProjectionForQuery() {
1929 case MODE_JOIN_CONTACT:
1930 case MODE_STREQUENT:
1934 case MODE_QUERY_PICK:
1936 case MODE_INSERT_OR_EDIT_CONTACT:
1938 case MODE_PICK_CONTACT:
1939 case MODE_PICK_OR_CREATE_CONTACT: {
1940 return CONTACTS_SUMMARY_PROJECTION;
1942 case MODE_LEGACY_PICK_PERSON:
1943 case MODE_LEGACY_PICK_OR_CREATE_PERSON: {
1944 return LEGACY_PEOPLE_PROJECTION ;
1946 case MODE_PICK_PHONE: {
1947 return PHONES_PROJECTION;
1949 case MODE_LEGACY_PICK_PHONE: {
1950 return LEGACY_PHONES_PROJECTION;
1952 case MODE_PICK_POSTAL: {
1953 return POSTALS_PROJECTION;
1955 case MODE_LEGACY_PICK_POSTAL: {
1956 return LEGACY_POSTALS_PROJECTION;
1958 case MODE_QUERY_PICK_TO_VIEW: {
1959 if (mQueryMode == QUERY_MODE_MAILTO) {
1960 return CONTACTS_SUMMARY_PROJECTION_FROM_EMAIL;
1961 } else if (mQueryMode == QUERY_MODE_TEL) {
1962 return PHONES_PROJECTION;
1968 // Default to normal aggregate projection
1969 return CONTACTS_SUMMARY_PROJECTION;
1972 private Bitmap loadContactPhoto(Uri lookupUri, BitmapFactory.Options options) {
1973 Cursor cursor = null;
1977 // TODO we should have a "photo" directory under the lookup URI itself
1978 Uri contactUri = Contacts.lookupContact(getContentResolver(), lookupUri);
1979 Uri photoUri = Uri.withAppendedPath(contactUri, Contacts.Photo.CONTENT_DIRECTORY);
1980 cursor = getContentResolver().query(photoUri, new String[] {Photo.PHOTO},
1982 if (cursor != null && cursor.moveToFirst()) {
1983 bm = ContactsUtils.loadContactPhoto(cursor, 0, options);
1986 if (cursor != null) {
1992 final int[] fallbacks = {
1993 R.drawable.ic_contact_picture,
1994 R.drawable.ic_contact_picture_2,
1995 R.drawable.ic_contact_picture_3
1997 bm = BitmapFactory.decodeResource(getResources(),
1998 fallbacks[new Random().nextInt(fallbacks.length)]);
2005 * Return the selection arguments for a default query based on the
2006 * {@link #mDisplayOnlyPhones} flag.
2008 private String getContactSelection() {
2009 if (mDisplayOnlyPhones) {
2010 return CLAUSE_ONLY_VISIBLE + " AND " + CLAUSE_ONLY_PHONES;
2012 return CLAUSE_ONLY_VISIBLE;
2016 private Uri getContactFilterUri(String filter) {
2017 if (!TextUtils.isEmpty(filter)) {
2018 return Uri.withAppendedPath(Contacts.CONTENT_FILTER_URI, Uri.encode(filter));
2020 return Contacts.CONTENT_URI;
2024 private Uri getPeopleFilterUri(String filter) {
2025 if (!TextUtils.isEmpty(filter)) {
2026 return Uri.withAppendedPath(People.CONTENT_FILTER_URI, Uri.encode(filter));
2028 return People.CONTENT_URI;
2032 private Uri getJoinSuggestionsUri(String filter) {
2033 Builder builder = Contacts.CONTENT_URI.buildUpon();
2034 builder.appendEncodedPath(String.valueOf(mQueryAggregateId));
2035 builder.appendEncodedPath(AggregationSuggestions.CONTENT_DIRECTORY);
2036 if (!TextUtils.isEmpty(filter)) {
2037 builder.appendEncodedPath(Uri.encode(filter));
2039 builder.appendQueryParameter("limit", String.valueOf(MAX_SUGGESTIONS));
2040 return builder.build();
2043 private String getSortOrder(String[] projectionType) {
2044 if (mSortOrder == ContactsContract.Preferences.SORT_ORDER_PRIMARY) {
2045 return Contacts.SORT_KEY_PRIMARY;
2047 return Contacts.SORT_KEY_ALTERNATIVE;
2052 mAdapter.setLoading(true);
2054 // Cancel any pending queries
2055 mQueryHandler.cancelOperation(QUERY_TOKEN);
2056 mQueryHandler.setLoadingJoinSuggestions(false);
2058 mSortOrder = mContactsPrefs.getSortOrder();
2059 mDisplayOrder = mContactsPrefs.getDisplayOrder();
2061 // When sort order and display order contradict each other, we want to
2062 // highlight the part of the name used for sorting.
2063 mHighlightWhenScrolling = false;
2064 if (mSortOrder == ContactsContract.Preferences.SORT_ORDER_PRIMARY &&
2065 mDisplayOrder == ContactsContract.Preferences.DISPLAY_ORDER_ALTERNATIVE) {
2066 mHighlightWhenScrolling = true;
2067 } else if (mSortOrder == ContactsContract.Preferences.SORT_ORDER_ALTERNATIVE &&
2068 mDisplayOrder == ContactsContract.Preferences.DISPLAY_ORDER_PRIMARY) {
2069 mHighlightWhenScrolling = true;
2072 String[] projection = getProjectionForQuery();
2073 String callingPackage = getCallingPackage();
2074 Uri uri = getUriToQuery();
2075 if (!TextUtils.isEmpty(callingPackage)) {
2076 uri = uri.buildUpon()
2077 .appendQueryParameter(ContactsContract.REQUESTING_PACKAGE_PARAM_KEY,
2082 // Kick off the new query
2085 mQueryHandler.startQuery(QUERY_TOKEN, null,
2086 uri, projection, getContactSelection(), null,
2087 getSortOrder(projection));
2091 case MODE_PICK_CONTACT:
2092 case MODE_PICK_OR_CREATE_CONTACT:
2093 case MODE_INSERT_OR_EDIT_CONTACT:
2094 mQueryHandler.startQuery(QUERY_TOKEN, null, uri,
2095 projection, getContactSelection(), null,
2096 getSortOrder(projection));
2099 case MODE_LEGACY_PICK_PERSON:
2100 case MODE_LEGACY_PICK_OR_CREATE_PERSON:
2101 mQueryHandler.startQuery(QUERY_TOKEN, null, uri,
2102 projection, null, null,
2103 getSortOrder(projection));
2107 case MODE_QUERY_PICK: {
2108 mQueryHandler.startQuery(QUERY_TOKEN, null, uri,
2109 projection, null, null,
2110 getSortOrder(projection));
2114 case MODE_QUERY_PICK_TO_VIEW: {
2115 mQueryHandler.startQuery(QUERY_TOKEN, null, uri, projection, null, null,
2116 getSortOrder(projection));
2121 mQueryHandler.startQuery(QUERY_TOKEN, null, uri,
2122 projection, Contacts.STARRED + "=1", null,
2123 getSortOrder(projection));
2127 mQueryHandler.startQuery(QUERY_TOKEN, null, uri,
2129 Contacts.TIMES_CONTACTED + " > 0", null,
2130 Contacts.TIMES_CONTACTED + " DESC, "
2131 + getSortOrder(projection));
2134 case MODE_STREQUENT:
2135 mQueryHandler.startQuery(QUERY_TOKEN, null, uri, projection, null, null, null);
2138 case MODE_PICK_PHONE:
2139 case MODE_LEGACY_PICK_PHONE:
2140 mQueryHandler.startQuery(QUERY_TOKEN, null, uri,
2141 projection, null, null, getSortOrder(projection));
2144 case MODE_PICK_POSTAL:
2145 mQueryHandler.startQuery(QUERY_TOKEN, null, uri,
2146 projection, null, null, getSortOrder(projection));
2149 case MODE_LEGACY_PICK_POSTAL:
2150 mQueryHandler.startQuery(QUERY_TOKEN, null, uri,
2152 ContactMethods.KIND + "=" + android.provider.Contacts.KIND_POSTAL, null,
2153 getSortOrder(projection));
2156 case MODE_JOIN_CONTACT:
2157 mQueryHandler.setLoadingJoinSuggestions(true);
2158 mQueryHandler.startQuery(QUERY_TOKEN, null, uri, projection,
2165 * Called from a background thread to do the filter and return the resulting cursor.
2167 * @param filter the text that was entered to filter on
2168 * @return a cursor with the results of the filter
2170 Cursor doFilter(String filter) {
2171 final ContentResolver resolver = getContentResolver();
2173 String[] projection = getProjectionForQuery();
2177 case MODE_PICK_CONTACT:
2178 case MODE_PICK_OR_CREATE_CONTACT:
2179 case MODE_INSERT_OR_EDIT_CONTACT: {
2180 return resolver.query(getContactFilterUri(filter), projection,
2181 getContactSelection(), null, getSortOrder(projection));
2184 case MODE_LEGACY_PICK_PERSON:
2185 case MODE_LEGACY_PICK_OR_CREATE_PERSON: {
2186 return resolver.query(getPeopleFilterUri(filter), projection, null, null,
2187 getSortOrder(projection));
2190 case MODE_STARRED: {
2191 return resolver.query(getContactFilterUri(filter), projection,
2192 Contacts.STARRED + "=1", null,
2193 getSortOrder(projection));
2196 case MODE_FREQUENT: {
2197 return resolver.query(getContactFilterUri(filter), projection,
2198 Contacts.TIMES_CONTACTED + " > 0", null,
2199 Contacts.TIMES_CONTACTED + " DESC, "
2200 + getSortOrder(projection));
2203 case MODE_STREQUENT: {
2205 if (!TextUtils.isEmpty(filter)) {
2206 uri = Uri.withAppendedPath(Contacts.CONTENT_STREQUENT_FILTER_URI,
2207 Uri.encode(filter));
2209 uri = Contacts.CONTENT_STREQUENT_URI;
2211 return resolver.query(uri, projection, null, null, null);
2214 case MODE_PICK_PHONE: {
2215 Uri uri = getUriToQuery();
2216 if (!TextUtils.isEmpty(filter)) {
2217 uri = Uri.withAppendedPath(Phone.CONTENT_FILTER_URI, Uri.encode(filter));
2219 return resolver.query(uri, projection, null, null,
2220 getSortOrder(projection));
2223 case MODE_LEGACY_PICK_PHONE: {
2224 //TODO: Support filtering here (bug 2092503)
2228 case MODE_JOIN_CONTACT: {
2230 // We are on a background thread. Run queries one after the other synchronously
2231 Cursor cursor = resolver.query(getJoinSuggestionsUri(filter), projection, null,
2233 mAdapter.setSuggestionsCursor(cursor);
2234 mJoinModeShowAllContacts = false;
2235 return resolver.query(getContactFilterUri(filter), projection,
2236 Contacts._ID + " != " + mQueryAggregateId + " AND " + CLAUSE_ONLY_VISIBLE,
2237 null, getSortOrder(projection));
2240 throw new UnsupportedOperationException("filtering not allowed in mode " + mMode);
2243 private Cursor getShowAllContactsLabelCursor(String[] projection) {
2244 MatrixCursor matrixCursor = new MatrixCursor(projection);
2245 Object[] row = new Object[projection.length];
2246 // The only columns we care about is the id
2247 row[SUMMARY_ID_COLUMN_INDEX] = JOIN_MODE_SHOW_ALL_CONTACTS_ID;
2248 matrixCursor.addRow(row);
2249 return matrixCursor;
2253 * Calls the currently selected list item.
2254 * @return true if the call was initiated, false otherwise
2256 boolean callSelection() {
2257 ListView list = getListView();
2258 if (list.hasFocus()) {
2259 Cursor cursor = (Cursor) list.getSelectedItem();
2260 return callContact(cursor);
2265 boolean callContact(Cursor cursor) {
2266 return callOrSmsContact(cursor, false /*call*/);
2269 boolean smsContact(Cursor cursor) {
2270 return callOrSmsContact(cursor, true /*sms*/);
2274 * Calls the contact which the cursor is point to.
2275 * @return true if the call was initiated, false otherwise
2277 boolean callOrSmsContact(Cursor cursor, boolean sendSms) {
2278 if (cursor != null) {
2279 boolean hasPhone = cursor.getInt(SUMMARY_HAS_PHONE_COLUMN_INDEX) != 0;
2281 // There is no phone number.
2286 String phone = null;
2287 Cursor phonesCursor = null;
2288 phonesCursor = queryPhoneNumbers(cursor.getLong(SUMMARY_ID_COLUMN_INDEX));
2289 if (phonesCursor == null || phonesCursor.getCount() == 0) {
2293 } else if (phonesCursor.getCount() == 1) {
2294 // only one number, call it.
2295 phone = phonesCursor.getString(phonesCursor.getColumnIndex(Phone.NUMBER));
2297 phonesCursor.moveToPosition(-1);
2298 while (phonesCursor.moveToNext()) {
2299 if (phonesCursor.getInt(phonesCursor.
2300 getColumnIndex(Phone.IS_SUPER_PRIMARY)) != 0) {
2301 // Found super primary, call it.
2302 phone = phonesCursor.
2303 getString(phonesCursor.getColumnIndex(Phone.NUMBER));
2309 if (phone == null) {
2310 // Display dialog to choose a number to call.
2311 PhoneDisambigDialog phoneDialog = new PhoneDisambigDialog(
2312 this, phonesCursor, sendSms);
2316 ContactsUtils.initiateSms(this, phone);
2318 ContactsUtils.initiateCall(this, phone);
2327 private Cursor queryPhoneNumbers(long contactId) {
2328 Uri baseUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
2329 Uri dataUri = Uri.withAppendedPath(baseUri, Contacts.Data.CONTENT_DIRECTORY);
2331 Cursor c = getContentResolver().query(dataUri,
2332 new String[] {Phone._ID, Phone.NUMBER, Phone.IS_SUPER_PRIMARY},
2333 Data.MIMETYPE + "=?", new String[] {Phone.CONTENT_ITEM_TYPE}, null);
2334 if (c != null && c.moveToFirst()) {
2341 * Signal an error to the user.
2343 void signalError() {
2344 //TODO play an error beep or something...
2347 Cursor getItemForView(View view) {
2348 ListView listView = getListView();
2349 int index = listView.getPositionForView(view);
2353 return (Cursor) listView.getAdapter().getItem(index);
2356 private static class QueryHandler extends AsyncQueryHandler {
2357 protected final WeakReference<ContactsListActivity> mActivity;
2358 protected boolean mLoadingJoinSuggestions = false;
2360 public QueryHandler(Context context) {
2361 super(context.getContentResolver());
2362 mActivity = new WeakReference<ContactsListActivity>((ContactsListActivity) context);
2365 public void setLoadingJoinSuggestions(boolean flag) {
2366 mLoadingJoinSuggestions = flag;
2370 protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
2371 final ContactsListActivity activity = mActivity.get();
2372 if (activity != null && !activity.isFinishing()) {
2374 // Whenever we get a suggestions cursor, we need to immediately kick off
2375 // another query for the complete list of contacts
2376 if (cursor != null && mLoadingJoinSuggestions) {
2377 mLoadingJoinSuggestions = false;
2378 if (cursor.getCount() > 0) {
2379 activity.mAdapter.setSuggestionsCursor(cursor);
2382 activity.mAdapter.setSuggestionsCursor(null);
2385 if (activity.mAdapter.mSuggestionsCursorCount == 0
2386 || !activity.mJoinModeShowAllContacts) {
2387 startQuery(QUERY_TOKEN, null, activity.getContactFilterUri(
2388 activity.mQueryData),
2389 CONTACTS_SUMMARY_PROJECTION,
2390 Contacts._ID + " != " + activity.mQueryAggregateId
2391 + " AND " + CLAUSE_ONLY_VISIBLE, null,
2392 activity.getSortOrder(CONTACTS_SUMMARY_PROJECTION));
2396 cursor = activity.getShowAllContactsLabelCursor(CONTACTS_SUMMARY_PROJECTION);
2399 // activity.setTextFilter(null);
2400 activity.mAdapter.changeCursor(cursor);
2402 // Now that the cursor is populated again, it's possible to restore the list state
2403 if (activity.mListState != null) {
2404 activity.mList.onRestoreInstanceState(activity.mListState);
2405 activity.mListState = null;
2413 final static class ContactListItemCache {
2415 public TextView headerText;
2416 public View divider;
2417 public TextView nameView;
2418 public View callView;
2419 public ImageView callButton;
2420 public CharArrayBuffer nameBuffer = new CharArrayBuffer(128);
2421 public TextView labelView;
2422 public TextView dataView;
2423 public CharArrayBuffer dataBuffer = new CharArrayBuffer(128);
2424 public ImageView presenceView;
2425 public QuickContactBadge photoView;
2426 public ImageView nonQuickContactPhotoView;
2427 public CharArrayBuffer highlightedTextBuffer = new CharArrayBuffer(128);
2428 public TextWithHighlighting textWithHighlighting;
2431 final static class PhotoInfo {
2432 public int position;
2433 public long photoId;
2435 public PhotoInfo(int position, long photoId) {
2436 this.position = position;
2437 this.photoId = photoId;
2439 public QuickContactBadge photoView;
2442 final static class PinnedHeaderCache {
2443 public TextView titleView;
2444 public ColorStateList textColor;
2445 public Drawable background;
2448 private final class ContactItemListAdapter extends ResourceCursorAdapter
2449 implements SectionIndexer, OnScrollListener, PinnedHeaderListView.PinnedHeaderAdapter {
2450 private SectionIndexer mIndexer;
2451 private String mAlphabet;
2452 private boolean mLoading = true;
2453 private CharSequence mUnknownNameText;
2454 private boolean mDisplayPhotos = false;
2455 private boolean mDisplayCallButton = false;
2456 private boolean mDisplayAdditionalData = true;
2457 private HashMap<Long, SoftReference<Bitmap>> mBitmapCache = null;
2458 private HashSet<ImageView> mItemsMissingImages = null;
2459 private int mFrequentSeparatorPos = ListView.INVALID_POSITION;
2460 private boolean mDisplaySectionHeaders = true;
2461 private int[] mSectionPositions;
2462 private Cursor mSuggestionsCursor;
2463 private int mSuggestionsCursorCount;
2464 private ImageFetchHandler mHandler;
2465 private ImageDbFetcher mImageFetcher;
2467 private static final int FETCH_IMAGE_MSG = 1;
2469 public ContactItemListAdapter(Context context) {
2470 super(context, R.layout.contacts_list_item, null, false);
2472 mHandler = new ImageFetchHandler();
2473 mAlphabet = context.getString(com.android.internal.R.string.fast_scroll_alphabet);
2475 mUnknownNameText = context.getText(android.R.string.unknownName);
2477 case MODE_LEGACY_PICK_POSTAL:
2478 case MODE_PICK_POSTAL:
2479 mDisplaySectionHeaders = false;
2481 case MODE_LEGACY_PICK_PHONE:
2482 case MODE_PICK_PHONE:
2483 mDisplaySectionHeaders = false;
2489 // Do not display the second line of text if in a specific SEARCH query mode, usually for
2490 // matching a specific E-mail or phone number. Any contact details
2491 // shown would be identical, and columns might not even be present
2492 // in the returned cursor.
2493 if (mQueryMode != QUERY_MODE_NONE) {
2494 mDisplayAdditionalData = false;
2497 if ((mMode & MODE_MASK_NO_DATA) == MODE_MASK_NO_DATA) {
2498 mDisplayAdditionalData = false;
2501 if ((mMode & MODE_MASK_SHOW_CALL_BUTTON) == MODE_MASK_SHOW_CALL_BUTTON) {
2502 mDisplayCallButton = true;
2505 if ((mMode & MODE_MASK_SHOW_PHOTOS) == MODE_MASK_SHOW_PHOTOS) {
2506 mDisplayPhotos = true;
2507 setViewResource(R.layout.contacts_list_item_photo);
2508 mBitmapCache = new HashMap<Long, SoftReference<Bitmap>>();
2509 mItemsMissingImages = new HashSet<ImageView>();
2512 if (mMode == MODE_STREQUENT || mMode == MODE_FREQUENT) {
2513 mDisplaySectionHeaders = false;
2517 private class ImageFetchHandler extends Handler {
2520 public void handleMessage(Message message) {
2521 if (ContactsListActivity.this.isFinishing()) {
2524 switch(message.what) {
2525 case FETCH_IMAGE_MSG: {
2526 final ImageView imageView = (ImageView) message.obj;
2527 if (imageView == null) {
2531 final PhotoInfo info = (PhotoInfo)imageView.getTag();
2536 final long photoId = info.photoId;
2541 SoftReference<Bitmap> photoRef = mBitmapCache.get(photoId);
2542 if (photoRef == null) {
2545 Bitmap photo = photoRef.get();
2546 if (photo == null) {
2547 mBitmapCache.remove(photoId);
2551 // Make sure the photoId on this image view has not changed
2552 // while we were loading the image.
2553 synchronized (imageView) {
2554 final PhotoInfo updatedInfo = (PhotoInfo)imageView.getTag();
2555 long currentPhotoId = updatedInfo.photoId;
2556 if (currentPhotoId == photoId) {
2557 imageView.setImageBitmap(photo);
2558 mItemsMissingImages.remove(imageView);
2566 public void clearImageFecthing() {
2567 removeMessages(FETCH_IMAGE_MSG);
2571 private class ImageDbFetcher implements Runnable {
2573 private ImageView mImageView;
2575 public ImageDbFetcher(long photoId, ImageView imageView) {
2576 this.mPhotoId = photoId;
2577 this.mImageView = imageView;
2581 if (ContactsListActivity.this.isFinishing()) {
2585 if (Thread.interrupted()) {
2586 // shutdown has been called.
2589 Bitmap photo = null;
2591 photo = ContactsUtils.loadContactPhoto(mContext, mPhotoId, null);
2592 } catch (OutOfMemoryError e) {
2593 // Not enough memory for the photo, do nothing.
2596 if (photo == null) {
2600 mBitmapCache.put(mPhotoId, new SoftReference<Bitmap>(photo));
2602 if (Thread.interrupted()) {
2603 // shutdown has been called.
2607 // Update must happen on UI thread
2608 Message msg = new Message();
2609 msg.what = FETCH_IMAGE_MSG;
2610 msg.obj = mImageView;
2611 mHandler.sendMessage(msg);
2615 public void setSuggestionsCursor(Cursor cursor) {
2616 if (mSuggestionsCursor != null) {
2617 mSuggestionsCursor.close();
2619 mSuggestionsCursor = cursor;
2620 mSuggestionsCursorCount = cursor == null ? 0 : cursor.getCount();
2623 private SectionIndexer getNewIndexer(Cursor cursor) {
2624 if (Locale.getDefault().getLanguage().equals(Locale.JAPAN.getLanguage())) {
2625 return new JapaneseContactListIndexer(cursor,
2626 SUMMARY_SORT_KEY_PRIMARY_COLUMN_INDEX);
2628 return new AlphabetIndexer(cursor, getSummaryDisplayNameColumnIndex(), mAlphabet);
2633 * Callback on the UI thread when the content observer on the backing cursor fires.
2634 * Instead of calling requery we need to do an async query so that the requery doesn't
2635 * block the UI thread for a long time.
2638 protected void onContentChanged() {
2639 CharSequence constraint = getTextFilter();
2640 if (!TextUtils.isEmpty(constraint)) {
2641 // Reset the filter state then start an async filter operation
2642 Filter filter = getFilter();
2643 filter.filter(constraint);
2645 // Start an async query
2650 public void setLoading(boolean loading) {
2655 public boolean isEmpty() {
2656 if ((mMode & MODE_MASK_CREATE_NEW) == MODE_MASK_CREATE_NEW) {
2657 // This mode mask adds a header and we always want it to show up, even
2658 // if the list is empty, so always claim the list is not empty.
2661 if (mCursor == null || mLoading) {
2662 // We don't want the empty state to show when loading.
2665 return super.isEmpty();
2671 public int getItemViewType(int position) {
2672 if (position == 0 && (mMode & MODE_MASK_SHOW_NUMBER_OF_CONTACTS) != 0) {
2673 return IGNORE_ITEM_VIEW_TYPE;
2675 if (isShowAllContactsItemPosition(position)) {
2676 return IGNORE_ITEM_VIEW_TYPE;
2678 if (getSeparatorId(position) != 0) {
2679 // We don't want the separator view to be recycled.
2680 return IGNORE_ITEM_VIEW_TYPE;
2682 return super.getItemViewType(position);
2686 public View getView(int position, View convertView, ViewGroup parent) {
2688 throw new IllegalStateException(
2689 "this should only be called when the cursor is valid");
2692 // handle the total contacts item
2693 if (position == 0 && (mMode & MODE_MASK_SHOW_NUMBER_OF_CONTACTS) != 0) {
2694 return getTotalContactCountView(parent);
2697 if (isShowAllContactsItemPosition(position)) {
2698 LayoutInflater inflater =
2699 (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
2700 return inflater.inflate(R.layout.contacts_list_show_all_item, parent, false);
2703 // Handle the separator specially
2704 int separatorId = getSeparatorId(position);
2705 if (separatorId != 0) {
2706 LayoutInflater inflater =
2707 (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
2708 TextView view = (TextView) inflater.inflate(R.layout.list_separator, parent, false);
2709 view.setText(separatorId);
2713 boolean showingSuggestion;
2715 if (mSuggestionsCursorCount != 0 && position < mSuggestionsCursorCount + 2) {
2716 showingSuggestion = true;
2717 cursor = mSuggestionsCursor;
2719 showingSuggestion = false;
2723 int realPosition = getRealPosition(position);
2724 if (!cursor.moveToPosition(realPosition)) {
2725 throw new IllegalStateException("couldn't move cursor to position " + position);
2729 if (convertView == null || convertView.getTag() == null) {
2730 v = newView(mContext, cursor, parent);
2734 bindView(v, mContext, cursor);
2735 bindSectionHeader(v, realPosition, mDisplaySectionHeaders && !showingSuggestion);
2740 private View getTotalContactCountView(ViewGroup parent) {
2741 final LayoutInflater inflater = getLayoutInflater();
2742 View view = inflater.inflate(R.layout.total_contacts, parent, false);
2744 TextView totalContacts = (TextView) view.findViewById(R.id.totalContactsText);
2745 TextView searchForMore = (TextView) view.findViewById(R.id.searchForMoreText);
2748 int count = getRealCount();
2750 if (mMode == MODE_QUERY || mMode == MODE_QUERY_PICK) {
2751 text = getQuantityText(count, R.string.listFoundAllContactsZero,
2752 R.plurals.listFoundAllContacts);
2753 searchForMore.setVisibility(View.GONE);
2754 } else if (mSearchMode && !TextUtils.isEmpty(getTextFilter())) {
2755 text = getQuantityText(count, R.string.listFoundAllContactsZero,
2756 R.plurals.searchFoundContacts);
2757 searchForMore.setVisibility(View.VISIBLE);
2759 if (mDisplayOnlyPhones) {
2760 text = getQuantityText(count, R.string.listTotalPhoneContactsZero,
2761 R.plurals.listTotalPhoneContacts);
2763 text = getQuantityText(count, R.string.listTotalAllContactsZero,
2764 R.plurals.listTotalAllContacts);
2766 searchForMore.setVisibility(View.GONE);
2768 totalContacts.setText(text);
2772 // TODO: fix PluralRules to handle zero correctly and use Resources.getQuantityText directly
2773 private String getQuantityText(int count, int zeroResourceId, int pluralResourceId) {
2775 return getString(zeroResourceId);
2777 String format = getResources().getQuantityText(pluralResourceId, count).toString();
2778 return String.format(format, count);
2782 private boolean isShowAllContactsItemPosition(int position) {
2783 return mMode == MODE_JOIN_CONTACT && mJoinModeShowAllContacts
2784 && mSuggestionsCursorCount != 0 && position == mSuggestionsCursorCount + 2;
2787 private int getSeparatorId(int position) {
2788 int separatorId = 0;
2789 if (position == mFrequentSeparatorPos) {
2790 separatorId = R.string.favoritesFrquentSeparator;
2792 if (mSuggestionsCursorCount != 0) {
2793 if (position == 0) {
2794 separatorId = R.string.separatorJoinAggregateSuggestions;
2795 } else if (position == mSuggestionsCursorCount + 1) {
2796 separatorId = R.string.separatorJoinAggregateAll;
2803 public View newView(Context context, Cursor cursor, ViewGroup parent) {
2804 final View view = super.newView(context, cursor, parent);
2806 final ContactListItemCache cache = new ContactListItemCache();
2807 cache.header = view.findViewById(R.id.header);
2808 cache.headerText = (TextView)view.findViewById(R.id.header_text);
2809 cache.divider = view.findViewById(R.id.list_divider);
2810 cache.nameView = (TextView) view.findViewById(R.id.name);
2811 cache.callView = view.findViewById(R.id.call_view);
2812 cache.callButton = (ImageView) view.findViewById(R.id.call_button);
2813 if (cache.callButton != null) {
2814 cache.callButton.setOnClickListener(ContactsListActivity.this);
2816 cache.labelView = (TextView) view.findViewById(R.id.label);
2817 cache.dataView = (TextView) view.findViewById(R.id.data);
2818 cache.presenceView = (ImageView) view.findViewById(R.id.presence);
2819 cache.photoView = (QuickContactBadge) view.findViewById(R.id.photo);
2820 if (cache.photoView != null) {
2821 cache.photoView.setExcludeMimes(new String[] {Contacts.CONTENT_ITEM_TYPE});
2823 cache.nonQuickContactPhotoView = (ImageView) view.findViewById(R.id.noQuickContactPhoto);
2824 cache.textWithHighlighting = mHighlightingAnimation.createTextWithHighlighting();
2830 public void bindView(View view, Context context, Cursor cursor) {
2831 final ContactListItemCache cache = (ContactListItemCache) view.getTag();
2833 TextView dataView = cache.dataView;
2834 TextView labelView = cache.labelView;
2835 int typeColumnIndex;
2836 int dataColumnIndex;
2837 int labelColumnIndex;
2839 int nameColumnIndex;
2840 boolean displayAdditionalData = mDisplayAdditionalData;
2841 boolean highlightingEnabled = false;
2843 case MODE_PICK_PHONE:
2844 case MODE_LEGACY_PICK_PHONE: {
2845 nameColumnIndex = PHONE_DISPLAY_NAME_COLUMN_INDEX;
2846 dataColumnIndex = PHONE_NUMBER_COLUMN_INDEX;
2847 typeColumnIndex = PHONE_TYPE_COLUMN_INDEX;
2848 labelColumnIndex = PHONE_LABEL_COLUMN_INDEX;
2849 defaultType = Phone.TYPE_HOME;
2852 case MODE_PICK_POSTAL:
2853 case MODE_LEGACY_PICK_POSTAL: {
2854 nameColumnIndex = POSTAL_DISPLAY_NAME_COLUMN_INDEX;
2855 dataColumnIndex = POSTAL_ADDRESS_COLUMN_INDEX;
2856 typeColumnIndex = POSTAL_TYPE_COLUMN_INDEX;
2857 labelColumnIndex = POSTAL_LABEL_COLUMN_INDEX;
2858 defaultType = StructuredPostal.TYPE_HOME;
2862 nameColumnIndex = getSummaryDisplayNameColumnIndex();
2863 dataColumnIndex = -1;
2864 typeColumnIndex = -1;
2865 labelColumnIndex = -1;
2866 defaultType = Phone.TYPE_HOME;
2867 displayAdditionalData = false;
2868 highlightingEnabled = mHighlightWhenScrolling && mMode != MODE_STREQUENT;
2873 cursor.copyStringToBuffer(nameColumnIndex, cache.nameBuffer);
2874 int size = cache.nameBuffer.sizeCopied;
2876 if (highlightingEnabled) {
2877 buildDisplayNameWithHighlighting(cache.nameView, cursor, cache.nameBuffer,
2878 cache.highlightedTextBuffer, cache.textWithHighlighting);
2880 cache.nameView.setText(cache.nameBuffer.data, 0, size);
2883 cache.nameView.setText(mUnknownNameText);
2886 boolean hasPhone = cursor.getColumnCount() >= SUMMARY_HAS_PHONE_COLUMN_INDEX
2887 && cursor.getInt(SUMMARY_HAS_PHONE_COLUMN_INDEX) != 0;
2889 // Make the call button visible if requested.
2890 if (mDisplayCallButton && hasPhone) {
2891 int pos = cursor.getPosition();
2892 cache.callView.setVisibility(View.VISIBLE);
2893 cache.callButton.setTag(pos);
2895 cache.callView.setVisibility(View.GONE);
2898 // Set the photo, if requested
2899 if (mDisplayPhotos) {
2900 boolean useQuickContact = (mMode & MODE_MASK_DISABLE_QUIKCCONTACT) == 0;
2903 if (!cursor.isNull(SUMMARY_PHOTO_ID_COLUMN_INDEX)) {
2904 photoId = cursor.getLong(SUMMARY_PHOTO_ID_COLUMN_INDEX);
2907 ImageView viewToUse;
2908 if (useQuickContact) {
2909 viewToUse = cache.photoView;
2910 // Build soft lookup reference
2911 final long contactId = cursor.getLong(SUMMARY_ID_COLUMN_INDEX);
2912 final String lookupKey = cursor.getString(SUMMARY_LOOKUP_KEY_COLUMN_INDEX);
2913 cache.photoView.assignContactUri(Contacts.getLookupUri(contactId, lookupKey));
2914 cache.photoView.setVisibility(View.VISIBLE);
2915 cache.nonQuickContactPhotoView.setVisibility(View.INVISIBLE);
2917 viewToUse = cache.nonQuickContactPhotoView;
2918 cache.photoView.setVisibility(View.INVISIBLE);
2919 cache.nonQuickContactPhotoView.setVisibility(View.VISIBLE);
2923 final int position = cursor.getPosition();
2924 viewToUse.setTag(new PhotoInfo(position, photoId));
2927 viewToUse.setImageResource(R.drawable.ic_contact_list_picture);
2930 Bitmap photo = null;
2932 // Look for the cached bitmap
2933 SoftReference<Bitmap> ref = mBitmapCache.get(photoId);
2936 if (photo == null) {
2937 mBitmapCache.remove(photoId);
2941 // Bind the photo, or use the fallback no photo resource
2942 if (photo != null) {
2943 viewToUse.setImageBitmap(photo);
2946 viewToUse.setImageResource(R.drawable.ic_contact_list_picture);
2948 // Add it to a set of images that are populated asynchronously.
2949 mItemsMissingImages.add(viewToUse);
2951 if (mScrollState != OnScrollListener.SCROLL_STATE_FLING) {
2953 // Scrolling is idle or slow, go get the image right now.
2954 sendFetchImageMessage(viewToUse);
2960 ImageView presenceView = cache.presenceView;
2961 if ((mMode & MODE_MASK_NO_PRESENCE) == 0) {
2962 // Set the proper icon (star or presence or nothing)
2964 if (!cursor.isNull(SUMMARY_PRESENCE_STATUS_COLUMN_INDEX)) {
2965 serverStatus = cursor.getInt(SUMMARY_PRESENCE_STATUS_COLUMN_INDEX);
2966 presenceView.setImageResource(
2967 Presence.getPresenceIconResourceId(serverStatus));
2968 presenceView.setVisibility(View.VISIBLE);
2970 presenceView.setVisibility(View.GONE);
2973 presenceView.setVisibility(View.GONE);
2976 if (!displayAdditionalData) {
2977 cache.dataView.setVisibility(View.GONE);
2978 cache.labelView.setVisibility(View.GONE);
2983 cursor.copyStringToBuffer(dataColumnIndex, cache.dataBuffer);
2985 size = cache.dataBuffer.sizeCopied;
2987 dataView.setText(cache.dataBuffer.data, 0, size);
2988 dataView.setVisibility(View.VISIBLE);
2990 dataView.setVisibility(View.GONE);
2994 if (!cursor.isNull(typeColumnIndex)) {
2995 labelView.setVisibility(View.VISIBLE);
2997 final int type = cursor.getInt(typeColumnIndex);
2998 final String label = cursor.getString(labelColumnIndex);
3000 if (mMode == MODE_LEGACY_PICK_POSTAL || mMode == MODE_PICK_POSTAL) {
3001 labelView.setText(StructuredPostal.getTypeLabel(context.getResources(), type,
3004 labelView.setText(Phone.getTypeLabel(context.getResources(), type, label));
3007 // There is no label, hide the the view
3008 labelView.setVisibility(View.GONE);
3013 * Computes the span of the display name that has highlighted parts and configures
3014 * the display name text view accordingly.
3016 private void buildDisplayNameWithHighlighting(TextView textView, Cursor cursor,
3017 CharArrayBuffer buffer1, CharArrayBuffer buffer2,
3018 TextWithHighlighting textWithHighlighting) {
3019 int oppositeDisplayOrderColumnIndex;
3020 if (mDisplayOrder == ContactsContract.Preferences.DISPLAY_ORDER_PRIMARY) {
3021 oppositeDisplayOrderColumnIndex = SUMMARY_DISPLAY_NAME_ALTERNATIVE_COLUMN_INDEX;
3023 oppositeDisplayOrderColumnIndex = SUMMARY_DISPLAY_NAME_PRIMARY_COLUMN_INDEX;
3025 cursor.copyStringToBuffer(oppositeDisplayOrderColumnIndex, buffer2);
3027 textWithHighlighting.setText(buffer1, buffer2);
3028 textView.setText(textWithHighlighting);
3031 private void bindSectionHeader(View view, int position, boolean displaySectionHeaders) {
3032 final ContactListItemCache cache = (ContactListItemCache) view.getTag();
3033 if (!displaySectionHeaders) {
3034 cache.header.setVisibility(View.GONE);
3035 cache.divider.setVisibility(View.VISIBLE);
3037 final int section = getSectionForPosition(position);
3038 if (getPositionForSection(section) == position) {
3039 String title = mIndexer.getSections()[section].toString().trim();
3040 if (!TextUtils.isEmpty(title)) {
3041 cache.headerText.setText(title);
3042 cache.header.setVisibility(View.VISIBLE);
3044 cache.header.setVisibility(View.GONE);
3047 cache.header.setVisibility(View.GONE);
3050 // move the divider for the last item in a section
3051 if (getPositionForSection(section + 1) - 1 == position) {
3052 cache.divider.setVisibility(View.GONE);
3054 cache.divider.setVisibility(View.VISIBLE);
3060 public void changeCursor(Cursor cursor) {
3063 // Get the split between starred and frequent items, if the mode is strequent
3064 mFrequentSeparatorPos = ListView.INVALID_POSITION;
3065 int cursorCount = 0;
3066 if (cursor != null && (cursorCount = cursor.getCount()) > 0
3067 && mMode == MODE_STREQUENT) {
3069 for (int i = 0; cursor.moveToNext(); i++) {
3070 int starred = cursor.getInt(SUMMARY_STARRED_COLUMN_INDEX);
3073 // Only add the separator when there are starred items present
3074 mFrequentSeparatorPos = i;
3081 super.changeCursor(cursor);
3082 // Update the indexer for the fast scroll widget
3083 updateIndexer(cursor);
3086 private void updateIndexer(Cursor cursor) {
3087 if (mIndexer == null) {
3088 mIndexer = getNewIndexer(cursor);
3090 if (Locale.getDefault().equals(Locale.JAPAN)) {
3091 if (mIndexer instanceof JapaneseContactListIndexer) {
3092 ((JapaneseContactListIndexer)mIndexer).setCursor(cursor);
3094 mIndexer = getNewIndexer(cursor);
3097 if (mIndexer instanceof AlphabetIndexer) {
3098 ((AlphabetIndexer)mIndexer).setCursor(cursor);
3100 mIndexer = getNewIndexer(cursor);
3105 int sectionCount = mIndexer.getSections().length;
3106 if (mSectionPositions == null || mSectionPositions.length != sectionCount) {
3107 mSectionPositions = new int[sectionCount];
3109 for (int i = 0; i < sectionCount; i++) {
3110 mSectionPositions[i] = ListView.INVALID_POSITION;
3115 * Run the query on a helper thread. Beware that this code does not run
3116 * on the main UI thread!
3119 public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
3120 return doFilter(constraint.toString());
3123 public Object [] getSections() {
3124 if (mMode == MODE_STARRED) {
3125 return new String[] { " " };
3127 return mIndexer.getSections();
3131 public int getPositionForSection(int sectionIndex) {
3132 if (mMode == MODE_STARRED) {
3136 if (sectionIndex < 0 || sectionIndex >= mSectionPositions.length) {
3140 if (mIndexer == null) {
3141 Cursor cursor = mAdapter.getCursor();
3142 if (cursor == null) {
3143 // No cursor, the section doesn't exist so just return 0
3146 mIndexer = getNewIndexer(cursor);
3149 int position = mSectionPositions[sectionIndex];
3150 if (position == ListView.INVALID_POSITION) {
3151 position = mSectionPositions[sectionIndex] =
3152 mIndexer.getPositionForSection(sectionIndex);
3158 public int getSectionForPosition(int position) {
3159 // The current implementations of SectionIndexers (specifically the Japanese indexer)
3160 // only work in one direction: given a section they can calculate the position.
3161 // Here we are using that existing functionality to do the reverse mapping. We are
3162 // performing binary search in the mSectionPositions array, which itself is populated
3163 // lazily using the "forward" mapping supported by the indexer.
3166 int end = mSectionPositions.length;
3167 while (start != end) {
3169 // We are making the binary search slightly asymmetrical, because the
3170 // user is more likely to be scrolling the list from the top down.
3171 int pivot = start + (end - start) / 4;
3173 int value = getPositionForSection(pivot);
3174 if (value <= position) {
3181 // The variable "start" cannot be 0, as long as the indexer is implemented properly
3182 // and actually maps position = 0 to section = 0
3187 public boolean areAllItemsEnabled() {
3188 return mMode != MODE_STARRED
3189 && (mMode & MODE_MASK_SHOW_NUMBER_OF_CONTACTS) == 0
3190 && mSuggestionsCursorCount == 0;
3194 public boolean isEnabled(int position) {
3195 if ((mMode & MODE_MASK_SHOW_NUMBER_OF_CONTACTS) != 0) {
3196 if (position == 0) {
3202 if (mSuggestionsCursorCount > 0) {
3203 return position != 0 && position != mSuggestionsCursorCount + 1;
3205 return position != mFrequentSeparatorPos;
3209 public int getCount() {
3213 int superCount = super.getCount();
3214 if ((mMode & MODE_MASK_SHOW_NUMBER_OF_CONTACTS) != 0 && superCount > 0) {
3215 // We don't want to count this header if it's the only thing visible, so that
3216 // the empty text will display.
3219 if (mSuggestionsCursorCount != 0) {
3220 // When showing suggestions, we have 2 additional list items: the "Suggestions"
3221 // and "All contacts" headers.
3222 return mSuggestionsCursorCount + superCount + 2;
3224 else if (mFrequentSeparatorPos != ListView.INVALID_POSITION) {
3225 // When showing strequent list, we have an additional list item - the separator.
3226 return superCount + 1;
3233 * Gets the actual count of contacts and excludes all the headers.
3235 public int getRealCount() {
3236 return super.getCount();
3239 private int getRealPosition(int pos) {
3240 if ((mMode & MODE_MASK_SHOW_NUMBER_OF_CONTACTS) != 0) {
3243 if (mSuggestionsCursorCount != 0) {
3244 // When showing suggestions, we have 2 additional list items: the "Suggestions"
3245 // and "All contacts" separators.
3246 if (pos < mSuggestionsCursorCount + 2) {
3247 // We are in the upper partition (Suggestions). Adjusting for the "Suggestions"
3251 // We are in the lower partition (All contacts). Adjusting for the size
3252 // of the upper partition plus the two separators.
3253 return pos - mSuggestionsCursorCount - 2;
3255 } else if (mFrequentSeparatorPos == ListView.INVALID_POSITION) {
3256 // No separator, identity map
3258 } else if (pos <= mFrequentSeparatorPos) {
3259 // Before or at the separator, identity map
3262 // After the separator, remove 1 from the pos to get the real underlying pos
3268 public Object getItem(int pos) {
3269 if (mSuggestionsCursorCount != 0 && pos <= mSuggestionsCursorCount) {
3270 mSuggestionsCursor.moveToPosition(getRealPosition(pos));
3271 return mSuggestionsCursor;
3273 return super.getItem(getRealPosition(pos));
3278 public long getItemId(int pos) {
3279 if (mSuggestionsCursorCount != 0 && pos < mSuggestionsCursorCount + 2) {
3280 if (mSuggestionsCursor.moveToPosition(pos - 1)) {
3281 return mSuggestionsCursor.getLong(mRowIDColumn);
3286 return super.getItemId(getRealPosition(pos));
3289 public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
3290 int totalItemCount) {
3291 if (view instanceof PinnedHeaderListView) {
3292 ((PinnedHeaderListView)view).configureHeaderView(firstVisibleItem);
3296 public void onScrollStateChanged(AbsListView view, int scrollState) {
3297 if (mHighlightWhenScrolling) {
3298 if (scrollState != OnScrollListener.SCROLL_STATE_IDLE) {
3299 mHighlightingAnimation.startHighlighting();
3301 mHighlightingAnimation.stopHighlighting();
3305 mScrollState = scrollState;
3306 if (scrollState == OnScrollListener.SCROLL_STATE_FLING) {
3307 // If we are in a fling, stop loading images.
3308 clearImageFetching();
3309 } else if (mDisplayPhotos) {
3310 processMissingImageItems(view);
3314 private void processMissingImageItems(AbsListView view) {
3315 for (ImageView iv : mItemsMissingImages) {
3316 sendFetchImageMessage(iv);
3320 private void sendFetchImageMessage(ImageView view) {
3321 final PhotoInfo info = (PhotoInfo) view.getTag();
3325 final long photoId = info.photoId;
3329 mImageFetcher = new ImageDbFetcher(photoId, view);
3330 synchronized (ContactsListActivity.this) {
3331 // can't sync on sImageFetchThreadPool.
3332 if (sImageFetchThreadPool == null) {
3333 // Don't use more than 3 threads at a time to update. The thread pool will be
3334 // shared by all contact items.
3335 sImageFetchThreadPool = Executors.newFixedThreadPool(3);
3337 sImageFetchThreadPool.execute(mImageFetcher);
3343 * Stop the image fetching for ALL contacts, if one is in progress we'll
3344 * not query the database.
3346 * TODO: move this method to ContactsListActivity, it does not apply to the current
3349 public void clearImageFetching() {
3350 synchronized (ContactsListActivity.this) {
3351 if (sImageFetchThreadPool != null) {
3352 sImageFetchThreadPool.shutdownNow();
3353 sImageFetchThreadPool = null;
3357 mHandler.clearImageFecthing();
3361 * Computes the state of the pinned header. It can be invisible, fully
3362 * visible or partially pushed up out of the view.
3364 public int getPinnedHeaderState(int position) {
3365 if (mIndexer == null || mCursor == null || mCursor.getCount() == 0) {
3366 return PINNED_HEADER_GONE;
3369 int realPosition = getRealPosition(position);
3370 if (realPosition < 0) {
3371 return PINNED_HEADER_GONE;
3374 // The header should get pushed up if the top item shown
3375 // is the last item in a section for a particular letter.
3376 int section = getSectionForPosition(realPosition);
3377 int nextSectionPosition = getPositionForSection(section + 1);
3378 if (nextSectionPosition != -1 && realPosition == nextSectionPosition - 1) {
3379 return PINNED_HEADER_PUSHED_UP;
3382 return PINNED_HEADER_VISIBLE;
3386 * Configures the pinned header by setting the appropriate text label
3387 * and also adjusting color if necessary. The color needs to be
3388 * adjusted when the pinned header is being pushed up from the view.
3390 public void configurePinnedHeader(View header, int position, int alpha) {
3391 PinnedHeaderCache cache = (PinnedHeaderCache)header.getTag();
3392 if (cache == null) {
3393 cache = new PinnedHeaderCache();
3394 cache.titleView = (TextView)header.findViewById(R.id.header_text);
3395 cache.textColor = cache.titleView.getTextColors();
3396 cache.background = header.getBackground();
3397 header.setTag(cache);
3400 int realPosition = getRealPosition(position);
3401 int section = getSectionForPosition(realPosition);
3403 String title = mIndexer.getSections()[section].toString().trim();
3404 cache.titleView.setText(title);
3407 // Opaque: use the default background, and the original text color
3408 header.setBackgroundDrawable(cache.background);
3409 cache.titleView.setTextColor(cache.textColor);
3411 // Faded: use a solid color approximation of the background, and
3412 // a translucent text color
3413 header.setBackgroundColor(Color.rgb(
3414 Color.red(mPinnedHeaderBackgroundColor) * alpha / 255,
3415 Color.green(mPinnedHeaderBackgroundColor) * alpha / 255,
3416 Color.blue(mPinnedHeaderBackgroundColor) * alpha / 255));
3418 int textColor = cache.textColor.getDefaultColor();
3419 cache.titleView.setTextColor(Color.argb(alpha,
3420 Color.red(textColor), Color.green(textColor), Color.blue(textColor)));