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