ContactsProvider2 通讯录部分源码

原生Contacts联系人模块 测试:先编framework再编service/core再编service然后adb push framework.jar和services.jar。 源码:contacts的源码在ContactsProvider目录下,在ContactsProvider2类中可以通过UriMatcher找到所有访问路径。 相关数据库:导出com.android.provider.contacts下的cont... 阅读详情

/*
 * Copyright (C) 2009 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.providers.contacts;

import com.android.internal.content.SyncStateContentProviderHelper;
import com.android.providers.contacts.ContactLookupKey.LookupKeySegment;
import com.android.providers.contacts.ContactsDatabaseHelper.AggregatedPresenceColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.AggregationExceptionColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.Clauses;
import com.android.providers.contacts.ContactsDatabaseHelper.ContactsColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.ContactsStatusUpdatesColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.DataColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.DisplayNameSources;
import com.android.providers.contacts.ContactsDatabaseHelper.GroupsColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.MimetypesColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupType;
import com.android.providers.contacts.ContactsDatabaseHelper.NicknameLookupColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.PhoneColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.PhoneLookupColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.PresenceColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.RawContactsColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.SettingsColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.StatusUpdatesColumns;
import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
import com.google.android.collect.Lists;
import com.google.android.collect.Maps;
import com.google.android.collect.Sets;

import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.OnAccountsUpdateListener;
import android.app.SearchManager;
import android.content.ContentProviderOperation;
import android.content.ContentProviderResult;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.Entity;
import android.content.EntityIterator;
import android.content.IContentService;
import android.content.OperationApplicationException;
import android.content.SharedPreferences;
import android.content.SyncAdapterType;
import android.content.UriMatcher;
import android.content.SharedPreferences.Editor;
import android.content.res.AssetFileDescriptor;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.database.sqlite.SQLiteConstraintException;
import android.database.sqlite.SQLiteContentHelper;
import android.database.sqlite.SQLiteCursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteQueryBuilder;
import android.database.sqlite.SQLiteStatement;
import android.net.Uri;
import android.os.Bundle;
import android.os.MemoryFile;
import android.os.RemoteException;
import android.os.SystemProperties;
import android.pim.vcard.VCardComposer;
import android.preference.PreferenceManager;
import android.provider.BaseColumns;
import android.provider.ContactsContract;
import android.provider.LiveFolders;
import android.provider.OpenableColumns;
import android.provider.SyncStateContract;
import android.provider.ContactsContract.AggregationExceptions;
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.Data;
import android.provider.ContactsContract.Groups;
import android.provider.ContactsContract.PhoneLookup;
import android.provider.ContactsContract.RawContacts;
import android.provider.ContactsContract.Settings;
import android.provider.ContactsContract.StatusUpdates;
import android.provider.ContactsContract.CommonDataKinds.BaseTypes;
import android.provider.ContactsContract.CommonDataKinds.Email;
import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
import android.provider.ContactsContract.CommonDataKinds.Im;
import android.provider.ContactsContract.CommonDataKinds.Nickname;
import android.provider.ContactsContract.CommonDataKinds.Organization;
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.provider.ContactsContract.CommonDataKinds.Photo;
import android.provider.ContactsContract.CommonDataKinds.StructuredName;
import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
import android.telephony.PhoneNumberUtils;
import android.text.TextUtils;
import android.text.util.Rfc822Token;
import android.text.util.Rfc822Tokenizer;
import android.util.Log;

import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.ref.SoftReference;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CountDownLatch;

/**
 * Contacts content provider. The contract between this provider and applications
 * is defined in {@link ContactsContract}.
 */
public class ContactsProvider2 extends SQLiteContentProvider implements OnAccountsUpdateListener {

    private static final String TAG = "ContactsProvider";

    private static final boolean VERBOSE_LOGGING = Log.isLoggable(TAG, Log.VERBOSE);

    // TODO: carefully prevent all incoming nested queries; they can be gaping security holes
    // TODO: check for restricted flag during insert(), update(), and delete() calls

    /** Default for the maximum number of returned aggregation suggestions. */
    private static final int DEFAULT_MAX_SUGGESTIONS = 5;

    /**
     * Shared preference key for the legacy contact import version. The need for a version
     * as opposed to a boolean flag is that if we discover bugs in the contact import process,
     * we can trigger re-import by incrementing the import version.
     */
    private static final String PREF_CONTACTS_IMPORTED = "contacts_imported_v1";
    private static final int PREF_CONTACTS_IMPORT_VERSION = 1;

    private static final String AGGREGATE_CONTACTS = "sync.contacts.aggregate";

    private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);

    private static final String TIMES_CONTACED_SORT_COLUMN = "times_contacted_sort";

    private static final String STREQUENT_ORDER_BY = Contacts.STARRED + " DESC, "
            + TIMES_CONTACED_SORT_COLUMN + " DESC, "
            + Contacts.DISPLAY_NAME + " COLLATE LOCALIZED ASC";
    private static final String STREQUENT_LIMIT =
            "(SELECT COUNT(1) FROM " + Tables.CONTACTS + " WHERE "
            + Contacts.STARRED + "=1) + 25";

    private static final int CONTACTS = 1000;
    private static final int CONTACTS_ID = 1001;
    private static final int CONTACTS_LOOKUP = 1002;
    private static final int CONTACTS_LOOKUP_ID = 1003;
    private static final int CONTACTS_DATA = 1004;
    private static final int CONTACTS_FILTER = 1005;
    private static final int CONTACTS_STREQUENT = 1006;
    private static final int CONTACTS_STREQUENT_FILTER = 1007;
    private static final int CONTACTS_GROUP = 1008;
    private static final int CONTACTS_PHOTO = 1009;
    private static final int CONTACTS_AS_VCARD = 1010;

    private static final int RAW_CONTACTS = 2002;
    private static final int RAW_CONTACTS_ID = 2003;
    private static final int RAW_CONTACTS_DATA = 2004;
    private static final int RAW_CONTACT_ENTITY_ID = 2005;

    private static final int DATA = 3000;
    private static final int DATA_ID = 3001;
    private static final int PHONES = 3002;
    private static final int PHONES_ID = 3003;
    private static final int PHONES_FILTER = 3004;
    private static final int EMAILS = 3005;
    private static final int EMAILS_ID = 3006;
    private static final int EMAILS_LOOKUP = 3007;
    private static final int EMAILS_FILTER = 3008;
    private static final int POSTALS = 3009;
    private static final int POSTALS_ID = 3010;

    private static final int PHONE_LOOKUP = 4000;

    private static final int AGGREGATION_EXCEPTIONS = 6000;
    private static final int AGGREGATION_EXCEPTION_ID = 6001;

    private static final int STATUS_UPDATES = 7000;
    private static final int STATUS_UPDATES_ID = 7001;

    private static final int AGGREGATION_SUGGESTIONS = 8000;

    private static final int SETTINGS = 9000;

    private static final int GROUPS = 10000;
    private static final int GROUPS_ID = 10001;
    private static final int GROUPS_SUMMARY = 10003;

    private static final int SYNCSTATE = 11000;
    private static final int SYNCSTATE_ID = 11001;

    private static final int SEARCH_SUGGESTIONS = 12001;
    private static final int SEARCH_SHORTCUT = 12002;

    private static final int LIVE_FOLDERS_CONTACTS = 14000;
    private static final int LIVE_FOLDERS_CONTACTS_WITH_PHONES = 14001;
    private static final int LIVE_FOLDERS_CONTACTS_FAVORITES = 14002;
    private static final int LIVE_FOLDERS_CONTACTS_GROUP_NAME = 14003;

    private static final int RAW_CONTACT_ENTITIES = 15001;

    private interface ContactsQuery {
        public static final String TABLE = Tables.RAW_CONTACTS;

        public static final String[] PROJECTION = new String[] {
            RawContactsColumns.CONCRETE_ID,
            RawContacts.ACCOUNT_NAME,
            RawContacts.ACCOUNT_TYPE,
        };

        public static final int RAW_CONTACT_ID = 0;
        public static final int ACCOUNT_NAME = 1;
        public static final int ACCOUNT_TYPE = 2;
    }

    private interface DataContactsQuery {
        public static final String TABLE = "data "
                + "JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id) "
                + "JOIN contacts ON (raw_contacts.contact_id = contacts._id)";

        public static final String[] PROJECTION = new String[] {
            RawContactsColumns.CONCRETE_ID,
            DataColumns.CONCRETE_ID,
            ContactsColumns.CONCRETE_ID
        };

        public static final int RAW_CONTACT_ID = 0;
        public static final int DATA_ID = 1;
        public static final int CONTACT_ID = 2;
    }

    private interface DisplayNameQuery {
        public static final String TABLE = Tables.DATA_JOIN_MIMETYPES;

        public static final String[] COLUMNS = new String[] {
            MimetypesColumns.MIMETYPE,
            Data.IS_PRIMARY,
            Data.DATA1,
            Organization.TITLE,
        };

        public static final int MIMETYPE = 0;
        public static final int IS_PRIMARY = 1;
        public static final int DATA = 2;
        public static final int TITLE = 3;
    }

    private interface DataDeleteQuery {
        public static final String TABLE = Tables.DATA_JOIN_MIMETYPES;

        public static final String[] CONCRETE_COLUMNS = new String[] {
            DataColumns.CONCRETE_ID,
            MimetypesColumns.MIMETYPE,
            Data.RAW_CONTACT_ID,
            Data.IS_PRIMARY,
            Data.DATA1,
        };

        public static final String[] COLUMNS = new String[] {
            Data._ID,
            MimetypesColumns.MIMETYPE,
            Data.RAW_CONTACT_ID,
            Data.IS_PRIMARY,
            Data.DATA1,
        };

        public static final int _ID = 0;
        public static final int MIMETYPE = 1;
        public static final int RAW_CONTACT_ID = 2;
        public static final int IS_PRIMARY = 3;
        public static final int DATA1 = 4;
    }

    private interface DataUpdateQuery {
        String[] COLUMNS = { Data._ID, Data.RAW_CONTACT_ID, Data.MIMETYPE };

        int _ID = 0;
        int RAW_CONTACT_ID = 1;
        int MIMETYPE = 2;
    }


    private interface NicknameLookupQuery {
        String TABLE = Tables.NICKNAME_LOOKUP;

        String[] COLUMNS = new String[] {
            NicknameLookupColumns.CLUSTER
        };

        int CLUSTER = 0;
    }

    private interface RawContactsQuery {
        String TABLE = Tables.RAW_CONTACTS;

        String[] COLUMNS = new String[] {
                ContactsContract.RawContacts.DELETED
        };

        int DELETED = 0;
    }

    private static final HashMap<String, Integer> sDisplayNameSources;
    static {
        sDisplayNameSources = new HashMap<String, Integer>();
        sDisplayNameSources.put(StructuredName.CONTENT_ITEM_TYPE,
                DisplayNameSources.STRUCTURED_NAME);
        sDisplayNameSources.put(Nickname.CONTENT_ITEM_TYPE,
                DisplayNameSources.NICKNAME);
        sDisplayNameSources.put(Organization.CONTENT_ITEM_TYPE,
                DisplayNameSources.ORGANIZATION);
        sDisplayNameSources.put(Phone.CONTENT_ITEM_TYPE,
                DisplayNameSources.PHONE);
        sDisplayNameSources.put(Email.CONTENT_ITEM_TYPE,
                DisplayNameSources.EMAIL);
    }

    public static final String DEFAULT_ACCOUNT_TYPE = "com.google";
    public static final String FEATURE_LEGACY_HOSTED_OR_GOOGLE = "legacy_hosted_or_google";

    /** Sql where statement for filtering on groups. */
    private static final String CONTACTS_IN_GROUP_SELECT =
            Contacts._ID + " IN "
                    + "(SELECT " + RawContacts.CONTACT_ID
                    + " FROM " + Tables.RAW_CONTACTS
                    + " WHERE " + RawContactsColumns.CONCRETE_ID + " IN "
                            + "(SELECT " + DataColumns.CONCRETE_RAW_CONTACT_ID
                            + " FROM " + Tables.DATA_JOIN_MIMETYPES
                            + " WHERE " + Data.MIMETYPE + "='" + GroupMembership.CONTENT_ITEM_TYPE
                                    + "' AND " + GroupMembership.GROUP_ROW_ID + "="
                                    + "(SELECT " + Tables.GROUPS + "." + Groups._ID
                                    + " FROM " + Tables.GROUPS
                                    + " WHERE " + Groups.TITLE + "=?)))";

    /** Contains just BaseColumns._COUNT */
    private static final HashMap<String, String> sCountProjectionMap;
    /** Contains just the contacts columns */
    private static final HashMap<String, String> sContactsProjectionMap;
    /** Used for pushing starred contacts to the top of a times contacted list **/
    private static final HashMap<String, String> sStrequentStarredProjectionMap;
    private static final HashMap<String, String> sStrequentFrequentProjectionMap;
    /** Contains just the contacts vCard columns */
    private static final HashMap<String, String> sContactsVCardProjectionMap;
    /** Contains just the raw contacts columns */
    private static final HashMap<String, String> sRawContactsProjectionMap;
    /** Contains the columns from the raw contacts entity view*/
    private static final HashMap<String, String> sRawContactsEntityProjectionMap;
    /** Contains columns from the data view */
    private static final HashMap<String, String> sDataProjectionMap;
    /** Contains columns from the data view */
    private static final HashMap<String, String> sDistinctDataProjectionMap;
    /** Contains the data and contacts columns, for joined tables */
    private static final HashMap<String, String> sPhoneLookupProjectionMap;
    /** Contains the just the {@link Groups} columns */
    private static final HashMap<String, String> sGroupsProjectionMap;
    /** Contains {@link Groups} columns along with summary details */
    private static final HashMap<String, String> sGroupsSummaryProjectionMap;
    /** Contains the agg_exceptions columns */
    private static final HashMap<String, String> sAggregationExceptionsProjectionMap;
    /** Contains the agg_exceptions columns */
    private static final HashMap<String, String> sSettingsProjectionMap;
    /** Contains StatusUpdates columns */
    private static final HashMap<String, String> sStatusUpdatesProjectionMap;
    /** Contains Live Folders columns */
    private static final HashMap<String, String> sLiveFoldersProjectionMap;

    /** Precompiled sql statement for setting a data record to the primary. */
    private SQLiteStatement mSetPrimaryStatement;
    /** Precompiled sql statement for setting a data record to the super primary. */
    private SQLiteStatement mSetSuperPrimaryStatement;
    /** Precompiled sql statement for incrementing times contacted for a contact */
    private SQLiteStatement mContactsLastTimeContactedUpdate;
    /** Precompiled sql statement for updating a contact display name */
    private SQLiteStatement mRawContactDisplayNameUpdate;
    /** Precompiled sql statement for marking a raw contact as dirty */
    private SQLiteStatement mRawContactDirtyUpdate;
    /** Precompiled sql statement for updating an aggregated status update */
    private SQLiteStatement mLastStatusUpdate;
    private SQLiteStatement mNameLookupInsert;
    private SQLiteStatement mNameLookupDelete;
    private SQLiteStatement mStatusUpdateAutoTimestamp;
    private SQLiteStatement mStatusUpdateInsert;
    private SQLiteStatement mStatusUpdateReplace;
    private SQLiteStatement mStatusAttributionUpdate;
    private SQLiteStatement mStatusUpdateDelete;

    private long mMimeTypeIdEmail;
    private long mMimeTypeIdIm;
    private StringBuilder mSb = new StringBuilder();

    static {
        // Contacts URI matching table
        final UriMatcher matcher = sUriMatcher;
        matcher.addURI(ContactsContract.AUTHORITY, "contacts", CONTACTS);
        matcher.addURI(ContactsContract.AUTHORITY, "contacts/#", CONTACTS_ID);
        matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/data", CONTACTS_DATA);
        matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/suggestions",
                AGGREGATION_SUGGESTIONS);
        matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/suggestions/*",
                AGGREGATION_SUGGESTIONS);
        matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/photo", CONTACTS_PHOTO);
        matcher.addURI(ContactsContract.AUTHORITY, "contacts/filter/*", CONTACTS_FILTER);
        matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*", CONTACTS_LOOKUP);
        matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/#", CONTACTS_LOOKUP_ID);
        matcher.addURI(ContactsContract.AUTHORITY, "contacts/as_vcard/*", CONTACTS_AS_VCARD);
        matcher.addURI(ContactsContract.AUTHORITY, "contacts/strequent/", CONTACTS_STREQUENT);
        matcher.addURI(ContactsContract.AUTHORITY, "contacts/strequent/filter/*",
                CONTACTS_STREQUENT_FILTER);
        matcher.addURI(ContactsContract.AUTHORITY, "contacts/group/*", CONTACTS_GROUP);

        matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts", RAW_CONTACTS);
        matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#", RAW_CONTACTS_ID);
        matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#/data", RAW_CONTACTS_DATA);
        matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#/entity", RAW_CONTACT_ENTITY_ID);

        matcher.addURI(ContactsContract.AUTHORITY, "raw_contact_entities", RAW_CONTACT_ENTITIES);

        matcher.addURI(ContactsContract.AUTHORITY, "data", DATA);
        matcher.addURI(ContactsContract.AUTHORITY, "data/#", DATA_ID);
        matcher.addURI(ContactsContract.AUTHORITY, "data/phones", PHONES);
        matcher.addURI(ContactsContract.AUTHORITY, "data/phones/#", PHONES_ID);
        matcher.addURI(ContactsContract.AUTHORITY, "data/phones/filter", PHONES_FILTER);
        matcher.addURI(ContactsContract.AUTHORITY, "data/phones/filter/*", PHONES_FILTER);
        matcher.addURI(ContactsContract.AUTHORITY, "data/emails", EMAILS);
        matcher.addURI(ContactsContract.AUTHORITY, "data/emails/#", EMAILS_ID);
        matcher.addURI(ContactsContract.AUTHORITY, "data/emails/lookup/*", EMAILS_LOOKUP);
        matcher.addURI(ContactsContract.AUTHORITY, "data/emails/filter", EMAILS_FILTER);
        matcher.addURI(ContactsContract.AUTHORITY, "data/emails/filter/*", EMAILS_FILTER);
        matcher.addURI(ContactsContract.AUTHORITY, "data/postals", POSTALS);
        matcher.addURI(ContactsContract.AUTHORITY, "data/postals/#", POSTALS_ID);

        matcher.addURI(ContactsContract.AUTHORITY, "groups", GROUPS);
        matcher.addURI(ContactsContract.AUTHORITY, "groups/#", GROUPS_ID);
        matcher.addURI(ContactsContract.AUTHORITY, "groups_summary", GROUPS_SUMMARY);

        matcher.addURI(ContactsContract.AUTHORITY, SyncStateContentProviderHelper.PATH, SYNCSTATE);
        matcher.addURI(ContactsContract.AUTHORITY, SyncStateContentProviderHelper.PATH + "/#",
                SYNCSTATE_ID);

        matcher.addURI(ContactsContract.AUTHORITY, "phone_lookup/*", PHONE_LOOKUP);
        matcher.addURI(ContactsContract.AUTHORITY, "aggregation_exceptions",
                AGGREGATION_EXCEPTIONS);
        matcher.addURI(ContactsContract.AUTHORITY, "aggregation_exceptions/*",
                AGGREGATION_EXCEPTION_ID);

        matcher.addURI(ContactsContract.AUTHORITY, "settings", SETTINGS);

        matcher.addURI(ContactsContract.AUTHORITY, "status_updates", STATUS_UPDATES);
        matcher.addURI(ContactsContract.AUTHORITY, "status_updates/#", STATUS_UPDATES_ID);

        matcher.addURI(ContactsContract.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY,
                SEARCH_SUGGESTIONS);
        matcher.addURI(ContactsContract.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY + "/*",
                SEARCH_SUGGESTIONS);
        matcher.addURI(ContactsContract.AUTHORITY, SearchManager.SUGGEST_URI_PATH_SHORTCUT + "/#",
                SEARCH_SHORTCUT);

        matcher.addURI(ContactsContract.AUTHORITY, "live_folders/contacts",
                LIVE_FOLDERS_CONTACTS);
        matcher.addURI(ContactsContract.AUTHORITY, "live_folders/contacts/*",
                LIVE_FOLDERS_CONTACTS_GROUP_NAME);
        matcher.addURI(ContactsContract.AUTHORITY, "live_folders/contacts_with_phones",
                LIVE_FOLDERS_CONTACTS_WITH_PHONES);
        matcher.addURI(ContactsContract.AUTHORITY, "live_folders/favorites",
                LIVE_FOLDERS_CONTACTS_FAVORITES);
    }

    static {
        sCountProjectionMap = new HashMap<String, String>();
        sCountProjectionMap.put(BaseColumns._COUNT, "COUNT(*)");

        sContactsProjectionMap = new HashMap<String, String>();
        sContactsProjectionMap.put(Contacts._ID, Contacts._ID);
        sContactsProjectionMap.put(Contacts.DISPLAY_NAME, Contacts.DISPLAY_NAME);
        sContactsProjectionMap.put(Contacts.LAST_TIME_CONTACTED, Contacts.LAST_TIME_CONTACTED);
        sContactsProjectionMap.put(Contacts.TIMES_CONTACTED, Contacts.TIMES_CONTACTED);
        sContactsProjectionMap.put(Contacts.STARRED, Contacts.STARRED);
        sContactsProjectionMap.put(Contacts.IN_VISIBLE_GROUP, Contacts.IN_VISIBLE_GROUP);
        sContactsProjectionMap.put(Contacts.PHOTO_ID, Contacts.PHOTO_ID);
        sContactsProjectionMap.put(Contacts.CUSTOM_RINGTONE, Contacts.CUSTOM_RINGTONE);
        sContactsProjectionMap.put(Contacts.HAS_PHONE_NUMBER, Contacts.HAS_PHONE_NUMBER);
        sContactsProjectionMap.put(Contacts.SEND_TO_VOICEMAIL, Contacts.SEND_TO_VOICEMAIL);
        sContactsProjectionMap.put(Contacts.LOOKUP_KEY, Contacts.LOOKUP_KEY);

        // Handle projections for Contacts-level statuses
        addProjection(sContactsProjectionMap, Contacts.CONTACT_PRESENCE,
                Tables.AGGREGATED_PRESENCE + "." + StatusUpdates.PRESENCE);
        addProjection(sContactsProjectionMap, Contacts.CONTACT_STATUS,
                ContactsStatusUpdatesColumns.CONCRETE_STATUS);
        addProjection(sContactsProjectionMap, Contacts.CONTACT_STATUS_TIMESTAMP,
                ContactsStatusUpdatesColumns.CONCRETE_STATUS_TIMESTAMP);
        addProjection(sContactsProjectionMap, Contacts.CONTACT_STATUS_RES_PACKAGE,
                ContactsStatusUpdatesColumns.CONCRETE_STATUS_RES_PACKAGE);
        addProjection(sContactsProjectionMap, Contacts.CONTACT_STATUS_LABEL,
                ContactsStatusUpdatesColumns.CONCRETE_STATUS_LABEL);
        addProjection(sContactsProjectionMap, Contacts.CONTACT_STATUS_ICON,
                ContactsStatusUpdatesColumns.CONCRETE_STATUS_ICON);

        sStrequentStarredProjectionMap = new HashMap<String, String>(sContactsProjectionMap);
        sStrequentStarredProjectionMap.put(TIMES_CONTACED_SORT_COLUMN,
                  Long.MAX_VALUE + " AS " + TIMES_CONTACED_SORT_COLUMN);

        sStrequentFrequentProjectionMap = new HashMap<String, String>(sContactsProjectionMap);
        sStrequentFrequentProjectionMap.put(TIMES_CONTACED_SORT_COLUMN,
                  Contacts.TIMES_CONTACTED + " AS " + TIMES_CONTACED_SORT_COLUMN);

        sContactsVCardProjectionMap = Maps.newHashMap();
        sContactsVCardProjectionMap.put(OpenableColumns.DISPLAY_NAME, Contacts.DISPLAY_NAME
                + " || '.vcf' AS " + OpenableColumns.DISPLAY_NAME);
        sContactsVCardProjectionMap.put(OpenableColumns.SIZE, "0 AS " + OpenableColumns.SIZE);

        sRawContactsProjectionMap = new HashMap<String, String>();
        sRawContactsProjectionMap.put(RawContacts._ID, RawContacts._ID);
        sRawContactsProjectionMap.put(RawContacts.CONTACT_ID, RawContacts.CONTACT_ID);
        sRawContactsProjectionMap.put(RawContacts.ACCOUNT_NAME, RawContacts.ACCOUNT_NAME);
        sRawContactsProjectionMap.put(RawContacts.ACCOUNT_TYPE, RawContacts.ACCOUNT_TYPE);
        sRawContactsProjectionMap.put(RawContacts.SOURCE_ID, RawContacts.SOURCE_ID);
        sRawContactsProjectionMap.put(RawContacts.VERSION, RawContacts.VERSION);
        sRawContactsProjectionMap.put(RawContacts.DIRTY, RawContacts.DIRTY);
        sRawContactsProjectionMap.put(RawContacts.DELETED, RawContacts.DELETED);
        sRawContactsProjectionMap.put(RawContacts.TIMES_CONTACTED, RawContacts.TIMES_CONTACTED);
        sRawContactsProjectionMap.put(RawContacts.LAST_TIME_CONTACTED,
                RawContacts.LAST_TIME_CONTACTED);
        sRawContactsProjectionMap.put(RawContacts.CUSTOM_RINGTONE, RawContacts.CUSTOM_RINGTONE);
        sRawContactsProjectionMap.put(RawContacts.SEND_TO_VOICEMAIL, RawContacts.SEND_TO_VOICEMAIL);
        sRawContactsProjectionMap.put(RawContacts.STARRED, RawContacts.STARRED);
        sRawContactsProjectionMap.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE);
        sRawContactsProjectionMap.put(RawContacts.SYNC1, RawContacts.SYNC1);
        sRawContactsProjectionMap.put(RawContacts.SYNC2, RawContacts.SYNC2);
        sRawContactsProjectionMap.put(RawContacts.SYNC3, RawContacts.SYNC3);
        sRawContactsProjectionMap.put(RawContacts.SYNC4, RawContacts.SYNC4);

        sDataProjectionMap = new HashMap<String, String>();
        sDataProjectionMap.put(Data._ID, Data._ID);
        sDataProjectionMap.put(Data.RAW_CONTACT_ID, Data.RAW_CONTACT_ID);
        sDataProjectionMap.put(Data.DATA_VERSION, Data.DATA_VERSION);
        sDataProjectionMap.put(Data.IS_PRIMARY, Data.IS_PRIMARY);
        sDataProjectionMap.put(Data.IS_SUPER_PRIMARY, Data.IS_SUPER_PRIMARY);
        sDataProjectionMap.put(Data.RES_PACKAGE, Data.RES_PACKAGE);
        sDataProjectionMap.put(Data.MIMETYPE, Data.MIMETYPE);
        sDataProjectionMap.put(Data.DATA1, Data.DATA1);
        sDataProjectionMap.put(Data.DATA2, Data.DATA2);
        sDataProjectionMap.put(Data.DATA3, Data.DATA3);
        sDataProjectionMap.put(Data.DATA4, Data.DATA4);
        sDataProjectionMap.put(Data.DATA5, Data.DATA5);
        sDataProjectionMap.put(Data.DATA6, Data.DATA6);
        sDataProjectionMap.put(Data.DATA7, Data.DATA7);
        sDataProjectionMap.put(Data.DATA8, Data.DATA8);
        sDataProjectionMap.put(Data.DATA9, Data.DATA9);
        sDataProjectionMap.put(Data.DATA10, Data.DATA10);
        sDataProjectionMap.put(Data.DATA11, Data.DATA11);
        sDataProjectionMap.put(Data.DATA12, Data.DATA12);
        sDataProjectionMap.put(Data.DATA13, Data.DATA13);
        sDataProjectionMap.put(Data.DATA14, Data.DATA14);
        sDataProjectionMap.put(Data.DATA15, Data.DATA15);
        sDataProjectionMap.put(Data.SYNC1, Data.SYNC1);
        sDataProjectionMap.put(Data.SYNC2, Data.SYNC2);
        sDataProjectionMap.put(Data.SYNC3, Data.SYNC3);
        sDataProjectionMap.put(Data.SYNC4, Data.SYNC4);
        sDataProjectionMap.put(Data.CONTACT_ID, Data.CONTACT_ID);
        sDataProjectionMap.put(RawContacts.ACCOUNT_NAME, RawContacts.ACCOUNT_NAME);
        sDataProjectionMap.put(RawContacts.ACCOUNT_TYPE, RawContacts.ACCOUNT_TYPE);
        sDataProjectionMap.put(RawContacts.SOURCE_ID, RawContacts.SOURCE_ID);
        sDataProjectionMap.put(RawContacts.VERSION, RawContacts.VERSION);
        sDataProjectionMap.put(RawContacts.DIRTY, RawContacts.DIRTY);
        sDataProjectionMap.put(Contacts.LOOKUP_KEY, Contacts.LOOKUP_KEY);
        sDataProjectionMap.put(Contacts.DISPLAY_NAME, Contacts.DISPLAY_NAME);
        sDataProjectionMap.put(Contacts.CUSTOM_RINGTONE, Contacts.CUSTOM_RINGTONE);
        sDataProjectionMap.put(Contacts.SEND_TO_VOICEMAIL, Contacts.SEND_TO_VOICEMAIL);
        sDataProjectionMap.put(Contacts.LAST_TIME_CONTACTED, Contacts.LAST_TIME_CONTACTED);
        sDataProjectionMap.put(Contacts.TIMES_CONTACTED, Contacts.TIMES_CONTACTED);
        sDataProjectionMap.put(Contacts.STARRED, Contacts.STARRED);
        sDataProjectionMap.put(Contacts.PHOTO_ID, Contacts.PHOTO_ID);
        sDataProjectionMap.put(Contacts.IN_VISIBLE_GROUP, Contacts.IN_VISIBLE_GROUP);
        sDataProjectionMap.put(GroupMembership.GROUP_SOURCE_ID, GroupMembership.GROUP_SOURCE_ID);

        HashMap<String, String> columns;
        columns = new HashMap<String, String>();
        columns.put(RawContacts._ID, RawContacts._ID);
        columns.put(RawContacts.CONTACT_ID, RawContacts.CONTACT_ID);
        columns.put(RawContacts.ACCOUNT_NAME, RawContacts.ACCOUNT_NAME);
        columns.put(RawContacts.ACCOUNT_TYPE, RawContacts.ACCOUNT_TYPE);
        columns.put(RawContacts.SOURCE_ID, RawContacts.SOURCE_ID);
        columns.put(RawContacts.VERSION, RawContacts.VERSION);
        columns.put(RawContacts.DIRTY, RawContacts.DIRTY);
        columns.put(RawContacts.DELETED, RawContacts.DELETED);
        columns.put(RawContacts.IS_RESTRICTED, RawContacts.IS_RESTRICTED);
        columns.put(RawContacts.SYNC1, RawContacts.SYNC1);
        columns.put(RawContacts.SYNC2, RawContacts.SYNC2);
        columns.put(RawContacts.SYNC3, RawContacts.SYNC3);
        columns.put(RawContacts.SYNC4, RawContacts.SYNC4);
        columns.put(Data.RES_PACKAGE, Data.RES_PACKAGE);
        columns.put(Data.MIMETYPE, Data.MIMETYPE);
        columns.put(Data.DATA1, Data.DATA1);
        columns.put(Data.DATA2, Data.DATA2);
        columns.put(Data.DATA3, Data.DATA3);
        columns.put(Data.DATA4, Data.DATA4);
        columns.put(Data.DATA5, Data.DATA5);
        columns.put(Data.DATA6, Data.DATA6);
        columns.put(Data.DATA7, Data.DATA7);
        columns.put(Data.DATA8, Data.DATA8);
        columns.put(Data.DATA9, Data.DATA9);
        columns.put(Data.DATA10, Data.DATA10);
        columns.put(Data.DATA11, Data.DATA11);
        columns.put(Data.DATA12, Data.DATA12);
        columns.put(Data.DATA13, Data.DATA13);
        columns.put(Data.DATA14, Data.DATA14);
        columns.put(Data.DATA15, Data.DATA15);
        columns.put(Data.SYNC1, Data.SYNC1);
        columns.put(Data.SYNC2, Data.SYNC2);
        columns.put(Data.SYNC3, Data.SYNC3);
        columns.put(Data.SYNC4, Data.SYNC4);
        columns.put(RawContacts.Entity.DATA_ID, RawContacts.Entity.DATA_ID);
        columns.put(Data.STARRED, Data.STARRED);
        columns.put(Data.DATA_VERSION, Data.DATA_VERSION);
        columns.put(Data.IS_PRIMARY, Data.IS_PRIMARY);
        columns.put(Data.IS_SUPER_PRIMARY, Data.IS_SUPER_PRIMARY);
        columns.put(GroupMembership.GROUP_SOURCE_ID, GroupMembership.GROUP_SOURCE_ID);
        sRawContactsEntityProjectionMap = columns;

        // Handle projections for Contacts-level statuses
        addProjection(sDataProjectionMap, Contacts.CONTACT_PRESENCE,
                Tables.AGGREGATED_PRESENCE + "." + StatusUpdates.PRESENCE);
        addProjection(sDataProjectionMap, Contacts.CONTACT_STATUS,
                ContactsStatusUpdatesColumns.CONCRETE_STATUS);
        addProjection(sDataProjectionMap, Contacts.CONTACT_STATUS_TIMESTAMP,
                ContactsStatusUpdatesColumns.CONCRETE_STATUS_TIMESTAMP);
        addProjection(sDataProjectionMap, Contacts.CONTACT_STATUS_RES_PACKAGE,
                ContactsStatusUpdatesColumns.CONCRETE_STATUS_RES_PACKAGE);
        addProjection(sDataProjectionMap, Contacts.CONTACT_STATUS_LABEL,
                ContactsStatusUpdatesColumns.CONCRETE_STATUS_LABEL);
        addProjection(sDataProjectionMap, Contacts.CONTACT_STATUS_ICON,
                ContactsStatusUpdatesColumns.CONCRETE_STATUS_ICON);

        // Handle projections for Data-level statuses
        addProjection(sDataProjectionMap, Data.PRESENCE,
                Tables.PRESENCE + "." + StatusUpdates.PRESENCE);
        addProjection(sDataProjectionMap, Data.STATUS,
                StatusUpdatesColumns.CONCRETE_STATUS);
        addProjection(sDataProjectionMap, Data.STATUS_TIMESTAMP,
                StatusUpdatesColumns.CONCRETE_STATUS_TIMESTAMP);
        addProjection(sDataProjectionMap, Data.STATUS_RES_PACKAGE,
                StatusUpdatesColumns.CONCRETE_STATUS_RES_PACKAGE);
        addProjection(sDataProjectionMap, Data.STATUS_LABEL,
                StatusUpdatesColumns.CONCRETE_STATUS_LABEL);
        addProjection(sDataProjectionMap, Data.STATUS_ICON,
                StatusUpdatesColumns.CONCRETE_STATUS_ICON);

        // Projection map for data grouped by contact (not raw contact) and some data field(s)
        sDistinctDataProjectionMap = new HashMap<String, String>();
        sDistinctDataProjectionMap.put(Data._ID,
                "MIN(" + Data._ID + ") AS " + Data._ID);
        sDistinctDataProjectionMap.put(Data.DATA_VERSION, Data.DATA_VERSION);
        sDistinctDataProjectionMap.put(Data.IS_PRIMARY, Data.IS_PRIMARY);
        sDistinctDataProjectionMap.put(Data.IS_SUPER_PRIMARY, Data.IS_SUPER_PRIMARY);
        sDistinctDataProjectionMap.put(Data.RES_PACKAGE, Data.RES_PACKAGE);
        sDistinctDataProjectionMap.put(Data.MIMETYPE, Data.MIMETYPE);
        sDistinctDataProjectionMap.put(Data.DATA1, Data.DATA1);
        sDistinctDataProjectionMap.put(Data.DATA2, Data.DATA2);
        sDistinctDataProjectionMap.put(Data.DATA3, Data.DATA3);
        sDistinctDataProjectionMap.put(Data.DATA4, Data.DATA4);
        sDistinctDataProjectionMap.put(Data.DATA5, Data.DATA5);
        sDistinctDataProjectionMap.put(Data.DATA6, Data.DATA6);
        sDistinctDataProjectionMap.put(Data.DATA7, Data.DATA7);
        sDistinctDataProjectionMap.put(Data.DATA8, Data.DATA8);
        sDistinctDataProjectionMap.put(Data.DATA9, Data.DATA9);
        sDistinctDataProjectionMap.put(Data.DATA10, Data.DATA10);
        sDistinctDataProjectionMap.put(Data.DATA11, Data.DATA11);
        sDistinctDataProjectionMap.put(Data.DATA12, Data.DATA12);
        sDistinctDataProjectionMap.put(Data.DATA13, Data.DATA13);
        sDistinctDataProjectionMap.put(Data.DATA14, Data.DATA14);
        sDistinctDataProjectionMap.put(Data.DATA15, Data.DATA15);
        sDistinctDataProjectionMap.put(Data.SYNC1, Data.SYNC1);
        sDistinctDataProjectionMap.put(Data.SYNC2, Data.SYNC2);
        sDistinctDataProjectionMap.put(Data.SYNC3, Data.SYNC3);
        sDistinctDataProjectionMap.put(Data.SYNC4, Data.SYNC4);
        sDistinctDataProjectionMap.put(RawContacts.CONTACT_ID, RawContacts.CONTACT_ID);
        sDistinctDataProjectionMap.put(Contacts.LOOKUP_KEY, Contacts.LOOKUP_KEY);
        sDistinctDataProjectionMap.put(Contacts.DISPLAY_NAME, Contacts.DISPLAY_NAME);
        sDistinctDataProjectionMap.put(Contacts.CUSTOM_RINGTONE, Contacts.CUSTOM_RINGTONE);
        sDistinctDataProjectionMap.put(Contacts.SEND_TO_VOICEMAIL, Contacts.SEND_TO_VOICEMAIL);
        sDistinctDataProjectionMap.put(Contacts.LAST_TIME_CONTACTED, Contacts.LAST_TIME_CONTACTED);
        sDistinctDataProjectionMap.put(Contacts.TIMES_CONTACTED, Contacts.TIMES_CONTACTED);
        sDistinctDataProjectionMap.put(Contacts.STARRED, Contacts.STARRED);
        sDistinctDataProjectionMap.put(Contacts.PHOTO_ID, Contacts.PHOTO_ID);
        sDistinctDataProjectionMap.put(Contacts.IN_VISIBLE_GROUP, Contacts.IN_VISIBLE_GROUP);
        sDistinctDataProjectionMap.put(GroupMembership.GROUP_SOURCE_ID,
                GroupMembership.GROUP_SOURCE_ID);

        // Handle projections for Contacts-level statuses
        addProjection(sDistinctDataProjectionMap, Contacts.CONTACT_PRESENCE,
                Tables.AGGREGATED_PRESENCE + "." + StatusUpdates.PRESENCE);
        addProjection(sDistinctDataProjectionMap, Contacts.CONTACT_STATUS,
                ContactsStatusUpdatesColumns.CONCRETE_STATUS);
        addProjection(sDistinctDataProjectionMap, Contacts.CONTACT_STATUS_TIMESTAMP,
                ContactsStatusUpdatesColumns.CONCRETE_STATUS_TIMESTAMP);
        addProjection(sDistinctDataProjectionMap, Contacts.CONTACT_STATUS_RES_PACKAGE,
                ContactsStatusUpdatesColumns.CONCRETE_STATUS_RES_PACKAGE);
        addProjection(sDistinctDataProjectionMap, Contacts.CONTACT_STATUS_LABEL,
                ContactsStatusUpdatesColumns.CONCRETE_STATUS_LABEL);
        addProjection(sDistinctDataProjectionMap, Contacts.CONTACT_STATUS_ICON,
                ContactsStatusUpdatesColumns.CONCRETE_STATUS_ICON);

        // Handle projections for Data-level statuses
        addProjection(sDistinctDataProjectionMap, Data.PRESENCE,
                Tables.PRESENCE + "." + StatusUpdates.PRESENCE);
        addProjection(sDistinctDataProjectionMap, Data.STATUS,
                StatusUpdatesColumns.CONCRETE_STATUS);
        addProjection(sDistinctDataProjectionMap, Data.STATUS_TIMESTAMP,
                StatusUpdatesColumns.CONCRETE_STATUS_TIMESTAMP);
        addProjection(sDistinctDataProjectionMap, Data.STATUS_RES_PACKAGE,
                StatusUpdatesColumns.CONCRETE_STATUS_RES_PACKAGE);
        addProjection(sDistinctDataProjectionMap, Data.STATUS_LABEL,
                StatusUpdatesColumns.CONCRETE_STATUS_LABEL);
        addProjection(sDistinctDataProjectionMap, Data.STATUS_ICON,
                StatusUpdatesColumns.CONCRETE_STATUS_ICON);

        sPhoneLookupProjectionMap = new HashMap<String, String>();
        sPhoneLookupProjectionMap.put(PhoneLookup._ID,
                ContactsColumns.CONCRETE_ID + " AS " + PhoneLookup._ID);
        sPhoneLookupProjectionMap.put(PhoneLookup.LOOKUP_KEY,
                Contacts.LOOKUP_KEY + " AS " + PhoneLookup.LOOKUP_KEY);
        sPhoneLookupProjectionMap.put(PhoneLookup.DISPLAY_NAME,
                ContactsColumns.CONCRETE_DISPLAY_NAME + " AS " + PhoneLookup.DISPLAY_NAME);
        sPhoneLookupProjectionMap.put(PhoneLookup.LAST_TIME_CONTACTED,
                ContactsColumns.CONCRETE_LAST_TIME_CONTACTED
                        + " AS " + PhoneLookup.LAST_TIME_CONTACTED);
        sPhoneLookupProjectionMap.put(PhoneLookup.TIMES_CONTACTED,
                ContactsColumns.CONCRETE_TIMES_CONTACTED + " AS " + PhoneLookup.TIMES_CONTACTED);
        sPhoneLookupProjectionMap.put(PhoneLookup.STARRED,
                ContactsColumns.CONCRETE_STARRED + " AS " + PhoneLookup.STARRED);
        sPhoneLookupProjectionMap.put(PhoneLookup.IN_VISIBLE_GROUP,
                Contacts.IN_VISIBLE_GROUP + " AS " + PhoneLookup.IN_VISIBLE_GROUP);
        sPhoneLookupProjectionMap.put(PhoneLookup.PHOTO_ID,
                Contacts.PHOTO_ID + " AS " + PhoneLookup.PHOTO_ID);
        sPhoneLookupProjectionMap.put(PhoneLookup.CUSTOM_RINGTONE,
                ContactsColumns.CONCRETE_CUSTOM_RINGTONE + " AS " + PhoneLookup.CUSTOM_RINGTONE);
        sPhoneLookupProjectionMap.put(PhoneLookup.HAS_PHONE_NUMBER,
                Contacts.HAS_PHONE_NUMBER + " AS " + PhoneLookup.HAS_PHONE_NUMBER);
        sPhoneLookupProjectionMap.put(PhoneLookup.SEND_TO_VOICEMAIL,
                ContactsColumns.CONCRETE_SEND_TO_VOICEMAIL
                        + " AS " + PhoneLookup.SEND_TO_VOICEMAIL);
        sPhoneLookupProjectionMap.put(PhoneLookup.NUMBER,
                Phone.NUMBER + " AS " + PhoneLookup.NUMBER);
        sPhoneLookupProjectionMap.put(PhoneLookup.TYPE,
                Phone.TYPE + " AS " + PhoneLookup.TYPE);
        sPhoneLookupProjectionMap.put(PhoneLookup.LABEL,
                Phone.LABEL + " AS " + PhoneLookup.LABEL);

        // Groups projection map
        columns = new HashMap<String, String>();
        columns.put(Groups._ID, Groups._ID);
        columns.put(Groups.ACCOUNT_NAME, Groups.ACCOUNT_NAME);
        columns.put(Groups.ACCOUNT_TYPE, Groups.ACCOUNT_TYPE);
        columns.put(Groups.SOURCE_ID, Groups.SOURCE_ID);
        columns.put(Groups.DIRTY, Groups.DIRTY);
        columns.put(Groups.VERSION, Groups.VERSION);
        columns.put(Groups.RES_PACKAGE, Groups.RES_PACKAGE);
        columns.put(Groups.TITLE, Groups.TITLE);
        columns.put(Groups.TITLE_RES, Groups.TITLE_RES);
        columns.put(Groups.GROUP_VISIBLE, Groups.GROUP_VISIBLE);
        columns.put(Groups.SYSTEM_ID, Groups.SYSTEM_ID);
        columns.put(Groups.DELETED, Groups.DELETED);
        columns.put(Groups.NOTES, Groups.NOTES);
        columns.put(Groups.SHOULD_SYNC, Groups.SHOULD_SYNC);
        columns.put(Groups.SYNC1, Groups.SYNC1);
        columns.put(Groups.SYNC2, Groups.SYNC2);
        columns.put(Groups.SYNC3, Groups.SYNC3);
        columns.put(Groups.SYNC4, Groups.SYNC4);
        sGroupsProjectionMap = columns;

        // RawContacts and groups projection map
        columns = new HashMap<String, String>();
        columns.putAll(sGroupsProjectionMap);
        columns.put(Groups.SUMMARY_COUNT, "(SELECT COUNT(DISTINCT " + ContactsColumns.CONCRETE_ID
                + ") FROM " + Tables.DATA_JOIN_MIMETYPES_RAW_CONTACTS_CONTACTS + " WHERE "
                + Clauses.MIMETYPE_IS_GROUP_MEMBERSHIP + " AND " + Clauses.BELONGS_TO_GROUP
                + ") AS " + Groups.SUMMARY_COUNT);
        columns.put(Groups.SUMMARY_WITH_PHONES, "(SELECT COUNT(DISTINCT "
                + ContactsColumns.CONCRETE_ID + ") FROM "
                + Tables.DATA_JOIN_MIMETYPES_RAW_CONTACTS_CONTACTS + " WHERE "
                + Clauses.MIMETYPE_IS_GROUP_MEMBERSHIP + " AND " + Clauses.BELONGS_TO_GROUP
                + " AND " + Contacts.HAS_PHONE_NUMBER + ") AS " + Groups.SUMMARY_WITH_PHONES);
        sGroupsSummaryProjectionMap = columns;

        // Aggregate exception projection map
        columns = new HashMap<String, String>();
        columns.put(AggregationExceptionColumns._ID, Tables.AGGREGATION_EXCEPTIONS + "._id AS _id");
        columns.put(AggregationExceptions.TYPE, AggregationExceptions.TYPE);
        columns.put(AggregationExceptions.RAW_CONTACT_ID1, AggregationExceptions.RAW_CONTACT_ID1);
        columns.put(AggregationExceptions.RAW_CONTACT_ID2, AggregationExceptions.RAW_CONTACT_ID2);
        sAggregationExceptionsProjectionMap = columns;

        // Settings projection map
        columns = new HashMap<String, String>();
        columns.put(Settings.ACCOUNT_NAME, Settings.ACCOUNT_NAME);
        columns.put(Settings.ACCOUNT_TYPE, Settings.ACCOUNT_TYPE);
        columns.put(Settings.UNGROUPED_VISIBLE, Settings.UNGROUPED_VISIBLE);
        columns.put(Settings.SHOULD_SYNC, Settings.SHOULD_SYNC);
        columns.put(Settings.ANY_UNSYNCED, "(CASE WHEN MIN(" + Settings.SHOULD_SYNC
                + ",(SELECT (CASE WHEN MIN(" + Groups.SHOULD_SYNC + ") IS NULL THEN 1 ELSE MIN("
                + Groups.SHOULD_SYNC + ") END) FROM " + Tables.GROUPS + " WHERE "
                + GroupsColumns.CONCRETE_ACCOUNT_NAME + "=" + SettingsColumns.CONCRETE_ACCOUNT_NAME
                + " AND " + GroupsColumns.CONCRETE_ACCOUNT_TYPE + "="
                + SettingsColumns.CONCRETE_ACCOUNT_TYPE + "))=0 THEN 1 ELSE 0 END) AS "
                + Settings.ANY_UNSYNCED);
        columns.put(Settings.UNGROUPED_COUNT, "(SELECT COUNT(*) FROM (SELECT 1 FROM "
                + Tables.SETTINGS_JOIN_RAW_CONTACTS_DATA_MIMETYPES_CONTACTS + " GROUP BY "
                + Clauses.GROUP_BY_ACCOUNT_CONTACT_ID + " HAVING " + Clauses.HAVING_NO_GROUPS
                + ")) AS " + Settings.UNGROUPED_COUNT);
        columns.put(Settings.UNGROUPED_WITH_PHONES, "(SELECT COUNT(*) FROM (SELECT 1 FROM "
                + Tables.SETTINGS_JOIN_RAW_CONTACTS_DATA_MIMETYPES_CONTACTS + " WHERE "
                + Contacts.HAS_PHONE_NUMBER + " GROUP BY " + Clauses.GROUP_BY_ACCOUNT_CONTACT_ID
                + " HAVING " + Clauses.HAVING_NO_GROUPS + ")) AS "
                + Settings.UNGROUPED_WITH_PHONES);
        sSettingsProjectionMap = columns;

        columns = new HashMap<String, String>();
        columns.put(PresenceColumns.RAW_CONTACT_ID, PresenceColumns.RAW_CONTACT_ID);
        columns.put(StatusUpdates.DATA_ID,
                DataColumns.CONCRETE_ID + " AS " + StatusUpdates.DATA_ID);
        columns.put(StatusUpdates.IM_ACCOUNT, StatusUpdates.IM_ACCOUNT);
        columns.put(StatusUpdates.IM_HANDLE, StatusUpdates.IM_HANDLE);
        columns.put(StatusUpdates.PROTOCOL, StatusUpdates.PROTOCOL);
        // We cannot allow a null in the custom protocol field, because SQLite3 does not
        // properly enforce uniqueness of null values
        columns.put(StatusUpdates.CUSTOM_PROTOCOL, "(CASE WHEN " + StatusUpdates.CUSTOM_PROTOCOL
                + "='' THEN NULL ELSE " + StatusUpdates.CUSTOM_PROTOCOL + " END) AS "
                + StatusUpdates.CUSTOM_PROTOCOL);
        columns.put(StatusUpdates.PRESENCE, StatusUpdates.PRESENCE);
        columns.put(StatusUpdates.STATUS, StatusUpdates.STATUS);
        columns.put(StatusUpdates.STATUS_TIMESTAMP, StatusUpdates.STATUS_TIMESTAMP);
        columns.put(StatusUpdates.STATUS_RES_PACKAGE, StatusUpdates.STATUS_RES_PACKAGE);
        columns.put(StatusUpdates.STATUS_ICON, StatusUpdates.STATUS_ICON);
        columns.put(StatusUpdates.STATUS_LABEL, StatusUpdates.STATUS_LABEL);
        sStatusUpdatesProjectionMap = columns;

        // Live folder projection
        sLiveFoldersProjectionMap = new HashMap<String, String>();
        sLiveFoldersProjectionMap.put(LiveFolders._ID,
                Contacts._ID + " AS " + LiveFolders._ID);
        sLiveFoldersProjectionMap.put(LiveFolders.NAME,
                Contacts.DISPLAY_NAME + " AS " + LiveFolders.NAME);

        // TODO: Put contact photo back when we have a way to display a default icon
        // for contacts without a photo
        // sLiveFoldersProjectionMap.put(LiveFolders.ICON_BITMAP,
        //      Photos.DATA + " AS " + LiveFolders.ICON_BITMAP);
    }

    private static void addProjection(HashMap<String, String> map, String toField, String fromField) {
        map.put(toField, fromField + " AS " + toField);
    }

    /**
     * Handles inserts and update for a specific Data type.
     */
    private abstract class DataRowHandler {

        protected final String mMimetype;
        protected long mMimetypeId;

        public DataRowHandler(String mimetype) {
            mMimetype = mimetype;

            // To ensure the data column position. This is dead code if properly configured.
            if (StructuredName.DISPLAY_NAME != Data.DATA1 || Nickname.NAME != Data.DATA1
                    || Organization.COMPANY != Data.DATA1 || Phone.NUMBER != Data.DATA1
                    || Email.DATA != Data.DATA1) {
                throw new AssertionError("Some of ContactsContract.CommonDataKinds class primary"
                        + " data is not in DATA1 column");
            }
        }

        protected long getMimeTypeId() {
            if (mMimetypeId == 0) {
                mMimetypeId = mDbHelper.getMimeTypeId(mMimetype);
            }
            return mMimetypeId;
        }

        /**
         * Inserts a row into the {@link Data} table.
         */
        public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) {
            final long dataId = db.insert(Tables.DATA, null, values);

            Integer primary = values.getAsInteger(Data.IS_PRIMARY);
            if (primary != null && primary != 0) {
                setIsPrimary(rawContactId, dataId, getMimeTypeId());
            }

            return dataId;
        }

        /**
         * Validates data and updates a {@link Data} row using the cursor, which contains
         * the current data.
         */
        public void update(SQLiteDatabase db, ContentValues values, Cursor c,
                boolean callerIsSyncAdapter) {
            long dataId = c.getLong(DataUpdateQuery._ID);
            long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID);

            if (values.containsKey(Data.IS_SUPER_PRIMARY)) {
                long mimeTypeId = getMimeTypeId();
                setIsSuperPrimary(rawContactId, dataId, mimeTypeId);
                setIsPrimary(rawContactId, dataId, mimeTypeId);

                // Now that we've taken care of setting these, remove them from "values".
                values.remove(Data.IS_SUPER_PRIMARY);
                values.remove(Data.IS_PRIMARY);
            } else if (values.containsKey(Data.IS_PRIMARY)) {
                setIsPrimary(rawContactId, dataId, getMimeTypeId());

                // Now that we've taken care of setting this, remove it from "values".
                values.remove(Data.IS_PRIMARY);
            }

            if (values.size() > 0) {
                mDb.update(Tables.DATA, values, Data._ID + " = " + dataId, null);
            }

            if (!callerIsSyncAdapter) {
                setRawContactDirty(rawContactId);
            }
        }

        public int delete(SQLiteDatabase db, Cursor c) {
            long dataId = c.getLong(DataDeleteQuery._ID);
            long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID);
            boolean primary = c.getInt(DataDeleteQuery.IS_PRIMARY) != 0;
            int count = db.delete(Tables.DATA, Data._ID + "=" + dataId, null);
            db.delete(Tables.PRESENCE, PresenceColumns.RAW_CONTACT_ID + "=" + rawContactId, null);
            if (count != 0 && primary) {
                fixPrimary(db, rawContactId);
            }
            return count;
        }

        private void fixPrimary(SQLiteDatabase db, long rawContactId) {
            long newPrimaryId = findNewPrimaryDataId(db, rawContactId);
            if (newPrimaryId != -1) {
                setIsPrimary(rawContactId, newPrimaryId, getMimeTypeId());
            }
        }

        protected long findNewPrimaryDataId(SQLiteDatabase db, long rawContactId) {
            long primaryId = -1;
            int primaryType = -1;
            Cursor c = queryData(db, rawContactId);
            try {
                while (c.moveToNext()) {
                    long dataId = c.getLong(DataDeleteQuery._ID);
                    int type = c.getInt(DataDeleteQuery.DATA1);
                    if (primaryType == -1 || getTypeRank(type) < getTypeRank(primaryType)) {
                        primaryId = dataId;
                        primaryType = type;
                    }
                }
            } finally {
                c.close();
            }
            return primaryId;
        }

        /**
         * Returns the rank of a specific record type to be used in determining the primary
         * row. Lower number represents higher priority.
         */
        protected int getTypeRank(int type) {
            return 0;
        }

        protected Cursor queryData(SQLiteDatabase db, long rawContactId) {
            return db.query(DataDeleteQuery.TABLE, DataDeleteQuery.CONCRETE_COLUMNS,
                    Data.RAW_CONTACT_ID + "=" + rawContactId +
                    " AND " + MimetypesColumns.MIMETYPE + "='" + mMimetype + "'",
                    null, null, null, null);
        }

        protected void fixRawContactDisplayName(SQLiteDatabase db, long rawContactId) {
            String bestDisplayName = null;
            int bestDisplayNameSource = DisplayNameSources.UNDEFINED;

            Cursor c = db.query(DisplayNameQuery.TABLE, DisplayNameQuery.COLUMNS,
                    Data.RAW_CONTACT_ID + "=" + rawContactId, null, null, null, null);
            try {
                while (c.moveToNext()) {
                    String mimeType = c.getString(DisplayNameQuery.MIMETYPE);

                    // Display name is at DATA1 in all type.  This is ensured in the constructor.
                    String name = c.getString(DisplayNameQuery.DATA);
                    if (TextUtils.isEmpty(name)
                            && Organization.CONTENT_ITEM_TYPE.equals(mimeType)) {
                        name = c.getString(DisplayNameQuery.TITLE);
                    }
                    boolean primary = StructuredName.CONTENT_ITEM_TYPE.equals(mimeType)
                        || (c.getInt(DisplayNameQuery.IS_PRIMARY) != 0);

                    if (name != null) {
                        Integer source = sDisplayNameSources.get(mimeType);
                        if (source != null
                                && (source > bestDisplayNameSource
                                        || (source == bestDisplayNameSource && primary))) {
                            bestDisplayNameSource = source;
                            bestDisplayName = name;
                        }
                    }
                }

            } finally {
                c.close();
            }

            setDisplayName(rawContactId, bestDisplayName, bestDisplayNameSource);
            if (!isNewRawContact(rawContactId)) {
                mContactAggregator.updateDisplayName(db, rawContactId);
            }
        }

        public boolean isAggregationRequired() {
            return true;
        }

        /**
         * Return set of values, using current values at given {@link Data#_ID}
         * as baseline, but augmented with any updates.
         */
        public ContentValues getAugmentedValues(SQLiteDatabase db, long dataId,
                ContentValues update) {
            final ContentValues values = new ContentValues();
            final Cursor cursor = db.query(Tables.DATA, null, Data._ID + "=" + dataId,
                    null, null, null, null);
            try {
                if (cursor.moveToFirst()) {
                    for (int i = 0; i < cursor.getColumnCount(); i++) {
                        final String key = cursor.getColumnName(i);
                        values.put(key, cursor.getString(i));
                    }
                }
            } finally {
                cursor.close();
            }
            values.putAll(update);
            return values;
        }
    }

    public class CustomDataRowHandler extends DataRowHandler {

        public CustomDataRowHandler(String mimetype) {
            super(mimetype);
        }
    }

    public class StructuredNameRowHandler extends DataRowHandler {
        private final NameSplitter mSplitter;

        public StructuredNameRowHandler(NameSplitter splitter) {
            super(StructuredName.CONTENT_ITEM_TYPE);
            mSplitter = splitter;
        }

        @Override
        public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) {
            fixStructuredNameComponents(values, values);

            long dataId = super.insert(db, rawContactId, values);

            String name = values.getAsString(StructuredName.DISPLAY_NAME);
            insertNameLookupForStructuredName(rawContactId, dataId, name);
            fixRawContactDisplayName(db, rawContactId);
            return dataId;
        }

        @Override
        public void update(SQLiteDatabase db, ContentValues values, Cursor c,
                boolean callerIsSyncAdapter) {
            final long dataId = c.getLong(DataUpdateQuery._ID);
            final long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID);

            final ContentValues augmented = getAugmentedValues(db, dataId, values);
            fixStructuredNameComponents(augmented, values);

            super.update(db, values, c, callerIsSyncAdapter);

            if (values.containsKey(StructuredName.DISPLAY_NAME)) {
                String name = values.getAsString(StructuredName.DISPLAY_NAME);
                deleteNameLookup(dataId);
                insertNameLookupForStructuredName(rawContactId, dataId, name);
            }
            fixRawContactDisplayName(db, rawContactId);
        }

        @Override
        public int delete(SQLiteDatabase db, Cursor c) {
            long dataId = c.getLong(DataDeleteQuery._ID);
            long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID);

            int count = super.delete(db, c);

            deleteNameLookup(dataId);
            fixRawContactDisplayName(db, rawContactId);
            return count;
        }

        /**
         * Specific list of structured fields.
         */
        private final String[] STRUCTURED_FIELDS = new String[] {
                StructuredName.PREFIX, StructuredName.GIVEN_NAME, StructuredName.MIDDLE_NAME,
                StructuredName.FAMILY_NAME, StructuredName.SUFFIX
        };

        /**
         * Parses the supplied display name, but only if the incoming values do
         * not already contain structured name parts. Also, if the display name
         * is not provided, generate one by concatenating first name and last
         * name.
         */
        private void fixStructuredNameComponents(ContentValues augmented, ContentValues update) {
            final String unstruct = update.getAsString(StructuredName.DISPLAY_NAME);

            final boolean touchedUnstruct = !TextUtils.isEmpty(unstruct);
            final boolean touchedStruct = !areAllEmpty(update, STRUCTURED_FIELDS);

            if (touchedUnstruct && !touchedStruct) {
                NameSplitter.Name name = new NameSplitter.Name();
                mSplitter.split(name, unstruct);
                name.toValues(update);
            } else if (!touchedUnstruct
                    && (touchedStruct || areAnySpecified(update, STRUCTURED_FIELDS))) {
                // We need to update the display name when any structured components
                // are specified, even when they are null, which is why we are checking
                // areAnySpecified.  The touchedStruct in the condition is an optimization:
                // if there are non-null values, we know for a fact that some values are present.
                NameSplitter.Name name = new NameSplitter.Name();
                name.fromValues(augmented);
                final String joined = mSplitter.join(name);
                update.put(StructuredName.DISPLAY_NAME, joined);
            }
        }
    }

    public class StructuredPostalRowHandler extends DataRowHandler {
        private PostalSplitter mSplitter;

        public StructuredPostalRowHandler(PostalSplitter splitter) {
            super(StructuredPostal.CONTENT_ITEM_TYPE);
            mSplitter = splitter;
        }

        @Override
        public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) {
            fixStructuredPostalComponents(values, values);
            return super.insert(db, rawContactId, values);
        }

        @Override
        public void update(SQLiteDatabase db, ContentValues values, Cursor c,
                boolean callerIsSyncAdapter) {
            final long dataId = c.getLong(DataUpdateQuery._ID);
            final ContentValues augmented = getAugmentedValues(db, dataId, values);
            fixStructuredPostalComponents(augmented, values);
            super.update(db, values, c, callerIsSyncAdapter);
        }

        /**
         * Specific list of structured fields.
         */
        private final String[] STRUCTURED_FIELDS = new String[] {
                StructuredPostal.STREET, StructuredPostal.POBOX, StructuredPostal.NEIGHBORHOOD,
                StructuredPostal.CITY, StructuredPostal.REGION, StructuredPostal.POSTCODE,
                StructuredPostal.COUNTRY,
        };

        /**
         * Prepares the given {@link StructuredPostal} row, building
         * {@link StructuredPostal#FORMATTED_ADDRESS} to match the structured
         * values when missing. When structured components are missing, the
         * unstructured value is assigned to {@link StructuredPostal#STREET}.
         */
        private void fixStructuredPostalComponents(ContentValues augmented, ContentValues update) {
            final String unstruct = update.getAsString(StructuredPostal.FORMATTED_ADDRESS);

            final boolean touchedUnstruct = !TextUtils.isEmpty(unstruct);
            final boolean touchedStruct = !areAllEmpty(update, STRUCTURED_FIELDS);

            final PostalSplitter.Postal postal = new PostalSplitter.Postal();

            if (touchedUnstruct && !touchedStruct) {
                mSplitter.split(postal, unstruct);
                postal.toValues(update);
            } else if (!touchedUnstruct
                    && (touchedStruct || areAnySpecified(update, STRUCTURED_FIELDS))) {
                // See comment in
                postal.fromValues(augmented);
                final String joined = mSplitter.join(postal);
                update.put(StructuredPostal.FORMATTED_ADDRESS, joined);
            }
        }
    }

    public class CommonDataRowHandler extends DataRowHandler {

        private final String mTypeColumn;
        private final String mLabelColumn;

        public CommonDataRowHandler(String mimetype, String typeColumn, String labelColumn) {
            super(mimetype);
            mTypeColumn = typeColumn;
            mLabelColumn = labelColumn;
        }

        @Override
        public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) {
            enforceTypeAndLabel(values, values);
            return super.insert(db, rawContactId, values);
        }

        @Override
        public void update(SQLiteDatabase db, ContentValues values, Cursor c,
                boolean callerIsSyncAdapter) {
            final long dataId = c.getLong(DataUpdateQuery._ID);
            final ContentValues augmented = getAugmentedValues(db, dataId, values);
            enforceTypeAndLabel(augmented, values);
            super.update(db, values, c, callerIsSyncAdapter);
        }

        /**
         * If the given {@link ContentValues} defines {@link #mTypeColumn},
         * enforce that {@link #mLabelColumn} only appears when type is
         * {@link BaseTypes#TYPE_CUSTOM}. Exception is thrown otherwise.
         */
        private void enforceTypeAndLabel(ContentValues augmented, ContentValues update) {
            final boolean hasType = !TextUtils.isEmpty(augmented.getAsString(mTypeColumn));
            final boolean hasLabel = !TextUtils.isEmpty(augmented.getAsString(mLabelColumn));

            if (hasLabel && !hasType) {
                // When label exists, assert that some type is defined
                throw new IllegalArgumentException(mTypeColumn + " must be specified when "
                        + mLabelColumn + " is defined.");
            }
        }
    }

    public class OrganizationDataRowHandler extends CommonDataRowHandler {

        public OrganizationDataRowHandler() {
            super(Organization.CONTENT_ITEM_TYPE, Organization.TYPE, Organization.LABEL);
        }

        @Override
        public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) {
            String company = values.getAsString(Organization.COMPANY);
            String title = values.getAsString(Organization.TITLE);

            long dataId = super.insert(db, rawContactId, values);

            fixRawContactDisplayName(db, rawContactId);
            insertNameLookupForOrganization(rawContactId, dataId, company, title);
            return dataId;
        }

        @Override
        public void update(SQLiteDatabase db, ContentValues values, Cursor c,
                boolean callerIsSyncAdapter) {
            String company = values.getAsString(Organization.COMPANY);
            String title = values.getAsString(Organization.TITLE);
            long dataId = c.getLong(DataUpdateQuery._ID);
            long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID);

            super.update(db, values, c, callerIsSyncAdapter);

            fixRawContactDisplayName(db, rawContactId);
            deleteNameLookup(dataId);
            insertNameLookupForOrganization(rawContactId, dataId, company, title);
        }

        @Override
        public int delete(SQLiteDatabase db, Cursor c) {
            long dataId = c.getLong(DataUpdateQuery._ID);
            long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID);

            int count = super.delete(db, c);
            fixRawContactDisplayName(db, rawContactId);
            deleteNameLookup(dataId);
            return count;
        }

        @Override
        protected int getTypeRank(int type) {
            switch (type) {
                case Organization.TYPE_WORK: return 0;
                case Organization.TYPE_CUSTOM: return 1;
                case Organization.TYPE_OTHER: return 2;
                default: return 1000;
            }
        }

        @Override
        public boolean isAggregationRequired() {
            return false;
        }
    }

    public class EmailDataRowHandler extends CommonDataRowHandler {

        public EmailDataRowHandler() {
            super(Email.CONTENT_ITEM_TYPE, Email.TYPE, Email.LABEL);
        }

        @Override
        public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) {
            String address = values.getAsString(Email.DATA);

            long dataId = super.insert(db, rawContactId, values);

            fixRawContactDisplayName(db, rawContactId);
            insertNameLookupForEmail(rawContactId, dataId, address);
            return dataId;
        }

        @Override
        public void update(SQLiteDatabase db, ContentValues values, Cursor c,
                boolean callerIsSyncAdapter) {
            long dataId = c.getLong(DataUpdateQuery._ID);
            long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID);
            String address = values.getAsString(Email.DATA);

            super.update(db, values, c, callerIsSyncAdapter);

            deleteNameLookup(dataId);
            insertNameLookupForEmail(rawContactId, dataId, address);
            fixRawContactDisplayName(db, rawContactId);
        }

        @Override
        public int delete(SQLiteDatabase db, Cursor c) {
            long dataId = c.getLong(DataDeleteQuery._ID);
            long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID);

            int count = super.delete(db, c);

            deleteNameLookup(dataId);
            fixRawContactDisplayName(db, rawContactId);
            return count;
        }

        @Override
        protected int getTypeRank(int type) {
            switch (type) {
                case Email.TYPE_HOME: return 0;
                case Email.TYPE_WORK: return 1;
                case Email.TYPE_CUSTOM: return 2;
                case Email.TYPE_OTHER: return 3;
                default: return 1000;
            }
        }
    }

    public class NicknameDataRowHandler extends CommonDataRowHandler {

        public NicknameDataRowHandler() {
            super(Nickname.CONTENT_ITEM_TYPE, Nickname.TYPE, Nickname.LABEL);
        }

        @Override
        public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) {
            String nickname = values.getAsString(Nickname.NAME);

            long dataId = super.insert(db, rawContactId, values);

            fixRawContactDisplayName(db, rawContactId);
            insertNameLookupForNickname(rawContactId, dataId, nickname);
            return dataId;
        }

        @Override
        public void update(SQLiteDatabase db, ContentValues values, Cursor c,
                boolean callerIsSyncAdapter) {
            long dataId = c.getLong(DataUpdateQuery._ID);
            long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID);
            String nickname = values.getAsString(Nickname.NAME);

            super.update(db, values, c, callerIsSyncAdapter);

            deleteNameLookup(dataId);
            insertNameLookupForNickname(rawContactId, dataId, nickname);
            fixRawContactDisplayName(db, rawContactId);
        }

        @Override
        public int delete(SQLiteDatabase db, Cursor c) {
            long dataId = c.getLong(DataDeleteQuery._ID);
            long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID);

            int count = super.delete(db, c);

            deleteNameLookup(dataId);
            fixRawContactDisplayName(db, rawContactId);
            return count;
        }
    }

    public class PhoneDataRowHandler extends CommonDataRowHandler {

        public PhoneDataRowHandler() {
            super(Phone.CONTENT_ITEM_TYPE, Phone.TYPE, Phone.LABEL);
        }

        @Override
        public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) {
            long dataId;
            if (values.containsKey(Phone.NUMBER)) {
                String number = values.getAsString(Phone.NUMBER);
                String normalizedNumber = computeNormalizedNumber(number, values);

                dataId = super.insert(db, rawContactId, values);

                updatePhoneLookup(db, rawContactId, dataId, number, normalizedNumber);
                mContactAggregator.updateHasPhoneNumber(db, rawContactId);
                fixRawContactDisplayName(db, rawContactId);
            } else {
                dataId = super.insert(db, rawContactId, values);
            }
            return dataId;
        }

        @Override
        public void update(SQLiteDatabase db, ContentValues values, Cursor c,
                boolean callerIsSyncAdapter) {
            long dataId = c.getLong(DataUpdateQuery._ID);
            long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID);
            if (values.containsKey(Phone.NUMBER)) {
                String number = values.getAsString(Phone.NUMBER);
                String normalizedNumber = computeNormalizedNumber(number, values);

                super.update(db, values, c, callerIsSyncAdapter);

                updatePhoneLookup(db, rawContactId, dataId, number, normalizedNumber);
                mContactAggregator.updateHasPhoneNumber(db, rawContactId);
                fixRawContactDisplayName(db, rawContactId);
            } else {
                super.update(db, values, c, callerIsSyncAdapter);
            }
        }

        @Override
        public int delete(SQLiteDatabase db, Cursor c) {
            long dataId = c.getLong(DataDeleteQuery._ID);
            long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID);

            int count = super.delete(db, c);

            updatePhoneLookup(db, rawContactId, dataId, null, null);
            mContactAggregator.updateHasPhoneNumber(db, rawContactId);
            fixRawContactDisplayName(db, rawContactId);
            return count;
        }

        private String computeNormalizedNumber(String number, ContentValues values) {
            String normalizedNumber = null;
            if (number != null) {
                normalizedNumber = PhoneNumberUtils.getStrippedReversed(number);
            }
            values.put(PhoneColumns.NORMALIZED_NUMBER, normalizedNumber);
            return normalizedNumber;
        }

        private void updatePhoneLookup(SQLiteDatabase db, long rawContactId, long dataId,
                String number, String normalizedNumber) {
            if (number != null) {
                ContentValues phoneValues = new ContentValues();
                phoneValues.put(PhoneLookupColumns.RAW_CONTACT_ID, rawContactId);
                phoneValues.put(PhoneLookupColumns.DATA_ID, dataId);
                phoneValues.put(PhoneLookupColumns.NORMALIZED_NUMBER, normalizedNumber);
                db.replace(Tables.PHONE_LOOKUP, null, phoneValues);
            } else {
                db.delete(Tables.PHONE_LOOKUP, PhoneLookupColumns.DATA_ID + "=" + dataId, null);
            }
        }

        @Override
        protected int getTypeRank(int type) {
            switch (type) {
                case Phone.TYPE_MOBILE: return 0;
                case Phone.TYPE_WORK: return 1;
                case Phone.TYPE_HOME: return 2;
                case Phone.TYPE_PAGER: return 3;
                case Phone.TYPE_CUSTOM: return 4;
                case Phone.TYPE_OTHER: return 5;
                case Phone.TYPE_FAX_WORK: return 6;
                case Phone.TYPE_FAX_HOME: return 7;
                default: return 1000;
            }
        }
    }

    public class GroupMembershipRowHandler extends DataRowHandler {

        public GroupMembershipRowHandler() {
            super(GroupMembership.CONTENT_ITEM_TYPE);
        }

        @Override
        public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) {
            resolveGroupSourceIdInValues(rawContactId, db, values, true);
            long dataId = super.insert(db, rawContactId, values);
            updateVisibility(rawContactId);
            return dataId;
        }

        @Override
        public void update(SQLiteDatabase db, ContentValues values, Cursor c,
                boolean callerIsSyncAdapter) {
            long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID);
            resolveGroupSourceIdInValues(rawContactId, db, values, false);
            super.update(db, values, c, callerIsSyncAdapter);
            updateVisibility(rawContactId);
        }

        @Override
        public int delete(SQLiteDatabase db, Cursor c) {
            long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID);
            int count = super.delete(db, c);
            updateVisibility(rawContactId);
            return count;
        }

        private void updateVisibility(long rawContactId) {
            long contactId = mDbHelper.getContactId(rawContactId);
            if (contactId != 0) {
                mDbHelper.updateContactVisible(contactId);
            }
        }

        private void resolveGroupSourceIdInValues(long rawContactId, SQLiteDatabase db,
                ContentValues values, boolean isInsert) {
            boolean containsGroupSourceId = values.containsKey(GroupMembership.GROUP_SOURCE_ID);
            boolean containsGroupId = values.containsKey(GroupMembership.GROUP_ROW_ID);
            if (containsGroupSourceId && containsGroupId) {
                throw new IllegalArgumentException(
                        "you are not allowed to set both the GroupMembership.GROUP_SOURCE_ID "
                                + "and GroupMembership.GROUP_ROW_ID");
            }

            if (!containsGroupSourceId && !containsGroupId) {
                if (isInsert) {
                    throw new IllegalArgumentException(
                            "you must set exactly one of GroupMembership.GROUP_SOURCE_ID "
                                    + "and GroupMembership.GROUP_ROW_ID");
                } else {
                    return;
                }
            }

            if (containsGroupSourceId) {
                final String sourceId = values.getAsString(GroupMembership.GROUP_SOURCE_ID);
                final long groupId = getOrMakeGroup(db, rawContactId, sourceId);
                values.remove(GroupMembership.GROUP_SOURCE_ID);
                values.put(GroupMembership.GROUP_ROW_ID, groupId);
            }
        }

        @Override
        public boolean isAggregationRequired() {
            return false;
        }
    }

    public class PhotoDataRowHandler extends DataRowHandler {

        public PhotoDataRowHandler() {
            super(Photo.CONTENT_ITEM_TYPE);
        }

        @Override
        public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) {
            long dataId = super.insert(db, rawContactId, values);
            if (!isNewRawContact(rawContactId)) {
                mContactAggregator.updatePhotoId(db, rawContactId);
            }
            return dataId;
        }

        @Override
        public void update(SQLiteDatabase db, ContentValues values, Cursor c,
                boolean callerIsSyncAdapter) {
            long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID);
            super.update(db, values, c, callerIsSyncAdapter);
            mContactAggregator.updatePhotoId(db, rawContactId);
        }

        @Override
        public int delete(SQLiteDatabase db, Cursor c) {
            long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID);
            int count = super.delete(db, c);
            mContactAggregator.updatePhotoId(db, rawContactId);
            return count;
        }

        @Override
        public boolean isAggregationRequired() {
            return false;
        }
    }


    private HashMap<String, DataRowHandler> mDataRowHandlers;
    private final ContactAggregationScheduler mAggregationScheduler;
    private ContactsDatabaseHelper mDbHelper;

    private NameSplitter mNameSplitter;
    private NameLookupBuilder mNameLookupBuilder;
    private HashMap<String, SoftReference<String[]>> mNicknameClusterCache =
            new HashMap<String, SoftReference<String[]>>();
    private PostalSplitter mPostalSplitter;

    private ContactAggregator mContactAggregator;
    private LegacyApiSupport mLegacyApiSupport;
    private GlobalSearchSupport mGlobalSearchSupport;

    private ContentValues mValues = new ContentValues();

    private volatile CountDownLatch mAccessLatch;
    private boolean mImportMode;

    private HashSet<Long> mInsertedRawContacts = Sets.newHashSet();
    private HashSet<Long> mUpdatedRawContacts = Sets.newHashSet();
    private HashMap<Long, Object> mUpdatedSyncStates = Maps.newHashMap();

    private boolean mVisibleTouched = false;

    private boolean mSyncToNetwork;

    public ContactsProvider2() {
        this(new ContactAggregationScheduler());
    }

    /**
     * Constructor for testing.
     */
    /* package */ ContactsProvider2(ContactAggregationScheduler scheduler) {
        mAggregationScheduler = scheduler;
    }

    @Override
    public boolean onCreate() {
        super.onCreate();
        try {
            return initialize();
        } catch (RuntimeException e) {
            Log.e(TAG, "Cannot start provider", e);
            return false;
        }
    }

    private boolean initialize() {
        final Context context = getContext();
        mDbHelper = (ContactsDatabaseHelper)getDatabaseHelper();
        mGlobalSearchSupport = new GlobalSearchSupport(this);
        mLegacyApiSupport = new LegacyApiSupport(context, mDbHelper, this, mGlobalSearchSupport);
        mContactAggregator = new ContactAggregator(this, mDbHelper, mAggregationScheduler);
        mContactAggregator.setEnabled(SystemProperties.getBoolean(AGGREGATE_CONTACTS, true));

        final SQLiteDatabase db = mDbHelper.getReadableDatabase();

        mSetPrimaryStatement = db.compileStatement(
                "UPDATE " + Tables.DATA +
                " SET " + Data.IS_PRIMARY + "=(_id=?)" +
                " WHERE " + DataColumns.MIMETYPE_ID + "=?" +
                "   AND " + Data.RAW_CONTACT_ID + "=?");

        mSetSuperPrimaryStatement = db.compileStatement(
                "UPDATE " + Tables.DATA +
                " SET " + Data.IS_SUPER_PRIMARY + "=(" + Data._ID + "=?)" +
                " WHERE " + DataColumns.MIMETYPE_ID + "=?" +
                "   AND " + Data.RAW_CONTACT_ID + " IN (" +
                        "SELECT " + RawContacts._ID +
                        " FROM " + Tables.RAW_CONTACTS +
                        " WHERE " + RawContacts.CONTACT_ID + " =(" +
                                "SELECT " + RawContacts.CONTACT_ID +
                                " FROM " + Tables.RAW_CONTACTS +
                                " WHERE " + RawContacts._ID + "=?))");

        mContactsLastTimeContactedUpdate = db.compileStatement(
                "UPDATE " + Tables.CONTACTS +
                " SET " + Contacts.LAST_TIME_CONTACTED + "=? " +
                "WHERE " + Contacts._ID + "=?");

        mRawContactDisplayNameUpdate = db.compileStatement(
                "UPDATE " + Tables.RAW_CONTACTS +
                " SET " + RawContactsColumns.DISPLAY_NAME + "=?,"
                        + RawContactsColumns.DISPLAY_NAME_SOURCE + "=?" +
                " WHERE " + RawContacts._ID + "=?");

        mRawContactDirtyUpdate = db.compileStatement("UPDATE " + Tables.RAW_CONTACTS + " SET "
                + RawContacts.DIRTY + "=1 WHERE " + RawContacts._ID + "=?");

        mLastStatusUpdate = db.compileStatement(
                "UPDATE " + Tables.CONTACTS
                + " SET " + ContactsColumns.LAST_STATUS_UPDATE_ID + "=" +
                        "(SELECT " + DataColumns.CONCRETE_ID +
                        " FROM " + Tables.STATUS_UPDATES +
                        " JOIN " + Tables.DATA +
                        "   ON (" + StatusUpdatesColumns.DATA_ID + "="
                                + DataColumns.CONCRETE_ID + ")" +
                        " JOIN " + Tables.RAW_CONTACTS +
                        "   ON (" + DataColumns.CONCRETE_RAW_CONTACT_ID + "="
                                + RawContactsColumns.CONCRETE_ID + ")" +
                        " WHERE " + RawContacts.CONTACT_ID + "=?" +
                        " ORDER BY " + StatusUpdates.STATUS_TIMESTAMP + " DESC,"
                                + StatusUpdates.STATUS +
                        " LIMIT 1)"
                + " WHERE " + ContactsColumns.CONCRETE_ID + "=?");

        final Locale locale = Locale.getDefault();
        mNameSplitter = new NameSplitter(
                context.getString(com.android.internal.R.string.common_name_prefixes),
                context.getString(com.android.internal.R.string.common_last_name_prefixes),
                context.getString(com.android.internal.R.string.common_name_suffixes),
                context.getString(com.android.internal.R.string.common_name_conjunctions),
                locale);
        mNameLookupBuilder = new StructuredNameLookupBuilder(mNameSplitter);
        mPostalSplitter = new PostalSplitter(locale);

        mNameLookupInsert = db.compileStatement("INSERT OR IGNORE INTO " + Tables.NAME_LOOKUP + "("
                + NameLookupColumns.RAW_CONTACT_ID + "," + NameLookupColumns.DATA_ID + ","
                + NameLookupColumns.NAME_TYPE + "," + NameLookupColumns.NORMALIZED_NAME
                + ") VALUES (?,?,?,?)");
        mNameLookupDelete = db.compileStatement("DELETE FROM " + Tables.NAME_LOOKUP + " WHERE "
                + NameLookupColumns.DATA_ID + "=?");

        mStatusUpdateInsert = db.compileStatement(
                "INSERT INTO " + Tables.STATUS_UPDATES + "("
                        + StatusUpdatesColumns.DATA_ID + ", "
                        + StatusUpdates.STATUS + ","
                        + StatusUpdates.STATUS_RES_PACKAGE + ","
                        + StatusUpdates.STATUS_ICON + ","
                        + StatusUpdates.STATUS_LABEL + ")" +
                " VALUES (?,?,?,?,?)");

        mStatusUpdateReplace = db.compileStatement(
                "INSERT OR REPLACE INTO " + Tables.STATUS_UPDATES + "("
                        + StatusUpdatesColumns.DATA_ID + ", "
                        + StatusUpdates.STATUS_TIMESTAMP + ","
                        + StatusUpdates.STATUS + ","
                        + StatusUpdates.STATUS_RES_PACKAGE + ","
                        + StatusUpdates.STATUS_ICON + ","
                        + StatusUpdates.STATUS_LABEL + ")" +
                " VALUES (?,?,?,?,?,?)");

        mStatusUpdateAutoTimestamp = db.compileStatement(
                "UPDATE " + Tables.STATUS_UPDATES +
                " SET " + StatusUpdates.STATUS_TIMESTAMP + "=?,"
                        + StatusUpdates.STATUS + "=?" +
                " WHERE " + StatusUpdatesColumns.DATA_ID + "=?"
                        + " AND " + StatusUpdates.STATUS + "!=?");

        mStatusAttributionUpdate = db.compileStatement(
                "UPDATE " + Tables.STATUS_UPDATES +
                " SET " + StatusUpdates.STATUS_RES_PACKAGE + "=?,"
                        + StatusUpdates.STATUS_ICON + "=?,"
                        + StatusUpdates.STATUS_LABEL + "=?" +
                " WHERE " + StatusUpdatesColumns.DATA_ID + "=?");

        mStatusUpdateDelete = db.compileStatement(
                "DELETE FROM " + Tables.STATUS_UPDATES +
                " WHERE " + StatusUpdatesColumns.DATA_ID + "=?");

        mDataRowHandlers = new HashMap<String, DataRowHandler>();

        mDataRowHandlers.put(Email.CONTENT_ITEM_TYPE, new EmailDataRowHandler());
        mDataRowHandlers.put(Im.CONTENT_ITEM_TYPE,
                new CommonDataRowHandler(Im.CONTENT_ITEM_TYPE, Im.TYPE, Im.LABEL));
        mDataRowHandlers.put(Nickname.CONTENT_ITEM_TYPE, new CommonDataRowHandler(
                StructuredPostal.CONTENT_ITEM_TYPE, StructuredPostal.TYPE, StructuredPostal.LABEL));
        mDataRowHandlers.put(Organization.CONTENT_ITEM_TYPE, new OrganizationDataRowHandler());
        mDataRowHandlers.put(Phone.CONTENT_ITEM_TYPE, new PhoneDataRowHandler());
        mDataRowHandlers.put(Nickname.CONTENT_ITEM_TYPE, new NicknameDataRowHandler());
        mDataRowHandlers.put(StructuredName.CONTENT_ITEM_TYPE,
                new StructuredNameRowHandler(mNameSplitter));
        mDataRowHandlers.put(StructuredPostal.CONTENT_ITEM_TYPE,
                new StructuredPostalRowHandler(mPostalSplitter));
        mDataRowHandlers.put(GroupMembership.CONTENT_ITEM_TYPE, new GroupMembershipRowHandler());
        mDataRowHandlers.put(Photo.CONTENT_ITEM_TYPE, new PhotoDataRowHandler());

        if (isLegacyContactImportNeeded()) {
            importLegacyContactsAsync();
        }

        verifyAccounts();

        mMimeTypeIdEmail = mDbHelper.getMimeTypeId(Email.CONTENT_ITEM_TYPE);
        mMimeTypeIdIm = mDbHelper.getMimeTypeId(Im.CONTENT_ITEM_TYPE);
        return (db != null);
    }

    protected void verifyAccounts() {
        AccountManager.get(getContext()).addOnAccountsUpdatedListener(this, null, false);
        onAccountsUpdated(AccountManager.get(getContext()).getAccounts());
    }

    /* Visible for testing */
    @Override
    protected ContactsDatabaseHelper getDatabaseHelper(final Context context) {
        return ContactsDatabaseHelper.getInstance(context);
    }

    /* package */ ContactAggregationScheduler getContactAggregationScheduler() {
        return mAggregationScheduler;
    }

    /* package */ NameSplitter getNameSplitter() {
        return mNameSplitter;
    }

    protected boolean isLegacyContactImportNeeded() {
        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
        return prefs.getInt(PREF_CONTACTS_IMPORTED, 0) < PREF_CONTACTS_IMPORT_VERSION;
    }

    protected LegacyContactImporter getLegacyContactImporter() {
        return new LegacyContactImporter(getContext(), this);
    }

    /**
     * Imports legacy contacts in a separate thread.  As long as the import process is running
     * all other access to the contacts is blocked.
     */
    private void importLegacyContactsAsync() {
        mAccessLatch = new CountDownLatch(1);

        Thread importThread = new Thread("LegacyContactImport") {
            @Override
            public void run() {
                if (importLegacyContacts()) {

                    /*
                     * When the import process is done, we can unlock the provider and
                     * start aggregating the imported contacts asynchronously.
                     */
                    mAccessLatch.countDown();
                    mAccessLatch = null;
                    scheduleContactAggregation();
                }
            }
        };

        importThread.start();
    }

    private boolean importLegacyContacts() {
        LegacyContactImporter importer = getLegacyContactImporter();
        if (importLegacyContacts(importer)) {
            SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
            Editor editor = prefs.edit();
            editor.putInt(PREF_CONTACTS_IMPORTED, PREF_CONTACTS_IMPORT_VERSION);
            editor.commit();
            return true;
        } else {
            return false;
        }
    }

    /* Visible for testing */
    /* package */ boolean importLegacyContacts(LegacyContactImporter importer) {
        boolean aggregatorEnabled = mContactAggregator.isEnabled();
        mContactAggregator.setEnabled(false);
        mImportMode = true;
        try {
            importer.importContacts();
            mContactAggregator.setEnabled(aggregatorEnabled);
            return true;
        } catch (Throwable e) {
           Log.e(TAG, "Legacy contact import failed", e);
           return false;
        } finally {
            mImportMode = false;
        }
    }

    @Override
    protected void finalize() throws Throwable {
        if (mContactAggregator != null) {
            mContactAggregator.quit();
        }

        super.finalize();
    }

    /**
     * Wipes all data from the contacts database.
     */
    /* package */ void wipeData() {
        mDbHelper.wipeData();
    }

    /**
     * While importing and aggregating contacts, this content provider will
     * block all attempts to change contacts data. In particular, it will hold
     * up all contact syncs. As soon as the import process is complete, all
     * processes waiting to write to the provider are unblocked and can proceed
     * to compete for the database transaction monitor.
     */
    private void waitForAccess() {
        CountDownLatch latch = mAccessLatch;
        if (latch != null) {
            while (true) {
                try {
                    latch.await();
                    mAccessLatch = null;
                    return;
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        }
    }

    @Override
    public Uri insert(Uri uri, ContentValues values) {
        waitForAccess();
        return super.insert(uri, values);
    }

    @Override
    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
        waitForAccess();
        return super.update(uri, values, selection, selectionArgs);
    }

    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        waitForAccess();
        return super.delete(uri, selection, selectionArgs);
    }

    @Override
    public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
            throws OperationApplicationException {
        waitForAccess();
        return super.applyBatch(operations);
    }

    @Override
    protected void onBeginTransaction() {
        if (VERBOSE_LOGGING) {
            Log.v(TAG, "onBeginTransaction");
        }
        super.onBeginTransaction();
        mContactAggregator.clearPendingAggregations();
        clearTransactionalChanges();
    }

    private void clearTransactionalChanges() {
        mInsertedRawContacts.clear();
        mUpdatedRawContacts.clear();
        mUpdatedSyncStates.clear();
    }

    @Override
    protected void beforeTransactionCommit() {
        if (VERBOSE_LOGGING) {
            Log.v(TAG, "beforeTransactionCommit");
        }
        super.beforeTransactionCommit();
        flushTransactionalChanges();
        mContactAggregator.aggregateInTransaction(mDb);
        if (mVisibleTouched) {
            mVisibleTouched = false;
            mDbHelper.updateAllVisible();
        }
    }

    private void flushTransactionalChanges() {
        if (VERBOSE_LOGGING) {
            Log.v(TAG, "flushTransactionChanges");
        }
        for (long rawContactId : mInsertedRawContacts) {
            mContactAggregator.insertContact(mDb, rawContactId);
        }

        String ids;
        if (!mUpdatedRawContacts.isEmpty()) {
            ids = buildIdsString(mUpdatedRawContacts);
            mDb.execSQL("UPDATE raw_contacts SET version = version + 1 WHERE _id in " + ids,
                    new Object[]{});
        }

        for (Map.Entry<Long, Object> entry : mUpdatedSyncStates.entrySet()) {
            long id = entry.getKey();
            mDbHelper.getSyncState().update(mDb, id, entry.getValue());
        }

        clearTransactionalChanges();
    }

    private String buildIdsString(HashSet<Long> ids) {
        StringBuilder idsBuilder = null;
        for (long id : ids) {
            if (idsBuilder == null) {
                idsBuilder = new StringBuilder();
                idsBuilder.append("(");
            } else {
                idsBuilder.append(",");
            }
            idsBuilder.append(id);
        }
        idsBuilder.append(")");
        return idsBuilder.toString();
    }

    @Override
    protected void notifyChange() {
        notifyChange(mSyncToNetwork);
        mSyncToNetwork = false;
    }

    protected void notifyChange(boolean syncToNetwork) {
        getContext().getContentResolver().notifyChange(ContactsContract.AUTHORITY_URI, null,
                syncToNetwork);
    }

    protected void scheduleContactAggregation() {
        mContactAggregator.schedule();
    }

    private boolean isNewRawContact(long rawContactId) {
        return mInsertedRawContacts.contains(rawContactId);
    }

    private DataRowHandler getDataRowHandler(final String mimeType) {
        DataRowHandler handler = mDataRowHandlers.get(mimeType);
        if (handler == null) {
            handler = new CustomDataRowHandler(mimeType);
            mDataRowHandlers.put(mimeType, handler);
        }
        return handler;
    }

    @Override
    protected Uri insertInTransaction(Uri uri, ContentValues values) {
        if (VERBOSE_LOGGING) {
            Log.v(TAG, "insertInTransaction: " + uri);
        }

        final boolean callerIsSyncAdapter =
                readBooleanQueryParameter(uri, ContactsContract.CALLER_IS_SYNCADAPTER, false);

        final int match = sUriMatcher.match(uri);
        long id = 0;

        switch (match) {
            case SYNCSTATE:
                id = mDbHelper.getSyncState().insert(mDb, values);
                break;

            case CONTACTS: {
                insertContact(values);
                break;
            }

            case RAW_CONTACTS: {
                final Account account = readAccountFromQueryParams(uri);
                id = insertRawContact(values, account);
                mSyncToNetwork |= !callerIsSyncAdapter;
                break;
            }

            case RAW_CONTACTS_DATA: {
                values.put(Data.RAW_CONTACT_ID, uri.getPathSegments().get(1));
                id = insertData(values, callerIsSyncAdapter);
                mSyncToNetwork |= !callerIsSyncAdapter;
                break;
            }

            case DATA: {
                id = insertData(values, callerIsSyncAdapter);
                mSyncToNetwork |= !callerIsSyncAdapter;
                break;
            }

            case GROUPS: {
                final Account account = readAccountFromQueryParams(uri);
                id = insertGroup(uri, values, account, callerIsSyncAdapter);
                mSyncToNetwork |= !callerIsSyncAdapter;
                break;
            }

            case SETTINGS: {
                id = insertSettings(uri, values);
                mSyncToNetwork |= !callerIsSyncAdapter;
                break;
            }

            case STATUS_UPDATES: {
                id = insertStatusUpdate(values);
                break;
            }

            default:
                mSyncToNetwork = true;
                return mLegacyApiSupport.insert(uri, values);
        }

        if (id < 0) {
            return null;
        }

        return ContentUris.withAppendedId(uri, id);
    }

    /**
     * If account is non-null then store it in the values. If the account is already
     * specified in the values then it must be consistent with the account, if it is non-null.
     * @param values the ContentValues to read from and update
     * @param account the explicitly provided Account
     * @return false if the accounts are inconsistent
     */
    private boolean resolveAccount(ContentValues values, Account account) {
        // If either is specified then both must be specified.
        final String accountName = values.getAsString(RawContacts.ACCOUNT_NAME);
        final String accountType = values.getAsString(RawContacts.ACCOUNT_TYPE);
        if (!TextUtils.isEmpty(accountName) || !TextUtils.isEmpty(accountType)) {
            final Account valuesAccount = new Account(accountName, accountType);
            if (account != null && !valuesAccount.equals(account)) {
                return false;
            }
            account = valuesAccount;
        }
        if (account != null) {
            values.put(RawContacts.ACCOUNT_NAME, account.name);
            values.put(RawContacts.ACCOUNT_TYPE, account.type);
        }
        return true;
    }

    /**
     * Inserts an item in the contacts table
     *
     * @param values the values for the new row
     * @return the row ID of the newly created row
     */
    private long insertContact(ContentValues values) {
        throw new UnsupportedOperationException("Aggregate contacts are created automatically");
    }

    /**
     * Inserts an item in the contacts table
     *
     * @param values the values for the new row
     * @param account the account this contact should be associated with. may be null.
     * @return the row ID of the newly created row
     */
    private long insertRawContact(ContentValues values, Account account) {
        ContentValues overriddenValues = new ContentValues(values);
        overriddenValues.putNull(RawContacts.CONTACT_ID);
        if (!resolveAccount(overriddenValues, account)) {
            return -1;
        }

        if (values.containsKey(RawContacts.DELETED)
                && values.getAsInteger(RawContacts.DELETED) != 0) {
            overriddenValues.put(RawContacts.AGGREGATION_MODE,
                    RawContacts.AGGREGATION_MODE_DISABLED);
        }

        long rawContactId =
                mDb.insert(Tables.RAW_CONTACTS, RawContacts.CONTACT_ID, overriddenValues);
        mContactAggregator.markNewForAggregation(rawContactId);

        // Trigger creation of a Contact based on this RawContact at the end of transaction
        mInsertedRawContacts.add(rawContactId);
        return rawContactId;
    }

    /**
     * Inserts an item in the data table
     *
     * @param values the values for the new row
     * @return the row ID of the newly created row
     */
    private long insertData(ContentValues values, boolean callerIsSyncAdapter) {
        long id = 0;
        mValues.clear();
        mValues.putAll(values);

        long rawContactId = mValues.getAsLong(Data.RAW_CONTACT_ID);

        // Replace package with internal mapping
        final String packageName = mValues.getAsString(Data.RES_PACKAGE);
        if (packageName != null) {
            mValues.put(DataColumns.PACKAGE_ID, mDbHelper.getPackageId(packageName));
        }
        mValues.remove(Data.RES_PACKAGE);

        // Replace mimetype with internal mapping
        final String mimeType = mValues.getAsString(Data.MIMETYPE);
        if (TextUtils.isEmpty(mimeType)) {
            throw new IllegalArgumentException(Data.MIMETYPE + " is required");
        }

        mValues.put(DataColumns.MIMETYPE_ID, mDbHelper.getMimeTypeId(mimeType));
        mValues.remove(Data.MIMETYPE);

        DataRowHandler rowHandler = getDataRowHandler(mimeType);
        id = rowHandler.insert(mDb, rawContactId, mValues);
        if (!callerIsSyncAdapter) {
            setRawContactDirty(rawContactId);
        }
        mUpdatedRawContacts.add(rawContactId);

        if (rowHandler.isAggregationRequired()) {
            triggerAggregation(rawContactId);
        }
        return id;
    }

    private void triggerAggregation(long rawContactId) {
        if (!mContactAggregator.isEnabled()) {
            return;
        }

        int aggregationMode = mDbHelper.getAggregationMode(rawContactId);
        switch (aggregationMode) {
            case RawContacts.AGGREGATION_MODE_DISABLED:
                break;

            case RawContacts.AGGREGATION_MODE_DEFAULT: {
                mContactAggregator.markForAggregation(rawContactId);
                break;
            }

            case RawContacts.AGGREGATION_MODE_SUSPENDED: {
                long contactId = mDbHelper.getContactId(rawContactId);

                if (contactId != 0) {
                    mContactAggregator.updateAggregateData(contactId);
                }
                break;
            }

            case RawContacts.AGGREGATION_MODE_IMMEDIATE: {
                long contactId = mDbHelper.getContactId(rawContactId);
                mContactAggregator.aggregateContact(mDb, rawContactId, contactId);
                break;
            }
        }
    }

    /**
     * Returns the group id of the group with sourceId and the same account as rawContactId.
     * If the group doesn't already exist then it is first created,
     * @param db SQLiteDatabase to use for this operation
     * @param rawContactId the contact this group is associated with
     * @param sourceId the sourceIf of the group to query or create
     * @return the group id of the existing or created group
     * @throws IllegalArgumentException if the contact is not associated with an account
     * @throws IllegalStateException if a group needs to be created but the creation failed
     */
    private long getOrMakeGroup(SQLiteDatabase db, long rawContactId, String sourceId) {
        Account account = null;
        Cursor c = db.query(ContactsQuery.TABLE, ContactsQuery.PROJECTION, RawContacts._ID + "="
                + rawContactId, null, null, null, null);
        try {
            if (c.moveToNext()) {
                final String accountName = c.getString(ContactsQuery.ACCOUNT_NAME);
                final String accountType = c.getString(ContactsQuery.ACCOUNT_TYPE);
                if (!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(accountType)) {
                    account = new Account(accountName, accountType);
                }
            }
        } finally {
            c.close();
        }
        if (account == null) {
            throw new IllegalArgumentException("if the groupmembership only "
                    + "has a sourceid the the contact must be associate with "
                    + "an account");
        }

        // look up the group that contains this sourceId and has the same account name and type
        // as the contact refered to by rawContactId
        c = db.query(Tables.GROUPS, new String[]{RawContacts._ID},
                Clauses.GROUP_HAS_ACCOUNT_AND_SOURCE_ID,
                new String[]{sourceId, account.name, account.type}, null, null, null);
        try {
            if (c.moveToNext()) {
                return c.getLong(0);
            } else {
                ContentValues groupValues = new ContentValues();
                groupValues.put(Groups.ACCOUNT_NAME, account.name);
                groupValues.put(Groups.ACCOUNT_TYPE, account.type);
                groupValues.put(Groups.SOURCE_ID, sourceId);
                long groupId = db.insert(Tables.GROUPS, Groups.ACCOUNT_NAME, groupValues);
                if (groupId < 0) {
                    throw new IllegalStateException("unable to create a new group with "
                            + "this sourceid: " + groupValues);
                }
                return groupId;
            }
        } finally {
            c.close();
        }
    }

    /**
     * Delete data row by row so that fixing of primaries etc work correctly.
     */
    private int deleteData(String selection, String[] selectionArgs, boolean callerIsSyncAdapter) {
        int count = 0;

        // Note that the query will return data according to the access restrictions,
        // so we don't need to worry about deleting data we don't have permission to read.
        Cursor c = query(Data.CONTENT_URI, DataDeleteQuery.COLUMNS, selection, selectionArgs, null);
        try {
            while(c.moveToNext()) {
                long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID);
                String mimeType = c.getString(DataDeleteQuery.MIMETYPE);
                DataRowHandler rowHandler = getDataRowHandler(mimeType);
                count += rowHandler.delete(mDb, c);
                if (!callerIsSyncAdapter) {
                    setRawContactDirty(rawContactId);
                    if (rowHandler.isAggregationRequired()) {
                        triggerAggregation(rawContactId);
                    }
                }
            }
        } finally {
            c.close();
        }

        return count;
    }

    /**
     * Delete a data row provided that it is one of the allowed mime types.
     */
    public int deleteData(long dataId, String[] allowedMimeTypes) {

        // Note that the query will return data according to the access restrictions,
        // so we don't need to worry about deleting data we don't have permission to read.
        Cursor c = query(Data.CONTENT_URI, DataDeleteQuery.COLUMNS, Data._ID + "=" + dataId, null,
                null);

        try {
            if (!c.moveToFirst()) {
                return 0;
            }

            String mimeType = c.getString(DataDeleteQuery.MIMETYPE);
            boolean valid = false;
            for (int i = 0; i < allowedMimeTypes.length; i++) {
                if (TextUtils.equals(mimeType, allowedMimeTypes[i])) {
                    valid = true;
                    break;
                }
            }

            if (!valid) {
                throw new IllegalArgumentException("Data type mismatch: expected "
                        + Lists.newArrayList(allowedMimeTypes));
            }

            DataRowHandler rowHandler = getDataRowHandler(mimeType);
            int count = rowHandler.delete(mDb, c);
            long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID);
            if (rowHandler.isAggregationRequired()) {
                triggerAggregation(rawContactId);
            }
            return count;
        } finally {
            c.close();
        }
    }

    /**
     * Inserts an item in the groups table
     */
    private long insertGroup(Uri uri, ContentValues values, Account account,
            boolean callerIsSyncAdapter) {
        ContentValues overriddenValues = new ContentValues(values);
        if (!resolveAccount(overriddenValues, account)) {
            return -1;
        }

        // Replace package with internal mapping
        final String packageName = overriddenValues.getAsString(Groups.RES_PACKAGE);
        if (packageName != null) {
            overriddenValues.put(GroupsColumns.PACKAGE_ID, mDbHelper.getPackageId(packageName));
        }
        overriddenValues.remove(Groups.RES_PACKAGE);

        if (!callerIsSyncAdapter) {
            overriddenValues.put(Groups.DIRTY, 1);
        }

        long result = mDb.insert(Tables.GROUPS, Groups.TITLE, overriddenValues);

        if (overriddenValues.containsKey(Groups.GROUP_VISIBLE)) {
            mVisibleTouched = true;
        }

        return result;
    }

    private long insertSettings(Uri uri, ContentValues values) {
        final long id = mDb.insert(Tables.SETTINGS, null, values);

        if (values.containsKey(Settings.UNGROUPED_VISIBLE)) {
            mVisibleTouched = true;
        }

        return id;
    }

    /**
     * Inserts a status update.
     */
    public long insertStatusUpdate(ContentValues values) {
        final String handle = values.getAsString(StatusUpdates.IM_HANDLE);
        final Integer protocol = values.getAsInteger(StatusUpdates.PROTOCOL);
        String customProtocol = null;

        if (protocol != null && protocol == Im.PROTOCOL_CUSTOM) {
            customProtocol = values.getAsString(StatusUpdates.CUSTOM_PROTOCOL);
            if (TextUtils.isEmpty(customProtocol)) {
                throw new IllegalArgumentException(
                        "CUSTOM_PROTOCOL is required when PROTOCOL=PROTOCOL_CUSTOM");
            }
        }

        long rawContactId = -1;
        long contactId = -1;
        Long dataId = values.getAsLong(StatusUpdates.DATA_ID);
        mSb.setLength(0);
        if (dataId != null) {
            // Lookup the contact info for the given data row.

            mSb.append(Tables.DATA + "." + Data._ID + "=");
            mSb.append(dataId);
        } else {
            // Lookup the data row to attach this presence update to

            if (TextUtils.isEmpty(handle) || protocol == null) {
                throw new IllegalArgumentException("PROTOCOL and IM_HANDLE are required");
            }

            // TODO: generalize to allow other providers to match against email
            boolean matchEmail = Im.PROTOCOL_GOOGLE_TALK == protocol;

            if (matchEmail) {

                // The following hack forces SQLite to use the (mimetype_id,data1) index, otherwise
                // the "OR" conjunction confuses it and it switches to a full scan of
                // the raw_contacts table.

                // This code relies on the fact that Im.DATA and Email.DATA are in fact the same
                // column - Data.DATA1
                mSb.append(DataColumns.MIMETYPE_ID + " IN (")
                        .append(mMimeTypeIdEmail)
                        .append(",")
                        .append(mMimeTypeIdIm)
                        .append(")" + " AND " + Data.DATA1 + "=");
                DatabaseUtils.appendEscapedSQLString(mSb, handle);
                mSb.append(" AND ((" + DataColumns.MIMETYPE_ID + "=")
                        .append(mMimeTypeIdIm)
                        .append(" AND " + Im.PROTOCOL + "=")
                        .append(protocol);
                if (customProtocol != null) {
                    mSb.append(" AND " + Im.CUSTOM_PROTOCOL + "=");
                    DatabaseUtils.appendEscapedSQLString(mSb, customProtocol);
                }
                mSb.append(") OR (" + DataColumns.MIMETYPE_ID + "=")
                        .append(mMimeTypeIdEmail)
                        .append("))");
            } else {
                mSb.append(DataColumns.MIMETYPE_ID + "=")
                        .append(mMimeTypeIdIm)
                        .append(" AND " + Im.PROTOCOL + "=")
                        .append(protocol)
                        .append(" AND " + Im.DATA + "=");
                DatabaseUtils.appendEscapedSQLString(mSb, handle);
                if (customProtocol != null) {
                    mSb.append(" AND " + Im.CUSTOM_PROTOCOL + "=");
                    DatabaseUtils.appendEscapedSQLString(mSb, customProtocol);
                }
            }

            if (values.containsKey(StatusUpdates.DATA_ID)) {
                mSb.append(" AND " + DataColumns.CONCRETE_ID + "=")
                        .append(values.getAsLong(StatusUpdates.DATA_ID));
            }
        }
        mSb.append(" AND ").append(getContactsRestrictions());

        Cursor cursor = null;
        try {
            cursor = mDb.query(DataContactsQuery.TABLE, DataContactsQuery.PROJECTION,
                    mSb.toString(), null, null, null,
                    Contacts.IN_VISIBLE_GROUP + " DESC, " + Data.RAW_CONTACT_ID);
            if (cursor.moveToFirst()) {
                dataId = cursor.getLong(DataContactsQuery.DATA_ID);
                rawContactId = cursor.getLong(DataContactsQuery.RAW_CONTACT_ID);
                contactId = cursor.getLong(DataContactsQuery.CONTACT_ID);
            } else {
                // No contact found, return a null URI
                return -1;
            }
        } finally {
            if (cursor != null) {
                cursor.close();
            }
        }

        if (values.containsKey(StatusUpdates.PRESENCE)) {
            if (customProtocol == null) {
                // We cannot allow a null in the custom protocol field, because SQLite3 does not
                // properly enforce uniqueness of null values
                customProtocol = "";
            }

            mValues.clear();
            mValues.put(StatusUpdates.DATA_ID, dataId);
            mValues.put(PresenceColumns.RAW_CONTACT_ID, rawContactId);
            mValues.put(PresenceColumns.CONTACT_ID, contactId);
            mValues.put(StatusUpdates.PROTOCOL, protocol);
            mValues.put(StatusUpdates.CUSTOM_PROTOCOL, customProtocol);
            mValues.put(StatusUpdates.IM_HANDLE, handle);
            if (values.containsKey(StatusUpdates.IM_ACCOUNT)) {
                mValues.put(StatusUpdates.IM_ACCOUNT, values.getAsString(StatusUpdates.IM_ACCOUNT));
            }
            mValues.put(StatusUpdates.PRESENCE,
                    values.getAsString(StatusUpdates.PRESENCE));

            // Insert the presence update
            mDb.replace(Tables.PRESENCE, null, mValues);
        }


        if (values.containsKey(StatusUpdates.STATUS)) {
            String status = values.getAsString(StatusUpdates.STATUS);
            String resPackage = values.getAsString(StatusUpdates.STATUS_RES_PACKAGE);
            Integer labelResource = values.getAsInteger(StatusUpdates.STATUS_LABEL);

            if (TextUtils.isEmpty(resPackage)
                    && (labelResource == null || labelResource == 0)
                    && protocol != null) {
                labelResource = Im.getProtocolLabelResource(protocol);
            }

            Long iconResource = values.getAsLong(StatusUpdates.STATUS_ICON);
            // TODO compute the default icon based on the protocol

            if (TextUtils.isEmpty(status)) {
                mStatusUpdateDelete.bindLong(1, dataId);
                mStatusUpdateDelete.execute();
            } else if (values.containsKey(StatusUpdates.STATUS_TIMESTAMP)) {
                long timestamp = values.getAsLong(StatusUpdates.STATUS_TIMESTAMP);
                mStatusUpdateReplace.bindLong(1, dataId);
                mStatusUpdateReplace.bindLong(2, timestamp);
                DatabaseUtils.bindObjectToProgram(mStatusUpdateReplace, 3, status);
                DatabaseUtils.bindObjectToProgram(mStatusUpdateReplace, 4, resPackage);
                DatabaseUtils.bindObjectToProgram(mStatusUpdateReplace, 5, iconResource);
                DatabaseUtils.bindObjectToProgram(mStatusUpdateReplace, 6, labelResource);
                mStatusUpdateReplace.execute();
            } else {

                try {
                    mStatusUpdateInsert.bindLong(1, dataId);
                    DatabaseUtils.bindObjectToProgram(mStatusUpdateInsert, 2, status);
                    DatabaseUtils.bindObjectToProgram(mStatusUpdateInsert, 3, resPackage);
                    DatabaseUtils.bindObjectToProgram(mStatusUpdateInsert, 4, iconResource);
                    DatabaseUtils.bindObjectToProgram(mStatusUpdateInsert, 5, labelResource);
                    mStatusUpdateInsert.executeInsert();
                } catch (SQLiteConstraintException e) {
                    // The row already exists - update it
                    long timestamp = System.currentTimeMillis();
                    mStatusUpdateAutoTimestamp.bindLong(1, timestamp);
                    DatabaseUtils.bindObjectToProgram(mStatusUpdateAutoTimestamp, 2, status);
                    mStatusUpdateAutoTimestamp.bindLong(3, dataId);
                    DatabaseUtils.bindObjectToProgram(mStatusUpdateAutoTimestamp, 4, status);
                    mStatusUpdateAutoTimestamp.execute();

                    DatabaseUtils.bindObjectToProgram(mStatusAttributionUpdate, 1, resPackage);
                    DatabaseUtils.bindObjectToProgram(mStatusAttributionUpdate, 2, iconResource);
                    DatabaseUtils.bindObjectToProgram(mStatusAttributionUpdate, 3, labelResource);
                    mStatusAttributionUpdate.bindLong(4, dataId);
                    mStatusAttributionUpdate.execute();
                }
            }
        }

        if (contactId != -1) {
            mLastStatusUpdate.bindLong(1, contactId);
            mLastStatusUpdate.bindLong(2, contactId);
            mLastStatusUpdate.execute();
        }

        return dataId;
    }

    @Override
    protected int deleteInTransaction(Uri uri, String selection, String[] selectionArgs) {
        if (VERBOSE_LOGGING) {
            Log.v(TAG, "deleteInTransaction: " + uri);
        }
        flushTransactionalChanges();
        final boolean callerIsSyncAdapter =
                readBooleanQueryParameter(uri, ContactsContract.CALLER_IS_SYNCADAPTER, false);
        final int match = sUriMatcher.match(uri);
        switch (match) {
            case SYNCSTATE:
                return mDbHelper.getSyncState().delete(mDb, selection, selectionArgs);

            case SYNCSTATE_ID:
                String selectionWithId =
                        (SyncStateContract.Columns._ID + "=" + ContentUris.parseId(uri) + " ")
                        + (selection == null ? "" : " AND (" + selection + ")");
                return mDbHelper.getSyncState().delete(mDb, selectionWithId, selectionArgs);

            case CONTACTS: {
                // TODO
                return 0;
            }

            case CONTACTS_ID: {
                long contactId = ContentUris.parseId(uri);
                return deleteContact(contactId);
            }

            case CONTACTS_LOOKUP:
            case CONTACTS_LOOKUP_ID: {
                final List<String> pathSegments = uri.getPathSegments();
                final int segmentCount = pathSegments.size();
                if (segmentCount < 3) {
                    throw new IllegalArgumentException("URI " + uri + " is missing a lookup key");
                }
                final String lookupKey = pathSegments.get(2);
                final long contactId = lookupContactIdByLookupKey(mDb, lookupKey);
                return deleteContact(contactId);
            }

            case RAW_CONTACTS: {
                int numDeletes = 0;
                Cursor c = mDb.query(Tables.RAW_CONTACTS, new String[]{RawContacts._ID},
                        appendAccountToSelection(uri, selection), selectionArgs, null, null, null);
                try {
                    while (c.moveToNext()) {
                        final long rawContactId = c.getLong(0);
                        numDeletes += deleteRawContact(rawContactId, callerIsSyncAdapter);
                    }
                } finally {
                    c.close();
                }
                return numDeletes;
            }

            case RAW_CONTACTS_ID: {
                final long rawContactId = ContentUris.parseId(uri);
                return deleteRawContact(rawContactId, callerIsSyncAdapter);
            }

            case DATA: {
                mSyncToNetwork |= !callerIsSyncAdapter;
                return deleteData(appendAccountToSelection(uri, selection), selectionArgs,
                        callerIsSyncAdapter);
            }

            case DATA_ID:
            case PHONES_ID:
            case EMAILS_ID:
            case POSTALS_ID: {
                long dataId = ContentUris.parseId(uri);
                mSyncToNetwork |= !callerIsSyncAdapter;
                return deleteData(Data._ID + "=" + dataId, null, callerIsSyncAdapter);
            }

            case GROUPS_ID: {
                mSyncToNetwork |= !callerIsSyncAdapter;
                return deleteGroup(uri, ContentUris.parseId(uri), callerIsSyncAdapter);
            }

            case GROUPS: {
                int numDeletes = 0;
                Cursor c = mDb.query(Tables.GROUPS, new String[]{Groups._ID},
                        appendAccountToSelection(uri, selection), selectionArgs, null, null, null);
                try {
                    while (c.moveToNext()) {
                        numDeletes += deleteGroup(uri, c.getLong(0), callerIsSyncAdapter);
                    }
                } finally {
                    c.close();
                }
                if (numDeletes > 0) {
                    mSyncToNetwork |= !callerIsSyncAdapter;
                }
                return numDeletes;
            }

            case SETTINGS: {
                mSyncToNetwork |= !callerIsSyncAdapter;
                return deleteSettings(uri, selection, selectionArgs);
            }

            case STATUS_UPDATES: {
                return deleteStatusUpdates(selection, selectionArgs);
            }

            default: {
                mSyncToNetwork = true;
                return mLegacyApiSupport.delete(uri, selection, selectionArgs);
            }
        }
    }

    private static boolean readBooleanQueryParameter(Uri uri, String name, boolean defaultValue) {
        final String flag = uri.getQueryParameter(name);
        return flag == null
                ? defaultValue
                : (!"false".equals(flag.toLowerCase()) && !"0".equals(flag.toLowerCase()));
    }

    public int deleteGroup(Uri uri, long groupId, boolean callerIsSyncAdapter) {
        final long groupMembershipMimetypeId = mDbHelper
                .getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE);
        mDb.delete(Tables.DATA, DataColumns.MIMETYPE_ID + "="
                + groupMembershipMimetypeId + " AND " + GroupMembership.GROUP_ROW_ID + "="
                + groupId, null);

        try {
            if (callerIsSyncAdapter) {
                return mDb.delete(Tables.GROUPS, Groups._ID + "=" + groupId, null);
            } else {
                mValues.clear();
                mValues.put(Groups.DELETED, 1);
                mValues.put(Groups.DIRTY, 1);
                return mDb.update(Tables.GROUPS, mValues, Groups._ID + "=" + groupId, null);
            }
        } finally {
            mVisibleTouched = true;
        }
    }

    private int deleteSettings(Uri uri, String selection, String[] selectionArgs) {
        final int count = mDb.delete(Tables.SETTINGS, selection, selectionArgs);
        mVisibleTouched = true;
        return count;
    }

    private int deleteContact(long contactId) {
        Cursor c = mDb.query(Tables.RAW_CONTACTS, new String[]{RawContacts._ID},
                RawContacts.CONTACT_ID + "=" + contactId, null, null, null, null);
        try {
            while (c.moveToNext()) {
                long rawContactId = c.getLong(0);
                markRawContactAsDeleted(rawContactId);
            }
        } finally {
            c.close();
        }

        return mDb.delete(Tables.CONTACTS, Contacts._ID + "=" + contactId, null);
    }

    public int deleteRawContact(long rawContactId, boolean callerIsSyncAdapter) {
        if (callerIsSyncAdapter) {
            mDb.delete(Tables.PRESENCE, PresenceColumns.RAW_CONTACT_ID + "=" + rawContactId, null);
            return mDb.delete(Tables.RAW_CONTACTS, RawContacts._ID + "=" + rawContactId, null);
        } else {
            mDbHelper.removeContactIfSingleton(rawContactId);
            return markRawContactAsDeleted(rawContactId);
        }
    }

    private int deleteStatusUpdates(String selection, String[] selectionArgs) {
        // TODO delete from both tables: presence and status_updates
        return mDb.delete(Tables.PRESENCE, selection, selectionArgs);
    }

    private int markRawContactAsDeleted(long rawContactId) {
        mSyncToNetwork = true;

        mValues.clear();
        mValues.put(RawContacts.DELETED, 1);
        mValues.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DISABLED);
        mValues.put(RawContactsColumns.AGGREGATION_NEEDED, 1);
        mValues.putNull(RawContacts.CONTACT_ID);
        mValues.put(RawContacts.DIRTY, 1);
        return updateRawContact(rawContactId, mValues);
    }

    @Override
    protected int updateInTransaction(Uri uri, ContentValues values, String selection,
            String[] selectionArgs) {
        if (VERBOSE_LOGGING) {
            Log.v(TAG, "updateInTransaction: " + uri);
        }

        int count = 0;

        final int match = sUriMatcher.match(uri);
        if (match == SYNCSTATE_ID && selection == null) {
            long rowId = ContentUris.parseId(uri);
            Object data = values.get(ContactsContract.SyncStateColumns.DATA);
            mUpdatedSyncStates.put(rowId, data);
            return 1;
        }
        flushTransactionalChanges();
        final boolean callerIsSyncAdapter =
                readBooleanQueryParameter(uri, ContactsContract.CALLER_IS_SYNCADAPTER, false);
        switch(match) {
            case SYNCSTATE:
                return mDbHelper.getSyncState().update(mDb, values,
                        appendAccountToSelection(uri, selection), selectionArgs);

            case SYNCSTATE_ID: {
                selection = appendAccountToSelection(uri, selection);
                String selectionWithId =
                        (SyncStateContract.Columns._ID + "=" + ContentUris.parseId(uri) + " ")
                        + (selection == null ? "" : " AND (" + selection + ")");
                return mDbHelper.getSyncState().update(mDb, values,
                        selectionWithId, selectionArgs);
            }

            case CONTACTS: {
                count = updateContactOptions(values, selection, selectionArgs);
                break;
            }

            case CONTACTS_ID: {
                count = updateContactOptions(ContentUris.parseId(uri), values);
                break;
            }

            case CONTACTS_LOOKUP:
            case CONTACTS_LOOKUP_ID: {
                final List<String> pathSegments = uri.getPathSegments();
                final int segmentCount = pathSegments.size();
                if (segmentCount < 3) {
                    throw new IllegalArgumentException("URI " + uri + " is missing a lookup key");
                }
                final String lookupKey = pathSegments.get(2);
                final long contactId = lookupContactIdByLookupKey(mDb, lookupKey);
                count = updateContactOptions(contactId, values);
                break;
            }

            case RAW_CONTACTS_DATA: {
                final String rawContactId = uri.getPathSegments().get(1);
                String selectionWithId = (Data.RAW_CONTACT_ID + "=" + rawContactId + " ")
                    + (selection == null ? "" : " AND " + selection);

                count = updateData(uri, values, selectionWithId, selectionArgs, callerIsSyncAdapter);

                break;
            }

            case DATA: {
                count = updateData(uri, values, appendAccountToSelection(uri, selection),
                        selectionArgs, callerIsSyncAdapter);
                if (count > 0) {
                    mSyncToNetwork |= !callerIsSyncAdapter;
                }
                break;
            }

            case DATA_ID:
            case PHONES_ID:
            case EMAILS_ID:
            case POSTALS_ID: {
                count = updateData(uri, values, selection, selectionArgs, callerIsSyncAdapter);
                if (count > 0) {
                    mSyncToNetwork |= !callerIsSyncAdapter;
                }
                break;
            }

            case RAW_CONTACTS: {
                selection = appendAccountToSelection(uri, selection);
                count = updateRawContacts(values, selection, selectionArgs);
                break;
            }

            case RAW_CONTACTS_ID: {
                long rawContactId = ContentUris.parseId(uri);
                if (selection != null) {
                    count = updateRawContacts(values, RawContacts._ID + "=" + rawContactId
                                    + " AND(" + selection + ")", selectionArgs);
                } else {
                    count = updateRawContacts(values, RawContacts._ID + "=" + rawContactId, null);
                }
                break;
            }

            case GROUPS: {
                count = updateGroups(uri, values, appendAccountToSelection(uri, selection),
                        selectionArgs, callerIsSyncAdapter);
                if (count > 0) {
                    mSyncToNetwork |= !callerIsSyncAdapter;
                }
                break;
            }

            case GROUPS_ID: {
                long groupId = ContentUris.parseId(uri);
                String selectionWithId = (Groups._ID + "=" + groupId + " ")
                        + (selection == null ? "" : " AND " + selection);
                count = updateGroups(uri, values, selectionWithId, selectionArgs,
                        callerIsSyncAdapter);
                if (count > 0) {
                    mSyncToNetwork |= !callerIsSyncAdapter;
                }
                break;
            }

            case AGGREGATION_EXCEPTIONS: {
                count = updateAggregationException(mDb, values);
                break;
            }

            case SETTINGS: {
                count = updateSettings(uri, values, selection, selectionArgs);
                mSyncToNetwork |= !callerIsSyncAdapter;
                break;
            }

            default: {
                mSyncToNetwork = true;
                return mLegacyApiSupport.update(uri, values, selection, selectionArgs);
            }
        }

        return count;
    }

    private int updateGroups(Uri uri, ContentValues values, String selectionWithId,
            String[] selectionArgs, boolean callerIsSyncAdapter) {

        ContentValues updatedValues;
        if (!callerIsSyncAdapter && !values.containsKey(Groups.DIRTY)) {
            updatedValues = mValues;
            updatedValues.clear();
            updatedValues.putAll(values);
            updatedValues.put(Groups.DIRTY, 1);
        } else {
            updatedValues = values;
        }

        int count = mDb.update(Tables.GROUPS, updatedValues, selectionWithId, selectionArgs);
        if (updatedValues.containsKey(Groups.GROUP_VISIBLE)) {
            mVisibleTouched = true;
        }
        if (updatedValues.containsKey(Groups.SHOULD_SYNC)
                && updatedValues.getAsInteger(Groups.SHOULD_SYNC) != 0) {
            final long groupId = ContentUris.parseId(uri);
            Cursor c = mDb.query(Tables.GROUPS, new String[]{Groups.ACCOUNT_NAME,
                    Groups.ACCOUNT_TYPE}, Groups._ID + "=" + groupId, null, null,
                    null, null);
            String accountName;
            String accountType;
            try {
                while (c.moveToNext()) {
                    accountName = c.getString(0);
                    accountType = c.getString(1);
                    if(!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(accountType)) {
                        Account account = new Account(accountName, accountType);
                    ContentResolver.requestSync(account, ContactsContract.AUTHORITY,
                                new Bundle());
                        break;
                    }
                }
            } finally {
                c.close();
            }
        }
        return count;
    }

    private int updateSettings(Uri uri, ContentValues values, String selection,
            String[] selectionArgs) {
        final int count = mDb.update(Tables.SETTINGS, values, selection, selectionArgs);
        if (values.containsKey(Settings.UNGROUPED_VISIBLE)) {
            mVisibleTouched = true;
        }
        return count;
    }

    private int updateRawContacts(ContentValues values, String selection, String[] selectionArgs) {
        if (values.containsKey(RawContacts.CONTACT_ID)) {
            throw new IllegalArgumentException(RawContacts.CONTACT_ID + " should not be included " +
                    "in content values. Contact IDs are assigned automatically");
        }

        int count = 0;
        Cursor cursor = mDb.query(mDbHelper.getRawContactView(),
                new String[] { RawContacts._ID }, selection,
                selectionArgs, null, null, null);
        try {
            while (cursor.moveToNext()) {
                long rawContactId = cursor.getLong(0);
                updateRawContact(rawContactId, values);
                count++;
            }
        } finally {
            cursor.close();
        }

        return count;
    }

    private int updateRawContact(long rawContactId, ContentValues values) {
        final String selection = RawContacts._ID + " = " + rawContactId;
        final boolean requestUndoDelete = (values.containsKey(RawContacts.DELETED)
                && values.getAsInteger(RawContacts.DELETED) == 0);
        int previousDeleted = 0;
        if (requestUndoDelete) {
            Cursor cursor = mDb.query(RawContactsQuery.TABLE, RawContactsQuery.COLUMNS, selection,
                    null, null, null, null);
            try {
                if (cursor.moveToFirst()) {
                    previousDeleted = cursor.getInt(RawContactsQuery.DELETED);
                }
            } finally {
                cursor.close();
            }
            values.put(ContactsContract.RawContacts.AGGREGATION_MODE,
                    ContactsContract.RawContacts.AGGREGATION_MODE_DEFAULT);
        }
        int count = mDb.update(Tables.RAW_CONTACTS, values, selection, null);
        if (count != 0) {
            if (values.containsKey(RawContacts.STARRED)) {
                mContactAggregator.updateStarred(rawContactId);
            }
            if (values.containsKey(RawContacts.SOURCE_ID)) {
                mContactAggregator.updateLookupKey(mDb, rawContactId);
            }
            if (requestUndoDelete && previousDeleted == 1) {
                // undo delete, needs aggregation again.
                mInsertedRawContacts.add(rawContactId);
            }
        }
        return count;
    }

    private int updateData(Uri uri, ContentValues values, String selection,
            String[] selectionArgs, boolean callerIsSyncAdapter) {
        mValues.clear();
        mValues.putAll(values);
        mValues.remove(Data._ID);
        mValues.remove(Data.RAW_CONTACT_ID);
        mValues.remove(Data.MIMETYPE);

        String packageName = values.getAsString(Data.RES_PACKAGE);
        if (packageName != null) {
            mValues.remove(Data.RES_PACKAGE);
            mValues.put(DataColumns.PACKAGE_ID, mDbHelper.getPackageId(packageName));
        }

        boolean containsIsSuperPrimary = mValues.containsKey(Data.IS_SUPER_PRIMARY);
        boolean containsIsPrimary = mValues.containsKey(Data.IS_PRIMARY);

        // Remove primary or super primary values being set to 0. This is disallowed by the
        // content provider.
        if (containsIsSuperPrimary && mValues.getAsInteger(Data.IS_SUPER_PRIMARY) == 0) {
            containsIsSuperPrimary = false;
            mValues.remove(Data.IS_SUPER_PRIMARY);
        }
        if (containsIsPrimary && mValues.getAsInteger(Data.IS_PRIMARY) == 0) {
            containsIsPrimary = false;
            mValues.remove(Data.IS_PRIMARY);
        }

        int count = 0;

        // Note that the query will return data according to the access restrictions,
        // so we don't need to worry about updating data we don't have permission to read.
        Cursor c = query(uri, DataUpdateQuery.COLUMNS, selection, selectionArgs, null);
        try {
            while(c.moveToNext()) {
                count += updateData(mValues, c, callerIsSyncAdapter);
            }
        } finally {
            c.close();
        }

        return count;
    }

    private int updateData(ContentValues values, Cursor c, boolean callerIsSyncAdapter) {
        if (values.size() == 0) {
            return 0;
        }

        final String mimeType = c.getString(DataUpdateQuery.MIMETYPE);
        DataRowHandler rowHandler = getDataRowHandler(mimeType);
        rowHandler.update(mDb, values, c, callerIsSyncAdapter);
        long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID);
        if (rowHandler.isAggregationRequired()) {
            triggerAggregation(rawContactId);
        }

        return 1;
    }

    private int updateContactOptions(ContentValues values, String selection,
            String[] selectionArgs) {
        int count = 0;
        Cursor cursor = mDb.query(mDbHelper.getContactView(),
                new String[] { Contacts._ID }, selection,
                selectionArgs, null, null, null);
        try {
            while (cursor.moveToNext()) {
                long contactId = cursor.getLong(0);
                updateContactOptions(contactId, values);
                count++;
            }
        } finally {
            cursor.close();
        }

        return count;
    }

    private int updateContactOptions(long contactId, ContentValues values) {

        mValues.clear();
        ContactsDatabaseHelper.copyStringValue(mValues, RawContacts.CUSTOM_RINGTONE,
                values, Contacts.CUSTOM_RINGTONE);
        ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.SEND_TO_VOICEMAIL,
                values, Contacts.SEND_TO_VOICEMAIL);
        ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.LAST_TIME_CONTACTED,
                values, Contacts.LAST_TIME_CONTACTED);
        ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.TIMES_CONTACTED,
                values, Contacts.TIMES_CONTACTED);
        ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.STARRED,
                values, Contacts.STARRED);

        // Nothing to update - just return
        if (mValues.size() == 0) {
            return 0;
        }

        if (mValues.containsKey(RawContacts.STARRED)) {
            // Mark dirty when changing starred to trigger sync
            mValues.put(RawContacts.DIRTY, 1);
        }

        mDb.update(Tables.RAW_CONTACTS, mValues, RawContacts.CONTACT_ID + "=" + contactId, null);

        // Copy changeable values to prevent automatically managed fields from
        // being explicitly updated by clients.
        mValues.clear();
        ContactsDatabaseHelper.copyStringValue(mValues, RawContacts.CUSTOM_RINGTONE,
                values, Contacts.CUSTOM_RINGTONE);
        ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.SEND_TO_VOICEMAIL,
                values, Contacts.SEND_TO_VOICEMAIL);
        ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.LAST_TIME_CONTACTED,
                values, Contacts.LAST_TIME_CONTACTED);
        ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.TIMES_CONTACTED,
                values, Contacts.TIMES_CONTACTED);
        ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.STARRED,
                values, Contacts.STARRED);

        return mDb.update(Tables.CONTACTS, mValues, Contacts._ID + "=" + contactId, null);
    }

    public void updateContactLastContactedTime(long contactId, long lastTimeContacted) {
        mContactsLastTimeContactedUpdate.bindLong(1, lastTimeContacted);
        mContactsLastTimeContactedUpdate.bindLong(2, contactId);
        mContactsLastTimeContactedUpdate.execute();
    }

    private int updateAggregationException(SQLiteDatabase db, ContentValues values) {
        int exceptionType = values.getAsInteger(AggregationExceptions.TYPE);
        long rcId1 = values.getAsInteger(AggregationExceptions.RAW_CONTACT_ID1);
        long rcId2 = values.getAsInteger(AggregationExceptions.RAW_CONTACT_ID2);

        long rawContactId1, rawContactId2;
        if (rcId1 < rcId2) {
            rawContactId1 = rcId1;
            rawContactId2 = rcId2;
        } else {
            rawContactId2 = rcId1;
            rawContactId1 = rcId2;
        }

        if (exceptionType == AggregationExceptions.TYPE_AUTOMATIC) {
            db.delete(Tables.AGGREGATION_EXCEPTIONS,
                    AggregationExceptions.RAW_CONTACT_ID1 + "=" + rawContactId1 + " AND "
                    + AggregationExceptions.RAW_CONTACT_ID2 + "=" + rawContactId2, null);
        } else {
            ContentValues exceptionValues = new ContentValues(3);
            exceptionValues.put(AggregationExceptions.TYPE, exceptionType);
            exceptionValues.put(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
            exceptionValues.put(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
            db.replace(Tables.AGGREGATION_EXCEPTIONS, AggregationExceptions._ID,
                    exceptionValues);
        }

        mContactAggregator.markForAggregation(rawContactId1);
        mContactAggregator.markForAggregation(rawContactId2);

        long contactId1 = mDbHelper.getContactId(rawContactId1);
        mContactAggregator.aggregateContact(db, rawContactId1, contactId1);

        long contactId2 = mDbHelper.getContactId(rawContactId2);
        mContactAggregator.aggregateContact(db, rawContactId2, contactId2);

        // The return value is fake - we just confirm that we made a change, not count actual
        // rows changed.
        return 1;
    }

    public void onAccountsUpdated(Account[] accounts) {
        mDb = mDbHelper.getWritableDatabase();
        if (mDb == null) return;

        HashSet<Account> existingAccounts = new HashSet<Account>();
        boolean hasUnassignedContacts[] = new boolean[]{false};
        mDb.beginTransaction();
        try {
            findValidAccounts(existingAccounts, hasUnassignedContacts,
                    Tables.RAW_CONTACTS, RawContacts.ACCOUNT_NAME, RawContacts.ACCOUNT_TYPE);
            findValidAccounts(existingAccounts, hasUnassignedContacts,
                    Tables.GROUPS, Groups.ACCOUNT_NAME, Groups.ACCOUNT_TYPE);
            findValidAccounts(existingAccounts, hasUnassignedContacts,
                    Tables.SETTINGS, Settings.ACCOUNT_NAME, Settings.ACCOUNT_TYPE);

            // Remove all valid accounts from the existing account set. What is left
            // in the existingAccounts set will be extra accounts whose data must be deleted.
            HashSet<Account> accountsToDelete = new HashSet<Account>(existingAccounts);
            for (Account account : accounts) {
                accountsToDelete.remove(account);
            }

            for (Account account : accountsToDelete) {
                Log.d(TAG, "removing data for removed account " + account);
                String[] params = new String[] {account.name, account.type};
                mDb.execSQL(
                        "DELETE FROM " + Tables.GROUPS +
                        " WHERE " + Groups.ACCOUNT_NAME + " = ?" +
                                " AND " + Groups.ACCOUNT_TYPE + " = ?", params);
                mDb.execSQL(
                        "DELETE FROM " + Tables.PRESENCE +
                        " WHERE " + PresenceColumns.RAW_CONTACT_ID + " IN (" +
                                "SELECT " + RawContacts._ID +
                                " FROM " + Tables.RAW_CONTACTS +
                                " WHERE " + RawContacts.ACCOUNT_NAME + " = ?" +
                                " AND " + RawContacts.ACCOUNT_TYPE + " = ?)", params);
                mDb.execSQL(
                        "DELETE FROM " + Tables.RAW_CONTACTS +
                        " WHERE " + RawContacts.ACCOUNT_NAME + " = ?" +
                        " AND " + RawContacts.ACCOUNT_TYPE + " = ?", params);
                mDb.execSQL(
                        "DELETE FROM " + Tables.SETTINGS +
                        " WHERE " + Settings.ACCOUNT_NAME + " = ?" +
                        " AND " + Settings.ACCOUNT_TYPE + " = ?", params);
            }

            if (hasUnassignedContacts[0]) {

                Account primaryAccount = null;
                for (Account account : accounts) {
                    if (isWritableAccount(account)) {
                        primaryAccount = account;
                        break;
                    }
                }

                if (primaryAccount != null) {
                    String[] params = new String[] {primaryAccount.name, primaryAccount.type};

                    mDb.execSQL(
                            "UPDATE " + Tables.RAW_CONTACTS +
                            " SET " + RawContacts.ACCOUNT_NAME + "=?,"
                                    + RawContacts.ACCOUNT_TYPE + "=?" +
                            " WHERE " + RawContacts.ACCOUNT_NAME + " IS NULL" +
                            " AND " + RawContacts.ACCOUNT_TYPE + " IS NULL", params);

                    // We don't currently support groups for unsynced accounts, so this is for
                    // the future
                    mDb.execSQL(
                            "UPDATE " + Tables.GROUPS +
                            " SET " + Groups.ACCOUNT_NAME + "=?,"
                                    + Groups.ACCOUNT_TYPE + "=?" +
                            " WHERE " + Groups.ACCOUNT_NAME + " IS NULL" +
                            " AND " + Groups.ACCOUNT_TYPE + " IS NULL", params);
                }
            }

            mDbHelper.getSyncState().onAccountsChanged(mDb, accounts);
            mDb.setTransactionSuccessful();
        } finally {
            mDb.endTransaction();
        }
    }

    /**
     * Finds all distinct accounts present in the specified table.
     */
    private void findValidAccounts(Set<Account> validAccounts, boolean[] hasUnassignedContacts,
            String table, String accountNameColumn, String accountTypeColumn) {
        Cursor c = mDb.rawQuery("SELECT DISTINCT " + accountNameColumn + "," + accountTypeColumn
                + " FROM " + table, null);
        try {
            while (c.moveToNext()) {
                if (c.isNull(0) && c.isNull(1)) {
                    hasUnassignedContacts[0] = true;
                } else {
                    validAccounts.add(new Account(c.getString(0), c.getString(1)));
                }
            }
        } finally {
            c.close();
        }
    }

    /**
     * Test all against {@link TextUtils#isEmpty(CharSequence)}.
     */
    private static boolean areAllEmpty(ContentValues values, String[] keys) {
        for (String key : keys) {
            if (!TextUtils.isEmpty(values.getAsString(key))) {
                return false;
            }
        }
        return true;
    }

    /**
     * Returns true if a value (possibly null) is specified for at least one of the supplied keys.
     */
    private static boolean areAnySpecified(ContentValues values, String[] keys) {
        for (String key : keys) {
            if (values.containsKey(key)) {
                return true;
            }
        }
        return false;
    }

    @Override
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
            String sortOrder) {
        if (VERBOSE_LOGGING) {
            Log.v(TAG, "query: " + uri);
        }

        final SQLiteDatabase db = mDbHelper.getReadableDatabase();

        SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
        String groupBy = null;
        String limit = getLimit(uri);

        // TODO: Consider writing a test case for RestrictionExceptions when you
        // write a new query() block to make sure it protects restricted data.
        final int match = sUriMatcher.match(uri);
        switch (match) {
            case SYNCSTATE:
                return mDbHelper.getSyncState().query(db, projection, selection,  selectionArgs,
                        sortOrder);

            case CONTACTS: {
                setTablesAndProjectionMapForContacts(qb, uri, projection);
                break;
            }

            case CONTACTS_ID: {
                long contactId = ContentUris.parseId(uri);
                setTablesAndProjectionMapForContacts(qb, uri, projection);
                qb.appendWhere(Contacts._ID + "=" + contactId);
                break;
            }

            case CONTACTS_LOOKUP:
            case CONTACTS_LOOKUP_ID: {
                List<String> pathSegments = uri.getPathSegments();
                int segmentCount = pathSegments.size();
                if (segmentCount < 3) {
                    throw new IllegalArgumentException("URI " + uri + " is missing a lookup key");
                }
                String lookupKey = pathSegments.get(2);
                if (segmentCount == 4) {
                    long contactId = Long.parseLong(pathSegments.get(3));
                    SQLiteQueryBuilder lookupQb = new SQLiteQueryBuilder();
                    setTablesAndProjectionMapForContacts(lookupQb, uri, projection);
                    lookupQb.appendWhere(Contacts._ID + "=" + contactId + " AND " +
                            Contacts.LOOKUP_KEY + "=");
                    lookupQb.appendWhereEscapeString(lookupKey);
                    Cursor c = query(db, lookupQb, projection, selection, selectionArgs, sortOrder,
                            groupBy, limit);
                    if (c.getCount() != 0) {
                        return c;
                    }

                    c.close();
                }

                setTablesAndProjectionMapForContacts(qb, uri, projection);
                qb.appendWhere(Contacts._ID + "=" + lookupContactIdByLookupKey(db, lookupKey));
                break;
            }

            case CONTACTS_AS_VCARD: {
                // When reading as vCard always use restricted view
                final String lookupKey = uri.getPathSegments().get(2);
                qb.setTables(mDbHelper.getContactView(true /* require restricted */));
                qb.setProjectionMap(sContactsVCardProjectionMap);
                qb.appendWhere(Contacts._ID + "=" + lookupContactIdByLookupKey(db, lookupKey));
                break;
            }

            case CONTACTS_FILTER: {
                setTablesAndProjectionMapForContacts(qb, uri, projection);
                if (uri.getPathSegments().size() > 2) {
                    String filterParam = uri.getLastPathSegment();
                    StringBuilder sb = new StringBuilder();
                    sb.append(Contacts._ID + " IN ");
                    appendContactFilterAsNestedQuery(sb, filterParam);
                    qb.appendWhere(sb.toString());
                }
                break;
            }

            case CONTACTS_STREQUENT_FILTER:
            case CONTACTS_STREQUENT: {
                String filterSql = null;
                if (match == CONTACTS_STREQUENT_FILTER
                        && uri.getPathSegments().size() > 3) {
                    String filterParam = uri.getLastPathSegment();
                    StringBuilder sb = new StringBuilder();
                    sb.append(Contacts._ID + " IN ");
                    appendContactFilterAsNestedQuery(sb, filterParam);
                    filterSql = sb.toString();
                }

                setTablesAndProjectionMapForContacts(qb, uri, projection);

                String[] starredProjection = null;
                String[] frequentProjection = null;
                if (projection != null) {
                    starredProjection = appendProjectionArg(projection, TIMES_CONTACED_SORT_COLUMN);
                    frequentProjection = appendProjectionArg(projection, TIMES_CONTACED_SORT_COLUMN);
                }

                // Build the first query for starred
                if (filterSql != null) {
                    qb.appendWhere(filterSql);
                }
                qb.setProjectionMap(sStrequentStarredProjectionMap);
                final String starredQuery = qb.buildQuery(starredProjection, Contacts.STARRED + "=1",
                        null, Contacts._ID, null, null, null);

                // Build the second query for frequent
                qb = new SQLiteQueryBuilder();
                setTablesAndProjectionMapForContacts(qb, uri, projection);
                if (filterSql != null) {
                    qb.appendWhere(filterSql);
                }
                qb.setProjectionMap(sStrequentFrequentProjectionMap);
                final String frequentQuery = qb.buildQuery(frequentProjection,
                        Contacts.TIMES_CONTACTED + " > 0 AND (" + Contacts.STARRED
                        + " = 0 OR " + Contacts.STARRED + " IS NULL)",
                        null, Contacts._ID, null, null, null);

                // Put them together
                final String query = qb.buildUnionQuery(new String[] {starredQuery, frequentQuery},
                        STREQUENT_ORDER_BY, STREQUENT_LIMIT);
                Cursor c = db.rawQuery(query, null);
                if (c != null) {
                    c.setNotificationUri(getContext().getContentResolver(),
                            ContactsContract.AUTHORITY_URI);
                }
                return c;
            }

            case CONTACTS_GROUP: {
                setTablesAndProjectionMapForContacts(qb, uri, projection);
                if (uri.getPathSegments().size() > 2) {
                    qb.appendWhere(CONTACTS_IN_GROUP_SELECT);
                    selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
                }
                break;
            }

            case CONTACTS_DATA: {
                long contactId = Long.parseLong(uri.getPathSegments().get(1));
                setTablesAndProjectionMapForData(qb, uri, projection, false);
                qb.appendWhere(" AND " + RawContacts.CONTACT_ID + "=" + contactId);
                break;
            }

            case CONTACTS_PHOTO: {
                long contactId = Long.parseLong(uri.getPathSegments().get(1));
                setTablesAndProjectionMapForData(qb, uri, projection, false);
                qb.appendWhere(" AND " + RawContacts.CONTACT_ID + "=" + contactId);
                qb.appendWhere(" AND " + Data._ID + "=" + Contacts.PHOTO_ID);
                break;
            }

            case PHONES: {
                setTablesAndProjectionMapForData(qb, uri, projection, false);
                qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Phone.CONTENT_ITEM_TYPE + "'");
                break;
            }

            case PHONES_ID: {
                setTablesAndProjectionMapForData(qb, uri, projection, false);
                qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Phone.CONTENT_ITEM_TYPE + "'");
                qb.appendWhere(" AND " + Data._ID + "=" + uri.getLastPathSegment());
                break;
            }

            case PHONES_FILTER: {
                setTablesAndProjectionMapForData(qb, uri, projection, true);
                qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Phone.CONTENT_ITEM_TYPE + "'");
                if (uri.getPathSegments().size() > 2) {
                    String filterParam = uri.getLastPathSegment();
                    StringBuilder sb = new StringBuilder();
                    sb.append("(");

                    boolean orNeeded = false;
                    String normalizedName = NameNormalizer.normalize(filterParam);
                    if (normalizedName.length() > 0) {
                        sb.append(Data.RAW_CONTACT_ID + " IN ");
                        appendRawContactsByNormalizedNameFilter(sb, normalizedName, null, false);
                        orNeeded = true;
                    }

                    if (isPhoneNumber(filterParam)) {
                        if (orNeeded) {
                            sb.append(" OR ");
                        }
                        String number = PhoneNumberUtils.convertKeypadLettersToDigits(filterParam);
                        String reversed = PhoneNumberUtils.getStrippedReversed(number);
                        sb.append(Data._ID +
                                " IN (SELECT " + PhoneLookupColumns.DATA_ID
                                  + " FROM " + Tables.PHONE_LOOKUP
                                  + " WHERE " + PhoneLookupColumns.NORMALIZED_NUMBER + " LIKE '%");
                        sb.append(reversed);
                        sb.append("')");
                    }
                    sb.append(")");
                    qb.appendWhere(" AND " + sb);
                }
                groupBy = PhoneColumns.NORMALIZED_NUMBER + "," + RawContacts.CONTACT_ID;
                if (sortOrder == null) {
                    sortOrder = Contacts.IN_VISIBLE_GROUP + " DESC, " + RawContacts.CONTACT_ID;
                }
                break;
            }

            case EMAILS: {
                setTablesAndProjectionMapForData(qb, uri, projection, false);
                qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Email.CONTENT_ITEM_TYPE + "'");
                break;
            }

            case EMAILS_ID: {
                setTablesAndProjectionMapForData(qb, uri, projection, false);
                qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Email.CONTENT_ITEM_TYPE + "'");
                qb.appendWhere(" AND " + Data._ID + "=" + uri.getLastPathSegment());
                break;
            }

            case EMAILS_LOOKUP: {
                setTablesAndProjectionMapForData(qb, uri, projection, false);
                qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Email.CONTENT_ITEM_TYPE + "'");
                if (uri.getPathSegments().size() > 2) {
                    qb.appendWhere(" AND " + Email.DATA + "=");
                    qb.appendWhereEscapeString(uri.getLastPathSegment());
                }
                break;
            }

            case EMAILS_FILTER: {
                setTablesAndProjectionMapForData(qb, uri, projection, true);
                String filterParam = null;
                if (uri.getPathSegments().size() > 3) {
                    filterParam = uri.getLastPathSegment();
                    if (TextUtils.isEmpty(filterParam)) {
                        filterParam = null;
                    }
                }

                if (filterParam == null) {
                    // If the filter is unspecified, return nothing
                    qb.appendWhere(" AND 0");
                } else {
                    StringBuilder sb = new StringBuilder();
                    sb.append(" AND " + Data._ID + " IN (");
                    sb.append(
                            "SELECT " + Data._ID +
                            " FROM " + Tables.DATA +
                            " WHERE " + DataColumns.MIMETYPE_ID + "=" + mMimeTypeIdEmail +
                            " AND " + Data.DATA1 + " LIKE ");
                    DatabaseUtils.appendEscapedSQLString(sb, filterParam + '%');
                    if (!filterParam.contains("@")) {
                        String normalizedName = NameNormalizer.normalize(filterParam);
                        if (normalizedName.length() > 0) {

                            /*
                             * Using a UNION instead of an "OR" to make SQLite use the right
                             * indexes. We need it to use the (mimetype,data1) index for the
                             * email lookup (see above), but not for the name lookup.
                             * SQLite is not smart enough to use the index on one side of an OR
                             * but not on the other. Using two separate nested queries
                             * and a UNION between them does the job.
                             */
                            sb.append(
                                    " UNION SELECT " + Data._ID +
                                    " FROM " + Tables.DATA +
                                    " WHERE +" + DataColumns.MIMETYPE_ID + "=" + mMimeTypeIdEmail +
                                    " AND " + Data.RAW_CONTACT_ID + " IN ");
                            appendRawContactsByNormalizedNameFilter(sb, normalizedName, null, false);
                        }
                    }
                    sb.append(")");
                    qb.appendWhere(sb);
                }
                groupBy = Email.DATA + "," + RawContacts.CONTACT_ID;
                if (sortOrder == null) {
                    sortOrder = Contacts.IN_VISIBLE_GROUP + " DESC, " + RawContacts.CONTACT_ID;
                }
                break;
            }

            case POSTALS: {
                setTablesAndProjectionMapForData(qb, uri, projection, false);
                qb.appendWhere(" AND " + Data.MIMETYPE + " = '"
                        + StructuredPostal.CONTENT_ITEM_TYPE + "'");
                break;
            }

            case POSTALS_ID: {
                setTablesAndProjectionMapForData(qb, uri, projection, false);
                qb.appendWhere(" AND " + Data.MIMETYPE + " = '"
                        + StructuredPostal.CONTENT_ITEM_TYPE + "'");
                qb.appendWhere(" AND " + Data._ID + "=" + uri.getLastPathSegment());
                break;
            }

            case RAW_CONTACTS: {
                setTablesAndProjectionMapForRawContacts(qb, uri);
                break;
            }

            case RAW_CONTACTS_ID: {
                long rawContactId = ContentUris.parseId(uri);
                setTablesAndProjectionMapForRawContacts(qb, uri);
                qb.appendWhere(" AND " + RawContacts._ID + "=" + rawContactId);
                break;
            }

            case RAW_CONTACTS_DATA: {
                long rawContactId = Long.parseLong(uri.getPathSegments().get(1));
                setTablesAndProjectionMapForData(qb, uri, projection, false);
                qb.appendWhere(" AND " + Data.RAW_CONTACT_ID + "=" + rawContactId);
                break;
            }

            case DATA: {
                setTablesAndProjectionMapForData(qb, uri, projection, false);
                break;
            }

            case DATA_ID: {
                setTablesAndProjectionMapForData(qb, uri, projection, false);
                qb.appendWhere(" AND " + Data._ID + "=" + ContentUris.parseId(uri));
                break;
            }

            case PHONE_LOOKUP: {

                if (TextUtils.isEmpty(sortOrder)) {
                    // Default the sort order to something reasonable so we get consistent
                    // results when callers don't request an ordering
                    sortOrder = RawContactsColumns.CONCRETE_ID;
                }

                String number = uri.getPathSegments().size() > 1 ? uri.getLastPathSegment() : "";
                mDbHelper.buildPhoneLookupAndContactQuery(qb, number);
                qb.setProjectionMap(sPhoneLookupProjectionMap);

                // Phone lookup cannot be combined with a selection
                selection = null;
                selectionArgs = null;
                break;
            }

            case GROUPS: {
                qb.setTables(mDbHelper.getGroupView());
                qb.setProjectionMap(sGroupsProjectionMap);
                appendAccountFromParameter(qb, uri);
                break;
            }

            case GROUPS_ID: {
                long groupId = ContentUris.parseId(uri);
                qb.setTables(mDbHelper.getGroupView());
                qb.setProjectionMap(sGroupsProjectionMap);
                qb.appendWhere(Groups._ID + "=" + groupId);
                break;
            }

            case GROUPS_SUMMARY: {
                qb.setTables(mDbHelper.getGroupView() + " AS groups");
                qb.setProjectionMap(sGroupsSummaryProjectionMap);
                appendAccountFromParameter(qb, uri);
                groupBy = Groups._ID;
                break;
            }

            case AGGREGATION_EXCEPTIONS: {
                qb.setTables(Tables.AGGREGATION_EXCEPTIONS);
                qb.setProjectionMap(sAggregationExceptionsProjectionMap);
                break;
            }

            case AGGREGATION_SUGGESTIONS: {
                long contactId = Long.parseLong(uri.getPathSegments().get(1));
                String filter = null;
                if (uri.getPathSegments().size() > 3) {
                    filter = uri.getPathSegments().get(3);
                }
                final int maxSuggestions;
                if (limit != null) {
                    maxSuggestions = Integer.parseInt(limit);
                } else {
                    maxSuggestions = DEFAULT_MAX_SUGGESTIONS;
                }

                setTablesAndProjectionMapForContacts(qb, uri, projection);

                return mContactAggregator.queryAggregationSuggestions(qb, projection, contactId,
                        maxSuggestions, filter);
            }

            case SETTINGS: {
                qb.setTables(Tables.SETTINGS);
                qb.setProjectionMap(sSettingsProjectionMap);
                appendAccountFromParameter(qb, uri);

                // When requesting specific columns, this query requires
                // late-binding of the GroupMembership MIME-type.
                final String groupMembershipMimetypeId = Long.toString(mDbHelper
                        .getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE));
                if (projection != null && projection.length != 0 &&
                        mDbHelper.isInProjection(projection, Settings.UNGROUPED_COUNT)) {
                    selectionArgs = insertSelectionArg(selectionArgs, groupMembershipMimetypeId);
                }
                if (projection != null && projection.length != 0 &&
                        mDbHelper.isInProjection(projection, Settings.UNGROUPED_WITH_PHONES)) {
                    selectionArgs = insertSelectionArg(selectionArgs, groupMembershipMimetypeId);
                }

                break;
            }

            case STATUS_UPDATES: {
                setTableAndProjectionMapForStatusUpdates(qb, projection);
                break;
            }

            case STATUS_UPDATES_ID: {
                setTableAndProjectionMapForStatusUpdates(qb, projection);
                qb.appendWhere(DataColumns.CONCRETE_ID + "=" + ContentUris.parseId(uri));
                break;
            }

            case SEARCH_SUGGESTIONS: {
                return mGlobalSearchSupport.handleSearchSuggestionsQuery(db, uri, limit);
            }

            case SEARCH_SHORTCUT: {
                long contactId = ContentUris.parseId(uri);
                return mGlobalSearchSupport.handleSearchShortcutRefresh(db, contactId, projection);
            }

            case LIVE_FOLDERS_CONTACTS:
                qb.setTables(mDbHelper.getContactView());
                qb.setProjectionMap(sLiveFoldersProjectionMap);
                break;

            case LIVE_FOLDERS_CONTACTS_WITH_PHONES:
                qb.setTables(mDbHelper.getContactView());
                qb.setProjectionMap(sLiveFoldersProjectionMap);
                qb.appendWhere(Contacts.HAS_PHONE_NUMBER + "=1");
                break;

            case LIVE_FOLDERS_CONTACTS_FAVORITES:
                qb.setTables(mDbHelper.getContactView());
                qb.setProjectionMap(sLiveFoldersProjectionMap);
                qb.appendWhere(Contacts.STARRED + "=1");
                break;

            case LIVE_FOLDERS_CONTACTS_GROUP_NAME:
                qb.setTables(mDbHelper.getContactView());
                qb.setProjectionMap(sLiveFoldersProjectionMap);
                qb.appendWhere(CONTACTS_IN_GROUP_SELECT);
                selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
                break;

            case RAW_CONTACT_ENTITIES: {
                setTablesAndProjectionMapForRawContactsEntities(qb, uri);
                break;
            }

            case RAW_CONTACT_ENTITY_ID: {
                long rawContactId = Long.parseLong(uri.getPathSegments().get(1));
                setTablesAndProjectionMapForRawContactsEntities(qb, uri);
                qb.appendWhere(" AND " + RawContacts._ID + "=" + rawContactId);
                break;
            }

            default:
                return mLegacyApiSupport.query(uri, projection, selection, selectionArgs,
                        sortOrder, limit);
        }

        return query(db, qb, projection, selection, selectionArgs, sortOrder, groupBy, limit);
    }

    private Cursor query(final SQLiteDatabase db, SQLiteQueryBuilder qb, String[] projection,
            String selection, String[] selectionArgs, String sortOrder, String groupBy,
            String limit) {
        if (projection != null && projection.length == 1
                && BaseColumns._COUNT.equals(projection[0])) {
            qb.setProjectionMap(sCountProjectionMap);
        }
        final Cursor c = qb.query(db, projection, selection, selectionArgs, groupBy, null,
                sortOrder, limit);
        if (c != null) {
            c.setNotificationUri(getContext().getContentResolver(), ContactsContract.AUTHORITY_URI);
        }
        return c;
    }

    private long lookupContactIdByLookupKey(SQLiteDatabase db, String lookupKey) {
        ContactLookupKey key = new ContactLookupKey();
        ArrayList<LookupKeySegment> segments = key.parse(lookupKey);

        long contactId = lookupContactIdBySourceIds(db, segments);
        if (contactId == -1) {
            contactId = lookupContactIdByDisplayNames(db, segments);
        }

        return contactId;
    }

    private interface LookupBySourceIdQuery {
        String TABLE = Tables.RAW_CONTACTS;

        String COLUMNS[] = {
                RawContacts.CONTACT_ID,
                RawContacts.ACCOUNT_TYPE,
                RawContacts.ACCOUNT_NAME,
                RawContacts.SOURCE_ID
        };

        int CONTACT_ID = 0;
        int ACCOUNT_TYPE = 1;
        int ACCOUNT_NAME = 2;
        int SOURCE_ID = 3;
    }

    private long lookupContactIdBySourceIds(SQLiteDatabase db,
                ArrayList<LookupKeySegment> segments) {
        int sourceIdCount = 0;
        for (int i = 0; i < segments.size(); i++) {
            LookupKeySegment segment = segments.get(i);
            if (segment.sourceIdLookup) {
                sourceIdCount++;
            }
        }

        if (sourceIdCount == 0) {
            return -1;
        }

        // First try sync ids
        StringBuilder sb = new StringBuilder();
        sb.append(RawContacts.SOURCE_ID + " IN (");
        for (int i = 0; i < segments.size(); i++) {
            LookupKeySegment segment = segments.get(i);
            if (segment.sourceIdLookup) {
                DatabaseUtils.appendEscapedSQLString(sb, segment.key);
                sb.append(",");
            }
        }
        sb.setLength(sb.length() - 1);      // Last comma
        sb.append(") AND " + RawContacts.CONTACT_ID + " NOT NULL");

        Cursor c = db.query(LookupBySourceIdQuery.TABLE, LookupBySourceIdQuery.COLUMNS,
                 sb.toString(), null, null, null, null);
        try {
            while (c.moveToNext()) {
                String accountType = c.getString(LookupBySourceIdQuery.ACCOUNT_TYPE);
                String accountName = c.getString(LookupBySourceIdQuery.ACCOUNT_NAME);
                int accountHashCode =
                        ContactLookupKey.getAccountHashCode(accountType, accountName);
                String sourceId = c.getString(LookupBySourceIdQuery.SOURCE_ID);
                for (int i = 0; i < segments.size(); i++) {
                    LookupKeySegment segment = segments.get(i);
                    if (segment.sourceIdLookup && accountHashCode == segment.accountHashCode
                            && segment.key.equals(sourceId)) {
                        segment.contactId = c.getLong(LookupBySourceIdQuery.CONTACT_ID);
                        break;
                    }
                }
            }
        } finally {
            c.close();
        }

        return getMostReferencedContactId(segments);
    }

    private interface LookupByDisplayNameQuery {
        String TABLE = Tables.NAME_LOOKUP_JOIN_RAW_CONTACTS;

        String COLUMNS[] = {
                RawContacts.CONTACT_ID,
                RawContacts.ACCOUNT_TYPE,
                RawContacts.ACCOUNT_NAME,
                NameLookupColumns.NORMALIZED_NAME
        };

        int CONTACT_ID = 0;
        int ACCOUNT_TYPE = 1;
        int ACCOUNT_NAME = 2;
        int NORMALIZED_NAME = 3;
    }

    private long lookupContactIdByDisplayNames(SQLiteDatabase db,
                ArrayList<LookupKeySegment> segments) {
        int displayNameCount = 0;
        for (int i = 0; i < segments.size(); i++) {
            LookupKeySegment segment = segments.get(i);
            if (!segment.sourceIdLookup) {
                displayNameCount++;
            }
        }

        if (displayNameCount == 0) {
            return -1;
        }

        // First try sync ids
        StringBuilder sb = new StringBuilder();
        sb.append(NameLookupColumns.NORMALIZED_NAME + " IN (");
        for (int i = 0; i < segments.size(); i++) {
            LookupKeySegment segment = segments.get(i);
            if (!segment.sourceIdLookup) {
                DatabaseUtils.appendEscapedSQLString(sb, segment.key);
                sb.append(",");
            }
        }
        sb.setLength(sb.length() - 1);      // Last comma
        sb.append(") AND " + NameLookupColumns.NAME_TYPE + "=" + NameLookupType.NAME_COLLATION_KEY
                + " AND " + RawContacts.CONTACT_ID + " NOT NULL");

        Cursor c = db.query(LookupByDisplayNameQuery.TABLE, LookupByDisplayNameQuery.COLUMNS,
                 sb.toString(), null, null, null, null);
        try {
            while (c.moveToNext()) {
                String accountType = c.getString(LookupByDisplayNameQuery.ACCOUNT_TYPE);
                String accountName = c.getString(LookupByDisplayNameQuery.ACCOUNT_NAME);
                int accountHashCode =
                        ContactLookupKey.getAccountHashCode(accountType, accountName);
                String name = c.getString(LookupByDisplayNameQuery.NORMALIZED_NAME);
                for (int i = 0; i < segments.size(); i++) {
                    LookupKeySegment segment = segments.get(i);
                    if (!segment.sourceIdLookup && accountHashCode == segment.accountHashCode
                            && segment.key.equals(name)) {
                        segment.contactId = c.getLong(LookupByDisplayNameQuery.CONTACT_ID);
                        break;
                    }
                }
            }
        } finally {
            c.close();
        }

        return getMostReferencedContactId(segments);
    }

    /**
     * Returns the contact ID that is mentioned the highest number of times.
     */
    private long getMostReferencedContactId(ArrayList<LookupKeySegment> segments) {
        Collections.sort(segments);

        long bestContactId = -1;
        int bestRefCount = 0;

        long contactId = -1;
        int count = 0;

        int segmentCount = segments.size();
        for (int i = 0; i < segmentCount; i++) {
            LookupKeySegment segment = segments.get(i);
            if (segment.contactId != -1) {
                if (segment.contactId == contactId) {
                    count++;
                } else {
                    if (count > bestRefCount) {
                        bestContactId = contactId;
                        bestRefCount = count;
                    }
                    contactId = segment.contactId;
                    count = 1;
                }
            }
        }
        if (count > bestRefCount) {
            return contactId;
        } else {
            return bestContactId;
        }
    }

    private void setTablesAndProjectionMapForContacts(SQLiteQueryBuilder qb, Uri uri,
            String[] projection) {
        StringBuilder sb = new StringBuilder();
        boolean excludeRestrictedData = false;
        String requestingPackage = uri.getQueryParameter(
                ContactsContract.REQUESTING_PACKAGE_PARAM_KEY);
        if (requestingPackage != null) {
            excludeRestrictedData = !mDbHelper.hasAccessToRestrictedData(requestingPackage);
        }
        sb.append(mDbHelper.getContactView(excludeRestrictedData));
        if (mDbHelper.isInProjection(projection,
                Contacts.CONTACT_PRESENCE)) {
            sb.append(" LEFT OUTER JOIN " + Tables.AGGREGATED_PRESENCE +
                    " ON (" + Contacts._ID + " = " + AggregatedPresenceColumns.CONTACT_ID + ")");
        }
        if (mDbHelper.isInProjection(projection,
                Contacts.CONTACT_STATUS,
                Contacts.CONTACT_STATUS_RES_PACKAGE,
                Contacts.CONTACT_STATUS_ICON,
                Contacts.CONTACT_STATUS_LABEL,
                Contacts.CONTACT_STATUS_TIMESTAMP)) {
            sb.append(" LEFT OUTER JOIN " + Tables.STATUS_UPDATES + " "
                    + ContactsStatusUpdatesColumns.ALIAS +
                    " ON (" + ContactsColumns.LAST_STATUS_UPDATE_ID + "="
                            + ContactsStatusUpdatesColumns.CONCRETE_DATA_ID + ")");
        }
        qb.setTables(sb.toString());
        qb.setProjectionMap(sContactsProjectionMap);
    }

    private void setTablesAndProjectionMapForRawContacts(SQLiteQueryBuilder qb, Uri uri) {
        StringBuilder sb = new StringBuilder();
        boolean excludeRestrictedData = false;
        String requestingPackage = uri.getQueryParameter(
                ContactsContract.REQUESTING_PACKAGE_PARAM_KEY);
        if (requestingPackage != null) {
            excludeRestrictedData = !mDbHelper.hasAccessToRestrictedData(requestingPackage);
        }
        sb.append(mDbHelper.getRawContactView(excludeRestrictedData));
        qb.setTables(sb.toString());
        qb.setProjectionMap(sRawContactsProjectionMap);
        appendAccountFromParameter(qb, uri);
    }

    private void setTablesAndProjectionMapForRawContactsEntities(SQLiteQueryBuilder qb, Uri uri) {
        // Note: currently, "export only" equals to "restricted", but may not in the future.
        boolean excludeRestrictedData = readBooleanQueryParameter(uri,
                Data.FOR_EXPORT_ONLY, false);

        String requestingPackage = uri.getQueryParameter(
                ContactsContract.REQUESTING_PACKAGE_PARAM_KEY);
        if (requestingPackage != null) {
            excludeRestrictedData = excludeRestrictedData
                    || !mDbHelper.hasAccessToRestrictedData(requestingPackage);
        }
        qb.setTables(mDbHelper.getContactEntitiesView(excludeRestrictedData));
        qb.setProjectionMap(sRawContactsEntityProjectionMap);
        appendAccountFromParameter(qb, uri);
    }

    private void setTablesAndProjectionMapForData(SQLiteQueryBuilder qb, Uri uri,
            String[] projection, boolean distinct) {
        StringBuilder sb = new StringBuilder();
        // Note: currently, "export only" equals to "restricted", but may not in the future.
        boolean excludeRestrictedData = readBooleanQueryParameter(uri,
                Data.FOR_EXPORT_ONLY, false);

        String requestingPackage = uri.getQueryParameter(
                ContactsContract.REQUESTING_PACKAGE_PARAM_KEY);
        if (requestingPackage != null) {
            excludeRestrictedData = excludeRestrictedData
                    || !mDbHelper.hasAccessToRestrictedData(requestingPackage);
        }

        sb.append(mDbHelper.getDataView(excludeRestrictedData));
        sb.append(" data");

        // Include aggregated presence when requested
        if (mDbHelper.isInProjection(projection, Data.CONTACT_PRESENCE)) {
            sb.append(" LEFT OUTER JOIN " + Tables.AGGREGATED_PRESENCE +
                    " ON (" + AggregatedPresenceColumns.CONCRETE_CONTACT_ID + "="
                    + RawContacts.CONTACT_ID + ")");
        }

        // Include aggregated status updates when requested
        if (mDbHelper.isInProjection(projection,
                Data.CONTACT_STATUS,
                Data.CONTACT_STATUS_RES_PACKAGE,
                Data.CONTACT_STATUS_ICON,
                Data.CONTACT_STATUS_LABEL,
                Data.CONTACT_STATUS_TIMESTAMP)) {
            sb.append(" LEFT OUTER JOIN " + Tables.STATUS_UPDATES + " "
                    + ContactsStatusUpdatesColumns.ALIAS +
                    " ON (" + ContactsColumns.LAST_STATUS_UPDATE_ID + "="
                            + ContactsStatusUpdatesColumns.CONCRETE_DATA_ID + ")");
        }

        // Include individual presence when requested
        if (mDbHelper.isInProjection(projection, Data.PRESENCE)) {
            sb.append(" LEFT OUTER JOIN " + Tables.PRESENCE +
                    " ON (" + StatusUpdates.DATA_ID + "="
                    + DataColumns.CONCRETE_ID + ")");
        }

        // Include individual status updates when requested
        if (mDbHelper.isInProjection(projection,
                Data.STATUS,
                Data.STATUS_RES_PACKAGE,
                Data.STATUS_ICON,
                Data.STATUS_LABEL,
                Data.STATUS_TIMESTAMP)) {
            sb.append(" LEFT OUTER JOIN " + Tables.STATUS_UPDATES +
                    " ON (" + StatusUpdatesColumns.CONCRETE_DATA_ID + "="
                            + DataColumns.CONCRETE_ID + ")");
        }

        qb.setTables(sb.toString());
        qb.setProjectionMap(distinct ? sDistinctDataProjectionMap : sDataProjectionMap);
        appendAccountFromParameter(qb, uri);
    }

    private void setTableAndProjectionMapForStatusUpdates(SQLiteQueryBuilder qb,
            String[] projection) {
        StringBuilder sb = new StringBuilder();
        sb.append(mDbHelper.getDataView());
        sb.append(" data");

        if (mDbHelper.isInProjection(projection, StatusUpdates.PRESENCE)) {
            sb.append(" LEFT OUTER JOIN " + Tables.PRESENCE +
                    " ON(" + Tables.PRESENCE + "." + StatusUpdates.DATA_ID
                    + "=" + DataColumns.CONCRETE_ID + ")");
        }

        if (mDbHelper.isInProjection(projection,
                StatusUpdates.STATUS,
                StatusUpdates.STATUS_RES_PACKAGE,
                StatusUpdates.STATUS_ICON,
                StatusUpdates.STATUS_LABEL,
                StatusUpdates.STATUS_TIMESTAMP)) {
            sb.append(" LEFT OUTER JOIN " + Tables.STATUS_UPDATES +
                    " ON(" + Tables.STATUS_UPDATES + "." + StatusUpdatesColumns.DATA_ID
                    + "=" + DataColumns.CONCRETE_ID + ")");
        }
        qb.setTables(sb.toString());
        qb.setProjectionMap(sStatusUpdatesProjectionMap);
    }

    private void appendAccountFromParameter(SQLiteQueryBuilder qb, Uri uri) {
        final String accountName = uri.getQueryParameter(RawContacts.ACCOUNT_NAME);
        final String accountType = uri.getQueryParameter(RawContacts.ACCOUNT_TYPE);
        if (!TextUtils.isEmpty(accountName)) {
            qb.appendWhere(RawContacts.ACCOUNT_NAME + "="
                    + DatabaseUtils.sqlEscapeString(accountName) + " AND "
                    + RawContacts.ACCOUNT_TYPE + "="
                    + DatabaseUtils.sqlEscapeString(accountType));
        } else {
            qb.appendWhere("1");
        }
    }

    private String appendAccountToSelection(Uri uri, String selection) {
        final String accountName = uri.getQueryParameter(RawContacts.ACCOUNT_NAME);
        final String accountType = uri.getQueryParameter(RawContacts.ACCOUNT_TYPE);
        if (!TextUtils.isEmpty(accountName)) {
            StringBuilder selectionSb = new StringBuilder(RawContacts.ACCOUNT_NAME + "="
                    + DatabaseUtils.sqlEscapeString(accountName) + " AND "
                    + RawContacts.ACCOUNT_TYPE + "="
                    + DatabaseUtils.sqlEscapeString(accountType));
            if (!TextUtils.isEmpty(selection)) {
                selectionSb.append(" AND (");
                selectionSb.append(selection);
                selectionSb.append(')');
            }
            return selectionSb.toString();
        } else {
            return selection;
        }
    }

    /**
     * Gets the value of the "limit" URI query parameter.
     *
     * @return A string containing a non-negative integer, or <code>null</code> if
     *         the parameter is not set, or is set to an invalid value.
     */
    private String getLimit(Uri url) {
        String limitParam = url.getQueryParameter("limit");
        if (limitParam == null) {
            return null;
        }
        // make sure that the limit is a non-negative integer
        try {
            int l = Integer.parseInt(limitParam);
            if (l < 0) {
                Log.w(TAG, "Invalid limit parameter: " + limitParam);
                return null;
            }
            return String.valueOf(l);
        } catch (NumberFormatException ex) {
            Log.w(TAG, "Invalid limit parameter: " + limitParam);
            return null;
        }
    }

    /**
     * Returns true if all the characters are meaningful as digits
     * in a phone number -- letters, digits, and a few punctuation marks.
     */
    private boolean isPhoneNumber(CharSequence cons) {
        int len = cons.length();

        for (int i = 0; i < len; i++) {
            char c = cons.charAt(i);

            if ((c >= '0') && (c <= '9')) {
                continue;
            }
            if ((c == ' ') || (c == '-') || (c == '(') || (c == ')') || (c == '.') || (c == '+')
                    || (c == '#') || (c == '*')) {
                continue;
            }
            if ((c >= 'A') && (c <= 'Z')) {
                continue;
            }
            if ((c >= 'a') && (c <= 'z')) {
                continue;
            }

            return false;
        }

        return true;
    }

    String getContactsRestrictions() {
        if (mDbHelper.hasAccessToRestrictedData()) {
            return "1";
        } else {
            return RawContacts.IS_RESTRICTED + "=0";
        }
    }

    public String getContactsRestrictionExceptionAsNestedQuery(String contactIdColumn) {
        if (mDbHelper.hasAccessToRestrictedData()) {
            return "1";
        } else {
            return "(SELECT " + RawContacts.IS_RESTRICTED + " FROM " + Tables.RAW_CONTACTS
                    + " WHERE " + RawContactsColumns.CONCRETE_ID + "=" + contactIdColumn + ")=0";
        }
    }

    @Override
    public AssetFileDescriptor openAssetFile(Uri uri, String mode) throws FileNotFoundException {
        int match = sUriMatcher.match(uri);
        switch (match) {
            case CONTACTS_PHOTO: {
                if (!"r".equals(mode)) {
                    throw new FileNotFoundException("Mode " + mode + " not supported.");
                }

                long contactId = Long.parseLong(uri.getPathSegments().get(1));

                String sql =
                        "SELECT " + Photo.PHOTO + " FROM " + mDbHelper.getDataView() +
                        " WHERE " + Data._ID + "=" + Contacts.PHOTO_ID
                                + " AND " + RawContacts.CONTACT_ID + "=" + contactId;
                SQLiteDatabase db = mDbHelper.getReadableDatabase();
                return SQLiteContentHelper.getBlobColumnAsAssetFile(db, sql, null);
            }

            case CONTACTS_AS_VCARD: {
                final String lookupKey = uri.getPathSegments().get(2);
                final long contactId = lookupContactIdByLookupKey(mDb, lookupKey);
                final String selection = Contacts._ID + "=" + contactId;

                // When opening a contact as file, we pass back contents as a
                // vCard-encoded stream. We build into a local buffer first,
                // then pipe into MemoryFile once the exact size is known.
                final ByteArrayOutputStream localStream = new ByteArrayOutputStream();
                outputRawContactsAsVCard(localStream, selection, null);
                return buildAssetFileDescriptor(localStream);
            }

            default:
                throw new FileNotFoundException("No file at: " + uri);
        }
    }

    private static final String CONTACT_MEMORY_FILE_NAME = "contactAssetFile";
    private static final String VCARD_TYPE_DEFAULT = "default";

    /**
     * Build a {@link AssetFileDescriptor} through a {@link MemoryFile} with the
     * contents of the given {@link ByteArrayOutputStream}.
     */
    private AssetFileDescriptor buildAssetFileDescriptor(ByteArrayOutputStream stream) {
        AssetFileDescriptor fd = null;
        try {
            stream.flush();

            final byte[] byteData = stream.toByteArray();
            final int size = byteData.length;

            final MemoryFile memoryFile = new MemoryFile(CONTACT_MEMORY_FILE_NAME, size);
            memoryFile.writeBytes(byteData, 0, 0, size);
            memoryFile.deactivate();

            fd = AssetFileDescriptor.fromMemoryFile(memoryFile);
        } catch (IOException e) {
            Log.w(TAG, "Problem writing stream into an AssetFileDescriptor: " + e.toString());
        }
        return fd;
    }

    /**
     * Output {@link RawContacts} matching the requested selection in the vCard
     * format to the given {@link OutputStream}. This method returns silently if
     * any errors encountered.
     */
    private void outputRawContactsAsVCard(OutputStream stream, String selection,
            String[] selectionArgs) {
        final Context context = this.getContext();
        final VCardComposer composer = new VCardComposer(context, VCARD_TYPE_DEFAULT, false);
        composer.addHandler(composer.new HandlerForOutputStream(stream));

        // No extra checks since composer always uses restricted views
        if (!composer.init(selection, selectionArgs))
            return;

        while (!composer.isAfterLast()) {
            if (!composer.createOneEntry()) {
                Log.w(TAG, "Failed to output a contact.");
            }
        }
        composer.terminate();
    }


    private static Account readAccountFromQueryParams(Uri uri) {
        final String name = uri.getQueryParameter(RawContacts.ACCOUNT_NAME);
        final String type = uri.getQueryParameter(RawContacts.ACCOUNT_TYPE);
        if (TextUtils.isEmpty(name) || TextUtils.isEmpty(type)) {
            return null;
        }
        return new Account(name, type);
    }


    /**
     * An implementation of EntityIterator that joins the contacts and data tables
     * and consumes all the data rows for a contact in order to build the Entity for a contact.
     */
    private static class RawContactsEntityIterator implements EntityIterator {
        private final Cursor mEntityCursor;
        private volatile boolean mIsClosed;

        private static final String[] DATA_KEYS = new String[]{
                Data.DATA1,
                Data.DATA2,
                Data.DATA3,
                Data.DATA4,
                Data.DATA5,
                Data.DATA6,
                Data.DATA7,
                Data.DATA8,
                Data.DATA9,
                Data.DATA10,
                Data.DATA11,
                Data.DATA12,
                Data.DATA13,
                Data.DATA14,
                Data.DATA15,
                Data.SYNC1,
                Data.SYNC2,
                Data.SYNC3,
                Data.SYNC4};

        public static final String[] PROJECTION = new String[]{
                RawContacts.ACCOUNT_NAME,
                RawContacts.ACCOUNT_TYPE,
                RawContacts.SOURCE_ID,
                RawContacts.VERSION,
                RawContacts.DIRTY,
                RawContacts.Entity.DATA_ID,
                Data.RES_PACKAGE,
                Data.MIMETYPE,
                Data.DATA1,
                Data.DATA2,
                Data.DATA3,
                Data.DATA4,
                Data.DATA5,
                Data.DATA6,
                Data.DATA7,
                Data.DATA8,
                Data.DATA9,
                Data.DATA10,
                Data.DATA11,
                Data.DATA12,
                Data.DATA13,
                Data.DATA14,
                Data.DATA15,
                Data.SYNC1,
                Data.SYNC2,
                Data.SYNC3,
                Data.SYNC4,
                RawContacts._ID,
                Data.IS_PRIMARY,
                Data.IS_SUPER_PRIMARY,
                Data.DATA_VERSION,
                GroupMembership.GROUP_SOURCE_ID,
                RawContacts.SYNC1,
                RawContacts.SYNC2,
                RawContacts.SYNC3,
                RawContacts.SYNC4,
                RawContacts.DELETED,
                RawContacts.CONTACT_ID,
                RawContacts.STARRED,
                RawContacts.IS_RESTRICTED};

        private static final int COLUMN_ACCOUNT_NAME = 0;
        private static final int COLUMN_ACCOUNT_TYPE = 1;
        private static final int COLUMN_SOURCE_ID = 2;
        private static final int COLUMN_VERSION = 3;
        private static final int COLUMN_DIRTY = 4;
        private static final int COLUMN_DATA_ID = 5;
        private static final int COLUMN_RES_PACKAGE = 6;
        private static final int COLUMN_MIMETYPE = 7;
        private static final int COLUMN_DATA1 = 8;
        private static final int COLUMN_RAW_CONTACT_ID = 27;
        private static final int COLUMN_IS_PRIMARY = 28;
        private static final int COLUMN_IS_SUPER_PRIMARY = 29;
        private static final int COLUMN_DATA_VERSION = 30;
        private static final int COLUMN_GROUP_SOURCE_ID = 31;
        private static final int COLUMN_SYNC1 = 32;
        private static final int COLUMN_SYNC2 = 33;
        private static final int COLUMN_SYNC3 = 34;
        private static final int COLUMN_SYNC4 = 35;
        private static final int COLUMN_DELETED = 36;
        private static final int COLUMN_CONTACT_ID = 37;
        private static final int COLUMN_STARRED = 38;
        private static final int COLUMN_IS_RESTRICTED = 39;

        public RawContactsEntityIterator(ContactsProvider2 provider, Uri entityUri,
                String contactsIdString,
                String selection, String[] selectionArgs, String sortOrder) {
            mIsClosed = false;
            Uri uri;
            if (contactsIdString != null) {
                uri = Uri.withAppendedPath(RawContacts.CONTENT_URI, contactsIdString);
                uri = Uri.withAppendedPath(uri, RawContacts.Entity.CONTENT_DIRECTORY);
            } else {
                uri = ContactsContract.RawContactsEntity.CONTENT_URI;
            }
            final Uri.Builder builder = uri.buildUpon();
            String query = entityUri.getQuery();
            builder.encodedQuery(query);
            mEntityCursor = provider.query(builder.build(),
                    PROJECTION, selection, selectionArgs, sortOrder);
            mEntityCursor.moveToFirst();
        }

        public void reset() throws RemoteException {
            if (mIsClosed) {
                throw new IllegalStateException("calling reset() when the iterator is closed");
            }
            mEntityCursor.moveToFirst();
        }

        public void close() {
            if (mIsClosed) {
                throw new IllegalStateException("closing when already closed");
            }
            mIsClosed = true;
            mEntityCursor.close();
        }

        public boolean hasNext() throws RemoteException {
            if (mIsClosed) {
                throw new IllegalStateException("calling hasNext() when the iterator is closed");
            }

            return !mEntityCursor.isAfterLast();
        }

        public Entity next() throws RemoteException {
            if (mIsClosed) {
                throw new IllegalStateException("calling next() when the iterator is closed");
            }
            if (!hasNext()) {
                throw new IllegalStateException("you may only call next() if hasNext() is true");
            }

            final SQLiteCursor c = (SQLiteCursor) mEntityCursor;

            final long rawContactId = c.getLong(COLUMN_RAW_CONTACT_ID);

            // we expect the cursor is already at the row we need to read from
            ContentValues contactValues = new ContentValues();
            contactValues.put(RawContacts.ACCOUNT_NAME, c.getString(COLUMN_ACCOUNT_NAME));
            contactValues.put(RawContacts.ACCOUNT_TYPE, c.getString(COLUMN_ACCOUNT_TYPE));
            contactValues.put(RawContacts._ID, rawContactId);
            contactValues.put(RawContacts.DIRTY, c.getLong(COLUMN_DIRTY));
            contactValues.put(RawContacts.VERSION, c.getLong(COLUMN_VERSION));
            contactValues.put(RawContacts.SOURCE_ID, c.getString(COLUMN_SOURCE_ID));
            contactValues.put(RawContacts.SYNC1, c.getString(COLUMN_SYNC1));
            contactValues.put(RawContacts.SYNC2, c.getString(COLUMN_SYNC2));
            contactValues.put(RawContacts.SYNC3, c.getString(COLUMN_SYNC3));
            contactValues.put(RawContacts.SYNC4, c.getString(COLUMN_SYNC4));
            contactValues.put(RawContacts.DELETED, c.getLong(COLUMN_DELETED));
            contactValues.put(RawContacts.CONTACT_ID, c.getLong(COLUMN_CONTACT_ID));
            contactValues.put(RawContacts.STARRED, c.getLong(COLUMN_STARRED));
            contactValues.put(RawContacts.IS_RESTRICTED, c.getInt(COLUMN_IS_RESTRICTED));
            Entity contact = new Entity(contactValues);

            // read data rows until the contact id changes
            do {
                if (rawContactId != c.getLong(COLUMN_RAW_CONTACT_ID)) {
                    break;
                }
//                if (c.isNull(COLUMN_CONTACT_ID)) {
//                    continue;
//                }
                // add the data to to the contact
                ContentValues dataValues = new ContentValues();
                dataValues.put(Data._ID, c.getLong(COLUMN_DATA_ID));
                dataValues.put(Data.RES_PACKAGE, c.getString(COLUMN_RES_PACKAGE));
                dataValues.put(Data.MIMETYPE, c.getString(COLUMN_MIMETYPE));
                dataValues.put(Data.IS_PRIMARY, c.getLong(COLUMN_IS_PRIMARY));
                dataValues.put(Data.IS_SUPER_PRIMARY, c.getLong(COLUMN_IS_SUPER_PRIMARY));
                dataValues.put(Data.DATA_VERSION, c.getLong(COLUMN_DATA_VERSION));
                if (!c.isNull(COLUMN_GROUP_SOURCE_ID)) {
                    dataValues.put(GroupMembership.GROUP_SOURCE_ID,
                            c.getString(COLUMN_GROUP_SOURCE_ID));
                }
                dataValues.put(Data.DATA_VERSION, c.getLong(COLUMN_DATA_VERSION));
                for (int i = 0; i < DATA_KEYS.length; i++) {
                    final int columnIndex = i + COLUMN_DATA1;
                    String key = DATA_KEYS[i];
                    if (c.isNull(columnIndex)) {
                        // don't put anything
                    } else if (c.isLong(columnIndex)) {
                        dataValues.put(key, c.getLong(columnIndex));
                    } else if (c.isFloat(columnIndex)) {
                        dataValues.put(key, c.getFloat(columnIndex));
                    } else if (c.isString(columnIndex)) {
                        dataValues.put(key, c.getString(columnIndex));
                    } else if (c.isBlob(columnIndex)) {
                        dataValues.put(key, c.getBlob(columnIndex));
                    }
                }
                contact.addSubValue(Data.CONTENT_URI, dataValues);
            } while (mEntityCursor.moveToNext());

            return contact;
        }
    }

    /**
     * An implementation of EntityIterator that joins the contacts and data tables
     * and consumes all the data rows for a contact in order to build the Entity for a contact.
     */
    private static class GroupsEntityIterator implements EntityIterator {
        private final Cursor mEntityCursor;
        private volatile boolean mIsClosed;

        private static final String[] PROJECTION = new String[]{
                Groups._ID,
                Groups.ACCOUNT_NAME,
                Groups.ACCOUNT_TYPE,
                Groups.SOURCE_ID,
                Groups.DIRTY,
                Groups.VERSION,
                Groups.RES_PACKAGE,
                Groups.TITLE,
                Groups.TITLE_RES,
                Groups.GROUP_VISIBLE,
                Groups.SYNC1,
                Groups.SYNC2,
                Groups.SYNC3,
                Groups.SYNC4,
                Groups.SYSTEM_ID,
                Groups.NOTES,
                Groups.DELETED,
                Groups.SHOULD_SYNC};

        private static final int COLUMN_ID = 0;
        private static final int COLUMN_ACCOUNT_NAME = 1;
        private static final int COLUMN_ACCOUNT_TYPE = 2;
        private static final int COLUMN_SOURCE_ID = 3;
        private static final int COLUMN_DIRTY = 4;
        private static final int COLUMN_VERSION = 5;
        private static final int COLUMN_RES_PACKAGE = 6;
        private static final int COLUMN_TITLE = 7;
        private static final int COLUMN_TITLE_RES = 8;
        private static final int COLUMN_GROUP_VISIBLE = 9;
        private static final int COLUMN_SYNC1 = 10;
        private static final int COLUMN_SYNC2 = 11;
        private static final int COLUMN_SYNC3 = 12;
        private static final int COLUMN_SYNC4 = 13;
        private static final int COLUMN_SYSTEM_ID = 14;
        private static final int COLUMN_NOTES = 15;
        private static final int COLUMN_DELETED = 16;
        private static final int COLUMN_SHOULD_SYNC = 17;

        public GroupsEntityIterator(ContactsProvider2 provider, String groupIdString, Uri uri,
                String selection, String[] selectionArgs, String sortOrder) {
            mIsClosed = false;

            final String updatedSortOrder = (sortOrder == null)
                    ? Groups._ID
                    : (Groups._ID + "," + sortOrder);

            final SQLiteDatabase db = provider.mDbHelper.getReadableDatabase();
            final SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
            qb.setTables(provider.mDbHelper.getGroupView());
            qb.setProjectionMap(sGroupsProjectionMap);
            if (groupIdString != null) {
                qb.appendWhere(Groups._ID + "=" + groupIdString);
            }
            final String accountName = uri.getQueryParameter(Groups.ACCOUNT_NAME);
            final String accountType = uri.getQueryParameter(Groups.ACCOUNT_TYPE);
            if (!TextUtils.isEmpty(accountName)) {
                qb.appendWhere(Groups.ACCOUNT_NAME + "="
                        + DatabaseUtils.sqlEscapeString(accountName) + " AND "
                        + Groups.ACCOUNT_TYPE + "="
                        + DatabaseUtils.sqlEscapeString(accountType));
            }
            mEntityCursor = qb.query(db, PROJECTION, selection, selectionArgs,
                    null, null, updatedSortOrder);
            mEntityCursor.moveToFirst();
        }

        public void close() {
            if (mIsClosed) {
                throw new IllegalStateException("closing when already closed");
            }
            mIsClosed = true;
            mEntityCursor.close();
        }

        public boolean hasNext() throws RemoteException {
            if (mIsClosed) {
                throw new IllegalStateException("calling hasNext() when the iterator is closed");
            }

            return !mEntityCursor.isAfterLast();
        }

        public void reset() throws RemoteException {
            if (mIsClosed) {
                throw new IllegalStateException("calling reset() when the iterator is closed");
            }
            mEntityCursor.moveToFirst();
        }

        public Entity next() throws RemoteException {
            if (mIsClosed) {
                throw new IllegalStateException("calling next() when the iterator is closed");
            }
            if (!hasNext()) {
                throw new IllegalStateException("you may only call next() if hasNext() is true");
            }

            final SQLiteCursor c = (SQLiteCursor) mEntityCursor;

            final long groupId = c.getLong(COLUMN_ID);

            // we expect the cursor is already at the row we need to read from
            ContentValues groupValues = new ContentValues();
            groupValues.put(Groups.ACCOUNT_NAME, c.getString(COLUMN_ACCOUNT_NAME));
            groupValues.put(Groups.ACCOUNT_TYPE, c.getString(COLUMN_ACCOUNT_TYPE));
            groupValues.put(Groups._ID, groupId);
            groupValues.put(Groups.DIRTY, c.getLong(COLUMN_DIRTY));
            groupValues.put(Groups.VERSION, c.getLong(COLUMN_VERSION));
            groupValues.put(Groups.SOURCE_ID, c.getString(COLUMN_SOURCE_ID));
            groupValues.put(Groups.RES_PACKAGE, c.getString(COLUMN_RES_PACKAGE));
            groupValues.put(Groups.TITLE, c.getString(COLUMN_TITLE));
            groupValues.put(Groups.TITLE_RES, c.getString(COLUMN_TITLE_RES));
            groupValues.put(Groups.GROUP_VISIBLE, c.getLong(COLUMN_GROUP_VISIBLE));
            groupValues.put(Groups.SYNC1, c.getString(COLUMN_SYNC1));
            groupValues.put(Groups.SYNC2, c.getString(COLUMN_SYNC2));
            groupValues.put(Groups.SYNC3, c.getString(COLUMN_SYNC3));
            groupValues.put(Groups.SYNC4, c.getString(COLUMN_SYNC4));
            groupValues.put(Groups.SYSTEM_ID, c.getString(COLUMN_SYSTEM_ID));
            groupValues.put(Groups.DELETED, c.getLong(COLUMN_DELETED));
            groupValues.put(Groups.NOTES, c.getString(COLUMN_NOTES));
            groupValues.put(Groups.SHOULD_SYNC, c.getString(COLUMN_SHOULD_SYNC));
            Entity group = new Entity(groupValues);

            mEntityCursor.moveToNext();

            return group;
        }
    }

    @Override
    public EntityIterator queryEntities(Uri uri, String selection, String[] selectionArgs,
            String sortOrder) {
        waitForAccess();

        final int match = sUriMatcher.match(uri);
        switch (match) {
            case RAW_CONTACTS:
            case RAW_CONTACTS_ID:
                String contactsIdString = null;
                if (match == RAW_CONTACTS_ID) {
                    contactsIdString = uri.getPathSegments().get(1);
                }

                return new RawContactsEntityIterator(this, uri, contactsIdString,
                        selection, selectionArgs, sortOrder);
            case GROUPS:
            case GROUPS_ID:
                String idString = null;
                if (match == GROUPS_ID) {
                    idString = uri.getPathSegments().get(1);
                }

                return new GroupsEntityIterator(this, idString,
                        uri, selection, selectionArgs, sortOrder);
            default:
                throw new UnsupportedOperationException("Unknown uri: " + uri);
        }
    }

    @Override
    public String getType(Uri uri) {
        final int match = sUriMatcher.match(uri);
        switch (match) {
            case CONTACTS:
            case CONTACTS_LOOKUP:
                return Contacts.CONTENT_TYPE;
            case CONTACTS_ID:
            case CONTACTS_LOOKUP_ID:
                return Contacts.CONTENT_ITEM_TYPE;
            case CONTACTS_AS_VCARD:
                return Contacts.CONTENT_VCARD_TYPE;
            case RAW_CONTACTS:
                return RawContacts.CONTENT_TYPE;
            case RAW_CONTACTS_ID:
                return RawContacts.CONTENT_ITEM_TYPE;
            case DATA_ID:
                return mDbHelper.getDataMimeType(ContentUris.parseId(uri));
            case PHONES:
                return Phone.CONTENT_TYPE;
            case PHONES_ID:
                return Phone.CONTENT_ITEM_TYPE;
            case EMAILS:
                return Email.CONTENT_TYPE;
            case EMAILS_ID:
                return Email.CONTENT_ITEM_TYPE;
            case POSTALS:
                return StructuredPostal.CONTENT_TYPE;
            case POSTALS_ID:
                return StructuredPostal.CONTENT_ITEM_TYPE;
            case AGGREGATION_EXCEPTIONS:
                return AggregationExceptions.CONTENT_TYPE;
            case AGGREGATION_EXCEPTION_ID:
                return AggregationExceptions.CONTENT_ITEM_TYPE;
            case SETTINGS:
                return Settings.CONTENT_TYPE;
            case AGGREGATION_SUGGESTIONS:
                return Contacts.CONTENT_TYPE;
            case SEARCH_SUGGESTIONS:
                return SearchManager.SUGGEST_MIME_TYPE;
            case SEARCH_SHORTCUT:
                return SearchManager.SHORTCUT_MIME_TYPE;
            default:
                return mLegacyApiSupport.getType(uri);
        }
    }

    private void setDisplayName(long rawContactId, String displayName, int bestDisplayNameSource) {
        if (displayName != null) {
            mRawContactDisplayNameUpdate.bindString(1, displayName);
        } else {
            mRawContactDisplayNameUpdate.bindNull(1);
        }
        mRawContactDisplayNameUpdate.bindLong(2, bestDisplayNameSource);
        mRawContactDisplayNameUpdate.bindLong(3, rawContactId);
        mRawContactDisplayNameUpdate.execute();
    }

    /**
     * Sets the {@link RawContacts#DIRTY} for the specified raw contact.
     */
    private void setRawContactDirty(long rawContactId) {
        mRawContactDirtyUpdate.bindLong(1, rawContactId);
        mRawContactDirtyUpdate.execute();
    }

    /*
     * Sets the given dataId record in the "data" table to primary, and resets all data records of
     * the same mimetype and under the same contact to not be primary.
     *
     * @param dataId the id of the data record to be set to primary.
     */
    private void setIsPrimary(long rawContactId, long dataId, long mimeTypeId) {
        mSetPrimaryStatement.bindLong(1, dataId);
        mSetPrimaryStatement.bindLong(2, mimeTypeId);
        mSetPrimaryStatement.bindLong(3, rawContactId);
        mSetPrimaryStatement.execute();
    }

    /*
     * Sets the given dataId record in the "data" table to "super primary", and resets all data
     * records of the same mimetype and under the same aggregate to not be "super primary".
     *
     * @param dataId the id of the data record to be set to primary.
     */
    private void setIsSuperPrimary(long rawContactId, long dataId, long mimeTypeId) {
        mSetSuperPrimaryStatement.bindLong(1, dataId);
        mSetSuperPrimaryStatement.bindLong(2, mimeTypeId);
        mSetSuperPrimaryStatement.bindLong(3, rawContactId);
        mSetSuperPrimaryStatement.execute();
    }

    public void insertNameLookupForEmail(long rawContactId, long dataId, String email) {
        if (TextUtils.isEmpty(email)) {
            return;
        }

        Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(email);
        if (tokens.length == 0) {
            return;
        }

        String address = tokens[0].getAddress();
        int at = address.indexOf('@');
        if (at != -1) {
            address = address.substring(0, at);
        }

        insertNameLookup(rawContactId, dataId,
                NameLookupType.EMAIL_BASED_NICKNAME, NameNormalizer.normalize(address));
    }

    /**
     * Normalizes the nickname and inserts it in the name lookup table.
     */
    public void insertNameLookupForNickname(long rawContactId, long dataId, String nickname) {
        if (TextUtils.isEmpty(nickname)) {
            return;
        }

        insertNameLookup(rawContactId, dataId,
                NameLookupType.NICKNAME, NameNormalizer.normalize(nickname));
    }

    public void insertNameLookupForOrganization(long rawContactId, long dataId, String company,
            String title) {
        if (!TextUtils.isEmpty(company)) {
            insertNameLookup(rawContactId, dataId,
                    NameLookupType.ORGANIZATION, NameNormalizer.normalize(company));
        }
        if (!TextUtils.isEmpty(title)) {
            insertNameLookup(rawContactId, dataId,
                    NameLookupType.ORGANIZATION, NameNormalizer.normalize(title));
        }
    }

    public void insertNameLookupForStructuredName(long rawContactId, long dataId, String name) {
        mNameLookupBuilder.insertNameLookup(rawContactId, dataId, name);
    }

    /**
     * Returns nickname cluster IDs or null. Maintains cache.
     */
    protected String[] getCommonNicknameClusters(String normalizedName) {
        SoftReference<String[]> ref;
        String[] clusters = null;
        synchronized (mNicknameClusterCache) {
            if (mNicknameClusterCache.containsKey(normalizedName)) {
                ref = mNicknameClusterCache.get(normalizedName);
                if (ref == null) {
                    return null;
                }
                clusters = ref.get();
            }
        }

        if (clusters == null) {
            clusters = loadNicknameClusters(normalizedName);
            ref = clusters == null ? null : new SoftReference<String[]>(clusters);
            synchronized (mNicknameClusterCache) {
                mNicknameClusterCache.put(normalizedName, ref);
            }
        }
        return clusters;
    }

    protected String[] loadNicknameClusters(String normalizedName) {
        SQLiteDatabase db = mDbHelper.getReadableDatabase();
        String[] clusters = null;
        Cursor cursor = db.query(NicknameLookupQuery.TABLE, NicknameLookupQuery.COLUMNS,
                NicknameLookupColumns.NAME + "=?", new String[] { normalizedName },
                null, null, null);
        try {
            int count = cursor.getCount();
            if (count > 0) {
                clusters = new String[count];
                for (int i = 0; i < count; i++) {
                    cursor.moveToNext();
                    clusters[i] = cursor.getString(NicknameLookupQuery.CLUSTER);
                }
            }
        } finally {
            cursor.close();
        }
        return clusters;
    }

    private class StructuredNameLookupBuilder extends NameLookupBuilder {

        public StructuredNameLookupBuilder(NameSplitter splitter) {
            super(splitter);
        }

        @Override
        protected void insertNameLookup(long rawContactId, long dataId, int lookupType,
                String name) {
            ContactsProvider2.this.insertNameLookup(rawContactId, dataId, lookupType, name);
        }

        @Override
        protected String[] getCommonNicknameClusters(String normalizedName) {
            return ContactsProvider2.this.getCommonNicknameClusters(normalizedName);
        }
    }

    /**
     * Inserts a record in the {@link Tables#NAME_LOOKUP} table.
     */
    public void insertNameLookup(long rawContactId, long dataId, int lookupType, String name) {
        DatabaseUtils.bindObjectToProgram(mNameLookupInsert, 1, rawContactId);
        DatabaseUtils.bindObjectToProgram(mNameLookupInsert, 2, dataId);
        DatabaseUtils.bindObjectToProgram(mNameLookupInsert, 3, lookupType);
        DatabaseUtils.bindObjectToProgram(mNameLookupInsert, 4, name);
        mNameLookupInsert.executeInsert();
    }

    /**
     * Deletes all {@link Tables#NAME_LOOKUP} table rows associated with the specified data element.
     */
    public void deleteNameLookup(long dataId) {
        DatabaseUtils.bindObjectToProgram(mNameLookupDelete, 1, dataId);
        mNameLookupDelete.execute();
    }

    public void appendContactFilterAsNestedQuery(StringBuilder sb, String filterParam) {
        sb.append("(" +
                "SELECT DISTINCT " + RawContacts.CONTACT_ID +
                " FROM " + Tables.RAW_CONTACTS +
                " JOIN " + Tables.NAME_LOOKUP +
                " ON(" + RawContactsColumns.CONCRETE_ID + "="
                        + NameLookupColumns.RAW_CONTACT_ID + ")" +
                " WHERE normalized_name GLOB '");
        sb.append(NameNormalizer.normalize(filterParam));
        sb.append("*' AND " + NameLookupColumns.NAME_TYPE + " IN("
                + NameLookupType.NAME_COLLATION_KEY + ","
                + NameLookupType.EMAIL_BASED_NICKNAME + ","
                + NameLookupType.NICKNAME + ","
                + NameLookupType.ORGANIZATION + "))");
    }

    public String getRawContactsByFilterAsNestedQuery(String filterParam) {
        StringBuilder sb = new StringBuilder();
        appendRawContactsByFilterAsNestedQuery(sb, filterParam, null);
        return sb.toString();
    }

    public void appendRawContactsByFilterAsNestedQuery(StringBuilder sb, String filterParam,
            String limit) {
        appendRawContactsByNormalizedNameFilter(sb, NameNormalizer.normalize(filterParam), limit,
                true);
    }

    private void appendRawContactsByNormalizedNameFilter(StringBuilder sb, String normalizedName,
            String limit, boolean allowEmailMatch) {
        sb.append("(" +
                "SELECT DISTINCT " + NameLookupColumns.RAW_CONTACT_ID +
                " FROM " + Tables.NAME_LOOKUP +
                " WHERE " + NameLookupColumns.NORMALIZED_NAME +
                " GLOB '");
        sb.append(normalizedName);
        sb.append("*' AND " + NameLookupColumns.NAME_TYPE + " IN ("
                + NameLookupType.NAME_COLLATION_KEY + ","
                + NameLookupType.NICKNAME + ","
                + NameLookupType.ORGANIZATION);
        if (allowEmailMatch) {
            sb.append("," + NameLookupType.EMAIL_BASED_NICKNAME);
        }
        sb.append(")");

        if (limit != null) {
            sb.append(" LIMIT ").append(limit);
        }
        sb.append(")");
    }

    /**
     * Inserts an argument at the beginning of the selection arg list.
     */
    private String[] insertSelectionArg(String[] selectionArgs, String arg) {
        if (selectionArgs == null) {
            return new String[] {arg};
        } else {
            int newLength = selectionArgs.length + 1;
            String[] newSelectionArgs = new String[newLength];
            newSelectionArgs[0] = arg;
            System.arraycopy(selectionArgs, 0, newSelectionArgs, 1, selectionArgs.length);
            return newSelectionArgs;
        }
    }

    private String[] appendProjectionArg(String[] projection, String arg) {
        if (projection == null) {
            return null;
        }
        final int length = projection.length;
        String[] newProjection = new String[length + 1];
        System.arraycopy(projection, 0, newProjection, 0, length);
        newProjection[length] = arg;
        return newProjection;
    }

    protected Account getDefaultAccount() {
        AccountManager accountManager = AccountManager.get(getContext());
        try {
            Account[] accounts = accountManager.getAccountsByTypeAndFeatures(DEFAULT_ACCOUNT_TYPE,
                    new String[] {FEATURE_LEGACY_HOSTED_OR_GOOGLE}, null, null).getResult();
            if (accounts != null && accounts.length > 0) {
                return accounts[0];
            }
        } catch (Throwable e) {
            Log.e(TAG, "Cannot determine the default account for contacts compatibility", e);
        }
        return null;
    }

    protected boolean isWritableAccount(Account account) {
        IContentService contentService = ContentResolver.getContentService();
        try {
            for (SyncAdapterType sync : contentService.getSyncAdapterTypes()) {
                if (ContactsContract.AUTHORITY.equals(sync.authority) &&
                        account.type.equals(sync.accountType)) {
                    return sync.supportsUploading();
                }
            }
        } catch (RemoteException e) {
            Log.e(TAG, "Could not acquire sync adapter types");
        }

        return false;
    }
}

android11 特殊符号开头的联系人显示到“#”分组下面 Android11 Contacts以及ContactsProvider源码修改 阅读详情

相关推荐

Android 7.0原生拨号器APK安装包+可编译源码工程(含截图与构建脚本)

直接可用的Android N(7.0)适配版谷歌拨号器APK文件(com.android.dialer.apk),附带完整Gradle工程源码,支持Android 5.0及以上系统。工程已预配置标准依赖:appcompat-v7、recyclerview-v7、cardview-v7、design、support-v13等,无需手动调整即可同步构建。资源包内置两套界面截图(m1.jpg、m2.jpg),方便快速了解UI效果;提供详细README.md和RUN_INSTRUCTIONS.md说明文档,涵盖导入

b3n4m5q6w7的博客 452

Android 5.0 Contacts源码分析

最近工作中要整理最新的Android L的联系人源码分析,在网上各种搜索都不怎么满意,要么版本太老,要么分析的太浅层次了,要么只有简单的使用。无奈就自己整理,发出来共享一下。

lsmfeixiang的专栏 6623

android1.0源文件

google版手机操作系统开发包android1.0源文件,可以加入eclipse关联源代码直接查看,很方便的。

Android ContactsProvider源码分析

Android源码目录packages\providers下的应用是下载,通话等内置基本应用提供数据存储和操作的provider应用,本文章将针对ContactsProvider源码的架构和实现展开分析。(注:本文使用使用android4.0版本进行分析) 1、架构设计       ContactsProvider中数据操作基类是AbstractContactsProvider.java(参见

Mr. DU 4220

android 2.3 源码 src

android 2.3 源码 src 还要求20字以上。。。好囧啊。。。。。。。。。。。。。。。。。

映泰TB250-BTC主板魔改BIOS支持8/9代CPU实战与开机故障排查

BIOS(基本输入输出系统)是计算机硬件与操作系统之间的桥梁,负责初始化硬件和提供运行时服务。其工作原理是通过存储在主板芯片中的固件程序,在上电自检(POST)阶段完成对CPU、内存等关键组件的检测与配置。魔改BIOS的技术价值在于突破硬件限制,通过修改微码和电源管理模块,让旧平台支持新处理器,从而以极低成本实现性能跃升,在预算有限或追求极致性价比的DIY、办公和软路由等应用场景中极具吸引力。本文以映泰TB250-BTC矿板为例,详细解析了通过软刷方式刷入魔改BIOS,并重点解决了刷写后常见的“开机风扇转一

weixin_29164185的博客 407

UriMatcher理解

UriMatcher主要用在ContentProvider中,用来判断Uri的正确性,如在insert之前先判断用户传进来的Uri是否与UriMatcher定义的匹配。   1.定义 private static final ProfileAwareUriMatcher sUriMatcher =   new ProfileAwareUriMatcher(UriMatcher.NO_MATC

cs_lht的专栏 4037

学习笔记( 11 ) - 解决:finalshell连接虚拟机(ubuntu)超时,主机ping不通虚拟机,主机没有VMnet8网卡, Connection refused: connect

解决:finalshell连接虚拟机(ubuntu)超时,主机ping不通虚拟机,主机没有VMnet8网卡, Connection refused: connect

烟溪彭于晏的博客 2万+

ContactsProvider2

Android的数据库体系可以分为三个层次: ContentProvider层;ContentProvider将对数据的增删改查操作进行了抽象,具体实例会注册到AMS中,提供跨进程的服务。ContentProvider可以用于管理各种数据存储源的访问,包括结构化数据(SQLite)和非结构化数据(如图像文件)。 SQLiteDatabase/SQLiteOpenHelper;这部分代码位于“/f...

Dylan_Sen的博客 1447

STM32实战开发(71):STM32人脸识别开发教程:基于摄像头模块的人脸识别系统

在嵌入式系统中,STM32凭借其强大的计算性能、丰富的外设接口以及低功耗特性,成为了实现人脸识别系统的理想平台。STM32虽然没有强大的计算能力,但可以通过合理利用外部硬件加速模块,如DSP、GPU、FPGA等,结合轻量级的人脸识别算法,实现高效的人脸识别系统。尽管STM32的计算资源相对有限,但通过合理的优化和使用高效的算法,我们依然能够实现高效的人脸识别功能。在本项目中,我们使用。在STM32开发中,虽然处理能力有限,但通过优化算法和合理利用硬件加速,我们仍然可以实现一个轻量级的、实时的人脸识别系统。

2201_76125393的博客 1662

〔重庆理工大学〕计算机组成原理课设报告--16位单周期CPU设计

本课程设计的实验平台为Altera Quartus II 软件和Altera Cyclone V Starter 工具箱。(1)Cyclone V是Altera FPGAs家族的一类,它面向低功耗应用。Cyclone V GX 5CGXFC5C6F27C7N 芯片有不同的转换器,按钮,LED灯,七段显示器,HDMI接口,SD卡读卡器以及其他链接等。Cyclone V GX 5CGXFC5C6F27C7N是Starter工具箱的重要组件。(2)测试台包含在测设备(DUT)和用于仿真的模拟输入。

Pretty Boy Fox的博客 1066

android中的数据库

在查看Android的soucre code的时候,经常会遇到查询数据库的代码,如 final ContentResolver cr = mContext.getContentResolver(); final int networkPrefSetting = Settings.Secure.getInt(cr, Settings.Secure.NETWORK_PREFERENCE, -1);

bxlsky的专栏 1839

华为OD机试真题 新系统2026-04-15 Python&JS 实现【失灵的键盘】

有一个键盘有2个按键失灵了,按下这些键时会连续输出其他键对应的字符两次。具体如下: 1. 按下j键一次,屏幕上显示uu(两个连续的u);  按下b键一次,屏幕上显示t(两个连续的t) 2. u键和t键是好的,按下u键一次时,屏幕只会显示一次u(正常按键);按下t键一次时,屏幕只会显示一次t(正常按键) 3. 假定屏幕上连续显示两个t一定是按了一次b键,而不是两次t键;假定按键t之后不会紧接着按键b,即tttt转义为两个b,而不可能是tb

MISAYAONE的博客 523

利用android源码实现获取汉字的拼音

最近做一个项目,需要实现输入人名得到对应的汉语拼音,在网上查了下,也有不少方案,但或多或少都有些问题(有个开源项目pinyin4j倒是挺好,有兴趣可以试试,这里只讲android自带的功能)。后来一想,android的通讯录不就有这样的功能吗,看看它是怎么做的不就行了。 经研究,发现和拼音转换相关的文件位于: packages\providers\ContactsProvider\src\co

随便写写 5710

1.07---共识机制详解(PoW、PoS、DPoS 对比)

【摘要】区块链能成为“无需信任的协作网络”,靠的正是它的“共识机制”。本节将深入剖析 PoW、PoS、DPoS 等核心共识模型,并结合以太坊从 PoW 到 PoS 的转型与 Solana 等新型方案,为你拆解这一决定区块链运行方式、性能、安全性的“发动机系统”。什么是共识机制?为何不是多数投票这么简单?三大主流共识算法(PoW、PoS、DPoS)机制对比与演化路线以太坊从 PoW 到 PoS 的转型细节(The Merge)Solana 的 PoH + PoS 混合共识机制解析。

鄙人kunzhi96,感恩遇见! 1万+

Juniper SRX PPPoE配置

set interfaces pp0 unit 1 ppp-options chap default-chap-secret 拨号用户名密码。set interfaces pp0 unit 0 ppp-options chap default-chap-secret 拨号密码。set interfaces pp0 unit 0 ppp-options chap local-name 拨号用户名。6号口接运营商进行拨号。

Jian Sun_的博客 750

Android 通讯录号码匹配规则 SQL

当你在系统通讯录存储8位号码联系人后,呼出时在刚刚存储8位号码前任意补足3位数, 查询到的联系人名称将被模糊匹配为8位号对应名字。举个例子,将 87354321 存储为 哈哈哈, 拨号盘拨打 01087354321,通话记录和拨号界面都会显示 哈哈哈。要想解决这个问题需要了解 系统联系人查询规则,可参考 Android6.0来电号码电话薄名称匹配 整体看下来后发现就是查询 Provider,在 packages/apps/Dialer/java/com/android/incallui/CallerInfo

cczhengv 1412

YOLOv11改进策略【卷积层】| 引入注意力卷积模块RFAConv,关注感受野空间特征 助力yolov11精度提升

RFAConv:创新空间注意力和标准卷积运算感受野空间特征是针对卷积核设计的,根据卷积核大小动态生成。以3×3卷积核为例,它由非重叠滑动窗口组成,每个窗口代表一个感受野滑块。

Limiiiing的博客 1823

Android 之往通讯录中添加数据和查询数据

 首先 我们要在E:/android-sdk-windows/platforms/android-8/sources/ContactsProvider  找到源码 ! 因为android自带的通讯录也是通过ContentProvider对外共享数据。 贴点源码: URI  这是ContactsProvider2.javastatic { // Contacts URI matching table final UriMatcher matcher = sUriMatche

Come on ! Just Do it ! 1万+

Probability and Statistics - 4th edition

Probability and Statistics - 4th edition

大中型超市收银系统(可直接运行)

光步前台收银系统系统功能:商品销售、商品退货、挂单、收银员权限、会员销售、单品取消、整单取消、商品查询、脱机销售、脱机数据上传,基本资料下载,开钱箱、小票打印、收款结算,支持多币种付款方式(现金、储值卡、礼券、微信等),支持多种促销。运行环境:.net 4.0登陆信息用户名:1001 密码:123456内置商品信息,商品编码可输入00001000 - 00010000,测试效果相关功能键F5 结算 F9挂单 F11商品查询 F8退货 Insert单品取消 Del整单取消 F12开钱箱F2 会员 Ctrl+Z退出系统 Home 锁屏 相关源码待公开

Android 之往通讯录中添加数据和查询数据【转】

http://blog.youkuaiyun.com/hosterZhu/archive/2011/01/20/6154461.aspx    首先 我们要在E:\android-sdk-windows\platforms\android-8\sources\ContactsProvider  找到源码 !  因为android自带的通讯录也是通过ContentProvider对外共享数据。  贴...

lcdxwhj2003的专栏 278

深度学习工具包 DeepLearningToolbox

深度学习工具包Deprecation notice.-----This toolbox is outdated and no longer maintained.There are much better tools available for deep learning than this toolbox, e.g. [Theano](http://deeplearning.net/software/theano/), [torch](http://torch.ch/) or [tensorflow](http://www.tensorflow.org/)I would suggest you use one of the tools mentioned above rather than use this toolbox.Best, Rasmus.DeepLearnToolbox================A Matlab toolbox for Deep Learning.Deep Learning is a new subfield of machine learning that focuses on learning deep hierarchical models of data.It is inspired by the human brain's apparent deep (layered, hierarchical) architecture.A good overview of the theory of Deep Learning theory is[Learning Deep Architectures for AI](http://www.iro.umontreal.ca/~bengioy/papers/ftml_book.pdf)For a more informal introduction, see the following videos by Geoffrey Hinton and Andrew Ng.* [The Next Generation of Neural Networks](http://www.youtube.com/watch?v=AyzOUbkUf3M) (Hinton, 2007)* [Recent Developments in Deep Learning](http://www.youtube.com/watch?v=VdIURAu1-aU) (Hinton, 2010)* [Unsupervised Feature Learning and Deep Learning](http://www.youtube.com/watch?v=ZmNOAtZIgIk) (Ng, 2011)If you use this toolbox in your research please cite [Prediction as a candidate for learning deep hierarchical models of data](http://www2.imm.dtu.dk/pubdb/views/publication_details.php?id=6284)```@MASTERSTHESIS\{IMM2012-06284, author = "R. B. Palm", title = "Prediction as a candidate for learning deep hierarchical models of data", year = "2012",}```Contact: rasmusbergpalm at gmail dot comDirectories included in the toolbox-----------------------------------`NN/` - A library for Feedforward Backpropagation Neural Networks`CNN/` - A library for Convolutional Neural Networks`DBN/` - A library for Deep Belief Networks`SAE/` - A library for Stacked Auto-Encoders`CAE/` - A library for Convolutional Auto-Encoders`util/` - Utility functions used by the libraries`data/` - Data used by the examples`tests/` - unit tests to verify toolbox is workingFor references on each library check REFS.mdSetup-----1. Download.2. addpath(genpath('DeepLearnToolbox'));Example: Deep Belief Network---------------------```matlabfunction test_example_DBNload mnist_uint8;train_x = double(train_x) / 255;test_x = double(test_x) / 255;train_y = double(train_y);test_y = double(test_y);%% ex1 train a 100 hidden unit RBM and visualize its weightsrand('state',0)dbn.sizes = [100];opts.numepochs = 1;opts.batchsize = 100;opts.momentum = 0;opts.alpha = 1;dbn = dbnsetup(dbn, train_x, opts);dbn = dbntrain(dbn, train_x, opts);figure; visualize(dbn.rbm{1}.W'); % Visualize the RBM weights%% ex2 train a 100-100 hidden unit DBN and use its weights to initialize a NNrand('state',0)%train dbndbn.sizes = [100 100];opts.numepochs = 1;opts.batchsize = 100;opts.momentum = 0;opts.alpha = 1;dbn = dbnsetup(dbn, train_x, opts);dbn = dbntrain(dbn, train_x, opts);%unfold dbn to nnnn = dbnunfoldtonn(dbn, 10);nn.activation_function = 'sigm';%train nnopts.numepochs = 1;opts.batchsize = 100;nn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.10, 'Too big error');```Example: Stacked Auto-Encoders---------------------```matlabfunction test_example_SAEload mnist_uint8;train_x = double(train_x)/255;test_x = double(test_x)/255;train_y = double(train_y);test_y = double(test_y);%% ex1 train a 100 hidden unit SDAE and use it to initialize a FFNN% Setup and train a stacked denoising autoencoder (SDAE)rand('state',0)sae = saesetup([784 100]);sae.ae{1}.activation_function = 'sigm';sae.ae{1}.learningRate = 1;sae.ae{1}.inputZeroMaskedFraction = 0.5;opts.numepochs = 1;opts.batchsize = 100;sae = saetrain(sae, train_x, opts);visualize(sae.ae{1}.W{1}(:,2:end)')% Use the SDAE to initialize a FFNNnn = nnsetup([784 100 10]);nn.activation_function = 'sigm';nn.learningRate = 1;nn.W{1} = sae.ae{1}.W{1};% Train the FFNNopts.numepochs = 1;opts.batchsize = 100;nn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.16, 'Too big error');```Example: Convolutional Neural Nets---------------------```matlabfunction test_example_CNNload mnist_uint8;train_x = double(reshape(train_x',28,28,60000))/255;test_x = double(reshape(test_x',28,28,10000))/255;train_y = double(train_y');test_y = double(test_y');%% ex1 Train a 6c-2s-12c-2s Convolutional neural network %will run 1 epoch in about 200 second and get around 11% error. %With 100 epochs you'll get around 1.2% errorrand('state',0)cnn.layers = { struct('type', 'i') %input layer struct('type', 'c', 'outputmaps', 6, 'kernelsize', 5) %convolution layer struct('type', 's', 'scale', 2) %sub sampling layer struct('type', 'c', 'outputmaps', 12, 'kernelsize', 5) %convolution layer struct('type', 's', 'scale', 2) %subsampling layer};cnn = cnnsetup(cnn, train_x, train_y);opts.alpha = 1;opts.batchsize = 50;opts.numepochs = 1;cnn = cnntrain(cnn, train_x, train_y, opts);[er, bad] = cnntest(cnn, test_x, test_y);%plot mean squared errorfigure; plot(cnn.rL);assert(er<0.12, 'Too big error');```Example: Neural Networks---------------------```matlabfunction test_example_NNload mnist_uint8;train_x = double(train_x) / 255;test_x = double(test_x) / 255;train_y = double(train_y);test_y = double(test_y);% normalize[train_x, mu, sigma] = zscore(train_x);test_x = normalize(test_x, mu, sigma);%% ex1 vanilla neural netrand('state',0)nn = nnsetup([784 100 10]);opts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samples[nn, L] = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.08, 'Too big error');%% ex2 neural net with L2 weight decayrand('state',0)nn = nnsetup([784 100 10]);nn.weightPenaltyL2 = 1e-4; % L2 weight decayopts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex3 neural net with dropoutrand('state',0)nn = nnsetup([784 100 10]);nn.dropoutFraction = 0.5; % Dropout fraction opts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex4 neural net with sigmoid activation functionrand('state',0)nn = nnsetup([784 100 10]);nn.activation_function = 'sigm'; % Sigmoid activation functionnn.learningRate = 1; % Sigm require a lower learning rateopts.numepochs = 1; % Number of full sweeps through dataopts.batchsize = 100; % Take a mean gradient step over this many samplesnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex5 plotting functionalityrand('state',0)nn = nnsetup([784 20 10]);opts.numepochs = 5; % Number of full sweeps through datann.output = 'softmax'; % use softmax outputopts.batchsize = 1000; % Take a mean gradient step over this many samplesopts.plot = 1; % enable plottingnn = nntrain(nn, train_x, train_y, opts);[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');%% ex6 neural net with sigmoid activation and plotting of validation and training error% split training data into training and validation datavx = train_x(1:10000,:);tx = train_x(10001:end,:);vy = train_y(1:10000,:);ty = train_y(10001:end,:);rand('state',0)nn = nnsetup([784 20 10]); nn.output = 'softmax'; % use softmax outputopts.numepochs = 5; % Number of full sweeps through dataopts.batchsize = 1000; % Take a mean gradient step over this many samplesopts.plot = 1; % enable plottingnn = nntrain(nn, tx, ty, opts, vx, vy); % nntrain takes validation set as last two arguments (optionally)[er, bad] = nntest(nn, test_x, test_y);assert(er < 0.1, 'Too big error');```[![Bitdeli Badge](https://d2weczhvl823v0.cloudfront.net/rasmusbergpalm/deeplearntoolbox/trend.png)](https://bitdeli.com/free "Bitdeli Badge")

ContentProvider深入研究

这几天对ContentProvider进行了一点研究。 至于如何写一个自定义的ContentProvider,我就不赘述了,网上很容易找到。我想说明的是另外一个问题: 有经验的人可能在Android系统中操作过通讯录数据库或者多媒体数据库,在操作的过程中,我们都是通过ContentProvider来实现的。我们知道,如果要实现一个ContentProvider来给外部共享数据的时候,需要写一个类

yinkai1205的专栏 2782

win10 跳过U8环境检测的IE WebControls.rar

win10 安装u8的时候 提示IE WebControls 不通过,这个工具可以欺骗u8的检查。 跳过U8环境检测的IE WebControls。提示导入失败后,重新检测通过。

Android项目实战--手机卫士08--获取手机联系人

首先,我把把昨天忘记了的两张设置向导的图片放出来先      我们从上面的第一张图片可以看到,我们有一个选择联系人这一操作,那么怎样才能获取到手机里面的联系人呢,其实方法有很多的,现在我们来讲一下我们这个项目里面用到的方法 其实要想知道怎样获取手机里面的联系人,去看一下Android自己的源码就知道的了 我们只要把platform/packages/providers/Contact

Miracle的专栏 6192

Android项目:手机安全卫士(6)—— 手机防盗设置向导(二)

Android项目:手机安全卫士(6)—— 手机防盗设置向导(二)1 介绍前面的文章介绍了设置向导的界面与切换逻辑,今天来看看每个界面后面涉及到的代码逻辑,主要知识点有: 读取手机 sim 卡序列号 监听手机重启完成的广播 读取手机通讯录 关于项目相关文章,请访问: Android 项目:手机安全卫士(1)—— 闪屏界面 Android 项目:手机安全卫士(2)—— 版本升级 Android 项目:

XWdoor 1803

Android复习练习十五(使用contentresolver查询和添加联系人)

需求:写一个应用,有两个按键,分别用来查询所有

苦咖啡的自留地 1712

利用内容观察者操作系统的联系人(查询,添加)

系统联系人的源码包位于/data/data/com.android.providers.contacts目录下,导出contacts2,db,用SQLLite查看数据库结构: raw_contacts表:保存联系人的引用,其中contact_id存放联系人的id data表:联系人信息的具体数据                 raw_contact_id与raw_contacts表中的co

kelingbest的专栏 1781
上一篇: 通讯录 实现简单查询
下一篇: 常用的计算机英语(全集)
Android_wy
博客等级 码龄16年 37粉丝 7原创
评论 9
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值