OSDN Git Service

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