• 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 android.app.Activity;
20 import android.app.AlertDialog;
21 import android.app.ListActivity;
22 import android.app.SearchManager;
23 import android.content.AsyncQueryHandler;
24 import android.content.ContentResolver;
25 import android.content.ContentUris;
26 import android.content.ContentValues;
27 import android.content.Context;
28 import android.content.DialogInterface;
29 import android.content.IContentProvider;
30 import android.content.ISyncAdapter;
31 import android.content.Intent;
32 import android.content.SharedPreferences;
33 import android.content.res.Resources;
34 import android.database.CharArrayBuffer;
35 import android.database.Cursor;
36 import android.graphics.Bitmap;
37 import android.graphics.BitmapFactory;
38 import android.graphics.Canvas;
39 import android.graphics.Paint;
40 import android.graphics.Rect;
41 import android.graphics.Typeface;
42 import android.graphics.drawable.BitmapDrawable;
43 import android.graphics.drawable.Drawable;
44 import android.net.Uri;
45 import android.os.Bundle;
46 import android.os.Handler;
47 import android.os.Parcelable;
48 import android.os.RemoteException;
49 import android.preference.PreferenceManager;
50 import android.provider.Contacts;
51 import android.provider.Contacts.ContactMethods;
52 import android.provider.Contacts.Groups;
53 import android.provider.Contacts.Intents;
54 import android.provider.Contacts.People;
55 import android.provider.Contacts.Phones;
56 import android.provider.Contacts.Presence;
57 import android.provider.Contacts.Intents.Insert;
58 import android.provider.Contacts.Intents.UI;
59 import android.text.TextUtils;
60 import android.util.Log;
61 import android.util.SparseArray;
62 import android.view.ContextMenu;
63 import android.view.Gravity;
64 import android.view.KeyEvent;
65 import android.view.LayoutInflater;
66 import android.view.Menu;
67 import android.view.MenuItem;
68 import android.view.View;
69 import android.view.ViewGroup;
70 import android.view.ContextMenu.ContextMenuInfo;
71 import android.view.inputmethod.InputMethodManager;
72 import android.widget.AdapterView;
73 import android.widget.AlphabetIndexer;
74 import android.widget.Filter;
75 import android.widget.ImageView;
76 import android.widget.ListView;
77 import android.widget.ResourceCursorAdapter;
78 import android.widget.SectionIndexer;
79 import android.widget.TextView;
80 
81 import java.lang.ref.SoftReference;
82 import java.lang.ref.WeakReference;
83 import java.util.ArrayList;
84 import java.util.Locale;
85 
86 /**
87  * Displays a list of contacts. Usually is embedded into the ContactsActivity.
88  */
89 public final class ContactsListActivity extends ListActivity
90         implements View.OnCreateContextMenuListener, DialogInterface.OnClickListener {
91     private static final String TAG = "ContactsListActivity";
92 
93     private static final boolean ENABLE_ACTION_ICON_OVERLAYS = false;
94 
95     private static final String LIST_STATE_KEY = "liststate";
96     private static final String FOCUS_KEY = "focused";
97 
98     static final int MENU_ITEM_VIEW_CONTACT = 1;
99     static final int MENU_ITEM_CALL = 2;
100     static final int MENU_ITEM_EDIT_BEFORE_CALL = 3;
101     static final int MENU_ITEM_SEND_SMS = 4;
102     static final int MENU_ITEM_SEND_IM = 5;
103     static final int MENU_ITEM_EDIT = 6;
104     static final int MENU_ITEM_DELETE = 7;
105     static final int MENU_ITEM_TOGGLE_STAR = 8;
106 
107     public static final int MENU_SEARCH = 1;
108     public static final int MENU_DIALER = 9;
109     public static final int MENU_NEW_CONTACT = 10;
110     public static final int MENU_DISPLAY_GROUP = 11;
111     public static final int MENU_IMPORT_CONTACTS = 12;
112     public static final int MENU_EXPORT_CONTACTS = 13;
113 
114     private static final int SUBACTIVITY_NEW_CONTACT = 1;
115 
116     /** Mask for picker mode */
117     static final int MODE_MASK_PICKER = 0x80000000;
118     /** Mask for no presence mode */
119     static final int MODE_MASK_NO_PRESENCE = 0x40000000;
120     /** Mask for enabling list filtering */
121     static final int MODE_MASK_NO_FILTER = 0x20000000;
122     /** Mask for having a "create new contact" header in the list */
123     static final int MODE_MASK_CREATE_NEW = 0x10000000;
124     /** Mask for showing photos in the list */
125     static final int MODE_MASK_SHOW_PHOTOS = 0x08000000;
126 
127     /** Unknown mode */
128     static final int MODE_UNKNOWN = 0;
129     /** Show members of the "Contacts" group */
130     static final int MODE_GROUP = 5;
131     /** Show all contacts sorted alphabetically */
132     static final int MODE_ALL_CONTACTS = 10;
133     /** Show all contacts with phone numbers, sorted alphabetically */
134     static final int MODE_WITH_PHONES = 15;
135     /** Show all starred contacts */
136     static final int MODE_STARRED = 20;
137     /** Show frequently contacted contacts */
138     static final int MODE_FREQUENT = 30;
139     /** Show starred and the frequent */
140     static final int MODE_STREQUENT = 35 | MODE_MASK_SHOW_PHOTOS;
141     /** Show all contacts and pick them when clicking */
142     static final int MODE_PICK_CONTACT = 40 | MODE_MASK_PICKER;
143     /** Show all contacts as well as the option to create a new one */
144     static final int MODE_PICK_OR_CREATE_CONTACT = 42 | MODE_MASK_PICKER | MODE_MASK_CREATE_NEW;
145     /** Show all contacts and pick them when clicking, and allow creating a new contact */
146     static final int MODE_INSERT_OR_EDIT_CONTACT = 45 | MODE_MASK_PICKER | MODE_MASK_CREATE_NEW;
147     /** Show all phone numbers and pick them when clicking */
148     static final int MODE_PICK_PHONE = 50 | MODE_MASK_PICKER | MODE_MASK_NO_PRESENCE;
149     /** Show all postal addresses and pick them when clicking */
150     static final int MODE_PICK_POSTAL =
151             55 | MODE_MASK_PICKER | MODE_MASK_NO_PRESENCE | MODE_MASK_NO_FILTER;
152     /** Run a search query */
153     static final int MODE_QUERY = 60 | MODE_MASK_NO_FILTER;
154     /** Run a search query in PICK mode, but that still launches to VIEW */
155     static final int MODE_QUERY_PICK_TO_VIEW = 65 | MODE_MASK_NO_FILTER | MODE_MASK_PICKER;
156 
157     static final int DEFAULT_MODE = MODE_ALL_CONTACTS;
158 
159     /**
160      * The type of data to display in the main contacts list.
161      */
162     static final String PREF_DISPLAY_TYPE = "display_system_group";
163 
164     /** Unknown display type. */
165     static final int DISPLAY_TYPE_UNKNOWN = -1;
166     /** Display all contacts */
167     static final int DISPLAY_TYPE_ALL = 0;
168     /** Display all contacts that have phone numbers */
169     static final int DISPLAY_TYPE_ALL_WITH_PHONES = 1;
170     /** Display a system group */
171     static final int DISPLAY_TYPE_SYSTEM_GROUP = 2;
172     /** Display a user group */
173     static final int DISPLAY_TYPE_USER_GROUP = 3;
174 
175     /**
176      * Info about what to display. If {@link #PREF_DISPLAY_TYPE}
177      * is {@link #DISPLAY_TYPE_SYSTEM_GROUP} then this will be the system id.
178      * If {@link #PREF_DISPLAY_TYPE} is {@link #DISPLAY_TYPE_USER_GROUP} then this will
179      * be the group name.
180      */
181     static final String PREF_DISPLAY_INFO = "display_group";
182 
183 
184     static final String NAME_COLUMN = People.DISPLAY_NAME;
185     static final String SORT_STRING = People.SORT_STRING;
186 
187     static final String[] CONTACTS_PROJECTION = new String[] {
188         People._ID, // 0
189         NAME_COLUMN, // 1
190         People.NUMBER, // 2
191         People.TYPE, // 3
192         People.LABEL, // 4
193         People.STARRED, // 5
194         People.PRIMARY_PHONE_ID, // 6
195         People.PRIMARY_EMAIL_ID, // 7
196         People.PRESENCE_STATUS, // 8
197         SORT_STRING, // 9
198     };
199 
200     static final String[] SIMPLE_CONTACTS_PROJECTION = new String[] {
201         People._ID, // 0
202         NAME_COLUMN, // 1
203     };
204 
205     static final String[] STREQUENT_PROJECTION = new String[] {
206         People._ID, // 0
207         NAME_COLUMN, // 1
208         People.NUMBER, // 2
209         People.TYPE, // 3
210         People.LABEL, // 4
211         People.STARRED, // 5
212         People.PRIMARY_PHONE_ID, // 6
213         People.PRIMARY_EMAIL_ID, // 7
214         People.PRESENCE_STATUS, // 8
215         "photo_data", // 9
216         People.TIMES_CONTACTED, // 10 (not displayed, but required for the order by to work)
217     };
218 
219     static final String[] PHONES_PROJECTION = new String[] {
220         Phones._ID, // 0
221         NAME_COLUMN, // 1
222         Phones.NUMBER, // 2
223         Phones.TYPE, // 3
224         Phones.LABEL, // 4
225         Phones.STARRED, // 5
226         Phones.PERSON_ID, // 6
227     };
228 
229     static final String[] CONTACT_METHODS_PROJECTION = new String[] {
230         ContactMethods._ID, // 0
231         NAME_COLUMN, // 1
232         ContactMethods.DATA, // 2
233         ContactMethods.TYPE, // 3
234         ContactMethods.LABEL, // 4
235         ContactMethods.STARRED, // 5
236         ContactMethods.PERSON_ID, // 6
237     };
238 
239     static final int ID_COLUMN_INDEX = 0;
240     static final int NAME_COLUMN_INDEX = 1;
241     static final int NUMBER_COLUMN_INDEX = 2;
242     static final int DATA_COLUMN_INDEX = 2;
243     static final int TYPE_COLUMN_INDEX = 3;
244     static final int LABEL_COLUMN_INDEX = 4;
245     static final int STARRED_COLUMN_INDEX = 5;
246     static final int PRIMARY_PHONE_ID_COLUMN_INDEX = 6;
247     static final int PRIMARY_EMAIL_ID_COLUMN_INDEX = 7;
248     static final int SERVER_STATUS_COLUMN_INDEX = 8;
249     static final int PHOTO_COLUMN_INDEX = 9;
250     static final int SORT_STRING_INDEX = 9;
251 
252     static final int PHONES_PERSON_ID_INDEX = 6;
253     static final int SIMPLE_CONTACTS_PERSON_ID_INDEX = 0;
254 
255     static final int DISPLAY_GROUP_INDEX_ALL_CONTACTS = 0;
256     static final int DISPLAY_GROUP_INDEX_ALL_CONTACTS_WITH_PHONES = 1;
257     static final int DISPLAY_GROUP_INDEX_MY_CONTACTS = 2;
258 
259     private static final int QUERY_TOKEN = 42;
260 
261     private static final String[] GROUPS_PROJECTION = new String[] {
262         Groups.SYSTEM_ID, // 0
263         Groups.NAME, // 1
264     };
265     private static final int GROUPS_COLUMN_INDEX_SYSTEM_ID = 0;
266     private static final int GROUPS_COLUMN_INDEX_NAME = 1;
267 
268     static final String GROUP_WITH_PHONES = "android_smartgroup_phone";
269 
270     ContactItemListAdapter mAdapter;
271 
272     int mMode = DEFAULT_MODE;
273     // The current display group
274     private String mDisplayInfo;
275     private int mDisplayType;
276     // The current list of display groups, during selection from menu
277     private CharSequence[] mDisplayGroups;
278     // If true position 2 in mDisplayGroups is the MyContacts group
279     private boolean mDisplayGroupsIncludesMyContacts = false;
280 
281     private int mDisplayGroupOriginalSelection;
282     private int mDisplayGroupCurrentSelection;
283 
284     private QueryHandler mQueryHandler;
285     private String mQuery;
286     private Uri mGroupFilterUri;
287     private Uri mGroupUri;
288     private boolean mJustCreated;
289     private boolean mSyncEnabled;
290 
291     /**
292      * Cursor row index that holds reference back to {@link People#_ID}, such as
293      * {@link ContactMethods#PERSON_ID}. Used when responding to a
294      * {@link Intent#ACTION_SEARCH} in mode {@link #MODE_QUERY_PICK_TO_VIEW}.
295      */
296     private int mQueryPersonIdIndex;
297 
298     /**
299      * Used to keep track of the scroll state of the list.
300      */
301     private Parcelable mListState = null;
302     private boolean mListHasFocus;
303 
304     private String mShortcutAction;
305     private boolean mDefaultMode = false;
306 
307     /**
308      * Internal query type when in mode {@link #MODE_QUERY_PICK_TO_VIEW}.
309      */
310     private int mQueryMode = QUERY_MODE_NONE;
311 
312     private static final int QUERY_MODE_NONE = -1;
313     private static final int QUERY_MODE_MAILTO = 1;
314     private static final int QUERY_MODE_TEL = 2;
315 
316     /**
317      * Data to use when in mode {@link #MODE_QUERY_PICK_TO_VIEW}. Usually
318      * provided by scheme-specific part of incoming {@link Intent#getData()}.
319      */
320     private String mQueryData;
321 
322     private Handler mHandler = new Handler();
323 
324     private class ImportTypeSelectedListener implements DialogInterface.OnClickListener {
325         public static final int IMPORT_FROM_SIM = 0;
326         public static final int IMPORT_FROM_SDCARD = 1;
327 
328         private int mIndex;
329 
ImportTypeSelectedListener()330         public ImportTypeSelectedListener() {
331             mIndex = IMPORT_FROM_SIM;
332         }
333 
onClick(DialogInterface dialog, int which)334         public void onClick(DialogInterface dialog, int which) {
335             if (which == DialogInterface.BUTTON_POSITIVE) {
336                 if (mIndex == IMPORT_FROM_SIM) {
337                     doImportFromSim();
338                 } else {
339                     doImportFromSDCard();
340                 }
341             } else if (which == DialogInterface.BUTTON_NEGATIVE) {
342 
343             } else {
344                 mIndex = which;
345             }
346         }
347     }
348 
349     private class DeleteClickListener implements DialogInterface.OnClickListener {
350         private Uri mUri;
351 
DeleteClickListener(Uri uri)352         public DeleteClickListener(Uri uri) {
353             mUri = uri;
354         }
355 
onClick(DialogInterface dialog, int which)356         public void onClick(DialogInterface dialog, int which) {
357             getContentResolver().delete(mUri, null, null);
358         }
359     }
360 
361     @Override
onCreate(Bundle icicle)362     protected void onCreate(Bundle icicle) {
363         super.onCreate(icicle);
364 
365         // Resolve the intent
366         final Intent intent = getIntent();
367 
368         // Allow the title to be set to a custom String using an extra on the intent
369         String title = intent.getStringExtra(Contacts.Intents.UI.TITLE_EXTRA_KEY);
370         if (title != null) {
371             setTitle(title);
372         }
373 
374         final String action = intent.getAction();
375         mMode = MODE_UNKNOWN;
376 
377         setContentView(R.layout.contacts_list_content);
378 
379         if (UI.LIST_DEFAULT.equals(action)) {
380             mDefaultMode = true;
381             // When mDefaultMode is true the mode is set in onResume(), since the preferneces
382             // activity may change it whenever this activity isn't running
383         } else if (UI.LIST_GROUP_ACTION.equals(action)) {
384             mMode = MODE_GROUP;
385             String groupName = intent.getStringExtra(UI.GROUP_NAME_EXTRA_KEY);
386             if (TextUtils.isEmpty(groupName)) {
387                 finish();
388                 return;
389             }
390             buildUserGroupUris(groupName);
391         } else if (UI.LIST_ALL_CONTACTS_ACTION.equals(action)) {
392             mMode = MODE_ALL_CONTACTS;
393         } else if (UI.LIST_STARRED_ACTION.equals(action)) {
394             mMode = MODE_STARRED;
395         } else if (UI.LIST_FREQUENT_ACTION.equals(action)) {
396             mMode = MODE_FREQUENT;
397         } else if (UI.LIST_STREQUENT_ACTION.equals(action)) {
398             mMode = MODE_STREQUENT;
399         } else if (UI.LIST_CONTACTS_WITH_PHONES_ACTION.equals(action)) {
400             mMode = MODE_WITH_PHONES;
401         } else if (Intent.ACTION_PICK.equals(action)) {
402             // XXX These should be showing the data from the URI given in
403             // the Intent.
404             final String type = intent.resolveType(this);
405             if (People.CONTENT_TYPE.equals(type)) {
406                 mMode = MODE_PICK_CONTACT;
407             } else if (Phones.CONTENT_TYPE.equals(type)) {
408                 mMode = MODE_PICK_PHONE;
409             } else if (ContactMethods.CONTENT_POSTAL_TYPE.equals(type)) {
410                 mMode = MODE_PICK_POSTAL;
411             }
412         } else if (Intent.ACTION_CREATE_SHORTCUT.equals(action)) {
413             if (intent.getComponent().getClassName().equals("alias.DialShortcut")) {
414                 mMode = MODE_PICK_PHONE;
415                 mShortcutAction = Intent.ACTION_CALL;
416                 setTitle(R.string.callShortcutActivityTitle);
417             } else if (intent.getComponent().getClassName().equals("alias.MessageShortcut")) {
418                 mMode = MODE_PICK_PHONE;
419                 mShortcutAction = Intent.ACTION_SENDTO;
420                 setTitle(R.string.messageShortcutActivityTitle);
421             } else {
422                 mMode = MODE_PICK_OR_CREATE_CONTACT;
423                 mShortcutAction = Intent.ACTION_VIEW;
424                 setTitle(R.string.shortcutActivityTitle);
425             }
426         } else if (Intent.ACTION_GET_CONTENT.equals(action)) {
427             final String type = intent.resolveType(this);
428             if (People.CONTENT_ITEM_TYPE.equals(type)) {
429                 mMode = MODE_PICK_OR_CREATE_CONTACT;
430             } else if (Phones.CONTENT_ITEM_TYPE.equals(type)) {
431                 mMode = MODE_PICK_PHONE;
432             } else if (ContactMethods.CONTENT_POSTAL_ITEM_TYPE.equals(type)) {
433                 mMode = MODE_PICK_POSTAL;
434             }
435         } else if (Intent.ACTION_INSERT_OR_EDIT.equals(action)) {
436             mMode = MODE_INSERT_OR_EDIT_CONTACT;
437         } else if (Intent.ACTION_SEARCH.equals(action)) {
438             // See if the suggestion was clicked with a search action key (call button)
439             if ("call".equals(intent.getStringExtra(SearchManager.ACTION_MSG))) {
440                 String query = intent.getStringExtra(SearchManager.QUERY);
441                 if (!TextUtils.isEmpty(query)) {
442                     Intent newIntent = new Intent(Intent.ACTION_CALL_PRIVILEGED,
443                             Uri.fromParts("tel", query, null));
444                     startActivity(newIntent);
445                 }
446                 finish();
447                 return;
448             }
449 
450             // See if search request has extras to specify query
451             if (intent.hasExtra(Insert.EMAIL)) {
452                 mMode = MODE_QUERY_PICK_TO_VIEW;
453                 mQueryMode = QUERY_MODE_MAILTO;
454                 mQueryData = intent.getStringExtra(Insert.EMAIL);
455             } else if (intent.hasExtra(Insert.PHONE)) {
456                 mMode = MODE_QUERY_PICK_TO_VIEW;
457                 mQueryMode = QUERY_MODE_TEL;
458                 mQueryData = intent.getStringExtra(Insert.PHONE);
459             } else {
460                 // Otherwise handle the more normal search case
461                 mMode = MODE_QUERY;
462             }
463 
464         // Since this is the filter activity it receives all intents
465         // dispatched from the SearchManager for security reasons
466         // so we need to re-dispatch from here to the intended target.
467         } else if (Intents.SEARCH_SUGGESTION_CLICKED.equals(action)) {
468             // See if the suggestion was clicked with a search action key (call button)
469             Intent newIntent;
470             if ("call".equals(intent.getStringExtra(SearchManager.ACTION_MSG))) {
471                 newIntent = new Intent(Intent.ACTION_CALL_PRIVILEGED, intent.getData());
472             } else {
473                 newIntent = new Intent(Intent.ACTION_VIEW, intent.getData());
474             }
475             startActivity(newIntent);
476             finish();
477             return;
478         } else if (Intents.SEARCH_SUGGESTION_DIAL_NUMBER_CLICKED.equals(action)) {
479             Intent newIntent = new Intent(Intent.ACTION_CALL_PRIVILEGED, intent.getData());
480             startActivity(newIntent);
481             finish();
482             return;
483         } else if (Intents.SEARCH_SUGGESTION_CREATE_CONTACT_CLICKED.equals(action)) {
484             String number = intent.getData().getSchemeSpecificPart();
485             Intent newIntent = new Intent(Intent.ACTION_INSERT, People.CONTENT_URI);
486             newIntent.putExtra(Intents.Insert.PHONE, number);
487             startActivity(newIntent);
488             finish();
489             return;
490         }
491 
492         if (mMode == MODE_UNKNOWN) {
493             mMode = DEFAULT_MODE;
494         }
495 
496         // Setup the UI
497         final ListView list = getListView();
498         list.setFocusable(true);
499         list.setOnCreateContextMenuListener(this);
500         if ((mMode & MODE_MASK_NO_FILTER) != MODE_MASK_NO_FILTER) {
501             list.setTextFilterEnabled(true);
502         }
503 
504         if ((mMode & MODE_MASK_CREATE_NEW) != 0) {
505             // Add the header for creating a new contact
506             final LayoutInflater inflater = getLayoutInflater();
507             View header = inflater.inflate(android.R.layout.simple_list_item_1, list, false);
508             TextView text = (TextView) header.findViewById(android.R.id.text1);
509             text.setText(R.string.pickerNewContactHeader);
510             list.addHeaderView(header);
511         }
512 
513         // Set the proper empty string
514         setEmptyText();
515 
516         mAdapter = new ContactItemListAdapter(this);
517         setListAdapter(mAdapter);
518 
519         // We manually save/restore the listview state
520         list.setSaveEnabled(false);
521 
522         mQueryHandler = new QueryHandler(this);
523         mJustCreated = true;
524 
525         // Check to see if sync is enabled
526         final ContentResolver resolver = getContentResolver();
527         IContentProvider provider = resolver.acquireProvider(Contacts.CONTENT_URI);
528         if (provider == null) {
529             // No contacts provider, bail.
530             finish();
531             return;
532         }
533 
534         try {
535             ISyncAdapter sa = provider.getSyncAdapter();
536             mSyncEnabled = sa != null;
537         } catch (RemoteException e) {
538             mSyncEnabled = false;
539         } finally {
540             resolver.releaseProvider(provider);
541         }
542     }
543 
setEmptyText()544     private void setEmptyText() {
545         TextView empty = (TextView) findViewById(R.id.emptyText);
546         // Center the text by default
547         int gravity = Gravity.CENTER;
548         switch (mMode) {
549             case MODE_GROUP:
550                 if (Groups.GROUP_MY_CONTACTS.equals(mDisplayInfo)) {
551                     if (mSyncEnabled) {
552                         empty.setText(getText(R.string.noContactsHelpTextWithSync));
553                     } else {
554                         empty.setText(getText(R.string.noContactsHelpText));
555                     }
556                     gravity = Gravity.NO_GRAVITY;
557                 } else {
558                     empty.setText(getString(R.string.groupEmpty, mDisplayInfo));
559                 }
560                 break;
561 
562             case MODE_STARRED:
563             case MODE_STREQUENT:
564             case MODE_FREQUENT:
565                 empty.setText(getText(R.string.noFavorites));
566                 break;
567 
568             case MODE_WITH_PHONES:
569                 empty.setText(getText(R.string.noContactsWithPhoneNumbers));
570                 break;
571 
572             default:
573                 empty.setText(getText(R.string.noContacts));
574                 break;
575         }
576         empty.setGravity(gravity);
577     }
578 
579     /**
580      * Builds the URIs to query when displaying a user group
581      *
582      * @param groupName the group being displayed
583      */
buildUserGroupUris(String groupName)584     private void buildUserGroupUris(String groupName) {
585         mGroupFilterUri = Uri.parse("content://contacts/groups/name/" + groupName
586                 + "/members/filter/");
587         mGroupUri = Uri.parse("content://contacts/groups/name/" + groupName + "/members");
588     }
589 
590     /**
591      * Builds the URIs to query when displaying a system group
592      *
593      * @param systemId the system group's ID
594      */
buildSystemGroupUris(String systemId)595     private void buildSystemGroupUris(String systemId) {
596         mGroupFilterUri = Uri.parse("content://contacts/groups/system_id/" + systemId
597                 + "/members/filter/");
598         mGroupUri = Uri.parse("content://contacts/groups/system_id/" + systemId + "/members");
599     }
600 
601     /**
602      * Sets the mode when the request is for "default"
603      */
setDefaultMode()604     private void setDefaultMode() {
605         // Load the preferences
606         SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
607 
608         // Lookup the group to display
609         mDisplayType = prefs.getInt(PREF_DISPLAY_TYPE, DISPLAY_TYPE_UNKNOWN);
610         switch (mDisplayType) {
611             case DISPLAY_TYPE_ALL_WITH_PHONES: {
612                 mMode = MODE_WITH_PHONES;
613                 mDisplayInfo = null;
614                 break;
615             }
616 
617             case DISPLAY_TYPE_SYSTEM_GROUP: {
618                 String systemId = prefs.getString(
619                         PREF_DISPLAY_INFO, null);
620                 if (!TextUtils.isEmpty(systemId)) {
621                     // Display the selected system group
622                     mMode = MODE_GROUP;
623                     buildSystemGroupUris(systemId);
624                     mDisplayInfo = systemId;
625                 } else {
626                     // No valid group is present, display everything
627                     mMode = MODE_WITH_PHONES;
628                     mDisplayInfo = null;
629                     mDisplayType = DISPLAY_TYPE_ALL;
630                 }
631                 break;
632             }
633 
634             case DISPLAY_TYPE_USER_GROUP: {
635                 String displayGroup = prefs.getString(
636                         PREF_DISPLAY_INFO, null);
637                 if (!TextUtils.isEmpty(displayGroup)) {
638                     // Display the selected user group
639                     mMode = MODE_GROUP;
640                     buildUserGroupUris(displayGroup);
641                     mDisplayInfo = displayGroup;
642                 } else {
643                     // No valid group is present, display everything
644                     mMode = MODE_WITH_PHONES;
645                     mDisplayInfo = null;
646                     mDisplayType = DISPLAY_TYPE_ALL;
647                 }
648                 break;
649             }
650 
651             case DISPLAY_TYPE_ALL: {
652                 mMode = MODE_ALL_CONTACTS;
653                 mDisplayInfo = null;
654                 break;
655             }
656 
657             default: {
658                 // We don't know what to display, default to My Contacts
659                 mMode = MODE_GROUP;
660                 mDisplayType = DISPLAY_TYPE_SYSTEM_GROUP;
661                 buildSystemGroupUris(Groups.GROUP_MY_CONTACTS);
662                 mDisplayInfo = Groups.GROUP_MY_CONTACTS;
663                 break;
664             }
665         }
666 
667         // Update the empty text view with the proper string, as the group may have changed
668         setEmptyText();
669     }
670 
671     @Override
onResume()672     protected void onResume() {
673         super.onResume();
674 
675         boolean runQuery = true;
676         Activity parent = getParent();
677 
678         // Do this before setting the filter. The filter thread relies
679         // on some state that is initialized in setDefaultMode
680         if (mDefaultMode) {
681             // If we're in default mode we need to possibly reset the mode due to a change
682             // in the preferences activity while we weren't running
683             setDefaultMode();
684         }
685 
686         // See if we were invoked with a filter
687         if (parent != null && parent instanceof DialtactsActivity) {
688             String filterText = ((DialtactsActivity) parent).getAndClearFilterText();
689             if (filterText != null && filterText.length() > 0) {
690                 getListView().setFilterText(filterText);
691                 // Don't start a new query since it will conflict with the filter
692                 runQuery = false;
693             } else if (mJustCreated) {
694                 getListView().clearTextFilter();
695             }
696         }
697 
698         if (mJustCreated && runQuery) {
699             // We need to start a query here the first time the activity is launched, as long
700             // as we aren't doing a filter.
701             startQuery();
702         }
703         mJustCreated = false;
704     }
705 
706     @Override
onRestart()707     protected void onRestart() {
708         super.onRestart();
709 
710         // The cursor was killed off in onStop(), so we need to get a new one here
711         // We do not perform the query if a filter is set on the list because the
712         // filter will cause the query to happen anyway
713         if (TextUtils.isEmpty(getListView().getTextFilter())) {
714             startQuery();
715         } else {
716             // Run the filtered query on the adapter
717             ((ContactItemListAdapter) getListAdapter()).onContentChanged();
718         }
719     }
720 
updateGroup()721     private void updateGroup() {
722         if (mDefaultMode) {
723             setDefaultMode();
724         }
725 
726         // Calling requery here may cause an ANR, so always do the async query
727         startQuery();
728     }
729 
730     @Override
onSaveInstanceState(Bundle icicle)731     protected void onSaveInstanceState(Bundle icicle) {
732         super.onSaveInstanceState(icicle);
733         // Save list state in the bundle so we can restore it after the QueryHandler has run
734         icicle.putParcelable(LIST_STATE_KEY, mList.onSaveInstanceState());
735         icicle.putBoolean(FOCUS_KEY, mList.hasFocus());
736     }
737 
738     @Override
onRestoreInstanceState(Bundle icicle)739     protected void onRestoreInstanceState(Bundle icicle) {
740         super.onRestoreInstanceState(icicle);
741         // Retrieve list state. This will be applied after the QueryHandler has run
742         mListState = icicle.getParcelable(LIST_STATE_KEY);
743         mListHasFocus = icicle.getBoolean(FOCUS_KEY);
744     }
745 
746     @Override
onStop()747     protected void onStop() {
748         super.onStop();
749 
750         // We don't want the list to display the empty state, since when we resume it will still
751         // be there and show up while the new query is happening. After the async query finished
752         // in response to onRestart() setLoading(false) will be called.
753         mAdapter.setLoading(true);
754         mAdapter.changeCursor(null);
755 
756         if (mMode == MODE_QUERY) {
757             // Make sure the search box is closed
758             SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE);
759             searchManager.stopSearch();
760         }
761     }
762 
763     @Override
onCreateOptionsMenu(Menu menu)764     public boolean onCreateOptionsMenu(Menu menu) {
765         // If Contacts was invoked by another Activity simply as a way of
766         // picking a contact, don't show the options menu
767         if ((mMode & MODE_MASK_PICKER) == MODE_MASK_PICKER) {
768             return false;
769         }
770 
771         // Search
772         menu.add(0, MENU_SEARCH, 0, R.string.menu_search)
773                 .setIcon(android.R.drawable.ic_menu_search);
774 
775         // New contact
776         menu.add(0, MENU_NEW_CONTACT, 0, R.string.menu_newContact)
777                 .setIcon(android.R.drawable.ic_menu_add)
778                 .setIntent(new Intent(Intents.Insert.ACTION, People.CONTENT_URI))
779                 .setAlphabeticShortcut('n');
780 
781         // Display group
782         if (mDefaultMode) {
783             menu.add(0, MENU_DISPLAY_GROUP, 0, R.string.menu_displayGroup)
784                     .setIcon(com.android.internal.R.drawable.ic_menu_allfriends);
785         }
786 
787         // Sync settings
788         if (mSyncEnabled) {
789             Intent syncIntent = new Intent(Intent.ACTION_VIEW);
790             syncIntent.setClass(this, ContactsGroupSyncSelector.class);
791             menu.add(0, 0, 0, R.string.syncGroupPreference)
792                     .setIcon(com.android.internal.R.drawable.ic_menu_refresh)
793                     .setIntent(syncIntent);
794         }
795 
796         // Contacts import (SIM/SDCard)
797         menu.add(0, MENU_IMPORT_CONTACTS, 0, R.string.importFromSim)
798                 .setIcon(R.drawable.ic_menu_import_contact);
799 
800         if (getResources().getBoolean(R.bool.config_allow_export_to_sdcard)) {
801             menu.add(0, MENU_EXPORT_CONTACTS, 0, R.string.export_contact_list)
802                     .setIcon(R.drawable.ic_menu_export_contact);
803         }
804 
805         return super.onCreateOptionsMenu(menu);
806     }
807 
808     /*
809      * Implements the handler for display group selection.
810      */
onClick(DialogInterface dialogInterface, int which)811     public void onClick(DialogInterface dialogInterface, int which) {
812         if (which == DialogInterface.BUTTON_POSITIVE) {
813             // The OK button was pressed
814             if (mDisplayGroupOriginalSelection != mDisplayGroupCurrentSelection) {
815                 // Set the group to display
816                 if (mDisplayGroupCurrentSelection == DISPLAY_GROUP_INDEX_ALL_CONTACTS) {
817                     // Display all
818                     mDisplayType = DISPLAY_TYPE_ALL;
819                     mDisplayInfo = null;
820                 } else if (mDisplayGroupCurrentSelection
821                         == DISPLAY_GROUP_INDEX_ALL_CONTACTS_WITH_PHONES) {
822                     // Display all with phone numbers
823                     mDisplayType = DISPLAY_TYPE_ALL_WITH_PHONES;
824                     mDisplayInfo = null;
825                 } else if (mDisplayGroupsIncludesMyContacts &&
826                         mDisplayGroupCurrentSelection == DISPLAY_GROUP_INDEX_MY_CONTACTS) {
827                     mDisplayType = DISPLAY_TYPE_SYSTEM_GROUP;
828                     mDisplayInfo = Groups.GROUP_MY_CONTACTS;
829                 } else {
830                     mDisplayType = DISPLAY_TYPE_USER_GROUP;
831                     mDisplayInfo = mDisplayGroups[mDisplayGroupCurrentSelection].toString();
832                 }
833 
834                 // Save the changes to the preferences
835                 SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
836                 prefs.edit()
837                         .putInt(PREF_DISPLAY_TYPE, mDisplayType)
838                         .putString(PREF_DISPLAY_INFO, mDisplayInfo)
839                         .commit();
840 
841                 // Update the display state
842                 updateGroup();
843             }
844         } else {
845             // A list item was selected, cache the position
846             mDisplayGroupCurrentSelection = which;
847         }
848     }
849 
850     @Override
onOptionsItemSelected(MenuItem item)851     public boolean onOptionsItemSelected(MenuItem item) {
852         switch (item.getItemId()) {
853             case MENU_DISPLAY_GROUP:
854                 AlertDialog.Builder builder = new AlertDialog.Builder(this)
855                     .setTitle(R.string.select_group_title)
856                     .setPositiveButton(android.R.string.ok, this)
857                     .setNegativeButton(android.R.string.cancel, null);
858 
859                 setGroupEntries(builder);
860 
861                 builder.show();
862                 return true;
863 
864             case MENU_SEARCH:
865                 startSearch(null, false, null, false);
866                 return true;
867 
868             case MENU_IMPORT_CONTACTS:
869                 if (getResources().getBoolean(R.bool.config_allow_import_from_sdcard)) {
870                     ImportTypeSelectedListener listener =
871                         new ImportTypeSelectedListener();
872                     AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(this)
873                         .setTitle(R.string.select_import_type_title)
874                         .setPositiveButton(android.R.string.ok, listener)
875                         .setNegativeButton(android.R.string.cancel, null);
876                     dialogBuilder.setSingleChoiceItems(new String[] {
877                             getString(R.string.import_from_sim),
878                             getString(R.string.import_from_sdcard)},
879                             ImportTypeSelectedListener.IMPORT_FROM_SIM, listener);
880                     dialogBuilder.show();
881                 } else {
882                     doImportFromSim();
883                 }
884                 return true;
885 
886             case MENU_EXPORT_CONTACTS:
887                 handleExportContacts();
888         }
889         return false;
890     }
891 
doImportFromSim()892     private void doImportFromSim() {
893         Intent importIntent = new Intent(Intent.ACTION_VIEW);
894         importIntent.setType("vnd.android.cursor.item/sim-contact");
895         importIntent.setClassName("com.android.phone", "com.android.phone.SimContacts");
896         startActivity(importIntent);
897     }
898 
doImportFromSDCard()899     private void doImportFromSDCard() {
900         Intent intent = new Intent(this, ImportVCardActivity.class);
901         startActivity(intent);
902     }
903 
handleExportContacts()904     private void handleExportContacts() {
905         VCardExporter exporter = new VCardExporter(ContactsListActivity.this, mHandler);
906         exporter.startExportVCardToSdCard();
907     }
908 
909     @Override
onActivityResult(int requestCode, int resultCode, Intent data)910     protected void onActivityResult(int requestCode, int resultCode,
911             Intent data) {
912         switch (requestCode) {
913             case SUBACTIVITY_NEW_CONTACT:
914                 if (resultCode == RESULT_OK) {
915                     // Contact was created, pass it back
916                     returnPickerResult(null, data.getStringExtra(Intent.EXTRA_SHORTCUT_NAME),
917                             data.getData(), 0);
918                 }
919         }
920     }
921 
922     @Override
onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo)923     public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) {
924         // If Contacts was invoked by another Activity simply as a way of
925         // picking a contact, don't show the context menu
926         if ((mMode & MODE_MASK_PICKER) == MODE_MASK_PICKER) {
927             return;
928         }
929 
930         AdapterView.AdapterContextMenuInfo info;
931         try {
932              info = (AdapterView.AdapterContextMenuInfo) menuInfo;
933         } catch (ClassCastException e) {
934             Log.e(TAG, "bad menuInfo", e);
935             return;
936         }
937 
938         Cursor cursor = (Cursor) getListAdapter().getItem(info.position);
939         if (cursor == null) {
940             // For some reason the requested item isn't available, do nothing
941             return;
942         }
943         long id = info.id;
944         Uri personUri = ContentUris.withAppendedId(People.CONTENT_URI, id);
945 
946         // Setup the menu header
947         menu.setHeaderTitle(cursor.getString(NAME_COLUMN_INDEX));
948 
949         // View contact details
950         menu.add(0, MENU_ITEM_VIEW_CONTACT, 0, R.string.menu_viewContact)
951                 .setIntent(new Intent(Intent.ACTION_VIEW, personUri));
952 
953         // Calling contact
954         long phoneId = cursor.getLong(PRIMARY_PHONE_ID_COLUMN_INDEX);
955         if (phoneId > 0) {
956             // Get the display label for the number
957             CharSequence label = cursor.getString(LABEL_COLUMN_INDEX);
958             int type = cursor.getInt(TYPE_COLUMN_INDEX);
959             label = Phones.getDisplayLabel(this, type, label);
960             Intent intent = new Intent(Intent.ACTION_CALL_PRIVILEGED,
961                     ContentUris.withAppendedId(Phones.CONTENT_URI, phoneId));
962             menu.add(0, MENU_ITEM_CALL, 0, String.format(getString(R.string.menu_callNumber), label))
963                     .setIntent(intent);
964 
965             // Send SMS item
966             menu.add(0, MENU_ITEM_SEND_SMS, 0, R.string.menu_sendSMS)
967                     .setIntent(new Intent(Intent.ACTION_SENDTO,
968                             Uri.fromParts("sms", cursor.getString(NUMBER_COLUMN_INDEX), null)));
969         }
970 
971         // Star toggling
972         int starState = cursor.getInt(STARRED_COLUMN_INDEX);
973         if (starState == 0) {
974             menu.add(0, MENU_ITEM_TOGGLE_STAR, 0, R.string.menu_addStar);
975         } else {
976             menu.add(0, MENU_ITEM_TOGGLE_STAR, 0, R.string.menu_removeStar);
977         }
978 
979         // Contact editing
980         menu.add(0, MENU_ITEM_EDIT, 0, R.string.menu_editContact)
981                 .setIntent(new Intent(Intent.ACTION_EDIT, personUri));
982         menu.add(0, MENU_ITEM_DELETE, 0, R.string.menu_deleteContact);
983     }
984 
985     @Override
onContextItemSelected(MenuItem item)986     public boolean onContextItemSelected(MenuItem item) {
987         AdapterView.AdapterContextMenuInfo info;
988         try {
989              info = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo();
990         } catch (ClassCastException e) {
991             Log.e(TAG, "bad menuInfo", e);
992             return false;
993         }
994 
995         Cursor cursor = (Cursor) getListAdapter().getItem(info.position);
996 
997         switch (item.getItemId()) {
998             case MENU_ITEM_TOGGLE_STAR: {
999                 // Toggle the star
1000                 ContentValues values = new ContentValues(1);
1001                 values.put(People.STARRED, cursor.getInt(STARRED_COLUMN_INDEX) == 0 ? 1 : 0);
1002                 Uri personUri = ContentUris.withAppendedId(People.CONTENT_URI,
1003                         cursor.getInt(ID_COLUMN_INDEX));
1004                 getContentResolver().update(personUri, values, null, null);
1005                 return true;
1006             }
1007 
1008             case MENU_ITEM_DELETE: {
1009                 // Get confirmation
1010                 Uri uri = ContentUris.withAppendedId(People.CONTENT_URI,
1011                         cursor.getLong(ID_COLUMN_INDEX));
1012                 //TODO make this dialog persist across screen rotations
1013                 new AlertDialog.Builder(ContactsListActivity.this)
1014                     .setTitle(R.string.deleteConfirmation_title)
1015                     .setIcon(android.R.drawable.ic_dialog_alert)
1016                     .setMessage(R.string.deleteConfirmation)
1017                     .setNegativeButton(android.R.string.cancel, null)
1018                     .setPositiveButton(android.R.string.ok, new DeleteClickListener(uri))
1019                     .show();
1020                 return true;
1021             }
1022         }
1023 
1024         return super.onContextItemSelected(item);
1025     }
1026 
1027     @Override
onKeyDown(int keyCode, KeyEvent event)1028     public boolean onKeyDown(int keyCode, KeyEvent event) {
1029         switch (keyCode) {
1030             case KeyEvent.KEYCODE_CALL: {
1031                 if (callSelection()) {
1032                     return true;
1033                 }
1034                 break;
1035             }
1036 
1037             case KeyEvent.KEYCODE_DEL: {
1038                 Object o = getListView().getSelectedItem();
1039                 if (o != null) {
1040                     Cursor cursor = (Cursor) o;
1041                     Uri uri = ContentUris.withAppendedId(People.CONTENT_URI,
1042                             cursor.getLong(ID_COLUMN_INDEX));
1043                     //TODO make this dialog persist across screen rotations
1044                     new AlertDialog.Builder(ContactsListActivity.this)
1045                         .setTitle(R.string.deleteConfirmation_title)
1046                         .setIcon(android.R.drawable.ic_dialog_alert)
1047                         .setMessage(R.string.deleteConfirmation)
1048                         .setNegativeButton(android.R.string.cancel, null)
1049                         .setPositiveButton(android.R.string.ok, new DeleteClickListener(uri))
1050                         .setCancelable(false)
1051                         .show();
1052                     return true;
1053                 }
1054                 break;
1055             }
1056         }
1057 
1058         return super.onKeyDown(keyCode, event);
1059     }
1060 
1061     @Override
onListItemClick(ListView l, View v, int position, long id)1062     protected void onListItemClick(ListView l, View v, int position, long id) {
1063         // Hide soft keyboard, if visible
1064         InputMethodManager inputMethodManager = (InputMethodManager)
1065                 getSystemService(Context.INPUT_METHOD_SERVICE);
1066         inputMethodManager.hideSoftInputFromWindow(mList.getWindowToken(), 0);
1067 
1068         if (mMode == MODE_INSERT_OR_EDIT_CONTACT) {
1069             Intent intent;
1070             if (position == 0) {
1071                 // Insert
1072                 intent = new Intent(Intent.ACTION_INSERT, People.CONTENT_URI);
1073             } else {
1074                 // Edit
1075                 intent = new Intent(Intent.ACTION_EDIT,
1076                         ContentUris.withAppendedId(People.CONTENT_URI, id));
1077             }
1078             intent.setFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
1079             final Bundle extras = getIntent().getExtras();
1080             if (extras != null) {
1081                 intent.putExtras(extras);
1082             }
1083             startActivity(intent);
1084             finish();
1085         } else if (id != -1) {
1086             if ((mMode & MODE_MASK_PICKER) == 0) {
1087                 Intent intent = new Intent(Intent.ACTION_VIEW,
1088                         ContentUris.withAppendedId(People.CONTENT_URI, id));
1089                 startActivity(intent);
1090             } else if (mMode == MODE_QUERY_PICK_TO_VIEW) {
1091                 // Started with query that should launch to view contact
1092                 Cursor c = (Cursor) mAdapter.getItem(position);
1093                 long personId = c.getLong(mQueryPersonIdIndex);
1094                 Intent intent = new Intent(Intent.ACTION_VIEW,
1095                         ContentUris.withAppendedId(People.CONTENT_URI, personId));
1096                 startActivity(intent);
1097                 finish();
1098             } else if (mMode == MODE_PICK_CONTACT
1099                     || mMode == MODE_PICK_OR_CREATE_CONTACT) {
1100                 Uri uri = ContentUris.withAppendedId(People.CONTENT_URI, id);
1101                 if (mShortcutAction != null) {
1102                     // Subtract one if we have Create Contact at the top
1103                     Cursor c = (Cursor) mAdapter.getItem(position
1104                             - (mMode == MODE_PICK_OR_CREATE_CONTACT? 1:0));
1105                     returnPickerResult(c, c.getString(NAME_COLUMN_INDEX), uri, id);
1106                 } else {
1107                     returnPickerResult(null, null, uri, id);
1108                 }
1109             } else if (mMode == MODE_PICK_PHONE) {
1110                 Uri uri = ContentUris.withAppendedId(Phones.CONTENT_URI, id);
1111                 if (mShortcutAction != null) {
1112                     Cursor c = (Cursor) mAdapter.getItem(position);
1113                     returnPickerResult(c, c.getString(NAME_COLUMN_INDEX), uri, id);
1114                 } else {
1115                     returnPickerResult(null, null, uri, id);
1116                 }
1117             } else if (mMode == MODE_PICK_POSTAL) {
1118                 setResult(RESULT_OK, new Intent().setData(
1119                         ContentUris.withAppendedId(ContactMethods.CONTENT_URI, id)));
1120                 finish();
1121             }
1122         } else if ((mMode & MODE_MASK_CREATE_NEW) == MODE_MASK_CREATE_NEW
1123                 && position == 0) {
1124             Intent newContact = new Intent(Intents.Insert.ACTION, People.CONTENT_URI);
1125             startActivityForResult(newContact, SUBACTIVITY_NEW_CONTACT);
1126         } else {
1127             signalError();
1128         }
1129     }
1130 
returnPickerResult(Cursor c, String name, Uri uri, long id)1131     private void returnPickerResult(Cursor c, String name, Uri uri, long id) {
1132         final Intent intent = new Intent();
1133 
1134         if (mShortcutAction != null) {
1135             Intent shortcutIntent;
1136             if (Intent.ACTION_VIEW.equals(mShortcutAction)) {
1137                 // This is a simple shortcut to view a contact.
1138                 shortcutIntent = new Intent(mShortcutAction, uri);
1139                 final Bitmap icon = People.loadContactPhoto(this, uri, 0, null);
1140                 if (icon != null) {
1141                     intent.putExtra(Intent.EXTRA_SHORTCUT_ICON, icon);
1142                 } else {
1143                     intent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE,
1144                             Intent.ShortcutIconResource.fromContext(this,
1145                                     R.drawable.ic_launcher_shortcut_contact));
1146                 }
1147             } else {
1148                 // This is a direct dial or sms shortcut.
1149                 String number = c.getString(DATA_COLUMN_INDEX);
1150                 int type = c.getInt(TYPE_COLUMN_INDEX);
1151                 String scheme;
1152                 int resid;
1153                 if (Intent.ACTION_CALL.equals(mShortcutAction)) {
1154                     scheme = "tel";
1155                     resid = R.drawable.badge_action_call;
1156                 } else {
1157                     scheme = "smsto";
1158                     resid = R.drawable.badge_action_sms;
1159                 }
1160                 // Make the URI a direct tel: URI so that it will always continue to work
1161                 Uri phoneUri = Uri.fromParts(scheme, number, null);
1162                 shortcutIntent = new Intent(mShortcutAction, phoneUri);
1163 
1164                 // Find the People._ID for this phone number
1165                 final long personId = c.getLong(PHONES_PERSON_ID_INDEX);
1166                 Uri personUri = ContentUris.withAppendedId(People.CONTENT_URI, personId);
1167                 intent.putExtra(Intent.EXTRA_SHORTCUT_ICON,
1168                         generatePhoneNumberIcon(personUri, type, resid));
1169 
1170             }
1171             shortcutIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
1172             intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent);
1173             intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, name);
1174             setResult(RESULT_OK, intent);
1175         } else {
1176             setResult(RESULT_OK, intent.setData(uri));
1177         }
1178         finish();
1179     }
1180 
1181     /**
1182      * Generates a phone number shortcut icon. Adds an overlay describing the type of the phone
1183      * number, and if there is a photo also adds the call action icon.
1184      *
1185      * @param personUri The person the phone number belongs to
1186      * @param type The type of the phone number
1187      * @param actionResId The ID for the action resource
1188      * @return The bitmap for the icon
1189      */
generatePhoneNumberIcon(Uri personUri, int type, int actionResId)1190     private Bitmap generatePhoneNumberIcon(Uri personUri, int type, int actionResId) {
1191         final Resources r = getResources();
1192         boolean drawPhoneOverlay = true;
1193 
1194         Bitmap photo = People.loadContactPhoto(this, personUri, 0, null);
1195         if (photo == null) {
1196             // If there isn't a photo use the generic phone action icon instead
1197             Bitmap phoneIcon = getPhoneActionIcon(r, actionResId);
1198             if (phoneIcon != null) {
1199                 photo = phoneIcon;
1200                 drawPhoneOverlay = false;
1201             } else {
1202                 return null;
1203             }
1204         }
1205 
1206         // Setup the drawing classes
1207         int iconSize = (int) r.getDimension(android.R.dimen.app_icon_size);
1208         Bitmap icon = Bitmap.createBitmap(iconSize, iconSize, Bitmap.Config.ARGB_8888);
1209         Canvas canvas = new Canvas(icon);
1210 
1211         // Copy in the photo
1212         Paint photoPaint = new Paint();
1213         photoPaint.setDither(true);
1214         photoPaint.setFilterBitmap(true);
1215         Rect src = new Rect(0,0, photo.getWidth(),photo.getHeight());
1216         Rect dst = new Rect(0,0, iconSize,iconSize);
1217         canvas.drawBitmap(photo, src, dst, photoPaint);
1218 
1219         // Create an overlay for the phone number type
1220         String overlay = null;
1221         switch (type) {
1222             case Phones.TYPE_HOME:
1223                 overlay = "H";
1224                 break;
1225 
1226             case Phones.TYPE_MOBILE:
1227                 overlay = "M";
1228                 break;
1229 
1230             case Phones.TYPE_WORK:
1231                 overlay = "W";
1232                 break;
1233 
1234             case Phones.TYPE_PAGER:
1235                 overlay = "P";
1236                 break;
1237 
1238             case Phones.TYPE_OTHER:
1239                 overlay = "O";
1240                 break;
1241         }
1242         if (overlay != null) {
1243             Paint textPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DEV_KERN_TEXT_FLAG);
1244             textPaint.setTextSize(20.0f);
1245             textPaint.setTypeface(Typeface.DEFAULT_BOLD);
1246             textPaint.setColor(r.getColor(R.color.textColorIconOverlay));
1247             textPaint.setShadowLayer(3f, 1, 1, r.getColor(R.color.textColorIconOverlayShadow));
1248             canvas.drawText(overlay, 2, 16, textPaint);
1249         }
1250 
1251         // Draw the phone action icon as an overlay
1252         if (ENABLE_ACTION_ICON_OVERLAYS && drawPhoneOverlay) {
1253             Bitmap phoneIcon = getPhoneActionIcon(r, actionResId);
1254             if (phoneIcon != null) {
1255                 src.set(0,0, phoneIcon.getWidth(),phoneIcon.getHeight());
1256                 int iconWidth = icon.getWidth();
1257                 dst.set(iconWidth - 20, -1, iconWidth, 19);
1258                 canvas.drawBitmap(phoneIcon, src, dst, photoPaint);
1259             }
1260         }
1261 
1262         return icon;
1263     }
1264 
1265     /**
1266      * Returns the icon for the phone call action.
1267      *
1268      * @param r The resources to load the icon from
1269      * @param resId The resource ID to load
1270      * @return the icon for the phone call action
1271      */
getPhoneActionIcon(Resources r, int resId)1272     private Bitmap getPhoneActionIcon(Resources r, int resId) {
1273         Drawable phoneIcon = r.getDrawable(resId);
1274         if (phoneIcon instanceof BitmapDrawable) {
1275             BitmapDrawable bd = (BitmapDrawable) phoneIcon;
1276             return bd.getBitmap();
1277         } else {
1278             return null;
1279         }
1280     }
1281 
getProjection()1282     String[] getProjection() {
1283         switch (mMode) {
1284             case MODE_GROUP:
1285             case MODE_ALL_CONTACTS:
1286             case MODE_WITH_PHONES:
1287             case MODE_PICK_CONTACT:
1288             case MODE_PICK_OR_CREATE_CONTACT:
1289             case MODE_QUERY:
1290             case MODE_STARRED:
1291             case MODE_FREQUENT:
1292             case MODE_INSERT_OR_EDIT_CONTACT:
1293                 return CONTACTS_PROJECTION;
1294 
1295             case MODE_STREQUENT:
1296                 return STREQUENT_PROJECTION;
1297 
1298             case MODE_PICK_PHONE:
1299                 return PHONES_PROJECTION;
1300 
1301             case MODE_PICK_POSTAL:
1302                 return CONTACT_METHODS_PROJECTION;
1303         }
1304         return null;
1305     }
1306 
getPeopleFilterUri(String filter)1307     private Uri getPeopleFilterUri(String filter) {
1308         if (!TextUtils.isEmpty(filter)) {
1309             return Uri.withAppendedPath(People.CONTENT_FILTER_URI, Uri.encode(filter));
1310         } else {
1311             return People.CONTENT_URI;
1312         }
1313     }
1314 
getSortOrder(String[] projectionType)1315     private static String getSortOrder(String[] projectionType) {
1316         if (Locale.getDefault().equals(Locale.JAPAN) &&
1317                 projectionType == CONTACTS_PROJECTION) {
1318             return SORT_STRING + " ASC";
1319         } else {
1320             return NAME_COLUMN + " COLLATE LOCALIZED ASC";
1321         }
1322     }
1323 
startQuery()1324     void startQuery() {
1325         mAdapter.setLoading(true);
1326 
1327         // Cancel any pending queries
1328         mQueryHandler.cancelOperation(QUERY_TOKEN);
1329 
1330         // Kick off the new query
1331         switch (mMode) {
1332             case MODE_GROUP:
1333                 mQueryHandler.startQuery(QUERY_TOKEN, null,
1334                         mGroupUri, CONTACTS_PROJECTION, null, null,
1335                         getSortOrder(CONTACTS_PROJECTION));
1336                 break;
1337 
1338             case MODE_ALL_CONTACTS:
1339             case MODE_PICK_CONTACT:
1340             case MODE_PICK_OR_CREATE_CONTACT:
1341             case MODE_INSERT_OR_EDIT_CONTACT:
1342                 mQueryHandler.startQuery(QUERY_TOKEN, null, People.CONTENT_URI, CONTACTS_PROJECTION,
1343                         null, null, getSortOrder(CONTACTS_PROJECTION));
1344                 break;
1345 
1346             case MODE_WITH_PHONES:
1347                 mQueryHandler.startQuery(QUERY_TOKEN, null, People.CONTENT_URI, CONTACTS_PROJECTION,
1348                         People.PRIMARY_PHONE_ID + " IS NOT NULL", null,
1349                         getSortOrder(CONTACTS_PROJECTION));
1350                 break;
1351 
1352             case MODE_QUERY: {
1353                 mQuery = getIntent().getStringExtra(SearchManager.QUERY);
1354                 mQueryHandler.startQuery(QUERY_TOKEN, null, getPeopleFilterUri(mQuery),
1355                         CONTACTS_PROJECTION, null, null,
1356                         getSortOrder(CONTACTS_PROJECTION));
1357                 break;
1358             }
1359 
1360             case MODE_QUERY_PICK_TO_VIEW: {
1361                 if (mQueryMode == QUERY_MODE_MAILTO) {
1362                     // Find all contacts with the given search string as either
1363                     // an E-mail or IM address.
1364                     mQueryPersonIdIndex = SIMPLE_CONTACTS_PERSON_ID_INDEX;
1365                     Uri uri = Uri.withAppendedPath(People.WITH_EMAIL_OR_IM_FILTER_URI,
1366                             Uri.encode(mQueryData));
1367                     mQueryHandler.startQuery(QUERY_TOKEN, null,
1368                             uri, SIMPLE_CONTACTS_PROJECTION, null, null,
1369                             getSortOrder(CONTACTS_PROJECTION));
1370 
1371                 } else if (mQueryMode == QUERY_MODE_TEL) {
1372                     mQueryPersonIdIndex = PHONES_PERSON_ID_INDEX;
1373                     mQueryHandler.startQuery(QUERY_TOKEN, null,
1374                             Uri.withAppendedPath(Phones.CONTENT_FILTER_URL, mQueryData),
1375                             PHONES_PROJECTION, null, null,
1376                             getSortOrder(PHONES_PROJECTION));
1377                 }
1378                 break;
1379             }
1380 
1381             case MODE_STARRED:
1382                 mQueryHandler.startQuery(QUERY_TOKEN, null, People.CONTENT_URI,
1383                         CONTACTS_PROJECTION,
1384                         People.STARRED + "=1", null, getSortOrder(CONTACTS_PROJECTION));
1385                 break;
1386 
1387             case MODE_FREQUENT:
1388                 mQueryHandler.startQuery(QUERY_TOKEN, null,
1389                         People.CONTENT_URI, CONTACTS_PROJECTION,
1390                         People.TIMES_CONTACTED + " > 0", null,
1391                         People.TIMES_CONTACTED + " DESC, " + getSortOrder(CONTACTS_PROJECTION));
1392                 break;
1393 
1394             case MODE_STREQUENT:
1395                 mQueryHandler.startQuery(QUERY_TOKEN, null,
1396                         Uri.withAppendedPath(People.CONTENT_URI, "strequent"), STREQUENT_PROJECTION,
1397                         null, null, null);
1398                 break;
1399 
1400             case MODE_PICK_PHONE:
1401                 mQueryHandler.startQuery(QUERY_TOKEN, null, Phones.CONTENT_URI, PHONES_PROJECTION,
1402                         null, null, getSortOrder(PHONES_PROJECTION));
1403                 break;
1404 
1405             case MODE_PICK_POSTAL:
1406                 mQueryHandler.startQuery(QUERY_TOKEN, null, ContactMethods.CONTENT_URI,
1407                         CONTACT_METHODS_PROJECTION,
1408                         ContactMethods.KIND + "=" + Contacts.KIND_POSTAL, null,
1409                         getSortOrder(CONTACT_METHODS_PROJECTION));
1410                 break;
1411         }
1412     }
1413 
1414     /**
1415      * Called from a background thread to do the filter and return the resulting cursor.
1416      *
1417      * @param filter the text that was entered to filter on
1418      * @return a cursor with the results of the filter
1419      */
doFilter(String filter)1420     Cursor doFilter(String filter) {
1421         final ContentResolver resolver = getContentResolver();
1422 
1423         switch (mMode) {
1424             case MODE_GROUP: {
1425                 Uri uri;
1426                 if (TextUtils.isEmpty(filter)) {
1427                     uri = mGroupUri;
1428                 } else {
1429                     uri = Uri.withAppendedPath(mGroupFilterUri, Uri.encode(filter));
1430                 }
1431                 return resolver.query(uri, CONTACTS_PROJECTION, null, null,
1432                         getSortOrder(CONTACTS_PROJECTION));
1433             }
1434 
1435             case MODE_ALL_CONTACTS:
1436             case MODE_PICK_CONTACT:
1437             case MODE_PICK_OR_CREATE_CONTACT:
1438             case MODE_INSERT_OR_EDIT_CONTACT: {
1439                 return resolver.query(getPeopleFilterUri(filter), CONTACTS_PROJECTION, null, null,
1440                         getSortOrder(CONTACTS_PROJECTION));
1441             }
1442 
1443             case MODE_WITH_PHONES: {
1444                 return resolver.query(getPeopleFilterUri(filter), CONTACTS_PROJECTION,
1445                         People.PRIMARY_PHONE_ID + " IS NOT NULL", null,
1446                         getSortOrder(CONTACTS_PROJECTION));
1447             }
1448 
1449             case MODE_STARRED: {
1450                 return resolver.query(getPeopleFilterUri(filter), CONTACTS_PROJECTION,
1451                         People.STARRED + "=1", null, getSortOrder(CONTACTS_PROJECTION));
1452             }
1453 
1454             case MODE_FREQUENT: {
1455                 return resolver.query(getPeopleFilterUri(filter), CONTACTS_PROJECTION,
1456                         People.TIMES_CONTACTED + " > 0", null,
1457                         People.TIMES_CONTACTED + " DESC, " + getSortOrder(CONTACTS_PROJECTION));
1458 
1459             }
1460 
1461             case MODE_STREQUENT: {
1462                 Uri uri;
1463                 if (!TextUtils.isEmpty(filter)) {
1464                     uri = Uri.withAppendedPath(People.CONTENT_URI, "strequent/filter/"
1465                             + Uri.encode(filter));
1466                 } else {
1467                     uri = Uri.withAppendedPath(People.CONTENT_URI, "strequent");
1468                 }
1469                 return resolver.query(uri, STREQUENT_PROJECTION, null, null, null);
1470             }
1471 
1472             case MODE_PICK_PHONE: {
1473                 Uri uri;
1474                 if (!TextUtils.isEmpty(filter)) {
1475                     uri = Uri.withAppendedPath(Phones.CONTENT_URI, "filter_name/"
1476                             + Uri.encode(filter));
1477                 } else {
1478                     uri = Phones.CONTENT_URI;
1479                 }
1480                 return resolver.query(uri, PHONES_PROJECTION, null, null,
1481                         getSortOrder(PHONES_PROJECTION));
1482             }
1483         }
1484         throw new UnsupportedOperationException("filtering not allowed in mode " + mMode);
1485     }
1486 
1487     /**
1488      * Calls the currently selected list item.
1489      * @return true if the call was initiated, false otherwise
1490      */
callSelection()1491     boolean callSelection() {
1492         ListView list = getListView();
1493         if (list.hasFocus()) {
1494             Cursor cursor = (Cursor) list.getSelectedItem();
1495             if (cursor != null) {
1496                 long phoneId = cursor.getLong(PRIMARY_PHONE_ID_COLUMN_INDEX);
1497                 if (phoneId == 0) {
1498                     // There is no phone number.
1499                     signalError();
1500                     return false;
1501                 }
1502                 Uri uri = ContentUris.withAppendedId(Phones.CONTENT_URI, phoneId);
1503                 Intent intent = new Intent(Intent.ACTION_CALL_PRIVILEGED, uri);
1504                 startActivity(intent);
1505                 return true;
1506             }
1507         }
1508 
1509         return false;
1510     }
1511 
1512     /**
1513      * Signal an error to the user.
1514      */
signalError()1515     void signalError() {
1516         //TODO play an error beep or something...
1517     }
1518 
getItemForView(View view)1519     Cursor getItemForView(View view) {
1520         ListView listView = getListView();
1521         int index = listView.getPositionForView(view);
1522         if (index < 0) {
1523             return null;
1524         }
1525         return (Cursor) listView.getAdapter().getItem(index);
1526     }
1527 
setGroupEntries(AlertDialog.Builder builder)1528     private void setGroupEntries(AlertDialog.Builder builder) {
1529         boolean syncEverything;
1530         // For now we only support a single account and the UI doesn't know what
1531         // the account name is, so we're using a global setting for SYNC_EVERYTHING.
1532         // Some day when we add multiple accounts to the UI this should use the per
1533         // account setting.
1534         String value = Contacts.Settings.getSetting(getContentResolver(), null,
1535                 Contacts.Settings.SYNC_EVERYTHING);
1536         if (value == null) {
1537             // If nothing is set yet we default to syncing everything
1538             syncEverything = true;
1539         } else {
1540             syncEverything = !TextUtils.isEmpty(value) && !"0".equals(value);
1541         }
1542 
1543         Cursor cursor;
1544         if (!syncEverything) {
1545             cursor = getContentResolver().query(Groups.CONTENT_URI, GROUPS_PROJECTION,
1546                     Groups.SHOULD_SYNC + " != 0", null, Groups.DEFAULT_SORT_ORDER);
1547         } else {
1548             cursor = getContentResolver().query(Groups.CONTENT_URI, GROUPS_PROJECTION,
1549                     null, null, Groups.DEFAULT_SORT_ORDER);
1550         }
1551         try {
1552             ArrayList<CharSequence> groups = new ArrayList<CharSequence>();
1553             ArrayList<CharSequence> prefStrings = new ArrayList<CharSequence>();
1554 
1555             // Add All Contacts
1556             groups.add(DISPLAY_GROUP_INDEX_ALL_CONTACTS, getString(R.string.showAllGroups));
1557             prefStrings.add("");
1558 
1559             // Add Contacts with phones
1560             groups.add(DISPLAY_GROUP_INDEX_ALL_CONTACTS_WITH_PHONES,
1561                     getString(R.string.groupNameWithPhones));
1562             prefStrings.add(GROUP_WITH_PHONES);
1563 
1564             int currentIndex = DISPLAY_GROUP_INDEX_ALL_CONTACTS;
1565             while (cursor.moveToNext()) {
1566                 String systemId = cursor.getString(GROUPS_COLUMN_INDEX_SYSTEM_ID);
1567                 String name = cursor.getString(GROUPS_COLUMN_INDEX_NAME);
1568                 if (cursor.isNull(GROUPS_COLUMN_INDEX_SYSTEM_ID)
1569                         && !Groups.GROUP_MY_CONTACTS.equals(systemId)) {
1570                     // All groups that aren't My Contacts, since that one is localized on the phone
1571 
1572                     // Localize the "Starred in Android" string which we get from the server side.
1573                     if (Groups.GROUP_ANDROID_STARRED.equals(name)) {
1574                         name = getString(R.string.starredInAndroid);
1575                     }
1576                     groups.add(name);
1577                     if (name.equals(mDisplayInfo)) {
1578                         currentIndex = groups.size() - 1;
1579                     }
1580                 } else {
1581                     // The My Contacts group
1582                     groups.add(DISPLAY_GROUP_INDEX_MY_CONTACTS,
1583                             getString(R.string.groupNameMyContacts));
1584                     if (mDisplayType == DISPLAY_TYPE_SYSTEM_GROUP
1585                             && Groups.GROUP_MY_CONTACTS.equals(mDisplayInfo)) {
1586                         currentIndex = DISPLAY_GROUP_INDEX_MY_CONTACTS;
1587                     }
1588                     mDisplayGroupsIncludesMyContacts = true;
1589                 }
1590             }
1591             if (mMode == MODE_ALL_CONTACTS) {
1592                 currentIndex = DISPLAY_GROUP_INDEX_ALL_CONTACTS;
1593             } else if (mMode == MODE_WITH_PHONES) {
1594                 currentIndex = DISPLAY_GROUP_INDEX_ALL_CONTACTS_WITH_PHONES;
1595             }
1596             mDisplayGroups = groups.toArray(new CharSequence[groups.size()]);
1597             builder.setSingleChoiceItems(mDisplayGroups, currentIndex, this);
1598             mDisplayGroupOriginalSelection = currentIndex;
1599         } finally {
1600             cursor.close();
1601         }
1602     }
1603 
1604     private static final class QueryHandler extends AsyncQueryHandler {
1605         private final WeakReference<ContactsListActivity> mActivity;
1606 
QueryHandler(Context context)1607         public QueryHandler(Context context) {
1608             super(context.getContentResolver());
1609             mActivity = new WeakReference<ContactsListActivity>((ContactsListActivity) context);
1610         }
1611 
1612         @Override
onQueryComplete(int token, Object cookie, Cursor cursor)1613         protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
1614             final ContactsListActivity activity = mActivity.get();
1615             if (activity != null && !activity.isFinishing()) {
1616                 activity.mAdapter.setLoading(false);
1617                 activity.getListView().clearTextFilter();
1618                 activity.mAdapter.changeCursor(cursor);
1619 
1620                 // Now that the cursor is populated again, it's possible to restore the list state
1621                 if (activity.mListState != null) {
1622                     activity.mList.onRestoreInstanceState(activity.mListState);
1623                     if (activity.mListHasFocus) {
1624                         activity.mList.requestFocus();
1625                     }
1626                     activity.mListHasFocus = false;
1627                     activity.mListState = null;
1628                 }
1629             } else {
1630                 cursor.close();
1631             }
1632         }
1633     }
1634 
1635     final static class ContactListItemCache {
1636         public TextView nameView;
1637         public CharArrayBuffer nameBuffer = new CharArrayBuffer(128);
1638         public TextView labelView;
1639         public CharArrayBuffer labelBuffer = new CharArrayBuffer(128);
1640         public TextView numberView;
1641         public CharArrayBuffer numberBuffer = new CharArrayBuffer(128);
1642         public ImageView presenceView;
1643         public ImageView photoView;
1644     }
1645 
1646     private final class ContactItemListAdapter extends ResourceCursorAdapter
1647             implements SectionIndexer {
1648         private SectionIndexer mIndexer;
1649         private String mAlphabet;
1650         private boolean mLoading = true;
1651         private CharSequence mUnknownNameText;
1652         private CharSequence[] mLocalizedLabels;
1653         private boolean mDisplayPhotos = false;
1654         private SparseArray<SoftReference<Bitmap>> mBitmapCache = null;
1655         private int mFrequentSeparatorPos = ListView.INVALID_POSITION;
1656 
ContactItemListAdapter(Context context)1657         public ContactItemListAdapter(Context context) {
1658             super(context, R.layout.contacts_list_item, null, false);
1659 
1660             mAlphabet = context.getString(com.android.internal.R.string.fast_scroll_alphabet);
1661 
1662             mUnknownNameText = context.getText(android.R.string.unknownName);
1663             switch (mMode) {
1664                 case MODE_PICK_POSTAL:
1665                     mLocalizedLabels = EditContactActivity.getLabelsForKind(mContext,
1666                             Contacts.KIND_POSTAL);
1667                     break;
1668                 default:
1669                     mLocalizedLabels = EditContactActivity.getLabelsForKind(mContext,
1670                             Contacts.KIND_PHONE);
1671                     break;
1672             }
1673 
1674             if ((mMode & MODE_MASK_SHOW_PHOTOS) == MODE_MASK_SHOW_PHOTOS) {
1675                 mDisplayPhotos = true;
1676                 setViewResource(R.layout.contacts_list_item_photo);
1677                 mBitmapCache = new SparseArray<SoftReference<Bitmap>>();
1678             }
1679         }
1680 
getNewIndexer(Cursor cursor)1681         private SectionIndexer getNewIndexer(Cursor cursor) {
1682             if (Locale.getDefault().getLanguage().equals(Locale.JAPAN.getLanguage())) {
1683                 return new JapaneseContactListIndexer(cursor, SORT_STRING_INDEX);
1684             } else {
1685                 return new AlphabetIndexer(cursor, NAME_COLUMN_INDEX, mAlphabet);
1686             }
1687         }
1688 
1689         /**
1690          * Callback on the UI thread when the content observer on the backing cursor fires.
1691          * Instead of calling requery we need to do an async query so that the requery doesn't
1692          * block the UI thread for a long time.
1693          */
1694         @Override
onContentChanged()1695         protected void onContentChanged() {
1696             CharSequence constraint = getListView().getTextFilter();
1697             if (!TextUtils.isEmpty(constraint)) {
1698                 // Reset the filter state then start an async filter operation
1699                 Filter filter = getFilter();
1700                 filter.filter(constraint);
1701             } else {
1702                 // Start an async query
1703                 startQuery();
1704             }
1705         }
1706 
setLoading(boolean loading)1707         public void setLoading(boolean loading) {
1708             mLoading = loading;
1709         }
1710 
1711         @Override
isEmpty()1712         public boolean isEmpty() {
1713             if ((mMode & MODE_MASK_CREATE_NEW) == MODE_MASK_CREATE_NEW) {
1714                 // This mode mask adds a header and we always want it to show up, even
1715                 // if the list is empty, so always claim the list is not empty.
1716                 return false;
1717             } else {
1718                 if (mLoading) {
1719                     // We don't want the empty state to show when loading.
1720                     return false;
1721                 } else {
1722                     return super.isEmpty();
1723                 }
1724             }
1725         }
1726 
1727         @Override
getItemViewType(int position)1728         public int getItemViewType(int position) {
1729             if (position == mFrequentSeparatorPos) {
1730                 // We don't want the separator view to be recycled.
1731                 return IGNORE_ITEM_VIEW_TYPE;
1732             }
1733             return super.getItemViewType(position);
1734         }
1735 
1736         @Override
getView(int position, View convertView, ViewGroup parent)1737         public View getView(int position, View convertView, ViewGroup parent) {
1738             if (!mDataValid) {
1739                 throw new IllegalStateException(
1740                         "this should only be called when the cursor is valid");
1741             }
1742 
1743             // Handle the separator specially
1744             if (position == mFrequentSeparatorPos) {
1745                 LayoutInflater inflater =
1746                         (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
1747                 TextView view = (TextView) inflater.inflate(R.layout.list_separator, parent, false);
1748                 view.setText(R.string.favoritesFrquentSeparator);
1749                 return view;
1750             }
1751 
1752             if (!mCursor.moveToPosition(getRealPosition(position))) {
1753                 throw new IllegalStateException("couldn't move cursor to position " + position);
1754             }
1755 
1756             View v;
1757             if (convertView == null) {
1758                 v = newView(mContext, mCursor, parent);
1759             } else {
1760                 v = convertView;
1761             }
1762             bindView(v, mContext, mCursor);
1763             return v;
1764         }
1765 
1766         @Override
newView(Context context, Cursor cursor, ViewGroup parent)1767         public View newView(Context context, Cursor cursor, ViewGroup parent) {
1768             final View view = super.newView(context, cursor, parent);
1769 
1770             final ContactListItemCache cache = new ContactListItemCache();
1771             cache.nameView = (TextView) view.findViewById(R.id.name);
1772             cache.labelView = (TextView) view.findViewById(R.id.label);
1773             cache.numberView = (TextView) view.findViewById(R.id.number);
1774             cache.presenceView = (ImageView) view.findViewById(R.id.presence);
1775             cache.photoView = (ImageView) view.findViewById(R.id.photo);
1776             view.setTag(cache);
1777 
1778             return view;
1779         }
1780 
1781         @Override
bindView(View view, Context context, Cursor cursor)1782         public void bindView(View view, Context context, Cursor cursor) {
1783             final ContactListItemCache cache = (ContactListItemCache) view.getTag();
1784 
1785             // Set the name
1786             cursor.copyStringToBuffer(NAME_COLUMN_INDEX, cache.nameBuffer);
1787             int size = cache.nameBuffer.sizeCopied;
1788             if (size != 0) {
1789                 cache.nameView.setText(cache.nameBuffer.data, 0, size);
1790             } else {
1791                 cache.nameView.setText(mUnknownNameText);
1792             }
1793 
1794             // Bail out early if using a specific SEARCH query mode, usually for
1795             // matching a specific E-mail or phone number. Any contact details
1796             // shown would be identical, and columns might not even be present
1797             // in the returned cursor.
1798             if (mQueryMode != QUERY_MODE_NONE) {
1799                 cache.numberView.setVisibility(View.GONE);
1800                 cache.labelView.setVisibility(View.GONE);
1801                 cache.presenceView.setVisibility(View.GONE);
1802                 return;
1803             }
1804 
1805             // Set the phone number
1806             TextView numberView = cache.numberView;
1807             TextView labelView = cache.labelView;
1808             cursor.copyStringToBuffer(NUMBER_COLUMN_INDEX, cache.numberBuffer);
1809             size = cache.numberBuffer.sizeCopied;
1810             if (size != 0) {
1811                 numberView.setText(cache.numberBuffer.data, 0, size);
1812                 numberView.setVisibility(View.VISIBLE);
1813                 labelView.setVisibility(View.VISIBLE);
1814             } else {
1815                 numberView.setVisibility(View.GONE);
1816                 labelView.setVisibility(View.GONE);
1817             }
1818 
1819             // Set the label
1820             if (!cursor.isNull(TYPE_COLUMN_INDEX)) {
1821                 int type = cursor.getInt(TYPE_COLUMN_INDEX);
1822 
1823                 if (type != People.Phones.TYPE_CUSTOM) {
1824                     try {
1825                         labelView.setText(mLocalizedLabels[type - 1]);
1826                     } catch (ArrayIndexOutOfBoundsException e) {
1827                         labelView.setText(mLocalizedLabels[People.Phones.TYPE_HOME - 1]);
1828                     }
1829                 } else {
1830                     cursor.copyStringToBuffer(LABEL_COLUMN_INDEX, cache.labelBuffer);
1831                     // Don't check size, if it's zero just don't show anything
1832                     labelView.setText(cache.labelBuffer.data, 0, cache.labelBuffer.sizeCopied);
1833                 }
1834             } else {
1835                 // There is no label, hide the the view
1836                 labelView.setVisibility(View.GONE);
1837             }
1838 
1839             // Set the proper icon (star or presence or nothing)
1840             ImageView presenceView = cache.presenceView;
1841             if ((mMode & MODE_MASK_NO_PRESENCE) == 0) {
1842                 int serverStatus;
1843                 if (!cursor.isNull(SERVER_STATUS_COLUMN_INDEX)) {
1844                     serverStatus = cursor.getInt(SERVER_STATUS_COLUMN_INDEX);
1845                     presenceView.setImageResource(
1846                             Presence.getPresenceIconResourceId(serverStatus));
1847                     presenceView.setVisibility(View.VISIBLE);
1848                 } else {
1849                     presenceView.setVisibility(View.GONE);
1850                 }
1851             } else {
1852                 presenceView.setVisibility(View.GONE);
1853             }
1854 
1855             // Set the photo, if requested
1856             if (mDisplayPhotos) {
1857                 Bitmap photo = null;
1858 
1859                 // Look for the cached bitmap
1860                 int pos = cursor.getPosition();
1861                 SoftReference<Bitmap> ref = mBitmapCache.get(pos);
1862                 if (ref != null) {
1863                     photo = ref.get();
1864                 }
1865 
1866                 if (photo == null) {
1867                     // Bitmap cache miss, decode it from the cursor
1868                     if (!cursor.isNull(PHOTO_COLUMN_INDEX)) {
1869                         try {
1870                             byte[] photoData = cursor.getBlob(PHOTO_COLUMN_INDEX);
1871                             photo = BitmapFactory.decodeByteArray(photoData, 0,
1872                                     photoData.length);
1873                             mBitmapCache.put(pos, new SoftReference<Bitmap>(photo));
1874                         } catch (OutOfMemoryError e) {
1875                             // Not enough memory for the photo, use the default one instead
1876                             photo = null;
1877                         }
1878                     }
1879                 }
1880 
1881                 // Bind the photo, or use the fallback no photo resource
1882                 if (photo != null) {
1883                     cache.photoView.setImageBitmap(photo);
1884                 } else {
1885                     cache.photoView.setImageResource(R.drawable.ic_contact_list_picture);
1886                 }
1887             }
1888         }
1889 
1890         @Override
changeCursor(Cursor cursor)1891         public void changeCursor(Cursor cursor) {
1892             // Get the split between starred and frequent items, if the mode is strequent
1893             mFrequentSeparatorPos = ListView.INVALID_POSITION;
1894             if (cursor != null && cursor.getCount() > 0 && mMode == MODE_STREQUENT) {
1895                 cursor.move(-1);
1896                 for (int i = 0; cursor.moveToNext(); i++) {
1897                     int starred = cursor.getInt(STARRED_COLUMN_INDEX);
1898                     if (starred == 0) {
1899                         if (i > 0) {
1900                             // Only add the separator when there are starred items present
1901                             mFrequentSeparatorPos = i;
1902                         }
1903                         break;
1904                     }
1905                 }
1906             }
1907 
1908             super.changeCursor(cursor);
1909 
1910             // Update the indexer for the fast scroll widget
1911             updateIndexer(cursor);
1912 
1913             // Clear the photo bitmap cache, if there is one
1914             if (mBitmapCache != null) {
1915                 mBitmapCache.clear();
1916             }
1917         }
1918 
updateIndexer(Cursor cursor)1919         private void updateIndexer(Cursor cursor) {
1920             if (mIndexer == null) {
1921                 mIndexer = getNewIndexer(cursor);
1922             } else {
1923                 if (Locale.getDefault().equals(Locale.JAPAN)) {
1924                     if (mIndexer instanceof JapaneseContactListIndexer) {
1925                         ((JapaneseContactListIndexer)mIndexer).setCursor(cursor);
1926                     } else {
1927                         mIndexer = getNewIndexer(cursor);
1928                     }
1929                 } else {
1930                     if (mIndexer instanceof AlphabetIndexer) {
1931                         ((AlphabetIndexer)mIndexer).setCursor(cursor);
1932                     } else {
1933                         mIndexer = getNewIndexer(cursor);
1934                     }
1935                 }
1936             }
1937         }
1938 
1939         /**
1940          * Run the query on a helper thread. Beware that this code does not run
1941          * on the main UI thread!
1942          */
1943         @Override
runQueryOnBackgroundThread(CharSequence constraint)1944         public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
1945             return doFilter(constraint.toString());
1946         }
1947 
getSections()1948         public Object [] getSections() {
1949             if (mMode == MODE_STREQUENT) {
1950                 return new String[] { " " };
1951             } else {
1952                 return mIndexer.getSections();
1953            }
1954         }
1955 
getPositionForSection(int sectionIndex)1956         public int getPositionForSection(int sectionIndex) {
1957             if (mMode == MODE_STREQUENT) {
1958                 return 0;
1959             }
1960 
1961             if (mIndexer == null) {
1962                 Cursor cursor = mAdapter.getCursor();
1963                 if (cursor == null) {
1964                     // No cursor, the section doesn't exist so just return 0
1965                     return 0;
1966                 }
1967                 mIndexer = getNewIndexer(cursor);
1968             }
1969 
1970             return mIndexer.getPositionForSection(sectionIndex);
1971         }
1972 
getSectionForPosition(int position)1973         public int getSectionForPosition(int position) {
1974             // Note: JapaneseContactListIndexer depends on the fact
1975             // this method always returns 0. If you change this,
1976             // please care it too.
1977             return 0;
1978         }
1979 
1980         @Override
areAllItemsEnabled()1981         public boolean areAllItemsEnabled() {
1982             return mMode != MODE_STREQUENT;
1983         }
1984 
1985         @Override
isEnabled(int position)1986         public boolean isEnabled(int position) {
1987             return position != mFrequentSeparatorPos;
1988         }
1989 
1990         @Override
getCount()1991         public int getCount() {
1992             if (mFrequentSeparatorPos != ListView.INVALID_POSITION) {
1993                 return super.getCount() + 1;
1994             } else {
1995                 return super.getCount();
1996             }
1997         }
1998 
getRealPosition(int pos)1999         private int getRealPosition(int pos) {
2000             if (mFrequentSeparatorPos == ListView.INVALID_POSITION) {
2001                 // No separator, identity map
2002                 return pos;
2003             } else if (pos <= mFrequentSeparatorPos) {
2004                 // Before or at the separator, identity map
2005                 return pos;
2006             } else {
2007                 // After the separator, remove 1 from the pos to get the real underlying pos
2008                 return pos - 1;
2009             }
2010 
2011         }
2012 
2013         @Override
getItem(int pos)2014         public Object getItem(int pos) {
2015             return super.getItem(getRealPosition(pos));
2016         }
2017 
2018         @Override
getItemId(int pos)2019         public long getItemId(int pos) {
2020             return super.getItemId(getRealPosition(pos));
2021         }
2022     }
2023 }
2024