OSDN Git Service

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