b2科目四模拟试题多少题驾考考爆了怎么补救
b2科目四模拟试题多少题 驾考考爆了怎么补救

contactsprovider_experiencedatasync_defaultcontainer

电脑杂谈  发布时间:2017-03-09 00:00:41  来源:网络整理
contactsprovidercontactsprovider

/*

*

* 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

*

*

*

* 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_FORITES = 14002;

private static final int LIVE_FOLDERS_CONTACTS_GROUP_NAME = 14003;

private static final int RAW_CONTACT_ENTITIES = 15001;

private intece 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 intece 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 intece 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 intece 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 intece DataUpdateQuery {

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

int _ID = 0;

int RAW_CONTACT_ID = 1;

int MIMETYPE = 2;

}

private intece NicknameLookupQuery {

String TABLE = Tables.NICKNAME_LOOKUP;

String[] COLUMNS = new String[] {

NicknameLookupColumns.CLUSTER

};

int CLUSTER = 0;

}

private intece 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_FORITES);

}

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.SYN, RawContacts.SYN);

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.SYN, Data.SYN);

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.SYN, RawContacts.SYN);

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.SYN, Data.SYN);

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.SYN, Data.SYN);

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.SYN, Groups.SYN);

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 + " HING " + Clauses.HING_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

+ " HING " + Clauses.HING_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.NEIORHOOD,

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_FORITES:

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 intece 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 intece 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.SYN};

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.SYN,

RawContacts._ID,

Data.IS_PRIMARY,

Data.IS_SUPER_PRIMARY,

Data.DATA_VERSION,

GroupMembership.GROUP_SOURCE_ID,

RawContacts.SYNC1,

RawContacts.SYNC2,

RawContacts.SYNC3,

RawContacts.SYN,

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_SYN = 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.SYN, c.getString(COLUMN_SYN));

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.SYN,

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_SYN = 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.SYN, c.getString(COLUMN_SYN));

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");

}


本文来自电脑杂谈,转载请注明本文网址:
http://www.pc-fly.com/a/tongxinshuyu/article-36539-1.html

    相关阅读
      发表评论  请自觉遵守互联网相关的政策法规,严禁发布、暴力、反动的言论

      热点图片
      拼命载入中...