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