OSDN Git Service

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