FileDocCategorySizeDatePackage
ContactsListActivity.javaAPI DocAndroid 1.5 API72131Wed May 06 22:42:44 BST 2009com.android.contacts

ContactsListActivity.java

/*
 * Copyright (C) 2007 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.contacts;

import static com.android.contacts.ShowOrCreateActivity.QUERY_KIND_EMAIL_OR_IM;

import android.app.Activity;
import android.app.AlertDialog;
import android.app.ListActivity;
import android.app.SearchManager;
import android.content.AsyncQueryHandler;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.DialogInterface;
import android.content.IContentProvider;
import android.content.ISyncAdapter;
import android.content.Intent;
import android.content.SharedPreferences;
import android.database.CharArrayBuffer;
import android.database.Cursor;
import android.database.CursorWrapper;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Bundle;
import android.os.Parcelable;
import android.os.RemoteException;
import android.preference.PreferenceManager;
import android.provider.Contacts;
import android.provider.Contacts.ContactMethods;
import android.provider.Contacts.Groups;
import android.provider.Contacts.Intents;
import android.provider.Contacts.People;
import android.provider.Contacts.Phones;
import android.provider.Contacts.Presence;
import android.provider.Contacts.Intents.Insert;
import android.provider.Contacts.Intents.UI;
import android.text.TextUtils;
import android.util.Log;
import android.util.SparseArray;
import android.view.ContextMenu;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.AdapterView;
import android.widget.AlphabetIndexer;
import android.widget.Filter;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.ResourceCursorAdapter;
import android.widget.SectionIndexer;
import android.widget.TextView;

import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Locale;

/**
 * Displays a list of contacts. Usually is embedded into the ContactsActivity.
 */
public final class ContactsListActivity extends ListActivity
        implements View.OnCreateContextMenuListener, DialogInterface.OnClickListener {
    private static final String TAG = "ContactsListActivity";

    private static final String LIST_STATE_KEY = "liststate";
    private static final String FOCUS_KEY = "focused";
    
    static final int MENU_ITEM_VIEW_CONTACT = 1;
    static final int MENU_ITEM_CALL = 2;
    static final int MENU_ITEM_EDIT_BEFORE_CALL = 3;
    static final int MENU_ITEM_SEND_SMS = 4;
    static final int MENU_ITEM_SEND_IM = 5;
    static final int MENU_ITEM_EDIT = 6;
    static final int MENU_ITEM_DELETE = 7;
    static final int MENU_ITEM_TOGGLE_STAR = 8;

    public static final int MENU_SEARCH = 1;
    public static final int MENU_DIALER = 9;
    public static final int MENU_NEW_CONTACT = 10;
    public static final int MENU_DISPLAY_GROUP = 11;

    private static final int SUBACTIVITY_NEW_CONTACT = 1;
    
    /** Mask for picker mode */
    static final int MODE_MASK_PICKER = 0x80000000;
    /** Mask for no presence mode */
    static final int MODE_MASK_NO_PRESENCE = 0x40000000;
    /** Mask for enabling list filtering */
    static final int MODE_MASK_NO_FILTER = 0x20000000;
    /** Mask for having a "create new contact" header in the list */
    static final int MODE_MASK_CREATE_NEW = 0x10000000;
    /** Mask for showing photos in the list */
    static final int MODE_MASK_SHOW_PHOTOS = 0x08000000;

    /** Unknown mode */
    static final int MODE_UNKNOWN = 0;
    /** Show members of the "Contacts" group */
    static final int MODE_GROUP = 5;
    /** Show all contacts sorted alphabetically */
    static final int MODE_ALL_CONTACTS = 10;
    /** Show all contacts with phone numbers, sorted alphabetically */
    static final int MODE_WITH_PHONES = 15;
    /** Show all starred contacts */
    static final int MODE_STARRED = 20;
    /** Show frequently contacted contacts */
    static final int MODE_FREQUENT = 30;
    /** Show starred and the frequent */
    static final int MODE_STREQUENT = 35 | MODE_MASK_SHOW_PHOTOS;
    /** Show all contacts and pick them when clicking */
    static final int MODE_PICK_CONTACT = 40 | MODE_MASK_PICKER;
    /** Show all contacts as well as the option to create a new one */
    static final int MODE_PICK_OR_CREATE_CONTACT = 42 | MODE_MASK_PICKER | MODE_MASK_CREATE_NEW;
    /** Show all contacts and pick them when clicking, and allow creating a new contact */
    static final int MODE_INSERT_OR_EDIT_CONTACT = 45 | MODE_MASK_PICKER | MODE_MASK_CREATE_NEW;
    /** Show all phone numbers and pick them when clicking */
    static final int MODE_PICK_PHONE = 50 | MODE_MASK_PICKER | MODE_MASK_NO_PRESENCE;
    /** Show all postal addresses and pick them when clicking */
    static final int MODE_PICK_POSTAL =
            55 | MODE_MASK_PICKER | MODE_MASK_NO_PRESENCE | MODE_MASK_NO_FILTER;
    /** Run a search query */
    static final int MODE_QUERY = 60 | MODE_MASK_NO_FILTER;
    /** Run a search query in PICK mode, but that still launches to VIEW */
    static final int MODE_QUERY_PICK_TO_VIEW = 65 | MODE_MASK_NO_FILTER | MODE_MASK_PICKER;

    static final int DEFAULT_MODE = MODE_ALL_CONTACTS;

    /**
     * The type of data to display in the main contacts list. 
     */
    static final String PREF_DISPLAY_TYPE = "display_system_group";

    /** Unknown display type. */
    static final int DISPLAY_TYPE_UNKNOWN = -1;
    /** Display all contacts */
    static final int DISPLAY_TYPE_ALL = 0;
    /** Display all contacts that have phone numbers */
    static final int DISPLAY_TYPE_ALL_WITH_PHONES = 1;
    /** Display a system group */
    static final int DISPLAY_TYPE_SYSTEM_GROUP = 2;
    /** Display a user group */
    static final int DISPLAY_TYPE_USER_GROUP = 3;

    /**
     * Info about what to display. If {@link #PREF_DISPLAY_TYPE}
     * is {@link #DISPLAY_TYPE_SYSTEM_GROUP} then this will be the system id.
     * If {@link #PREF_DISPLAY_TYPE} is {@link #DISPLAY_TYPE_USER_GROUP} then this will
     * be the group name.
     */ 
    static final String PREF_DISPLAY_INFO = "display_group";

    
    static final String NAME_COLUMN = People.DISPLAY_NAME;
    static final String SORT_STRING = People.SORT_STRING;
    
    static final String[] CONTACTS_PROJECTION = new String[] {
        People._ID, // 0
        NAME_COLUMN, // 1
        People.NUMBER, // 2
        People.TYPE, // 3
        People.LABEL, // 4
        People.STARRED, // 5
        People.PRIMARY_PHONE_ID, // 6
        People.PRIMARY_EMAIL_ID, // 7
        People.PRESENCE_STATUS, // 8
        SORT_STRING, // 9
    };
    
    static final String[] SIMPLE_CONTACTS_PROJECTION = new String[] {
        People._ID, // 0
        NAME_COLUMN, // 1
    };

    static final String[] STREQUENT_PROJECTION = new String[] {
        People._ID, // 0
        NAME_COLUMN, // 1
        People.NUMBER, // 2
        People.TYPE, // 3
        People.LABEL, // 4
        People.STARRED, // 5
        People.PRIMARY_PHONE_ID, // 6
        People.PRIMARY_EMAIL_ID, // 7
        People.PRESENCE_STATUS, // 8
        "photo_data", // 9
        People.TIMES_CONTACTED, // 10 (not displayed, but required for the order by to work)
    };

    static final String[] PHONES_PROJECTION = new String[] {
        Phones._ID, // 0
        NAME_COLUMN, // 1
        Phones.NUMBER, // 2
        Phones.TYPE, // 3
        Phones.LABEL, // 4
        Phones.STARRED, // 5
        Phones.PERSON_ID, // 6
    };

    static final String[] CONTACT_METHODS_PROJECTION = new String[] {
        ContactMethods._ID, // 0
        NAME_COLUMN, // 1
        ContactMethods.DATA, // 2
        ContactMethods.TYPE, // 3
        ContactMethods.LABEL, // 4
        ContactMethods.STARRED, // 5
        ContactMethods.PERSON_ID, // 6
    };

    static final int ID_COLUMN_INDEX = 0;
    static final int NAME_COLUMN_INDEX = 1;
    static final int NUMBER_COLUMN_INDEX = 2;
    static final int DATA_COLUMN_INDEX = 2;
    static final int TYPE_COLUMN_INDEX = 3;
    static final int LABEL_COLUMN_INDEX = 4;
    static final int STARRED_COLUMN_INDEX = 5;
    static final int PRIMARY_PHONE_ID_COLUMN_INDEX = 6;
    static final int PRIMARY_EMAIL_ID_COLUMN_INDEX = 7;
    static final int SERVER_STATUS_COLUMN_INDEX = 8;
    static final int PHOTO_COLUMN_INDEX = 9;
    static final int SORT_STRING_INDEX = 9;

    static final int PHONES_PERSON_ID_INDEX = 6;
    static final int SIMPLE_CONTACTS_PERSON_ID_INDEX = 0;
    
    static final int DISPLAY_GROUP_INDEX_ALL_CONTACTS = 0;
    static final int DISPLAY_GROUP_INDEX_ALL_CONTACTS_WITH_PHONES = 1;
    static final int DISPLAY_GROUP_INDEX_MY_CONTACTS = 2;

    private static final int QUERY_TOKEN = 42;

    private static final String[] GROUPS_PROJECTION = new String[] {
        Groups.SYSTEM_ID, // 0
        Groups.NAME, // 1
    };
    private static final int GROUPS_COLUMN_INDEX_SYSTEM_ID = 0;
    private static final int GROUPS_COLUMN_INDEX_NAME = 1;
    
    static final String GROUP_WITH_PHONES = "android_smartgroup_phone";

    ContactItemListAdapter mAdapter;

    int mMode = DEFAULT_MODE;
    // The current display group
    private String mDisplayInfo;
    private int mDisplayType;
    // The current list of display groups, during selection from menu
    private CharSequence[] mDisplayGroups;
    // If true position 2 in mDisplayGroups is the MyContacts group
    private boolean mDisplayGroupsIncludesMyContacts = false;

    private int mDisplayGroupOriginalSelection;
    private int mDisplayGroupCurrentSelection;
    
    private QueryHandler mQueryHandler;
    private String mQuery;
    private Uri mGroupFilterUri;
    private Uri mGroupUri;
    private boolean mJustCreated;
    private boolean mSyncEnabled;

    /**
     * Cursor row index that holds reference back to {@link People#_ID}, such as
     * {@link ContactMethods#PERSON_ID}. Used when responding to a
     * {@link Intent#ACTION_SEARCH} in mode {@link #MODE_QUERY_PICK_TO_VIEW}.
     */
    private int mQueryPersonIdIndex;

    /**
     * Used to keep track of the scroll state of the list.
     */
    private Parcelable mListState = null;
    private boolean mListHasFocus;

    private boolean mCreateShortcut;
    private boolean mDefaultMode = false;
    
    /**
     * Internal query type when in mode {@link #MODE_QUERY_PICK_TO_VIEW}.
     */
    private int mQueryMode = QUERY_MODE_NONE;

    private static final int QUERY_MODE_NONE = -1;
    private static final int QUERY_MODE_MAILTO = 1;
    private static final int QUERY_MODE_TEL = 2;
    
    /**
     * Data to use when in mode {@link #MODE_QUERY_PICK_TO_VIEW}. Usually
     * provided by scheme-specific part of incoming {@link Intent#getData()}.
     */
    private String mQueryData;
    
    private class DeleteClickListener implements DialogInterface.OnClickListener {
        private Uri mUri;

        public DeleteClickListener(Uri uri) {
            mUri = uri;
        }

        public void onClick(DialogInterface dialog, int which) {
            getContentResolver().delete(mUri, null, null);
        }
    }
    
    @Override
    protected void onCreate(Bundle icicle) {
        super.onCreate(icicle);

        // Resolve the intent
        final Intent intent = getIntent();

        // Allow the title to be set to a custom String using an extra on the intent
        String title = intent.getStringExtra(Contacts.Intents.UI.TITLE_EXTRA_KEY);
        if (title != null) {
            setTitle(title);
        }
        
        final String action = intent.getAction();
        mMode = MODE_UNKNOWN;
        
        setContentView(R.layout.contacts_list_content);

        if (UI.LIST_DEFAULT.equals(action)) {
            mDefaultMode = true;
            // When mDefaultMode is true the mode is set in onResume(), since the preferneces
            // activity may change it whenever this activity isn't running
        } else if (UI.LIST_GROUP_ACTION.equals(action)) {
            mMode = MODE_GROUP;
            String groupName = intent.getStringExtra(UI.GROUP_NAME_EXTRA_KEY);
            if (TextUtils.isEmpty(groupName)) {
                finish();
                return;
            }
            buildUserGroupUris(groupName);
        } else if (UI.LIST_ALL_CONTACTS_ACTION.equals(action)) {
            mMode = MODE_ALL_CONTACTS;
        } else if (UI.LIST_STARRED_ACTION.equals(action)) {
            mMode = MODE_STARRED;
        } else if (UI.LIST_FREQUENT_ACTION.equals(action)) {
            mMode = MODE_FREQUENT;
        } else if (UI.LIST_STREQUENT_ACTION.equals(action)) {
            mMode = MODE_STREQUENT;
        } else if (UI.LIST_CONTACTS_WITH_PHONES_ACTION.equals(action)) {
            mMode = MODE_WITH_PHONES;
        } else if (Intent.ACTION_PICK.equals(action)) {
            // XXX These should be showing the data from the URI given in
            // the Intent.
            final String type = intent.resolveType(this);
            if (People.CONTENT_TYPE.equals(type)) {
                mMode = MODE_PICK_CONTACT;
            } else if (Phones.CONTENT_TYPE.equals(type)) {
                mMode = MODE_PICK_PHONE;
            } else if (ContactMethods.CONTENT_POSTAL_TYPE.equals(type)) {
                mMode = MODE_PICK_POSTAL;
            }
        } else if (Intent.ACTION_CREATE_SHORTCUT.equals(action)) {
            mMode = MODE_PICK_OR_CREATE_CONTACT;
            mCreateShortcut = true;
        } else if (Intent.ACTION_GET_CONTENT.equals(action)) {
            final String type = intent.resolveType(this);
            if (People.CONTENT_ITEM_TYPE.equals(type)) {
                mMode = MODE_PICK_OR_CREATE_CONTACT;
            } else if (Phones.CONTENT_ITEM_TYPE.equals(type)) {
                mMode = MODE_PICK_PHONE;
            } else if (ContactMethods.CONTENT_POSTAL_ITEM_TYPE.equals(type)) {
                mMode = MODE_PICK_POSTAL;
            }
        } else if (Intent.ACTION_INSERT_OR_EDIT.equals(action)) {
            mMode = MODE_INSERT_OR_EDIT_CONTACT;
        } else if (Intent.ACTION_SEARCH.equals(action)) {
            // See if the suggestion was clicked with a search action key (call button)
            if ("call".equals(intent.getStringExtra(SearchManager.ACTION_MSG))) {
                String query = intent.getStringExtra(SearchManager.QUERY);
                if (!TextUtils.isEmpty(query)) {
                    Intent newIntent = new Intent(Intent.ACTION_CALL_PRIVILEGED,
                            Uri.fromParts("tel", query, null));
                    startActivity(newIntent);
                }
                finish();
                return;
            }
            
            // See if search request has extras to specify query
            if (intent.hasExtra(Insert.EMAIL)) {
                mMode = MODE_QUERY_PICK_TO_VIEW;
                mQueryMode = QUERY_MODE_MAILTO;
                mQueryData = intent.getStringExtra(Insert.EMAIL);
            } else if (intent.hasExtra(Insert.PHONE)) {
                mMode = MODE_QUERY_PICK_TO_VIEW;
                mQueryMode = QUERY_MODE_TEL;
                mQueryData = intent.getStringExtra(Insert.PHONE);
            } else {
                // Otherwise handle the more normal search case
                mMode = MODE_QUERY;
            }

        // Since this is the filter activity it receives all intents
        // dispatched from the SearchManager for security reasons
        // so we need to re-dispatch from here to the intended target.
        } else if (Intents.SEARCH_SUGGESTION_CLICKED.equals(action)) {
            // See if the suggestion was clicked with a search action key (call button)
            Intent newIntent;
            if ("call".equals(intent.getStringExtra(SearchManager.ACTION_MSG))) {
                newIntent = new Intent(Intent.ACTION_CALL_PRIVILEGED, intent.getData());
            } else {
                newIntent = new Intent(Intent.ACTION_VIEW, intent.getData());
            }
            startActivity(newIntent);
            finish();
            return;
        } else if (Intents.SEARCH_SUGGESTION_DIAL_NUMBER_CLICKED.equals(action)) {
            Intent newIntent = new Intent(Intent.ACTION_CALL_PRIVILEGED, intent.getData());
            startActivity(newIntent);
            finish();
            return;
        } else if (Intents.SEARCH_SUGGESTION_CREATE_CONTACT_CLICKED.equals(action)) {
            String number = intent.getData().getSchemeSpecificPart();
            Intent newIntent = new Intent(Intent.ACTION_INSERT, People.CONTENT_URI);
            newIntent.putExtra(Intents.Insert.PHONE, number);
            startActivity(newIntent);
            finish();
            return;
        }

        if (mMode == MODE_UNKNOWN) {
            mMode = DEFAULT_MODE;
        }

        // Setup the UI
        final ListView list = getListView();
        list.setFocusable(true);
        list.setOnCreateContextMenuListener(this);
        if ((mMode & MODE_MASK_NO_FILTER) != MODE_MASK_NO_FILTER) {
            list.setTextFilterEnabled(true);
        }

        if ((mMode & MODE_MASK_CREATE_NEW) != 0) {
            // Add the header for creating a new contact
            final LayoutInflater inflater = getLayoutInflater();
            View header = inflater.inflate(android.R.layout.simple_list_item_1, list, false);
            TextView text = (TextView) header.findViewById(android.R.id.text1);
            text.setText(R.string.pickerNewContactHeader);
            list.addHeaderView(header);
        }

        // Set the proper empty string
        setEmptyText();
        
        mAdapter = new ContactItemListAdapter(this);
        setListAdapter(mAdapter);

        // We manually save/restore the listview state
        list.setSaveEnabled(false);

        mQueryHandler = new QueryHandler(this);
        mJustCreated = true;

        // Check to see if sync is enabled
        final ContentResolver resolver = getContentResolver();
        IContentProvider provider = resolver.acquireProvider(Contacts.CONTENT_URI);
        if (provider == null) {
            // No contacts provider, bail.
            finish();
            return;
        }

        try {
            ISyncAdapter sa = provider.getSyncAdapter();
            mSyncEnabled = sa != null;
        } catch (RemoteException e) {
            mSyncEnabled = false;
        } finally {
            resolver.releaseProvider(provider);
        }
    }

    private void setEmptyText() {
        TextView empty = (TextView) findViewById(R.id.emptyText);
        // Center the text by default
        int gravity = Gravity.CENTER;
        switch (mMode) {
            case MODE_GROUP:
                if (Groups.GROUP_MY_CONTACTS.equals(mDisplayInfo)) {
                    if (mSyncEnabled) {
                        empty.setText(getText(R.string.noContactsHelpTextWithSync));
                    } else {
                        empty.setText(getText(R.string.noContactsHelpText));
                    }
                    gravity = Gravity.NO_GRAVITY;
                } else {
                    empty.setText(getString(R.string.groupEmpty, mDisplayInfo));
                }
                break;

            case MODE_STARRED:
            case MODE_STREQUENT:
            case MODE_FREQUENT:
                empty.setText(getText(R.string.noFavorites));
                break;

            case MODE_WITH_PHONES:
                empty.setText(getText(R.string.noContactsWithPhoneNumbers));
                break;

            default:
                empty.setText(getText(R.string.noContacts));
                break;
        }
        empty.setGravity(gravity);
    }

    /**
     * Builds the URIs to query when displaying a user group
     * 
     * @param groupName the group being displayed
     */
    private void buildUserGroupUris(String groupName) {
        mGroupFilterUri = Uri.parse("content://contacts/groups/name/" + groupName
                + "/members/filter/");
        mGroupUri = Uri.parse("content://contacts/groups/name/" + groupName + "/members");
    }

    /**
     * Builds the URIs to query when displaying a system group
     * 
     * @param systemId the system group's ID 
     */
    private void buildSystemGroupUris(String systemId) {
        mGroupFilterUri = Uri.parse("content://contacts/groups/system_id/" + systemId
                + "/members/filter/");
        mGroupUri = Uri.parse("content://contacts/groups/system_id/" + systemId + "/members");
    }

    /**
     * Sets the mode when the request is for "default"
     */
    private void setDefaultMode() {
        // Load the preferences
        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
        
        // Lookup the group to display
        mDisplayType = prefs.getInt(PREF_DISPLAY_TYPE, DISPLAY_TYPE_UNKNOWN);
        switch (mDisplayType) {
            case DISPLAY_TYPE_ALL_WITH_PHONES: {
                mMode = MODE_WITH_PHONES;
                mDisplayInfo = null;
                break;
            }

            case DISPLAY_TYPE_SYSTEM_GROUP: {
                String systemId = prefs.getString(
                        PREF_DISPLAY_INFO, null);
                if (!TextUtils.isEmpty(systemId)) {
                    // Display the selected system group
                    mMode = MODE_GROUP;
                    buildSystemGroupUris(systemId);
                    mDisplayInfo = systemId;
                } else {
                    // No valid group is present, display everything
                    mMode = MODE_WITH_PHONES;
                    mDisplayInfo = null;
                    mDisplayType = DISPLAY_TYPE_ALL;
                }
                break;
            }

            case DISPLAY_TYPE_USER_GROUP: {
                String displayGroup = prefs.getString(
                        PREF_DISPLAY_INFO, null);
                if (!TextUtils.isEmpty(displayGroup)) {
                    // Display the selected user group
                    mMode = MODE_GROUP;
                    buildUserGroupUris(displayGroup);
                    mDisplayInfo = displayGroup;
                } else {
                    // No valid group is present, display everything
                    mMode = MODE_WITH_PHONES;
                    mDisplayInfo = null;
                    mDisplayType = DISPLAY_TYPE_ALL;
                }
                break;
            }

            case DISPLAY_TYPE_ALL: {
                mMode = MODE_ALL_CONTACTS;
                mDisplayInfo = null;
                break;
            }

            default: {
                // We don't know what to display, default to My Contacts
                mMode = MODE_GROUP;
                mDisplayType = DISPLAY_TYPE_SYSTEM_GROUP;
                buildSystemGroupUris(Groups.GROUP_MY_CONTACTS);
                mDisplayInfo = Groups.GROUP_MY_CONTACTS;
                break;
            }
        }

        // Update the empty text view with the proper string, as the group may have changed
        setEmptyText();
    }

    @Override
    protected void onResume() {
        super.onResume();

        boolean runQuery = true;
        Activity parent = getParent();
        
        // Do this before setting the filter. The filter thread relies
        // on some state that is initialized in setDefaultMode
        if (mDefaultMode) {
            // If we're in default mode we need to possibly reset the mode due to a change
            // in the preferences activity while we weren't running
            setDefaultMode();
        }
        
        // See if we were invoked with a filter
        if (parent != null && parent instanceof DialtactsActivity) {
            String filterText = ((DialtactsActivity) parent).getAndClearFilterText();
            if (filterText != null && filterText.length() > 0) {
                getListView().setFilterText(filterText);
                // Don't start a new query since it will conflict with the filter
                runQuery = false;
            } else if (mJustCreated) {
                getListView().clearTextFilter();
            }
        }

        if (mJustCreated && runQuery) {
            // We need to start a query here the first time the activity is launched, as long
            // as we aren't doing a filter.
            startQuery();
        }
        mJustCreated = false;
    }
    
    @Override
    protected void onRestart() {
        super.onRestart();

        // The cursor was killed off in onStop(), so we need to get a new one here
        // We do not perform the query if a filter is set on the list because the
        // filter will cause the query to happen anyway
        if (TextUtils.isEmpty(getListView().getTextFilter())) {
            startQuery();
        } else {
            // Run the filtered query on the adapter
            ((ContactItemListAdapter) getListAdapter()).onContentChanged();
        }
    }
    
    private void updateGroup() {
        if (mDefaultMode) {
            setDefaultMode();
        }

        // Calling requery here may cause an ANR, so always do the async query
        startQuery();
    }

    @Override
    protected void onSaveInstanceState(Bundle icicle) {
        super.onSaveInstanceState(icicle);
        // Save list state in the bundle so we can restore it after the QueryHandler has run
        icicle.putParcelable(LIST_STATE_KEY, mList.onSaveInstanceState());
        icicle.putBoolean(FOCUS_KEY, mList.hasFocus());
    }

    @Override
    protected void onRestoreInstanceState(Bundle icicle) {
        super.onRestoreInstanceState(icicle);
        // Retrieve list state. This will be applied after the QueryHandler has run
        mListState = icicle.getParcelable(LIST_STATE_KEY);
        mListHasFocus = icicle.getBoolean(FOCUS_KEY);
    }

    @Override
    protected void onStop() {
        super.onStop();

        // We don't want the list to display the empty state, since when we resume it will still
        // be there and show up while the new query is happening. After the async query finished
        // in response to onRestart() setLoading(false) will be called.
        mAdapter.setLoading(true);
        mAdapter.changeCursor(null);

        if (mMode == MODE_QUERY) {
            // Make sure the search box is closed
            SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE);
            searchManager.stopSearch();
        }
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // If Contacts was invoked by another Activity simply as a way of
        // picking a contact, don't show the options menu
        if ((mMode & MODE_MASK_PICKER) == MODE_MASK_PICKER) {
            return false;
        }

        // Search
        menu.add(0, MENU_SEARCH, 0, R.string.menu_search)
                .setIcon(android.R.drawable.ic_menu_search);

        // New contact
        menu.add(0, MENU_NEW_CONTACT, 0, R.string.menu_newContact)
                .setIcon(android.R.drawable.ic_menu_add)
                .setIntent(new Intent(Intents.Insert.ACTION, People.CONTENT_URI))
                .setAlphabeticShortcut('n');

        // Display group
        if (mDefaultMode) {
            menu.add(0, MENU_DISPLAY_GROUP, 0, R.string.menu_displayGroup)
                    .setIcon(com.android.internal.R.drawable.ic_menu_allfriends);
        }

        // Sync settings
        if (mSyncEnabled) {
            Intent syncIntent = new Intent(Intent.ACTION_VIEW);
            syncIntent.setClass(this, ContactsGroupSyncSelector.class);
            menu.add(0, 0, 0, R.string.syncGroupPreference)
                    .setIcon(com.android.internal.R.drawable.ic_menu_refresh)
                    .setIntent(syncIntent);
        }
        
        // SIM import
        Intent importIntent = new Intent(Intent.ACTION_VIEW);
        importIntent.setType("vnd.android.cursor.item/sim-contact");
        importIntent.setClassName("com.android.phone", "com.android.phone.SimContacts");
        menu.add(0, 0, 0, R.string.importFromSim)
                .setIcon(R.drawable.ic_menu_import_contact)
                .setIntent(importIntent);

        return super.onCreateOptionsMenu(menu);
    }

    /*
     * Implements the handler for display group selection.
     */
    public void onClick(DialogInterface dialogInterface, int which) {
        if (which == DialogInterface.BUTTON_POSITIVE) {
            // The OK button was pressed
            if (mDisplayGroupOriginalSelection != mDisplayGroupCurrentSelection) {
                // Set the group to display
                if (mDisplayGroupCurrentSelection == DISPLAY_GROUP_INDEX_ALL_CONTACTS) {
                    // Display all
                    mDisplayType = DISPLAY_TYPE_ALL;
                    mDisplayInfo = null;
                } else if (mDisplayGroupCurrentSelection
                        == DISPLAY_GROUP_INDEX_ALL_CONTACTS_WITH_PHONES) {
                    // Display all with phone numbers
                    mDisplayType = DISPLAY_TYPE_ALL_WITH_PHONES;
                    mDisplayInfo = null;
                } else if (mDisplayGroupsIncludesMyContacts &&
                        mDisplayGroupCurrentSelection == DISPLAY_GROUP_INDEX_MY_CONTACTS) {
                    mDisplayType = DISPLAY_TYPE_SYSTEM_GROUP;
                    mDisplayInfo = Groups.GROUP_MY_CONTACTS;
                } else {
                    mDisplayType = DISPLAY_TYPE_USER_GROUP;
                    mDisplayInfo = mDisplayGroups[mDisplayGroupCurrentSelection].toString();
                }

                // Save the changes to the preferences
                SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
                prefs.edit()
                        .putInt(PREF_DISPLAY_TYPE, mDisplayType)
                        .putString(PREF_DISPLAY_INFO, mDisplayInfo)
                        .commit();

                // Update the display state
                updateGroup();
            }
        } else {
            // A list item was selected, cache the position
            mDisplayGroupCurrentSelection = which;
        }
    }
    
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case MENU_DISPLAY_GROUP:
                AlertDialog.Builder builder = new AlertDialog.Builder(this)
                    .setTitle(R.string.select_group_title)
                    .setPositiveButton(android.R.string.ok, this)
                    .setNegativeButton(android.R.string.cancel, null);
                
                setGroupEntries(builder);
                
                builder.show();
                return true;

            case MENU_SEARCH:
                startSearch(null, false, null, false);
                return true;
        }
        return false;
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode,
            Intent data) {
        switch (requestCode) {
            case SUBACTIVITY_NEW_CONTACT:
                if (resultCode == RESULT_OK) {
                    // Contact was created, pass it back
                    returnPickerResult(data.getStringExtra(Intent.EXTRA_SHORTCUT_NAME),
                            data.getData());
                }
        }
    }

    @Override
    public void onCreateContextMenu(ContextMenu menu, View view, ContextMenuInfo menuInfo) {
        // If Contacts was invoked by another Activity simply as a way of
        // picking a contact, don't show the context menu
        if ((mMode & MODE_MASK_PICKER) == MODE_MASK_PICKER) {
            return;
        }

        AdapterView.AdapterContextMenuInfo info;
        try {
             info = (AdapterView.AdapterContextMenuInfo) menuInfo;
        } catch (ClassCastException e) {
            Log.e(TAG, "bad menuInfo", e);
            return;
        }

        Cursor cursor = (Cursor) getListAdapter().getItem(info.position);
        if (cursor == null) {
            // For some reason the requested item isn't available, do nothing
            return;
        }
        long id = info.id;
        Uri personUri = ContentUris.withAppendedId(People.CONTENT_URI, id);

        // Setup the menu header
        menu.setHeaderTitle(cursor.getString(NAME_COLUMN_INDEX));

        // View contact details
        menu.add(0, MENU_ITEM_VIEW_CONTACT, 0, R.string.menu_viewContact)
                .setIntent(new Intent(Intent.ACTION_VIEW, personUri));

        // Calling contact
        long phoneId = cursor.getLong(PRIMARY_PHONE_ID_COLUMN_INDEX);
        if (phoneId > 0) {
            // Get the display label for the number
            CharSequence label = cursor.getString(LABEL_COLUMN_INDEX);
            int type = cursor.getInt(TYPE_COLUMN_INDEX);
            label = Phones.getDisplayLabel(this, type, label);
            Intent intent = new Intent(Intent.ACTION_CALL_PRIVILEGED,
                    ContentUris.withAppendedId(Phones.CONTENT_URI, phoneId));
            menu.add(0, MENU_ITEM_CALL, 0, String.format(getString(R.string.menu_callNumber), label))
                    .setIntent(intent);

            // Send SMS item
            menu.add(0, MENU_ITEM_SEND_SMS, 0, R.string.menu_sendSMS)
                    .setIntent(new Intent(Intent.ACTION_SENDTO,
                            Uri.fromParts("sms", cursor.getString(NUMBER_COLUMN_INDEX), null)));
        }

        // Star toggling
        int starState = cursor.getInt(STARRED_COLUMN_INDEX);
        if (starState == 0) {
            menu.add(0, MENU_ITEM_TOGGLE_STAR, 0, R.string.menu_addStar);
        } else {
            menu.add(0, MENU_ITEM_TOGGLE_STAR, 0, R.string.menu_removeStar);
        }

        // Contact editing
        menu.add(0, MENU_ITEM_EDIT, 0, R.string.menu_editContact)
                .setIntent(new Intent(Intent.ACTION_EDIT, personUri));
        menu.add(0, MENU_ITEM_DELETE, 0, R.string.menu_deleteContact);
    }

    @Override
    public boolean onContextItemSelected(MenuItem item) {
        AdapterView.AdapterContextMenuInfo info;
        try {
             info = (AdapterView.AdapterContextMenuInfo) item.getMenuInfo();
        } catch (ClassCastException e) {
            Log.e(TAG, "bad menuInfo", e);
            return false;
        }

        Cursor cursor = (Cursor) getListAdapter().getItem(info.position);

        switch (item.getItemId()) {
            case MENU_ITEM_TOGGLE_STAR: {
                // Toggle the star
                ContentValues values = new ContentValues(1);
                values.put(People.STARRED, cursor.getInt(STARRED_COLUMN_INDEX) == 0 ? 1 : 0);
                Uri personUri = ContentUris.withAppendedId(People.CONTENT_URI,
                        cursor.getInt(ID_COLUMN_INDEX));
                getContentResolver().update(personUri, values, null, null);
                return true;
            }

            case MENU_ITEM_DELETE: {
                // Get confirmation
                Uri uri = ContentUris.withAppendedId(People.CONTENT_URI,
                        cursor.getLong(ID_COLUMN_INDEX));
                //TODO make this dialog persist across screen rotations
                new AlertDialog.Builder(ContactsListActivity.this)
                    .setTitle(R.string.deleteConfirmation_title)
                    .setIcon(android.R.drawable.ic_dialog_alert)
                    .setMessage(R.string.deleteConfirmation)
                    .setNegativeButton(android.R.string.cancel, null)
                    .setPositiveButton(android.R.string.ok, new DeleteClickListener(uri))
                    .show();
                return true;
            }
        }

        return super.onContextItemSelected(item);
    }

    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        switch (keyCode) {
            case KeyEvent.KEYCODE_CALL: {
                if (callSelection()) {
                    return true;
                }
                break;
            }

            case KeyEvent.KEYCODE_DEL: {
                Object o = getListView().getSelectedItem();
                if (o != null) {
                    Cursor cursor = (Cursor) o;
                    Uri uri = ContentUris.withAppendedId(People.CONTENT_URI,
                            cursor.getLong(ID_COLUMN_INDEX));
                    //TODO make this dialog persist across screen rotations
                    new AlertDialog.Builder(ContactsListActivity.this)
                        .setTitle(R.string.deleteConfirmation_title)
                        .setIcon(android.R.drawable.ic_dialog_alert)
                        .setMessage(R.string.deleteConfirmation)
                        .setNegativeButton(android.R.string.cancel, null)
                        .setPositiveButton(android.R.string.ok, new DeleteClickListener(uri))
                        .setCancelable(false)
                        .show();
                    return true;
                }
                break;
            }
        }

        return super.onKeyDown(keyCode, event);
    }

    @Override
    protected void onListItemClick(ListView l, View v, int position, long id) {
        // Hide soft keyboard, if visible
        InputMethodManager inputMethodManager = (InputMethodManager)
                getSystemService(Context.INPUT_METHOD_SERVICE);
        inputMethodManager.hideSoftInputFromWindow(mList.getWindowToken(), 0);

        if (mMode == MODE_INSERT_OR_EDIT_CONTACT) {
            Intent intent;
            if (position == 0) {
                // Insert
                intent = new Intent(Intent.ACTION_INSERT, People.CONTENT_URI);
            } else {
                // Edit
                intent = new Intent(Intent.ACTION_EDIT,
                        ContentUris.withAppendedId(People.CONTENT_URI, id));
            }
            intent.setFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
            final Bundle extras = getIntent().getExtras();
            if (extras != null) {
                intent.putExtras(extras);
            }
            startActivity(intent);
            finish();
        } else if (id != -1) {
            if ((mMode & MODE_MASK_PICKER) == 0) {
                Intent intent = new Intent(Intent.ACTION_VIEW,
                        ContentUris.withAppendedId(People.CONTENT_URI, id));
                startActivity(intent);
            } else if (mMode == MODE_QUERY_PICK_TO_VIEW) {
                // Started with query that should launch to view contact
                Cursor c = (Cursor) mAdapter.getItem(position);
                long personId = c.getLong(mQueryPersonIdIndex);
                Intent intent = new Intent(Intent.ACTION_VIEW,
                        ContentUris.withAppendedId(People.CONTENT_URI, personId));
                startActivity(intent);
                finish();
            } else if (mMode == MODE_PICK_CONTACT 
                    || mMode == MODE_PICK_OR_CREATE_CONTACT) {
                Uri uri = ContentUris.withAppendedId(People.CONTENT_URI, id);
                if (mCreateShortcut) {
                    // Subtract one if we have Create Contact at the top
                    Cursor c = (Cursor) mAdapter.getItem(position
                            - (mMode == MODE_PICK_OR_CREATE_CONTACT? 1:0));
                    returnPickerResult(c.getString(NAME_COLUMN_INDEX), uri);
                } else {
                    returnPickerResult(null, uri);
                }
            } else if (mMode == MODE_PICK_PHONE) {
                setResult(RESULT_OK, new Intent().setData(
                        ContentUris.withAppendedId(Phones.CONTENT_URI, id)));
                finish();
            } else if (mMode == MODE_PICK_POSTAL) {
                setResult(RESULT_OK, new Intent().setData(
                        ContentUris.withAppendedId(ContactMethods.CONTENT_URI, id)));
                finish();
            }
        } else if ((mMode & MODE_MASK_CREATE_NEW) == MODE_MASK_CREATE_NEW
                && position == 0) {
            Intent newContact = new Intent(Intents.Insert.ACTION, People.CONTENT_URI);
            startActivityForResult(newContact, SUBACTIVITY_NEW_CONTACT);
        } else {
            signalError();
        }
    }

    private void returnPickerResult(String name, Uri uri) {
        final Intent intent = new Intent();
    
        if (mCreateShortcut) {
            Intent shortcutIntent = new Intent(Intent.ACTION_VIEW, uri);
            shortcutIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
            intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, shortcutIntent);
            intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, name);
            final Bitmap icon = People.loadContactPhoto(this, uri, 0, null);
            if (icon != null) {
                intent.putExtra(Intent.EXTRA_SHORTCUT_ICON, icon);
            } else {
                intent.putExtra(Intent.EXTRA_SHORTCUT_ICON_RESOURCE,
                        Intent.ShortcutIconResource.fromContext(this,
                                R.drawable.ic_launcher_shortcut_contact));
            }
            setResult(RESULT_OK, intent);
        } else {
            setResult(RESULT_OK, intent.setData(uri));
        }
        finish();
    }

    String[] getProjection() {
        switch (mMode) {
            case MODE_GROUP:
            case MODE_ALL_CONTACTS:
            case MODE_WITH_PHONES:
            case MODE_PICK_CONTACT:
            case MODE_PICK_OR_CREATE_CONTACT:
            case MODE_QUERY:
            case MODE_STARRED:
            case MODE_FREQUENT:
            case MODE_INSERT_OR_EDIT_CONTACT:
                return CONTACTS_PROJECTION;

            case MODE_STREQUENT:
                return STREQUENT_PROJECTION;

            case MODE_PICK_PHONE:
                return PHONES_PROJECTION;

            case MODE_PICK_POSTAL:
                return CONTACT_METHODS_PROJECTION;
        }
        return null;
    }

    private Uri getPeopleFilterUri(String filter) {
        if (!TextUtils.isEmpty(filter)) {
            return Uri.withAppendedPath(People.CONTENT_FILTER_URI, Uri.encode(filter));
        } else {
            return People.CONTENT_URI;
        }
    }

    private static String getSortOrder(String[] projectionType) {
        if (Locale.getDefault().equals(Locale.JAPAN) &&
                projectionType == CONTACTS_PROJECTION) {
            return SORT_STRING + " ASC";
        } else {
            return NAME_COLUMN + " COLLATE LOCALIZED ASC";
        }
    }
    
    void startQuery() {
        mAdapter.setLoading(true);
        
        // Cancel any pending queries
        mQueryHandler.cancelOperation(QUERY_TOKEN);

        // Kick off the new query
        switch (mMode) {
            case MODE_GROUP:
                mQueryHandler.startQuery(QUERY_TOKEN, null,
                        mGroupUri, CONTACTS_PROJECTION, null, null,
                        getSortOrder(CONTACTS_PROJECTION));
                break;

            case MODE_ALL_CONTACTS:
            case MODE_PICK_CONTACT:
            case MODE_PICK_OR_CREATE_CONTACT:
            case MODE_INSERT_OR_EDIT_CONTACT:
                mQueryHandler.startQuery(QUERY_TOKEN, null, People.CONTENT_URI, CONTACTS_PROJECTION,
                        null, null, getSortOrder(CONTACTS_PROJECTION));
                break;

            case MODE_WITH_PHONES:
                mQueryHandler.startQuery(QUERY_TOKEN, null, People.CONTENT_URI, CONTACTS_PROJECTION,
                        People.PRIMARY_PHONE_ID + " IS NOT NULL", null,
                        getSortOrder(CONTACTS_PROJECTION));
                break;

            case MODE_QUERY: {
                mQuery = getIntent().getStringExtra(SearchManager.QUERY);
                mQueryHandler.startQuery(QUERY_TOKEN, null, getPeopleFilterUri(mQuery),
                        CONTACTS_PROJECTION, null, null,
                        getSortOrder(CONTACTS_PROJECTION));
                break;
            }
            
            case MODE_QUERY_PICK_TO_VIEW: {
                if (mQueryMode == QUERY_MODE_MAILTO) {
                    // Find all contacts with the given search string as either
                    // an E-mail or IM address.
                    mQueryPersonIdIndex = SIMPLE_CONTACTS_PERSON_ID_INDEX;
                    Uri uri = Uri.withAppendedPath(People.WITH_EMAIL_OR_IM_FILTER_URI,
                            Uri.encode(mQueryData));
                    mQueryHandler.startQuery(QUERY_TOKEN, null,
                            uri, SIMPLE_CONTACTS_PROJECTION, null, null,
                            getSortOrder(CONTACTS_PROJECTION));
                    
                } else if (mQueryMode == QUERY_MODE_TEL) {
                    mQueryPersonIdIndex = PHONES_PERSON_ID_INDEX;
                    mQueryHandler.startQuery(QUERY_TOKEN, null,
                            Uri.withAppendedPath(Phones.CONTENT_FILTER_URL, mQueryData),
                            PHONES_PROJECTION, null, null,
                            getSortOrder(PHONES_PROJECTION));
                }
                break;
            }
            
            case MODE_STARRED:
                mQueryHandler.startQuery(QUERY_TOKEN, null, People.CONTENT_URI,
                        CONTACTS_PROJECTION,
                        People.STARRED + "=1", null, getSortOrder(CONTACTS_PROJECTION));
                break;

            case MODE_FREQUENT:
                mQueryHandler.startQuery(QUERY_TOKEN, null,
                        People.CONTENT_URI, CONTACTS_PROJECTION,
                        People.TIMES_CONTACTED + " > 0", null,
                        People.TIMES_CONTACTED + " DESC, " + getSortOrder(CONTACTS_PROJECTION));
                break;

            case MODE_STREQUENT:
                mQueryHandler.startQuery(QUERY_TOKEN, null,
                        Uri.withAppendedPath(People.CONTENT_URI, "strequent"), STREQUENT_PROJECTION,
                        null, null, null);
                break;

            case MODE_PICK_PHONE:
                mQueryHandler.startQuery(QUERY_TOKEN, null, Phones.CONTENT_URI, PHONES_PROJECTION,
                        null, null, getSortOrder(PHONES_PROJECTION));
                break;

            case MODE_PICK_POSTAL:
                mQueryHandler.startQuery(QUERY_TOKEN, null, ContactMethods.CONTENT_URI,
                        CONTACT_METHODS_PROJECTION,
                        ContactMethods.KIND + "=" + Contacts.KIND_POSTAL, null,
                        getSortOrder(CONTACT_METHODS_PROJECTION));
                break;
        }
    }

    /**
     * Called from a background thread to do the filter and return the resulting cursor.
     * 
     * @param filter the text that was entered to filter on
     * @return a cursor with the results of the filter
     */
    Cursor doFilter(String filter) {
        final ContentResolver resolver = getContentResolver();

        switch (mMode) {
            case MODE_GROUP: {
                Uri uri;
                if (TextUtils.isEmpty(filter)) {
                    uri = mGroupUri;
                } else {
                    uri = Uri.withAppendedPath(mGroupFilterUri, Uri.encode(filter));
                }
                return resolver.query(uri, CONTACTS_PROJECTION, null, null,
                        getSortOrder(CONTACTS_PROJECTION));
            }

            case MODE_ALL_CONTACTS:
            case MODE_PICK_CONTACT:
            case MODE_PICK_OR_CREATE_CONTACT:
            case MODE_INSERT_OR_EDIT_CONTACT: {
                return resolver.query(getPeopleFilterUri(filter), CONTACTS_PROJECTION, null, null,
                        getSortOrder(CONTACTS_PROJECTION));
            }

            case MODE_WITH_PHONES: {
                return resolver.query(getPeopleFilterUri(filter), CONTACTS_PROJECTION,
                        People.PRIMARY_PHONE_ID + " IS NOT NULL", null,
                        getSortOrder(CONTACTS_PROJECTION));
            }

            case MODE_STARRED: {
                return resolver.query(getPeopleFilterUri(filter), CONTACTS_PROJECTION,
                        People.STARRED + "=1", null, getSortOrder(CONTACTS_PROJECTION));
            }

            case MODE_FREQUENT: {
                return resolver.query(getPeopleFilterUri(filter), CONTACTS_PROJECTION,
                        People.TIMES_CONTACTED + " > 0", null,
                        People.TIMES_CONTACTED + " DESC, " + getSortOrder(CONTACTS_PROJECTION));
                
            }

            case MODE_STREQUENT: {
                Uri uri;
                if (!TextUtils.isEmpty(filter)) {
                    uri = Uri.withAppendedPath(People.CONTENT_URI, "strequent/filter/"
                            + Uri.encode(filter));
                } else {
                    uri = Uri.withAppendedPath(People.CONTENT_URI, "strequent");
                }
                return resolver.query(uri, STREQUENT_PROJECTION, null, null, null);
            }

            case MODE_PICK_PHONE: {
                Uri uri;
                if (!TextUtils.isEmpty(filter)) {
                    uri = Uri.withAppendedPath(Phones.CONTENT_URI, "filter_name/"
                            + Uri.encode(filter));
                } else {
                    uri = Phones.CONTENT_URI;
                }
                return resolver.query(uri, PHONES_PROJECTION, null, null,
                        getSortOrder(PHONES_PROJECTION));
            }
        }
        throw new UnsupportedOperationException("filtering not allowed in mode " + mMode);
    }

    /**
     * Calls the currently selected list item.
     * @return true if the call was initiated, false otherwise
     */
    boolean callSelection() {
        ListView list = getListView();
        if (list.hasFocus()) {
            Cursor cursor = (Cursor) list.getSelectedItem();
            if (cursor != null) {
                long phoneId = cursor.getLong(PRIMARY_PHONE_ID_COLUMN_INDEX);
                if (phoneId == 0) {
                    // There is no phone number.
                    signalError();
                    return false;
                }
                Uri uri = ContentUris.withAppendedId(Phones.CONTENT_URI, phoneId);
                Intent intent = new Intent(Intent.ACTION_CALL_PRIVILEGED, uri);
                startActivity(intent);
                return true;
            }
        }

        return false;
    }

    /**
     * Signal an error to the user.
     */
    void signalError() {
        //TODO play an error beep or something...
    }

    Cursor getItemForView(View view) {
        ListView listView = getListView();
        int index = listView.getPositionForView(view);
        if (index < 0) {
            return null;
        }
        return (Cursor) listView.getAdapter().getItem(index);
    }

    private void setGroupEntries(AlertDialog.Builder builder) {
        boolean syncEverything;
        // For now we only support a single account and the UI doesn't know what
        // the account name is, so we're using a global setting for SYNC_EVERYTHING.
        // Some day when we add multiple accounts to the UI this should use the per
        // account setting.
        String value = Contacts.Settings.getSetting(getContentResolver(), null,
                Contacts.Settings.SYNC_EVERYTHING);
        if (value == null) {
            // If nothing is set yet we default to syncing everything
            syncEverything = true;
        } else {
            syncEverything = !TextUtils.isEmpty(value) && !"0".equals(value);
        }

        Cursor cursor;
        if (!syncEverything) {
            cursor = getContentResolver().query(Groups.CONTENT_URI, GROUPS_PROJECTION,
                    Groups.SHOULD_SYNC + " != 0", null, Groups.DEFAULT_SORT_ORDER);
        } else {
            cursor = getContentResolver().query(Groups.CONTENT_URI, GROUPS_PROJECTION,
                    null, null, Groups.DEFAULT_SORT_ORDER);
        }
        try {
            ArrayList<CharSequence> groups = new ArrayList<CharSequence>();
            ArrayList<CharSequence> prefStrings = new ArrayList<CharSequence>();

            // Add All Contacts
            groups.add(DISPLAY_GROUP_INDEX_ALL_CONTACTS, getString(R.string.showAllGroups));
            prefStrings.add("");
            
            // Add Contacts with phones
            groups.add(DISPLAY_GROUP_INDEX_ALL_CONTACTS_WITH_PHONES,
                    getString(R.string.groupNameWithPhones));
            prefStrings.add(GROUP_WITH_PHONES);
            
            int currentIndex = DISPLAY_GROUP_INDEX_ALL_CONTACTS;
            while (cursor.moveToNext()) {
                String systemId = cursor.getString(GROUPS_COLUMN_INDEX_SYSTEM_ID);
                String name = cursor.getString(GROUPS_COLUMN_INDEX_NAME);
                if (cursor.isNull(GROUPS_COLUMN_INDEX_SYSTEM_ID)
                        && !Groups.GROUP_MY_CONTACTS.equals(systemId)) {
                    // All groups that aren't My Contacts, since that one is localized on the phone

                    // Localize the "Starred in Android" string which we get from the server side.
                    if (Groups.GROUP_ANDROID_STARRED.equals(name)) {
                        name = getString(R.string.starredInAndroid);
                    }
                    groups.add(name);
                    if (name.equals(mDisplayInfo)) {
                        currentIndex = groups.size() - 1;
                    }
                } else {
                    // The My Contacts group
                    groups.add(DISPLAY_GROUP_INDEX_MY_CONTACTS,
                            getString(R.string.groupNameMyContacts));
                    if (mDisplayType == DISPLAY_TYPE_SYSTEM_GROUP
                            && Groups.GROUP_MY_CONTACTS.equals(mDisplayInfo)) {
                        currentIndex = DISPLAY_GROUP_INDEX_MY_CONTACTS;
                    }
                    mDisplayGroupsIncludesMyContacts = true;
                }
            }
            if (mMode == MODE_ALL_CONTACTS) {
                currentIndex = DISPLAY_GROUP_INDEX_ALL_CONTACTS;
            } else if (mMode == MODE_WITH_PHONES) {
                currentIndex = DISPLAY_GROUP_INDEX_ALL_CONTACTS_WITH_PHONES;
            }
            mDisplayGroups = groups.toArray(new CharSequence[groups.size()]);
            builder.setSingleChoiceItems(mDisplayGroups, currentIndex, this);
            mDisplayGroupOriginalSelection = currentIndex;
        } finally {
            cursor.close();
        }
    }

    private static final class QueryHandler extends AsyncQueryHandler {
        private final WeakReference<ContactsListActivity> mActivity;

        public QueryHandler(Context context) {
            super(context.getContentResolver());
            mActivity = new WeakReference<ContactsListActivity>((ContactsListActivity) context);
        }

        @Override
        protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
            final ContactsListActivity activity = mActivity.get();
            if (activity != null && !activity.isFinishing()) {
                activity.mAdapter.setLoading(false);
                activity.getListView().clearTextFilter();                
                activity.mAdapter.changeCursor(cursor);
                
                // Now that the cursor is populated again, it's possible to restore the list state
                if (activity.mListState != null) {
                    activity.mList.onRestoreInstanceState(activity.mListState);
                    if (activity.mListHasFocus) {
                        activity.mList.requestFocus();
                    }
                    activity.mListHasFocus = false;
                    activity.mListState = null;
                }
            } else {
                cursor.close();
            }
        }
    }

    final static class ContactListItemCache {
        public TextView nameView;
        public CharArrayBuffer nameBuffer = new CharArrayBuffer(128);
        public TextView labelView;
        public CharArrayBuffer labelBuffer = new CharArrayBuffer(128);
        public TextView numberView;
        public CharArrayBuffer numberBuffer = new CharArrayBuffer(128);
        public ImageView presenceView;
        public ImageView photoView;
    }

    private final class ContactItemListAdapter extends ResourceCursorAdapter 
            implements SectionIndexer {
        private SectionIndexer mIndexer;
        private String mAlphabet;
        private boolean mLoading = true;
        private CharSequence mUnknownNameText;
        private CharSequence[] mLocalizedLabels;
        private boolean mDisplayPhotos = false;
        private SparseArray<SoftReference<Bitmap>> mBitmapCache = null;
        private int mFrequentSeparatorPos = ListView.INVALID_POSITION;

        public ContactItemListAdapter(Context context) {
            super(context, R.layout.contacts_list_item, null, false);
            
            mAlphabet = context.getString(com.android.internal.R.string.fast_scroll_alphabet);
            
            mUnknownNameText = context.getText(android.R.string.unknownName);
            switch (mMode) {
                case MODE_PICK_POSTAL:
                    mLocalizedLabels = EditContactActivity.getLabelsForKind(mContext,
                            Contacts.KIND_POSTAL);
                    break;
                default:
                    mLocalizedLabels = EditContactActivity.getLabelsForKind(mContext,
                            Contacts.KIND_PHONE);
                    break;
            }
            
            if ((mMode & MODE_MASK_SHOW_PHOTOS) == MODE_MASK_SHOW_PHOTOS) {
                mDisplayPhotos = true;
                setViewResource(R.layout.contacts_list_item_photo);
                mBitmapCache = new SparseArray<SoftReference<Bitmap>>();
            }
        }

        private SectionIndexer getNewIndexer(Cursor cursor) {
            if (Locale.getDefault().equals(Locale.JAPAN)) {
                return new JapaneseContactListIndexer(cursor, SORT_STRING_INDEX);
            } else {
                return new AlphabetIndexer(cursor, NAME_COLUMN_INDEX, mAlphabet);
            }
        }
        
        /**
         * Callback on the UI thread when the content observer on the backing cursor fires.
         * Instead of calling requery we need to do an async query so that the requery doesn't
         * block the UI thread for a long time. 
         */
        @Override
        protected void onContentChanged() {
            CharSequence constraint = getListView().getTextFilter();
            if (!TextUtils.isEmpty(constraint)) {
                // Reset the filter state then start an async filter operation
                Filter filter = getFilter();
                filter.filter(constraint);
            } else {
                // Start an async query
                startQuery();
            }
        }
        
        public void setLoading(boolean loading) {
            mLoading = loading;
        }

        @Override
        public boolean isEmpty() {
            if ((mMode & MODE_MASK_CREATE_NEW) == MODE_MASK_CREATE_NEW) {
                // This mode mask adds a header and we always want it to show up, even
                // if the list is empty, so always claim the list is not empty.
                return false;
            } else {
                if (mLoading) {
                    // We don't want the empty state to show when loading.
                    return false;
                } else {
                    return super.isEmpty();
                }
            }
        }

        @Override
        public int getItemViewType(int position) {
            if (position == mFrequentSeparatorPos) {
                // We don't want the separator view to be recycled.
                return IGNORE_ITEM_VIEW_TYPE;
            }
            return super.getItemViewType(position);
        }

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            if (!mDataValid) {
                throw new IllegalStateException(
                        "this should only be called when the cursor is valid");
            }

            // Handle the separator specially
            if (position == mFrequentSeparatorPos) {
                LayoutInflater inflater =
                        (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 
                TextView view = (TextView) inflater.inflate(R.layout.list_separator, parent, false);
                view.setText(R.string.favoritesFrquentSeparator);
                return view;
            }

            if (!mCursor.moveToPosition(getRealPosition(position))) {
                throw new IllegalStateException("couldn't move cursor to position " + position);
            }
            
            View v;
            if (convertView == null) {
                v = newView(mContext, mCursor, parent);
            } else {
                v = convertView;
            }
            bindView(v, mContext, mCursor);
            return v;
        }

        @Override
        public View newView(Context context, Cursor cursor, ViewGroup parent) {
            final View view = super.newView(context, cursor, parent);

            final ContactListItemCache cache = new ContactListItemCache();
            cache.nameView = (TextView) view.findViewById(R.id.name);
            cache.labelView = (TextView) view.findViewById(R.id.label);
            cache.numberView = (TextView) view.findViewById(R.id.number);
            cache.presenceView = (ImageView) view.findViewById(R.id.presence);
            cache.photoView = (ImageView) view.findViewById(R.id.photo);
            view.setTag(cache);

            return view;
        }

        @Override
        public void bindView(View view, Context context, Cursor cursor) {
            final ContactListItemCache cache = (ContactListItemCache) view.getTag();
            
            // Set the name           
            cursor.copyStringToBuffer(NAME_COLUMN_INDEX, cache.nameBuffer);
            int size = cache.nameBuffer.sizeCopied;
            if (size != 0) {
                cache.nameView.setText(cache.nameBuffer.data, 0, size);
            } else {
                cache.nameView.setText(mUnknownNameText);
            }
            
            // Bail out early if using a specific SEARCH query mode, usually for
            // matching a specific E-mail or phone number. Any contact details
            // shown would be identical, and columns might not even be present
            // in the returned cursor.
            if (mQueryMode != QUERY_MODE_NONE) {
                cache.numberView.setVisibility(View.GONE);
                cache.labelView.setVisibility(View.GONE);
                cache.presenceView.setVisibility(View.GONE);
                return;
            }
            
            // Set the phone number
            TextView numberView = cache.numberView;
            TextView labelView = cache.labelView;
            cursor.copyStringToBuffer(NUMBER_COLUMN_INDEX, cache.numberBuffer);
            size = cache.numberBuffer.sizeCopied;
            if (size != 0) {
                numberView.setText(cache.numberBuffer.data, 0, size);
                numberView.setVisibility(View.VISIBLE);
                labelView.setVisibility(View.VISIBLE);
            } else {
                numberView.setVisibility(View.GONE);
                labelView.setVisibility(View.GONE);
            }

            // Set the label
            if (!cursor.isNull(TYPE_COLUMN_INDEX)) {
                int type = cursor.getInt(TYPE_COLUMN_INDEX);

                if (type != People.Phones.TYPE_CUSTOM) {
                    try {
                        labelView.setText(mLocalizedLabels[type - 1]);
                    } catch (ArrayIndexOutOfBoundsException e) {
                        labelView.setText(mLocalizedLabels[People.Phones.TYPE_HOME - 1]);
                    }
                } else {
                    cursor.copyStringToBuffer(LABEL_COLUMN_INDEX, cache.labelBuffer);
                    // Don't check size, if it's zero just don't show anything
                    labelView.setText(cache.labelBuffer.data, 0, cache.labelBuffer.sizeCopied);
                }
            } else {
                // There is no label, hide the the view
                labelView.setVisibility(View.GONE);
            }

            // Set the proper icon (star or presence or nothing)
            ImageView presenceView = cache.presenceView;
            if ((mMode & MODE_MASK_NO_PRESENCE) == 0) {
                int serverStatus;
                if (!cursor.isNull(SERVER_STATUS_COLUMN_INDEX)) {
                    serverStatus = cursor.getInt(SERVER_STATUS_COLUMN_INDEX);
                    presenceView.setImageResource(
                            Presence.getPresenceIconResourceId(serverStatus));
                    presenceView.setVisibility(View.VISIBLE);
                } else {
                    presenceView.setVisibility(View.GONE);
                }
            } else {
                presenceView.setVisibility(View.GONE);
            }

            // Set the photo, if requested
            if (mDisplayPhotos) {
                Bitmap photo = null;

                // Look for the cached bitmap
                int pos = cursor.getPosition();
                SoftReference<Bitmap> ref = mBitmapCache.get(pos);
                if (ref != null) {
                    photo = ref.get();
                }

                if (photo == null) {
                    // Bitmap cache miss, decode it from the cursor
                    if (!cursor.isNull(PHOTO_COLUMN_INDEX)) {
                        try {
                            byte[] photoData = cursor.getBlob(PHOTO_COLUMN_INDEX);
                            photo = BitmapFactory.decodeByteArray(photoData, 0,
                                    photoData.length);
                            mBitmapCache.put(pos, new SoftReference<Bitmap>(photo));
                        } catch (OutOfMemoryError e) {
                            // Not enough memory for the photo, use the default one instead
                            photo = null;
                        }
                    }
                }

                // Bind the photo, or use the fallback no photo resource
                if (photo != null) {
                    cache.photoView.setImageBitmap(photo);
                } else {
                    cache.photoView.setImageResource(R.drawable.ic_contact_list_picture);
                }
            }
        }

        @Override
        public void changeCursor(Cursor cursor) {
            // Get the split between starred and frequent items, if the mode is strequent
            mFrequentSeparatorPos = ListView.INVALID_POSITION;
            if (cursor != null && cursor.getCount() > 0 && mMode == MODE_STREQUENT) {
                cursor.move(-1);
                for (int i = 0; cursor.moveToNext(); i++) {
                    int starred = cursor.getInt(STARRED_COLUMN_INDEX);
                    if (starred == 0) {
                        if (i > 0) {
                            // Only add the separator when there are starred items present
                            mFrequentSeparatorPos = i;
                        }
                        break;
                    }
                }
            }

            super.changeCursor(cursor);

            // Update the indexer for the fast scroll widget
            updateIndexer(cursor);

            // Clear the photo bitmap cache, if there is one
            if (mBitmapCache != null) {
                mBitmapCache.clear();
            }
        }
        
        private void updateIndexer(Cursor cursor) {
            if (mIndexer == null) {
                mIndexer = getNewIndexer(cursor);
            } else {
                if (Locale.getDefault().equals(Locale.JAPAN)) {
                    if (mIndexer instanceof JapaneseContactListIndexer) {
                        ((JapaneseContactListIndexer)mIndexer).setCursor(cursor);
                    } else {
                        mIndexer = getNewIndexer(cursor);
                    }
                } else {
                    if (mIndexer instanceof AlphabetIndexer) {
                        ((AlphabetIndexer)mIndexer).setCursor(cursor);
                    } else {
                        mIndexer = getNewIndexer(cursor);
                    }
                }
            }
        }
        
        /**
         * Run the query on a helper thread. Beware that this code does not run
         * on the main UI thread!
         */
        @Override
        public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
            return doFilter(constraint.toString());
        }
        
        public Object [] getSections() {
            if (mMode == MODE_STREQUENT) {
                return new String[] { " " };
            } else {
                return mIndexer.getSections();
           }
        }

        public int getPositionForSection(int sectionIndex) {
            if (mMode == MODE_STREQUENT) {
                return 0;
            }

            if (mIndexer == null) {
                Cursor cursor = mAdapter.getCursor();
                if (cursor == null) {
                    // No cursor, the section doesn't exist so just return 0
                    return 0;
                }
                mIndexer = getNewIndexer(cursor);
            }

            return mIndexer.getPositionForSection(sectionIndex);
        }

        public int getSectionForPosition(int position) {
            // Note: JapaneseContactListIndexer depends on the fact
            // this method always returns 0. If you change this,
            // please care it too.
            return 0;
        }

        @Override
        public boolean areAllItemsEnabled() {
            return mMode != MODE_STREQUENT;
        }

        @Override
        public boolean isEnabled(int position) {
            return position != mFrequentSeparatorPos;
        }

        @Override
        public int getCount() {
            if (mFrequentSeparatorPos != ListView.INVALID_POSITION) {
                return super.getCount() + 1;
            } else {
                return super.getCount();
            }
        }
        
        private int getRealPosition(int pos) {
            if (mFrequentSeparatorPos == ListView.INVALID_POSITION) {
                // No separator, identity map
                return pos;
            } else if (pos <= mFrequentSeparatorPos) {
                // Before or at the separator, identity map
                return pos;
            } else {
                // After the separator, remove 1 from the pos to get the real underlying pos
                return pos - 1;
            }
            
        }
        
        @Override
        public Object getItem(int pos) {
            return super.getItem(getRealPosition(pos));
        }
        
        @Override
        public long getItemId(int pos) {
            return super.getItemId(getRealPosition(pos)); 
        }
    }
}