OSDN Git Service

auto import from //depot/cupcake/@132589
[android-x86/packages-apps-Contacts.git] / src / com / android / contacts / ContactsListActivity.java
1 /*
2  * Copyright (C) 2007 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16
17 package com.android.contacts;
18
19 import 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.database.CharArrayBuffer;
34 import android.database.Cursor;
35 import android.graphics.Bitmap;
36 import android.net.Uri;
37 import android.os.Bundle;
38 import android.os.Parcelable;
39 import android.os.RemoteException;
40 import android.preference.PreferenceManager;
41 import android.provider.Contacts;
42 import android.provider.Contacts.ContactMethods;
43 import android.provider.Contacts.Groups;
44 import android.provider.Contacts.Intents;
45 import android.provider.Contacts.People;
46 import android.provider.Contacts.Phones;
47 import android.provider.Contacts.Presence;
48 import android.provider.Contacts.Intents.UI;
49 import android.text.TextUtils;
50 import android.util.Log;
51 import android.view.ContextMenu;
52 import android.view.Gravity;
53 import android.view.KeyEvent;
54 import android.view.LayoutInflater;
55 import android.view.Menu;
56 import android.view.MenuItem;
57 import android.view.View;
58 import android.view.ViewGroup;
59 import android.view.ContextMenu.ContextMenuInfo;
60 import android.widget.AdapterView;
61 import android.widget.AlphabetIndexer;
62 import android.widget.ImageView;
63 import android.widget.ListView;
64 import android.widget.ResourceCursorAdapter;
65 import android.widget.SectionIndexer;
66 import android.widget.TextView;
67
68 import java.lang.ref.WeakReference;
69 import java.util.ArrayList;
70
71 /**
72  * Displays a list of contacts. Usually is embedded into the ContactsActivity.
73  */
74 public final class ContactsListActivity extends ListActivity
75         implements View.OnCreateContextMenuListener, DialogInterface.OnClickListener {
76     private static final String TAG = "ContactsListActivity";
77
78     private static final String LIST_STATE_KEY = "liststate";
79     private static final String FOCUS_KEY = "focused";
80
81     static final int MENU_ITEM_VIEW_CONTACT = 1;
82     static final int MENU_ITEM_CALL = 2;
83     static final int MENU_ITEM_EDIT_BEFORE_CALL = 3;
84     static final int MENU_ITEM_SEND_SMS = 4;
85     static final int MENU_ITEM_SEND_IM = 5;
86     static final int MENU_ITEM_EDIT = 6;
87     static final int MENU_ITEM_DELETE = 7;
88     static final int MENU_ITEM_TOGGLE_STAR = 8;
89
90     public static final int MENU_DIALER = 9;
91     public static final int MENU_NEW_CONTACT = 10;
92     public static final int MENU_DISPLAY_GROUP = 11;
93
94     private static final int SUBACTIVITY_NEW_CONTACT = 1;
95     
96     /** Mask for picker mode */
97     static final int MODE_MASK_PICKER = 0x80000000;
98     /** Mask for no presence mode */
99     static final int MODE_MASK_NO_PRESENCE = 0x40000000;
100     /** Mask for enabling list filtering */
101     static final int MODE_MASK_NO_FILTER = 0x20000000;
102     /** Mask for having a "create new contact" header in the list */
103     static final int MODE_MASK_CREATE_NEW = 0x10000000;
104
105     /** Unknown mode */
106     static final int MODE_UNKNOWN = 0;
107     /** Show members of the "Contacts" group */
108     static final int MODE_GROUP = 5;
109     /** Show all contacts sorted alphabetically */
110     static final int MODE_ALL_CONTACTS = 10;
111     /** Show all contacts with phone numbers, sorted alphabetically */
112     static final int MODE_WITH_PHONES = 15;
113     /** Show all starred contacts */
114     static final int MODE_STARRED = 20;
115     /** Show frequently contacted contacts */
116     static final int MODE_FREQUENT = 30;
117     /** Show starred and the frequent */
118     static final int MODE_STREQUENT = 35;
119     /** Show all contacts and pick them when clicking */
120     static final int MODE_PICK_CONTACT = 40 | MODE_MASK_PICKER;
121     /** Show all contacts as well as the option to create a new one */
122     static final int MODE_PICK_OR_CREATE_CONTACT = 42 | MODE_MASK_PICKER | MODE_MASK_CREATE_NEW;
123     /** Show all contacts and pick them when clicking, and allow creating a new contact */
124     static final int MODE_INSERT_OR_EDIT_CONTACT = 45 | MODE_MASK_PICKER | MODE_MASK_CREATE_NEW;
125     /** Show all phone numbers and pick them when clicking */
126     static final int MODE_PICK_PHONE = 50 | MODE_MASK_PICKER | MODE_MASK_NO_PRESENCE;
127     /** Show all postal addresses and pick them when clicking */
128     static final int MODE_PICK_POSTAL =
129             55 | MODE_MASK_PICKER | MODE_MASK_NO_PRESENCE | MODE_MASK_NO_FILTER;
130     /** Run a search query */
131     static final int MODE_QUERY = 60 | MODE_MASK_NO_FILTER;
132
133     static final int DEFAULT_MODE = MODE_ALL_CONTACTS;
134
135     /**
136      * The type of data to display in the main contacts list. 
137      */
138     static final String PREF_DISPLAY_TYPE = "display_system_group";
139
140     /** Unknown display type. */
141     static final int DISPLAY_TYPE_UNKNOWN = -1;
142     /** Display all contacts */
143     static final int DISPLAY_TYPE_ALL = 0;
144     /** Display all contacts that have phone numbers */
145     static final int DISPLAY_TYPE_ALL_WITH_PHONES = 1;
146     /** Display a system group */
147     static final int DISPLAY_TYPE_SYSTEM_GROUP = 2;
148     /** Display a user group */
149     static final int DISPLAY_TYPE_USER_GROUP = 3;
150
151     /**
152      * Info about what to display. If {@link #PREF_DISPLAY_TYPE}
153      * is {@link #DISPLAY_TYPE_SYSTEM_GROUP} then this will be the system id.
154      * If {@link #PREF_DISPLAY_TYPE} is {@link #DISPLAY_TYPE_USER_GROUP} then this will
155      * be the group name.
156      */ 
157     static final String PREF_DISPLAY_INFO = "display_group";
158
159     
160     static final String NAME_COLUMN = People.DISPLAY_NAME;
161     
162     static final String[] CONTACTS_PROJECTION = new String[] {
163         People._ID, // 0
164         NAME_COLUMN, // 1
165         People.NUMBER, // 2
166         People.TYPE, // 3
167         People.LABEL, // 4
168         People.STARRED, // 5
169         People.PRIMARY_PHONE_ID, // 6
170         People.PRIMARY_EMAIL_ID, // 7
171         People.PRESENCE_STATUS, // 8
172     };
173
174     static final String[] STREQUENT_PROJECTION = new String[] {
175         People._ID, // 0
176         NAME_COLUMN, // 1
177         People.NUMBER, // 2
178         People.TYPE, // 3
179         People.LABEL, // 4
180         People.STARRED, // 5
181         People.PRIMARY_PHONE_ID, // 6
182         People.PRIMARY_EMAIL_ID, // 7
183         People.PRESENCE_STATUS, // 8
184         People.TIMES_CONTACTED, // 9 (not displayed, but required for the order by to work)
185     };
186
187     static final String[] PHONES_PROJECTION = new String[] {
188         Phones._ID, // 0
189         NAME_COLUMN, // 1
190         Phones.NUMBER, // 2
191         Phones.TYPE, // 3
192         Phones.LABEL, // 4
193         Phones.STARRED, // 5
194     };
195
196     static final String[] CONTACT_METHODS_PROJECTION = new String[] {
197         ContactMethods._ID, // 0
198         NAME_COLUMN, // 1
199         ContactMethods.DATA, // 2
200         ContactMethods.TYPE, // 3
201         ContactMethods.LABEL, // 4
202         ContactMethods.STARRED, // 5
203     };
204
205     static final int ID_COLUMN_INDEX = 0;
206     static final int NAME_COLUMN_INDEX = 1;
207     static final int NUMBER_COLUMN_INDEX = 2;
208     static final int DATA_COLUMN_INDEX = 2;
209     static final int TYPE_COLUMN_INDEX = 3;
210     static final int LABEL_COLUMN_INDEX = 4;
211     static final int STARRED_COLUMN_INDEX = 5;
212     static final int PRIMARY_PHONE_ID_COLUMN_INDEX = 6;
213     static final int PRIMARY_EMAIL_ID_COLUMN_INDEX = 7;
214     static final int SERVER_STATUS_COLUMN_INDEX = 8;
215
216     
217     static final int DISPLAY_GROUP_INDEX_ALL_CONTACTS = 0;
218     static final int DISPLAY_GROUP_INDEX_ALL_CONTACTS_WITH_PHONES = 1;
219     static final int DISPLAY_GROUP_INDEX_MY_CONTACTS = 2;
220     
221     static final String SORT_ORDER = NAME_COLUMN + " COLLATE LOCALIZED ASC";
222     
223     private static final int QUERY_TOKEN = 42;
224
225     private static final String[] GROUPS_PROJECTION = new String[] {
226         Groups.SYSTEM_ID, // 0
227         Groups.NAME, // 1
228     };
229     private static final int GROUPS_COLUMN_INDEX_SYSTEM_ID = 0;
230     private static final int GROUPS_COLUMN_INDEX_NAME = 1;
231     
232     static final String GROUP_WITH_PHONES = "android_smartgroup_phone";
233
234     ContactItemListAdapter mAdapter;
235
236     int mMode = DEFAULT_MODE;
237     // The current display group
238     private String mDisplayInfo;
239     private int mDisplayType;
240     // The current list of display groups, during selection from menu
241     private CharSequence[] mDisplayGroups;
242     // If true position 2 in mDisplayGroups is the MyContacts group
243     private boolean mDisplayGroupsIncludesMyContacts = false;
244
245     private int mDisplayGroupOriginalSelection;
246     private int mDisplayGroupCurrentSelection;
247     
248     private QueryHandler mQueryHandler;
249     private String mQuery;
250     private Uri mGroupFilterUri;
251     private Uri mGroupUri;
252     private boolean mJustCreated;
253     private boolean mSyncEnabled;
254
255     /**
256      * Used to keep track of the scroll state of the list.
257      */
258     private Parcelable mListState = null;
259     private boolean mListHasFocus;
260
261     private boolean mCreateShortcut;
262     private boolean mDefaultMode = false;
263
264     private class DeleteClickListener implements DialogInterface.OnClickListener {
265         private Uri mUri;
266
267         public DeleteClickListener(Uri uri) {
268             mUri = uri;
269         }
270
271         public void onClick(DialogInterface dialog, int which) {
272             getContentResolver().delete(mUri, null, null);
273         }
274     }
275
276     @Override
277     protected void onCreate(Bundle icicle) {
278         super.onCreate(icicle);
279
280         // Resolve the intent
281         final Intent intent = getIntent();
282
283         // Allow the title to be set to a custom String using an extra on the intent
284         String title = intent.getStringExtra(Contacts.Intents.UI.TITLE_EXTRA_KEY);
285         if (title != null) {
286             setTitle(title);
287         }
288         
289         final String action = intent.getAction();
290         mMode = MODE_UNKNOWN;
291         
292         setContentView(R.layout.contacts_list_content);
293
294         if (UI.LIST_DEFAULT.equals(action)) {
295             mDefaultMode = true;
296             // When mDefaultMode is true the mode is set in onResume(), since the preferneces
297             // activity may change it whenever this activity isn't running
298         } else if (UI.LIST_GROUP_ACTION.equals(action)) {
299             mMode = MODE_GROUP;
300             String groupName = intent.getStringExtra(UI.GROUP_NAME_EXTRA_KEY);
301             if (TextUtils.isEmpty(groupName)) {
302                 finish();
303                 return;
304             }
305             buildUserGroupUris(groupName);
306         } else if (UI.LIST_ALL_CONTACTS_ACTION.equals(action)) {
307             mMode = MODE_ALL_CONTACTS;
308         } else if (UI.LIST_STARRED_ACTION.equals(action)) {
309             mMode = MODE_STARRED;
310         } else if (UI.LIST_FREQUENT_ACTION.equals(action)) {
311             mMode = MODE_FREQUENT;
312         } else if (UI.LIST_STREQUENT_ACTION.equals(action)) {
313             mMode = MODE_STREQUENT;
314         } else if (UI.LIST_CONTACTS_WITH_PHONES_ACTION.equals(action)) {
315             mMode = MODE_WITH_PHONES;
316         } else if (Intent.ACTION_PICK.equals(action)) {
317             // XXX These should be showing the data from the URI given in
318             // the Intent.
319             final String type = intent.resolveType(this);
320             if (People.CONTENT_TYPE.equals(type)) {
321                 mMode = MODE_PICK_CONTACT;
322             } else if (Phones.CONTENT_TYPE.equals(type)) {
323                 mMode = MODE_PICK_PHONE;
324             } else if (ContactMethods.CONTENT_POSTAL_TYPE.equals(type)) {
325                 mMode = MODE_PICK_POSTAL;
326             }
327         } else if (Intent.ACTION_CREATE_SHORTCUT.equals(action)) {
328             mMode = MODE_PICK_OR_CREATE_CONTACT;
329             mCreateShortcut = true;
330         } else if (Intent.ACTION_GET_CONTENT.equals(action)) {
331             final String type = intent.resolveType(this);
332             if (People.CONTENT_ITEM_TYPE.equals(type)) {
333                 mMode = MODE_PICK_OR_CREATE_CONTACT;
334             } else if (Phones.CONTENT_ITEM_TYPE.equals(type)) {
335                 mMode = MODE_PICK_PHONE;
336             } else if (ContactMethods.CONTENT_POSTAL_ITEM_TYPE.equals(type)) {
337                 mMode = MODE_PICK_POSTAL;
338             }
339         } else if (Intent.ACTION_INSERT_OR_EDIT.equals(action)) {
340             mMode = MODE_INSERT_OR_EDIT_CONTACT;
341         } else if (Intent.ACTION_SEARCH.equals(action)) {
342             // See if the suggestion was clicked with a search action key (call button)
343             if ("call".equals(intent.getStringExtra(SearchManager.ACTION_MSG))) {
344                 String query = intent.getStringExtra(SearchManager.QUERY);
345                 if (!TextUtils.isEmpty(query)) {
346                     Intent newIntent = new Intent(Intent.ACTION_CALL_PRIVILEGED,
347                             Uri.fromParts("tel", query, null));
348                     startActivity(newIntent);
349                 }
350                 finish();
351                 return;
352             }
353             // Otherwise handle the more normal search case
354             mMode = MODE_QUERY;
355
356         // Since this is the filter activity it receives all intents
357         // dispatched from the SearchManager for security reasons
358         // so we need to re-dispatch from here to the intended target.
359         } else if (Intents.SEARCH_SUGGESTION_CLICKED.equals(action)) {
360             // See if the suggestion was clicked with a search action key (call button)
361             Intent newIntent;
362             if ("call".equals(intent.getStringExtra(SearchManager.ACTION_MSG))) {
363                 newIntent = new Intent(Intent.ACTION_CALL_PRIVILEGED, intent.getData());
364             } else {
365                 newIntent = new Intent(Intent.ACTION_VIEW, intent.getData());
366             }
367             startActivity(newIntent);
368             finish();
369             return;
370         } else if (Intents.SEARCH_SUGGESTION_DIAL_NUMBER_CLICKED.equals(action)) {
371             Intent newIntent = new Intent(Intent.ACTION_CALL_PRIVILEGED, intent.getData());
372             startActivity(newIntent);
373             finish();
374             return;
375         } else if (Intents.SEARCH_SUGGESTION_CREATE_CONTACT_CLICKED.equals(action)) {
376             String number = intent.getData().getSchemeSpecificPart();
377             Intent newIntent = new Intent(Intent.ACTION_INSERT, People.CONTENT_URI);
378             newIntent.putExtra(Intents.Insert.PHONE, number);
379             startActivity(newIntent);
380             finish();
381             return;
382         }
383
384         if (mMode == MODE_UNKNOWN) {
385             mMode = DEFAULT_MODE;
386         }
387
388         // Setup the UI
389         final ListView list = getListView();
390         list.setFocusable(true);
391         list.setOnCreateContextMenuListener(this);
392         if ((mMode & MODE_MASK_NO_FILTER) != MODE_MASK_NO_FILTER) {
393             list.setTextFilterEnabled(true);
394         }
395
396         if ((mMode & MODE_MASK_CREATE_NEW) != 0) {
397             // Add the header for creating a new contact
398             final LayoutInflater inflater = getLayoutInflater();
399             View header = inflater.inflate(android.R.layout.simple_list_item_1, list, false);
400             TextView text = (TextView) header.findViewById(android.R.id.text1);
401             text.setText(R.string.pickerNewContactHeader);
402             list.addHeaderView(header);
403         }
404
405         // Set the proper empty string
406         setEmptyText();
407         
408         mAdapter = new ContactItemListAdapter(this);
409         setListAdapter(mAdapter);
410
411         // We manually save/restore the listview state
412         list.setSaveEnabled(false);
413
414         mQueryHandler = new QueryHandler(this);
415         mJustCreated = true;
416
417         // Check to see if sync is enabled
418         final ContentResolver resolver = getContentResolver();
419         IContentProvider provider = resolver.acquireProvider(Contacts.CONTENT_URI);
420         if (provider == null) {
421             // No contacts provider, bail.
422             finish();
423             return;
424         }
425
426         try {
427             ISyncAdapter sa = provider.getSyncAdapter();
428             mSyncEnabled = sa != null;
429         } catch (RemoteException e) {
430             mSyncEnabled = false;
431         } finally {
432             resolver.releaseProvider(provider);
433         }
434     }
435
436     private void setEmptyText() {
437         TextView empty = (TextView) findViewById(R.id.emptyText);
438         // Center the text by default
439         int gravity = Gravity.CENTER;
440         switch (mMode) {
441             case MODE_GROUP:
442                 if (Groups.GROUP_MY_CONTACTS.equals(mDisplayInfo)) {
443                     if (mSyncEnabled) {
444                         empty.setText(getText(R.string.noContactsHelpTextWithSync));
445                     } else {
446                         empty.setText(getText(R.string.noContactsHelpText));
447                     }
448                     gravity = Gravity.NO_GRAVITY;
449                 } else {
450                     empty.setText(getString(R.string.groupEmpty, mDisplayInfo));
451                 }
452                 break;
453
454             case MODE_STARRED:
455             case MODE_STREQUENT:
456             case MODE_FREQUENT:
457                 empty.setText(getText(R.string.noFavorites));
458                 break;
459
460             case MODE_WITH_PHONES:
461                 empty.setText(getText(R.string.noContactsWithPhoneNumbers));
462                 break;
463
464             default:
465                 empty.setText(getText(R.string.noContacts));
466                 break;
467         }
468         empty.setGravity(gravity);
469     }
470
471     /**
472      * Builds the URIs to query when displaying a user group
473      * 
474      * @param groupName the group being displayed
475      */
476     private void buildUserGroupUris(String groupName) {
477         mGroupFilterUri = Uri.parse("content://contacts/groups/name/" + groupName
478                 + "/members/filter/");
479         mGroupUri = Uri.parse("content://contacts/groups/name/" + groupName + "/members");
480     }
481
482     /**
483      * Builds the URIs to query when displaying a system group
484      * 
485      * @param systemId the system group's ID 
486      */
487     private void buildSystemGroupUris(String systemId) {
488         mGroupFilterUri = Uri.parse("content://contacts/groups/system_id/" + systemId
489                 + "/members/filter/");
490         mGroupUri = Uri.parse("content://contacts/groups/system_id/" + systemId + "/members");
491     }
492
493     /**
494      * Sets the mode when the request is for "default"
495      */
496     private void setDefaultMode() {
497         // Load the preferences
498         SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
499         
500         // Lookup the group to display
501         mDisplayType = prefs.getInt(PREF_DISPLAY_TYPE, DISPLAY_TYPE_UNKNOWN);
502         switch (mDisplayType) {
503             case DISPLAY_TYPE_ALL_WITH_PHONES: {
504                 mMode = MODE_WITH_PHONES;
505                 mDisplayInfo = null;
506                 break;
507             }
508
509             case DISPLAY_TYPE_SYSTEM_GROUP: {
510                 String systemId = prefs.getString(
511                         PREF_DISPLAY_INFO, null);
512                 if (!TextUtils.isEmpty(systemId)) {
513                     // Display the selected system group
514                     mMode = MODE_GROUP;
515                     buildSystemGroupUris(systemId);
516                     mDisplayInfo = systemId;
517                 } else {
518                     // No valid group is present, display everything
519                     mMode = MODE_WITH_PHONES;
520                     mDisplayInfo = null;
521                     mDisplayType = DISPLAY_TYPE_ALL;
522                 }
523                 break;
524             }
525
526             case DISPLAY_TYPE_USER_GROUP: {
527                 String displayGroup = prefs.getString(
528                         PREF_DISPLAY_INFO, null);
529                 if (!TextUtils.isEmpty(displayGroup)) {
530                     // Display the selected user group
531                     mMode = MODE_GROUP;
532                     buildUserGroupUris(displayGroup);
533                     mDisplayInfo = displayGroup;
534                 } else {
535                     // No valid group is present, display everything
536                     mMode = MODE_WITH_PHONES;
537                     mDisplayInfo = null;
538                     mDisplayType = DISPLAY_TYPE_ALL;
539                 }
540                 break;
541             }
542
543             case DISPLAY_TYPE_ALL: {
544                 mMode = MODE_ALL_CONTACTS;
545                 mDisplayInfo = null;
546                 break;
547             }
548
549             default: {
550                 // We don't know what to display, default to My Contacts
551                 mMode = MODE_GROUP;
552                 mDisplayType = DISPLAY_TYPE_SYSTEM_GROUP;
553                 buildSystemGroupUris(Groups.GROUP_MY_CONTACTS);
554                 mDisplayInfo = Groups.GROUP_MY_CONTACTS;
555                 break;
556             }
557         }
558
559         // Update the empty text view with the proper string, as the group may have changed
560         setEmptyText();
561     }
562
563     @Override
564     protected void onResume() {
565         super.onResume();
566
567         boolean runQuery = true;
568         Activity parent = getParent();
569         
570         // Do this before setting the filter. The filter thread relies
571         // on some state that is initialized in setDefaultMode
572         if (mDefaultMode) {
573             // If we're in default mode we need to possibly reset the mode due to a change
574             // in the preferences activity while we weren't running
575             setDefaultMode();
576         }
577         
578         // See if we were invoked with a filter
579         if (parent != null && parent instanceof DialtactsActivity) {
580             String filterText = ((DialtactsActivity) parent).getAndClearFilterText();
581             if (filterText != null && filterText.length() > 0) {
582                 getListView().setFilterText(filterText);
583                 // Don't start a new query since it will conflict with the filter
584                 runQuery = false;
585             } else if (mJustCreated) {
586                 getListView().clearTextFilter();
587             }
588         }
589
590         if (runQuery) {
591             // Calling requery here may cause an ANR, so always do the async query
592             startQuery();
593         }
594         mJustCreated = false;
595     }
596     
597     private void updateGroup() {
598         if (mDefaultMode) {
599             setDefaultMode();
600         }
601
602         // Calling requery here may cause an ANR, so always do the async query
603         startQuery();
604     }
605
606     @Override
607     protected void onSaveInstanceState(Bundle icicle) {
608         super.onSaveInstanceState(icicle);
609         // Save list state in the bundle so we can restore it after the QueryHandler has run
610         icicle.putParcelable(LIST_STATE_KEY, mList.onSaveInstanceState());
611         icicle.putBoolean(FOCUS_KEY, mList.hasFocus());
612     }
613
614     @Override
615     protected void onRestoreInstanceState(Bundle icicle) {
616         super.onRestoreInstanceState(icicle);
617         // Retrieve list state. This will be applied after the QueryHandler has run
618         mListState = icicle.getParcelable(LIST_STATE_KEY);
619         mListHasFocus = icicle.getBoolean(FOCUS_KEY);
620     }
621
622     @Override
623     protected void onStop() {
624         super.onStop();
625
626         // We don't want the list to display the empty state, since when we resume it will still
627         // be there and show up while the new query is happening. After the async query finished
628         // in response to onResume() setLoading(false) will be called.
629         mAdapter.setLoading(true);
630         mAdapter.changeCursor(null);
631
632         if (mMode == MODE_QUERY) {
633             // Make sure the search box is closed
634             SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE);
635             searchManager.stopSearch();
636         }
637     }
638
639     @Override
640     public boolean onCreateOptionsMenu(Menu menu) {
641         // If Contacts was invoked by another Activity simply as a way of
642         // picking a contact, don't show the options menu
643         if ((mMode & MODE_MASK_PICKER) == MODE_MASK_PICKER) {
644             return false;
645         }
646
647         // New contact
648         menu.add(0, MENU_NEW_CONTACT, 0, R.string.menu_newContact)
649                 .setIcon(android.R.drawable.ic_menu_add)
650                 .setIntent(new Intent(Intents.Insert.ACTION, People.CONTENT_URI))
651                 .setAlphabeticShortcut('n');
652
653         // Display group
654         if (mDefaultMode) {
655             menu.add(0, MENU_DISPLAY_GROUP, 0, R.string.menu_displayGroup)
656                     .setIcon(com.android.internal.R.drawable.ic_menu_allfriends);
657         }
658
659         // Sync settings
660         if (mSyncEnabled) {
661             Intent syncIntent = new Intent(Intent.ACTION_VIEW);
662             syncIntent.setClass(this, ContactsGroupSyncSelector.class);
663             menu.add(0, 0, 0, R.string.syncGroupPreference)
664                     .setIcon(com.android.internal.R.drawable.ic_menu_refresh)
665                     .setIntent(syncIntent);
666         }
667         
668         // SIM import
669         Intent importIntent = new Intent(Intent.ACTION_VIEW);
670         importIntent.setType("vnd.android.cursor.item/sim-contact");
671         importIntent.setClassName("com.android.phone", "com.android.phone.SimContacts");
672         menu.add(0, 0, 0, R.string.importFromSim)
673                 .setIcon(R.drawable.ic_menu_import_contact)
674                 .setIntent(importIntent);
675
676         return super.onCreateOptionsMenu(menu);
677     }
678
679     /*
680      * Implements the handler for display group selection.
681      */
682     public void onClick(DialogInterface dialogInterface, int which) {
683         if (which == DialogInterface.BUTTON1) {
684             // The OK button was pressed
685             if (mDisplayGroupOriginalSelection != mDisplayGroupCurrentSelection) {
686                 // Set the group to display
687                 if (mDisplayGroupCurrentSelection == DISPLAY_GROUP_INDEX_ALL_CONTACTS) {
688                     // Display all
689                     mDisplayType = DISPLAY_TYPE_ALL;
690                     mDisplayInfo = null;
691                 } else if (mDisplayGroupCurrentSelection
692                         == DISPLAY_GROUP_INDEX_ALL_CONTACTS_WITH_PHONES) {
693                     // Display all with phone numbers
694                     mDisplayType = DISPLAY_TYPE_ALL_WITH_PHONES;
695                     mDisplayInfo = null;
696                 } else if (mDisplayGroupsIncludesMyContacts &&
697                         mDisplayGroupCurrentSelection == DISPLAY_GROUP_INDEX_MY_CONTACTS) {
698                     mDisplayType = DISPLAY_TYPE_SYSTEM_GROUP;
699                     mDisplayInfo = Groups.GROUP_MY_CONTACTS;
700                 } else {
701                     mDisplayType = DISPLAY_TYPE_USER_GROUP;
702                     mDisplayInfo = mDisplayGroups[mDisplayGroupCurrentSelection].toString();
703                 }
704
705                 // Save the changes to the preferences
706                 SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
707                 prefs.edit()
708                         .putInt(PREF_DISPLAY_TYPE, mDisplayType)
709                         .putString(PREF_DISPLAY_INFO, mDisplayInfo)
710                         .commit();
711
712                 // Update the display state
713                 updateGroup();
714             }
715         } else {
716             // A list item was selected, cache the position
717             mDisplayGroupCurrentSelection = which;
718         }
719     }
720     
721     @Override
722     public boolean onOptionsItemSelected(MenuItem item) {
723         if (item.getItemId() == MENU_DISPLAY_GROUP) {
724             AlertDialog.Builder builder = new AlertDialog.Builder(this)
725                 .setTitle(R.string.select_group_title)
726                 .setPositiveButton(android.R.string.ok, this)
727                 .setNegativeButton(android.R.string.cancel, null);
728             
729             setGroupEntries(builder);
730             
731             builder.show();
732             return true;
733         }
734         return false;
735     }
736
737     @Override
738     protected void onActivityResult(int requestCode, int resultCode,
739             Intent data) {
740         switch (requestCode) {
741             case SUBACTIVITY_NEW_CONTACT:
742                 if (resultCode == RESULT_OK) {
743                     // Contact was created, pass it back
744                     returnPickerResult(data.getStringExtra(Intent.EXTRA_SHORTCUT_NAME),
745                             data.getData());
746                 }
747         }
748     }
749
750     @Override
751     public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) {
752         // If Contacts was invoked by another Activity simply as a way of
753         // picking a contact, don't show the context menu
754         if ((mMode & MODE_MASK_PICKER) == MODE_MASK_PICKER) {
755             return;
756         }
757
758         AdapterView.AdapterContextMenuInfo info;
759         try {
760              info = (AdapterView.AdapterContextMenuInfo) menuInfo;
761         } catch (ClassCastException e) {
762             Log.e(TAG, "bad menuInfo", e);
763             return;
764         }
765
766         Cursor cursor = (Cursor) getListAdapter().getItem(info.position);
767         if (cursor == null) {
768             // For some reason the requested item isn't available, do nothing
769             return;
770         }
771         long id = info.id;
772         Uri personUri = ContentUris.withAppendedId(People.CONTENT_URI, id);
773
774         // Setup the menu header
775         menu.setHeaderTitle(cursor.getString(NAME_COLUMN_INDEX));
776
777         // View contact details
778         menu.add(0, MENU_ITEM_VIEW_CONTACT, 0, R.string.menu_viewContact)
779                 .setIntent(new Intent(Intent.ACTION_VIEW, personUri));
780
781         // Calling contact
782         long phoneId = cursor.getLong(PRIMARY_PHONE_ID_COLUMN_INDEX);
783         if (phoneId > 0) {
784             // Get the display label for the number
785             CharSequence label = cursor.getString(LABEL_COLUMN_INDEX);
786             int type = cursor.getInt(TYPE_COLUMN_INDEX);
787             label = Phones.getDisplayLabel(this, type, label);
788             Intent intent = new Intent(Intent.ACTION_CALL_PRIVILEGED,
789                     ContentUris.withAppendedId(Phones.CONTENT_URI, phoneId));
790             menu.add(0, MENU_ITEM_CALL, 0, String.format(getString(R.string.menu_callNumber), label))
791                     .setIntent(intent);
792
793             // Send SMS item
794             menu.add(0, MENU_ITEM_SEND_SMS, 0, R.string.menu_sendSMS)
795                     .setIntent(new Intent(Intent.ACTION_SENDTO,
796                             Uri.fromParts("sms", cursor.getString(NUMBER_COLUMN_INDEX), null)));
797         }
798
799         // Star toggling
800         int starState = cursor.getInt(STARRED_COLUMN_INDEX);
801         if (starState == 0) {
802             menu.add(0, MENU_ITEM_TOGGLE_STAR, 0, R.string.menu_addStar);
803         } else {
804             menu.add(0, MENU_ITEM_TOGGLE_STAR, 0, R.string.menu_removeStar);
805         }
806
807         // Contact editing
808         menu.add(0, MENU_ITEM_EDIT, 0, R.string.menu_editContact)
809                 .setIntent(new Intent(Intent.ACTION_EDIT, personUri));
810         menu.add(0, MENU_ITEM_DELETE, 0, R.string.menu_deleteContact);
811     }
812
813     @Override
814     public boolean onContextItemSelected(MenuItem item) {
815         AdapterView.AdapterContextMenuInfo info;
816         try {
817              info = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo();
818         } catch (ClassCastException e) {
819             Log.e(TAG, "bad menuInfo", e);
820             return false;
821         }
822
823         Cursor cursor = (Cursor) getListAdapter().getItem(info.position);
824
825         switch (item.getItemId()) {
826             case MENU_ITEM_TOGGLE_STAR: {
827                 // Toggle the star
828                 ContentValues values = new ContentValues(1);
829                 values.put(People.STARRED, cursor.getInt(STARRED_COLUMN_INDEX) == 0 ? 1 : 0);
830                 Uri personUri = ContentUris.withAppendedId(People.CONTENT_URI,
831                         cursor.getInt(ID_COLUMN_INDEX));
832                 getContentResolver().update(personUri, values, null, null);
833                 return true;
834             }
835
836             case MENU_ITEM_DELETE: {
837                 // Get confirmation
838                 Uri uri = ContentUris.withAppendedId(People.CONTENT_URI,
839                         cursor.getLong(ID_COLUMN_INDEX));
840                 //TODO make this dialog persist across screen rotations
841                 new AlertDialog.Builder(ContactsListActivity.this)
842                     .setTitle(R.string.deleteConfirmation_title)
843                     .setIcon(android.R.drawable.ic_dialog_alert)
844                     .setMessage(R.string.deleteConfirmation)
845                     .setNegativeButton(android.R.string.cancel, null)
846                     .setPositiveButton(android.R.string.ok, new DeleteClickListener(uri))
847                     .show();
848                 return true;
849             }
850         }
851
852         return super.onContextItemSelected(item);
853     }
854
855     @Override
856     public boolean onKeyDown(int keyCode, KeyEvent event) {
857         switch (keyCode) {
858             case KeyEvent.KEYCODE_CALL: {
859                 if (callSelection()) {
860                     return true;
861                 }
862                 break;
863             }
864
865             case KeyEvent.KEYCODE_DEL: {
866                 Object o = getListView().getSelectedItem();
867                 if (o != null) {
868                     Cursor cursor = (Cursor) o;
869                     Uri uri = ContentUris.withAppendedId(People.CONTENT_URI,
870                             cursor.getLong(ID_COLUMN_INDEX));
871                     //TODO make this dialog persist across screen rotations
872                     new AlertDialog.Builder(ContactsListActivity.this)
873                         .setTitle(R.string.deleteConfirmation_title)
874                         .setIcon(android.R.drawable.ic_dialog_alert)
875                         .setMessage(R.string.deleteConfirmation)
876                         .setNegativeButton(android.R.string.cancel, null)
877                         .setPositiveButton(android.R.string.ok, new DeleteClickListener(uri))
878                         .setCancelable(false)
879                         .show();
880                     return true;
881                 }
882                 break;
883             }
884         }
885
886         return super.onKeyDown(keyCode, event);
887     }
888
889     @Override
890     protected void onListItemClick(ListView l, View v, int position, long id) {
891         if (mMode == MODE_INSERT_OR_EDIT_CONTACT) {
892             Intent intent;
893             if (position == 0) {
894                 // Insert
895                 intent = new Intent(Intent.ACTION_INSERT, People.CONTENT_URI);
896             } else {
897                 // Edit
898                 intent = new Intent(Intent.ACTION_EDIT,
899                         ContentUris.withAppendedId(People.CONTENT_URI, id));
900             }
901             intent.setFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
902             final Bundle extras = getIntent().getExtras();
903             if (extras != null) {
904                 intent.putExtras(extras);
905             }
906             startActivity(intent);
907             finish();
908         } else if (id != -1) {
909             if ((mMode & MODE_MASK_PICKER) == 0) {
910                 Intent intent = new Intent(Intent.ACTION_VIEW,
911                         ContentUris.withAppendedId(People.CONTENT_URI, id));
912                 startActivity(intent);
913             } else if (mMode == MODE_PICK_CONTACT 
914                     || mMode == MODE_PICK_OR_CREATE_CONTACT) {
915                 Uri uri = ContentUris.withAppendedId(People.CONTENT_URI, id);
916                 if (mCreateShortcut) {
917                     // Subtract one if we have Create Contact at the top
918                     Cursor c = (Cursor) mAdapter.getItem(position
919                             - (mMode == MODE_PICK_OR_CREATE_CONTACT? 1:0));
920                     returnPickerResult(c.getString(NAME_COLUMN_INDEX), uri);
921                 } else {
922                     returnPickerResult(null, uri);
923                 }
924             } else if (mMode == MODE_PICK_PHONE) {
925                 setResult(RESULT_OK, new Intent().setData(
926                         ContentUris.withAppendedId(Phones.CONTENT_URI, id)));
927                 finish();
928             } else if (mMode == MODE_PICK_POSTAL) {
929                 setResult(RESULT_OK, new Intent().setData(
930                         ContentUris.withAppendedId(ContactMethods.CONTENT_URI, id)));
931                 finish();
932             }
933         } else if ((mMode & MODE_MASK_CREATE_NEW) == MODE_MASK_CREATE_NEW
934                 && position == 0) {
935             Intent newContact = new Intent(Intents.Insert.ACTION, People.CONTENT_URI);
936             startActivityForResult(newContact, SUBACTIVITY_NEW_CONTACT);
937         } else {
938             signalError();
939         }
940     }
941
942     private void returnPickerResult(String name, Uri uri) {
943         final Intent intent = new Intent();
944     
945         if (mCreateShortcut) {
946             Intent shortcutIntent = new Intent(Intent.ACTION_VIEW, uri);
947             shortcutIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
948             intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent);
949             intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, name);
950             final Bitmap icon = People.loadContactPhoto(this, uri, 0, null);
951             if (icon != null) {
952                 intent.putExtra(Intent.EXTRA_SHORTCUT_ICON, icon);
953             } else {
954                 intent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE,
955                         Intent.ShortcutIconResource.fromContext(this,
956                                 R.drawable.ic_launcher_contacts));
957             }
958             setResult(RESULT_OK, intent);
959         } else {
960             setResult(RESULT_OK, intent.setData(uri));
961         }
962         finish();
963     }
964
965     String[] getProjection() {
966         switch (mMode) {
967             case MODE_GROUP:
968             case MODE_ALL_CONTACTS:
969             case MODE_WITH_PHONES:
970             case MODE_PICK_CONTACT:
971             case MODE_PICK_OR_CREATE_CONTACT:
972             case MODE_QUERY:
973             case MODE_STARRED:
974             case MODE_FREQUENT:
975             case MODE_INSERT_OR_EDIT_CONTACT:
976                 return CONTACTS_PROJECTION;
977
978             case MODE_STREQUENT:
979                 return STREQUENT_PROJECTION;
980
981             case MODE_PICK_PHONE:
982                 return PHONES_PROJECTION;
983
984             case MODE_PICK_POSTAL:
985                 return CONTACT_METHODS_PROJECTION;
986         }
987         return null;
988     }
989
990     private Uri getPeopleFilterUri(String filter) {
991         if (!TextUtils.isEmpty(filter)) {
992             return Uri.withAppendedPath(People.CONTENT_FILTER_URI, Uri.encode(filter));
993         } else {
994             return People.CONTENT_URI;
995         }
996     }
997
998     void startQuery() {
999         mAdapter.setLoading(true);
1000         
1001         // Cancel any pending queries
1002         mQueryHandler.cancelOperation(QUERY_TOKEN);
1003
1004         // Kick off the new query
1005         switch (mMode) {
1006             case MODE_GROUP:
1007                 mQueryHandler.startQuery(QUERY_TOKEN, null,
1008                         mGroupUri, CONTACTS_PROJECTION, null, null, SORT_ORDER);
1009                 break;
1010
1011             case MODE_ALL_CONTACTS:
1012             case MODE_PICK_CONTACT:
1013             case MODE_PICK_OR_CREATE_CONTACT:
1014             case MODE_INSERT_OR_EDIT_CONTACT:
1015                 mQueryHandler.startQuery(QUERY_TOKEN, null, People.CONTENT_URI, CONTACTS_PROJECTION,
1016                         null, null, SORT_ORDER);
1017                 break;
1018
1019             case MODE_WITH_PHONES:
1020                 mQueryHandler.startQuery(QUERY_TOKEN, null, People.CONTENT_URI, CONTACTS_PROJECTION,
1021                         People.PRIMARY_PHONE_ID + " IS NOT NULL", null, People.DEFAULT_SORT_ORDER);
1022                 break;
1023
1024             case MODE_QUERY: {
1025                 mQuery = getIntent().getStringExtra(SearchManager.QUERY);
1026                 mQueryHandler.startQuery(QUERY_TOKEN, null, getPeopleFilterUri(mQuery),
1027                         CONTACTS_PROJECTION, null, null, SORT_ORDER);
1028                 break;
1029             }
1030
1031             case MODE_STARRED:
1032                 mQueryHandler.startQuery(QUERY_TOKEN, null, People.CONTENT_URI, CONTACTS_PROJECTION,
1033                         People.STARRED + "=1", null, SORT_ORDER);
1034                 break;
1035
1036             case MODE_FREQUENT:
1037                 mQueryHandler.startQuery(QUERY_TOKEN, null, People.CONTENT_URI, CONTACTS_PROJECTION,
1038                         People.TIMES_CONTACTED + " > 0", null,
1039                         People.TIMES_CONTACTED + " DESC, " + NAME_COLUMN + " ASC");
1040                 break;
1041
1042             case MODE_STREQUENT:
1043                 mQueryHandler.startQuery(QUERY_TOKEN, null,
1044                         Uri.withAppendedPath(People.CONTENT_URI, "strequent"), STREQUENT_PROJECTION,
1045                         null, null, null);
1046                 break;
1047
1048             case MODE_PICK_PHONE:
1049                 mQueryHandler.startQuery(QUERY_TOKEN, null, Phones.CONTENT_URI, PHONES_PROJECTION,
1050                         null, null, SORT_ORDER);
1051                 break;
1052
1053             case MODE_PICK_POSTAL:
1054                 mQueryHandler.startQuery(QUERY_TOKEN, null, ContactMethods.CONTENT_URI,
1055                         CONTACT_METHODS_PROJECTION,
1056                         ContactMethods.KIND + "=" + Contacts.KIND_POSTAL, null, SORT_ORDER);
1057                 break;
1058         }
1059     }
1060
1061     /**
1062      * Called from a background thread to do the filter and return the resulting cursor.
1063      * 
1064      * @param filter the text that was entered to filter on
1065      * @return a cursor with the results of the filter
1066      */
1067     Cursor doFilter(String filter) {
1068         final ContentResolver resolver = getContentResolver();
1069
1070         switch (mMode) {
1071             case MODE_GROUP: {
1072                 Uri uri;
1073                 if (TextUtils.isEmpty(filter)) {
1074                     uri = mGroupUri;
1075                 } else {
1076                     uri = Uri.withAppendedPath(mGroupFilterUri, Uri.encode(filter));
1077                 }
1078                 return resolver.query(uri, CONTACTS_PROJECTION, null, null,
1079                         People.DEFAULT_SORT_ORDER);
1080             }
1081
1082             case MODE_ALL_CONTACTS:
1083             case MODE_PICK_CONTACT:
1084             case MODE_PICK_OR_CREATE_CONTACT:
1085             case MODE_INSERT_OR_EDIT_CONTACT: {
1086                 return resolver.query(getPeopleFilterUri(filter), CONTACTS_PROJECTION, null, null,
1087                         SORT_ORDER);
1088             }
1089
1090             case MODE_WITH_PHONES: {
1091                 return resolver.query(getPeopleFilterUri(filter), CONTACTS_PROJECTION,
1092                         People.PRIMARY_PHONE_ID + " IS NOT NULL", null, SORT_ORDER);
1093             }
1094
1095             case MODE_STARRED: {
1096                 return resolver.query(getPeopleFilterUri(filter), CONTACTS_PROJECTION,
1097                         People.STARRED + "=1", null, SORT_ORDER);
1098             }
1099
1100             case MODE_FREQUENT: {
1101                 return resolver.query(getPeopleFilterUri(filter), CONTACTS_PROJECTION,
1102                         People.TIMES_CONTACTED + " > 0", null,
1103                         People.TIMES_CONTACTED + " DESC, " + NAME_COLUMN + " ASC");
1104             }
1105
1106             case MODE_STREQUENT: {
1107                 Uri uri;
1108                 if (!TextUtils.isEmpty(filter)) {
1109                     uri = Uri.withAppendedPath(People.CONTENT_URI, "strequent/filter/"
1110                             + Uri.encode(filter));
1111                 } else {
1112                     uri = Uri.withAppendedPath(People.CONTENT_URI, "strequent");
1113                 }
1114                 return resolver.query(uri, STREQUENT_PROJECTION, null, null, null);
1115             }
1116
1117             case MODE_PICK_PHONE: {
1118                 Uri uri;
1119                 if (!TextUtils.isEmpty(filter)) {
1120                     uri = Uri.withAppendedPath(Phones.CONTENT_URI, "filter_name/"
1121                             + Uri.encode(filter));
1122                 } else {
1123                     uri = Phones.CONTENT_URI;
1124                 }
1125                 return resolver.query(uri, PHONES_PROJECTION, null, null, SORT_ORDER);
1126             }
1127         }
1128         throw new UnsupportedOperationException("filtering not allowed in mode " + mMode);
1129     }
1130
1131     /**
1132      * Calls the currently selected list item.
1133      * @return true if the call was initiated, false otherwise
1134      */
1135     boolean callSelection() {
1136         ListView list = getListView();
1137         if (list.hasFocus()) {
1138             Cursor cursor = (Cursor) list.getSelectedItem();
1139             if (cursor != null) {
1140                 long phoneId = cursor.getLong(PRIMARY_PHONE_ID_COLUMN_INDEX);
1141                 if (phoneId == 0) {
1142                     // There is no phone number.
1143                     signalError();
1144                     return false;
1145                 }
1146                 Uri uri = ContentUris.withAppendedId(Phones.CONTENT_URI, phoneId);
1147                 Intent intent = new Intent(Intent.ACTION_CALL_PRIVILEGED, uri);
1148                 startActivity(intent);
1149                 return true;
1150             }
1151         }
1152
1153         return false;
1154     }
1155
1156     /**
1157      * Signal an error to the user.
1158      */
1159     void signalError() {
1160         //TODO play an error beep or something...
1161     }
1162
1163     Cursor getItemForView(View view) {
1164         ListView listView = getListView();
1165         int index = listView.getPositionForView(view);
1166         if (index < 0) {
1167             return null;
1168         }
1169         return (Cursor) listView.getAdapter().getItem(index);
1170     }
1171
1172     private void setGroupEntries(AlertDialog.Builder builder) {
1173         boolean syncEverything;
1174         // For now we only support a single account and the UI doesn't know what
1175         // the account name is, so we're using a global setting for SYNC_EVERYTHING.
1176         // Some day when we add multiple accounts to the UI this should use the per
1177         // account setting.
1178         String value = Contacts.Settings.getSetting(getContentResolver(), null,
1179                 Contacts.Settings.SYNC_EVERYTHING);
1180         if (value == null) {
1181             // If nothing is set yet we default to syncing everything
1182             syncEverything = true;
1183         } else {
1184             syncEverything = !TextUtils.isEmpty(value) && !"0".equals(value);
1185         }
1186
1187         Cursor cursor;
1188         if (!syncEverything) {
1189             cursor = getContentResolver().query(Groups.CONTENT_URI, GROUPS_PROJECTION,
1190                     Groups.SHOULD_SYNC + " != 0", null, Groups.DEFAULT_SORT_ORDER);
1191         } else {
1192             cursor = getContentResolver().query(Groups.CONTENT_URI, GROUPS_PROJECTION,
1193                     null, null, Groups.DEFAULT_SORT_ORDER);
1194         }
1195         try {
1196             ArrayList<CharSequence> groups = new ArrayList<CharSequence>();
1197             ArrayList<CharSequence> prefStrings = new ArrayList<CharSequence>();
1198
1199             // Add All Contacts
1200             groups.add(DISPLAY_GROUP_INDEX_ALL_CONTACTS, getString(R.string.showAllGroups));
1201             prefStrings.add("");
1202             
1203             // Add Contacts with phones
1204             groups.add(DISPLAY_GROUP_INDEX_ALL_CONTACTS_WITH_PHONES,
1205                     getString(R.string.groupNameWithPhones));
1206             prefStrings.add(GROUP_WITH_PHONES);
1207             
1208             int i = 3;
1209             int currentIndex = DISPLAY_GROUP_INDEX_ALL_CONTACTS;
1210             while (cursor.moveToNext()) {
1211                 String systemId = cursor.getString(GROUPS_COLUMN_INDEX_SYSTEM_ID);
1212                 String name = cursor.getString(GROUPS_COLUMN_INDEX_NAME);
1213                 if (cursor.isNull(GROUPS_COLUMN_INDEX_SYSTEM_ID)
1214                         && !Groups.GROUP_MY_CONTACTS.equals(systemId)) {
1215                     // All groups that aren't My Contacts, since that one is localized on the phone
1216                     groups.add(name);
1217                     if (name.equals(mDisplayInfo)) {
1218                         currentIndex = i;
1219                     }
1220                     i++;
1221                 } else {
1222                     // The My Contacts group
1223                     groups.add(DISPLAY_GROUP_INDEX_MY_CONTACTS,
1224                             getString(R.string.groupNameMyContacts));
1225                     if (mDisplayType == DISPLAY_TYPE_SYSTEM_GROUP
1226                             && Groups.GROUP_MY_CONTACTS.equals(mDisplayInfo)) {
1227                         currentIndex = DISPLAY_GROUP_INDEX_MY_CONTACTS;
1228                     }
1229                     mDisplayGroupsIncludesMyContacts = true;
1230                 }
1231             }
1232             if (mMode == MODE_ALL_CONTACTS) {
1233                 currentIndex = DISPLAY_GROUP_INDEX_ALL_CONTACTS;
1234             } else if (mMode == MODE_WITH_PHONES) {
1235                 currentIndex = DISPLAY_GROUP_INDEX_ALL_CONTACTS_WITH_PHONES;
1236             }
1237             mDisplayGroups = groups.toArray(new CharSequence[groups.size()]);
1238             builder.setSingleChoiceItems(mDisplayGroups,
1239                     currentIndex, this);
1240             mDisplayGroupOriginalSelection = currentIndex;
1241         } finally {
1242             cursor.close();
1243         }
1244     }
1245
1246     private static final class QueryHandler extends AsyncQueryHandler {
1247         private final WeakReference<ContactsListActivity> mActivity;
1248
1249         public QueryHandler(Context context) {
1250             super(context.getContentResolver());
1251             mActivity = new WeakReference<ContactsListActivity>((ContactsListActivity) context);
1252         }
1253
1254         @Override
1255         protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
1256             final ContactsListActivity activity = mActivity.get();
1257             if (activity != null && !activity.isFinishing()) {
1258                 activity.mAdapter.setLoading(false);
1259                 activity.getListView().clearTextFilter();                
1260                 activity.mAdapter.changeCursor(cursor);
1261     
1262                 // Now that the cursor is populated again, it's possible to restore the list state
1263                 if (activity.mListState != null) {
1264                     activity.mList.onRestoreInstanceState(activity.mListState);
1265                     if (activity.mListHasFocus) {
1266                         activity.mList.requestFocus();
1267                     }
1268                     activity.mListHasFocus = false;
1269                     activity.mListState = null;
1270                 }
1271             } else {
1272                 cursor.close();
1273             }
1274         }
1275     }
1276
1277     final static class ContactListItemCache {
1278         public TextView nameView;
1279         public CharArrayBuffer nameBuffer = new CharArrayBuffer(128);
1280         public TextView labelView;
1281         public CharArrayBuffer labelBuffer = new CharArrayBuffer(128);
1282         public TextView numberView;
1283         public CharArrayBuffer numberBuffer = new CharArrayBuffer(128);
1284         public ImageView presenceView;
1285     }
1286
1287     private final class ContactItemListAdapter extends ResourceCursorAdapter 
1288             implements SectionIndexer {
1289         
1290         private AlphabetIndexer mIndexer;
1291         private String mAlphabet;
1292         private boolean mLoading = true;
1293         private CharSequence mUnknownNameText;
1294         private CharSequence[] mLocalizedLabels;
1295
1296         public ContactItemListAdapter(Context context) {
1297             super(context, R.layout.contacts_list_item, null);
1298             
1299             mAlphabet = context.getString(com.android.internal.R.string.fast_scroll_alphabet);
1300             
1301             mUnknownNameText = context.getText(android.R.string.unknownName);
1302             switch (mMode) {
1303                 case MODE_PICK_POSTAL:
1304                     mLocalizedLabels = EditContactActivity.getLabelsForKind(mContext,
1305                             Contacts.KIND_POSTAL);
1306                     break;
1307                 default:
1308                     mLocalizedLabels = EditContactActivity.getLabelsForKind(mContext,
1309                             Contacts.KIND_PHONE);
1310                     break;
1311             }
1312         }
1313
1314         public void setLoading(boolean loading) {
1315             mLoading = loading;
1316         }
1317
1318         @Override
1319         public boolean isEmpty() {
1320             if ((mMode & MODE_MASK_CREATE_NEW) == MODE_MASK_CREATE_NEW) {
1321                 // This mode mask adds a header and we always want it to show up, even
1322                 // if the list is empty, so always claim the list is not empty.
1323                 return false;
1324             } else {
1325                 if (mLoading) {
1326                     // We don't want the empty state to show when loading.
1327                     return false;
1328                 } else {
1329                     return super.isEmpty();
1330                 }
1331             }
1332         }
1333         
1334         @Override
1335         public View newView(Context context, Cursor cursor, ViewGroup parent) {
1336             final View view = super.newView(context, cursor, parent);
1337
1338             final ContactListItemCache cache = new ContactListItemCache();
1339             cache.nameView = (TextView) view.findViewById(R.id.name);
1340             cache.labelView = (TextView) view.findViewById(R.id.label);
1341             cache.numberView = (TextView) view.findViewById(R.id.number);
1342             cache.presenceView = (ImageView) view.findViewById(R.id.presence);
1343             view.setTag(cache);
1344
1345             return view;
1346         }
1347
1348         @Override
1349         public void bindView(View view, Context context, Cursor cursor) {
1350             final ContactListItemCache cache = (ContactListItemCache) view.getTag();
1351             
1352             // Set the name           
1353             cursor.copyStringToBuffer(NAME_COLUMN_INDEX, cache.nameBuffer);
1354             int size = cache.nameBuffer.sizeCopied;
1355             if (size != 0) {
1356                 cache.nameView.setText(cache.nameBuffer.data, 0, size);
1357             } else {
1358                 cache.nameView.setText(mUnknownNameText);
1359             }
1360             
1361             // Set the phone number
1362             TextView numberView = cache.numberView;
1363             TextView labelView = cache.labelView;
1364             cursor.copyStringToBuffer(NUMBER_COLUMN_INDEX, cache.numberBuffer);
1365             size = cache.numberBuffer.sizeCopied;
1366             if (size != 0) {
1367                 numberView.setText(cache.numberBuffer.data, 0, size);
1368                 numberView.setVisibility(View.VISIBLE);
1369                 labelView.setVisibility(View.VISIBLE);
1370             } else {
1371                 numberView.setVisibility(View.GONE);
1372                 labelView.setVisibility(View.GONE);
1373             }
1374
1375             // Set the label
1376             if (!cursor.isNull(TYPE_COLUMN_INDEX)) {
1377                 int type = cursor.getInt(TYPE_COLUMN_INDEX);
1378
1379                 if (type != People.Phones.TYPE_CUSTOM) {
1380                     try {
1381                         labelView.setText(mLocalizedLabels[type - 1]);
1382                     } catch (ArrayIndexOutOfBoundsException e) {
1383                         labelView.setText(mLocalizedLabels[People.Phones.TYPE_HOME - 1]);
1384                     }
1385                 } else {
1386                     cursor.copyStringToBuffer(LABEL_COLUMN_INDEX, cache.labelBuffer);
1387                     // Don't check size, if it's zero just don't show anything
1388                     labelView.setText(cache.labelBuffer.data, 0, cache.labelBuffer.sizeCopied);
1389                 }
1390             } else {
1391                 // There is no label, hide the the view
1392                 labelView.setVisibility(View.GONE);
1393             }
1394
1395             // Set the proper icon (star or presence or nothing)
1396             ImageView presenceView = cache.presenceView;
1397             if (mMode != MODE_STREQUENT) {
1398                 if ((mMode & MODE_MASK_NO_PRESENCE) == 0) {
1399                     int serverStatus;
1400                     if (!cursor.isNull(SERVER_STATUS_COLUMN_INDEX)) {
1401                         serverStatus = cursor.getInt(SERVER_STATUS_COLUMN_INDEX);
1402                         presenceView.setImageResource(
1403                                 Presence.getPresenceIconResourceId(serverStatus));
1404                         presenceView.setVisibility(View.VISIBLE);
1405                     } else {
1406                         presenceView.setVisibility(View.GONE);
1407                     }
1408                 } else {
1409                     presenceView.setVisibility(View.GONE);
1410                 }
1411             } else {
1412                 if (cursor.getInt(STARRED_COLUMN_INDEX) != 0) {
1413                     presenceView.setImageResource(R.drawable.star_on);
1414                     presenceView.setVisibility(View.VISIBLE);
1415                 } else {
1416                     presenceView.setVisibility(View.GONE);
1417                 }
1418             }
1419         }
1420
1421         @Override
1422         public void changeCursor(Cursor cursor) {
1423             super.changeCursor(cursor);
1424             updateIndexer(cursor);
1425         }
1426         
1427         private void updateIndexer(Cursor cursor) {
1428             if (mIndexer == null) {
1429                 mIndexer = new AlphabetIndexer(cursor, NAME_COLUMN_INDEX, mAlphabet);
1430             } else {
1431                 mIndexer.setCursor(cursor);
1432             }
1433         }
1434         
1435         /**
1436          * Run the query on a helper thread. Beware that this code does not run
1437          * on the main UI thread!
1438          */
1439         @Override
1440         public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
1441             return doFilter(constraint.toString());
1442         }
1443         
1444         public Object [] getSections() {
1445             if (mMode == MODE_STREQUENT) {
1446                 return new String[] { " " };
1447             } else {
1448                 return mIndexer.getSections();
1449            }
1450         }
1451         
1452         public int getPositionForSection(int sectionIndex) {
1453             if (mMode == MODE_STREQUENT) {
1454                 return 0;
1455             }
1456
1457             if (mIndexer == null) {
1458                 Cursor cursor = mAdapter.getCursor();
1459                 if (cursor == null) {
1460                     // No cursor, the section doesn't exist so just return 0
1461                     return 0;
1462                 }
1463                 mIndexer = new AlphabetIndexer(cursor, NAME_COLUMN_INDEX, mAlphabet);
1464             }
1465
1466             return mIndexer.getPositionForSection(sectionIndex);
1467         }
1468         
1469         public int getSectionForPosition(int position) {
1470             return 0;
1471         }        
1472     }
1473 }
1474
1475