OSDN Git Service

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