Home | History | Annotate | Download | only in contacts
      1 /*
      2  * Copyright (C) 2009 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License
     15  */
     16 
     17 package com.android.providers.contacts;
     18 
     19 import com.android.internal.content.SyncStateContentProviderHelper;
     20 import com.android.providers.contacts.ContactLookupKey.LookupKeySegment;
     21 import com.android.providers.contacts.ContactsDatabaseHelper.AggregatedPresenceColumns;
     22 import com.android.providers.contacts.ContactsDatabaseHelper.AggregationExceptionColumns;
     23 import com.android.providers.contacts.ContactsDatabaseHelper.Clauses;
     24 import com.android.providers.contacts.ContactsDatabaseHelper.ContactsColumns;
     25 import com.android.providers.contacts.ContactsDatabaseHelper.ContactsStatusUpdatesColumns;
     26 import com.android.providers.contacts.ContactsDatabaseHelper.DataColumns;
     27 import com.android.providers.contacts.ContactsDatabaseHelper.GroupsColumns;
     28 import com.android.providers.contacts.ContactsDatabaseHelper.MimetypesColumns;
     29 import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupColumns;
     30 import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupType;
     31 import com.android.providers.contacts.ContactsDatabaseHelper.PhoneColumns;
     32 import com.android.providers.contacts.ContactsDatabaseHelper.PhoneLookupColumns;
     33 import com.android.providers.contacts.ContactsDatabaseHelper.PresenceColumns;
     34 import com.android.providers.contacts.ContactsDatabaseHelper.RawContactsColumns;
     35 import com.android.providers.contacts.ContactsDatabaseHelper.SettingsColumns;
     36 import com.android.providers.contacts.ContactsDatabaseHelper.StatusUpdatesColumns;
     37 import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
     38 import com.google.android.collect.Lists;
     39 import com.google.android.collect.Maps;
     40 import com.google.android.collect.Sets;
     41 
     42 import android.accounts.Account;
     43 import android.accounts.AccountManager;
     44 import android.accounts.OnAccountsUpdateListener;
     45 import android.app.Notification;
     46 import android.app.NotificationManager;
     47 import android.app.PendingIntent;
     48 import android.app.SearchManager;
     49 import android.content.ContentProviderOperation;
     50 import android.content.ContentProviderResult;
     51 import android.content.ContentResolver;
     52 import android.content.ContentUris;
     53 import android.content.ContentValues;
     54 import android.content.Context;
     55 import android.content.IContentService;
     56 import android.content.Intent;
     57 import android.content.OperationApplicationException;
     58 import android.content.SharedPreferences;
     59 import android.content.SyncAdapterType;
     60 import android.content.UriMatcher;
     61 import android.content.res.AssetFileDescriptor;
     62 import android.content.res.Configuration;
     63 import android.database.CharArrayBuffer;
     64 import android.database.Cursor;
     65 import android.database.CursorWrapper;
     66 import android.database.DatabaseUtils;
     67 import android.database.MatrixCursor;
     68 import android.database.MatrixCursor.RowBuilder;
     69 import android.database.sqlite.SQLiteConstraintException;
     70 import android.database.sqlite.SQLiteContentHelper;
     71 import android.database.sqlite.SQLiteDatabase;
     72 import android.database.sqlite.SQLiteQueryBuilder;
     73 import android.database.sqlite.SQLiteStatement;
     74 import android.net.Uri;
     75 import android.os.AsyncTask;
     76 import android.os.Bundle;
     77 import android.os.MemoryFile;
     78 import android.os.RemoteException;
     79 import android.os.SystemClock;
     80 import android.os.SystemProperties;
     81 import android.pim.vcard.VCardComposer;
     82 import android.pim.vcard.VCardConfig;
     83 import android.preference.PreferenceManager;
     84 import android.provider.BaseColumns;
     85 import android.provider.ContactsContract;
     86 import android.provider.LiveFolders;
     87 import android.provider.OpenableColumns;
     88 import android.provider.SyncStateContract;
     89 import android.provider.ContactsContract.AggregationExceptions;
     90 import android.provider.ContactsContract.ContactCounts;
     91 import android.provider.ContactsContract.Contacts;
     92 import android.provider.ContactsContract.Data;
     93 import android.provider.ContactsContract.DisplayNameSources;
     94 import android.provider.ContactsContract.FullNameStyle;
     95 import android.provider.ContactsContract.Groups;
     96 import android.provider.ContactsContract.Intents;
     97 import android.provider.ContactsContract.PhoneLookup;
     98 import android.provider.ContactsContract.PhoneticNameStyle;
     99 import android.provider.ContactsContract.ProviderStatus;
    100 import android.provider.ContactsContract.RawContacts;
    101 import android.provider.ContactsContract.SearchSnippetColumns;
    102 import android.provider.ContactsContract.Settings;
    103 import android.provider.ContactsContract.StatusUpdates;
    104 import android.provider.ContactsContract.CommonDataKinds.BaseTypes;
    105 import android.provider.ContactsContract.CommonDataKinds.Email;
    106 import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
    107 import android.provider.ContactsContract.CommonDataKinds.Im;
    108 import android.provider.ContactsContract.CommonDataKinds.Nickname;
    109 import android.provider.ContactsContract.CommonDataKinds.Organization;
    110 import android.provider.ContactsContract.CommonDataKinds.Phone;
    111 import android.provider.ContactsContract.CommonDataKinds.Photo;
    112 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
    113 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
    114 import android.telephony.PhoneNumberUtils;
    115 import android.text.TextUtils;
    116 import android.util.Log;
    117 
    118 import java.io.ByteArrayOutputStream;
    119 import java.io.FileNotFoundException;
    120 import java.io.IOException;
    121 import java.io.OutputStream;
    122 import java.text.SimpleDateFormat;
    123 import java.util.ArrayList;
    124 import java.util.Collections;
    125 import java.util.Date;
    126 import java.util.HashMap;
    127 import java.util.HashSet;
    128 import java.util.List;
    129 import java.util.Locale;
    130 import java.util.Map;
    131 import java.util.Set;
    132 import java.util.concurrent.CountDownLatch;
    133 
    134 /**
    135  * Contacts content provider. The contract between this provider and applications
    136  * is defined in {@link ContactsContract}.
    137  */
    138 public class ContactsProvider2 extends SQLiteContentProvider implements OnAccountsUpdateListener {
    139 
    140     private static final String TAG = "ContactsProvider";
    141 
    142     private static final boolean VERBOSE_LOGGING = Log.isLoggable(TAG, Log.VERBOSE);
    143 
    144     // TODO: carefully prevent all incoming nested queries; they can be gaping security holes
    145     // TODO: check for restricted flag during insert(), update(), and delete() calls
    146 
    147     /** Default for the maximum number of returned aggregation suggestions. */
    148     private static final int DEFAULT_MAX_SUGGESTIONS = 5;
    149 
    150     private static final String GOOGLE_MY_CONTACTS_GROUP_TITLE = "System Group: My Contacts";
    151     /**
    152      * Property key for the legacy contact import version. The need for a version
    153      * as opposed to a boolean flag is that if we discover bugs in the contact import process,
    154      * we can trigger re-import by incrementing the import version.
    155      */
    156     private static final String PROPERTY_CONTACTS_IMPORTED = "contacts_imported_v1";
    157     private static final int PROPERTY_CONTACTS_IMPORT_VERSION = 1;
    158     private static final String PREF_LOCALE = "locale";
    159 
    160     private static final String PROPERTY_AGGREGATION_ALGORITHM = "aggregation_v2";
    161     private static final int PROPERTY_AGGREGATION_ALGORITHM_VERSION = 2;
    162 
    163     private static final String AGGREGATE_CONTACTS = "sync.contacts.aggregate";
    164 
    165     private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
    166 
    167     private static final String TIMES_CONTACED_SORT_COLUMN = "times_contacted_sort";
    168 
    169     private static final String STREQUENT_ORDER_BY = Contacts.STARRED + " DESC, "
    170             + TIMES_CONTACED_SORT_COLUMN + " DESC, "
    171             + Contacts.DISPLAY_NAME + " COLLATE LOCALIZED ASC";
    172     private static final String STREQUENT_LIMIT =
    173             "(SELECT COUNT(1) FROM " + Tables.CONTACTS + " WHERE "
    174             + Contacts.STARRED + "=1) + 25";
    175 
    176     /* package */ static final String UPDATE_TIMES_CONTACTED_CONTACTS_TABLE =
    177             "UPDATE " + Tables.CONTACTS + " SET " + Contacts.TIMES_CONTACTED + "=" +
    178             " CASE WHEN " + Contacts.TIMES_CONTACTED + " IS NULL THEN 1 ELSE " +
    179             " (" + Contacts.TIMES_CONTACTED + " + 1) END WHERE " + Contacts._ID + "=?";
    180 
    181     /* package */ static final String UPDATE_TIMES_CONTACTED_RAWCONTACTS_TABLE =
    182             "UPDATE " + Tables.RAW_CONTACTS + " SET " + RawContacts.TIMES_CONTACTED + "=" +
    183             " CASE WHEN " + RawContacts.TIMES_CONTACTED + " IS NULL THEN 1 ELSE " +
    184             " (" + RawContacts.TIMES_CONTACTED + " + 1) END WHERE " + RawContacts.CONTACT_ID + "=?";
    185 
    186     /* package */ static final String PHONEBOOK_COLLATOR_NAME = "PHONEBOOK";
    187 
    188     private static final int CONTACTS = 1000;
    189     private static final int CONTACTS_ID = 1001;
    190     private static final int CONTACTS_LOOKUP = 1002;
    191     private static final int CONTACTS_LOOKUP_ID = 1003;
    192     private static final int CONTACTS_DATA = 1004;
    193     private static final int CONTACTS_FILTER = 1005;
    194     private static final int CONTACTS_STREQUENT = 1006;
    195     private static final int CONTACTS_STREQUENT_FILTER = 1007;
    196     private static final int CONTACTS_GROUP = 1008;
    197     private static final int CONTACTS_PHOTO = 1009;
    198     private static final int CONTACTS_AS_VCARD = 1010;
    199     private static final int CONTACTS_AS_MULTI_VCARD = 1011;
    200 
    201     private static final int RAW_CONTACTS = 2002;
    202     private static final int RAW_CONTACTS_ID = 2003;
    203     private static final int RAW_CONTACTS_DATA = 2004;
    204     private static final int RAW_CONTACT_ENTITY_ID = 2005;
    205 
    206     private static final int DATA = 3000;
    207     private static final int DATA_ID = 3001;
    208     private static final int PHONES = 3002;
    209     private static final int PHONES_ID = 3003;
    210     private static final int PHONES_FILTER = 3004;
    211     private static final int EMAILS = 3005;
    212     private static final int EMAILS_ID = 3006;
    213     private static final int EMAILS_LOOKUP = 3007;
    214     private static final int EMAILS_FILTER = 3008;
    215     private static final int POSTALS = 3009;
    216     private static final int POSTALS_ID = 3010;
    217 
    218     private static final int PHONE_LOOKUP = 4000;
    219 
    220     private static final int AGGREGATION_EXCEPTIONS = 6000;
    221     private static final int AGGREGATION_EXCEPTION_ID = 6001;
    222 
    223     private static final int STATUS_UPDATES = 7000;
    224     private static final int STATUS_UPDATES_ID = 7001;
    225 
    226     private static final int AGGREGATION_SUGGESTIONS = 8000;
    227 
    228     private static final int SETTINGS = 9000;
    229 
    230     private static final int GROUPS = 10000;
    231     private static final int GROUPS_ID = 10001;
    232     private static final int GROUPS_SUMMARY = 10003;
    233 
    234     private static final int SYNCSTATE = 11000;
    235     private static final int SYNCSTATE_ID = 11001;
    236 
    237     private static final int SEARCH_SUGGESTIONS = 12001;
    238     private static final int SEARCH_SHORTCUT = 12002;
    239 
    240     private static final int LIVE_FOLDERS_CONTACTS = 14000;
    241     private static final int LIVE_FOLDERS_CONTACTS_WITH_PHONES = 14001;
    242     private static final int LIVE_FOLDERS_CONTACTS_FAVORITES = 14002;
    243     private static final int LIVE_FOLDERS_CONTACTS_GROUP_NAME = 14003;
    244 
    245     private static final int RAW_CONTACT_ENTITIES = 15001;
    246 
    247     private static final int PROVIDER_STATUS = 16001;
    248 
    249     private interface DataContactsQuery {
    250         public static final String TABLE = "data "
    251                 + "JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id) "
    252                 + "JOIN contacts ON (raw_contacts.contact_id = contacts._id)";
    253 
    254         public static final String[] PROJECTION = new String[] {
    255             RawContactsColumns.CONCRETE_ID,
    256             DataColumns.CONCRETE_ID,
    257             ContactsColumns.CONCRETE_ID
    258         };
    259 
    260         public static final int RAW_CONTACT_ID = 0;
    261         public static final int DATA_ID = 1;
    262         public static final int CONTACT_ID = 2;
    263     }
    264 
    265     private interface DataDeleteQuery {
    266         public static final String TABLE = Tables.DATA_JOIN_MIMETYPES;
    267 
    268         public static final String[] CONCRETE_COLUMNS = new String[] {
    269             DataColumns.CONCRETE_ID,
    270             MimetypesColumns.MIMETYPE,
    271             Data.RAW_CONTACT_ID,
    272             Data.IS_PRIMARY,
    273             Data.DATA1,
    274         };
    275 
    276         public static final String[] COLUMNS = new String[] {
    277             Data._ID,
    278             MimetypesColumns.MIMETYPE,
    279             Data.RAW_CONTACT_ID,
    280             Data.IS_PRIMARY,
    281             Data.DATA1,
    282         };
    283 
    284         public static final int _ID = 0;
    285         public static final int MIMETYPE = 1;
    286         public static final int RAW_CONTACT_ID = 2;
    287         public static final int IS_PRIMARY = 3;
    288         public static final int DATA1 = 4;
    289     }
    290 
    291     private interface DataUpdateQuery {
    292         String[] COLUMNS = { Data._ID, Data.RAW_CONTACT_ID, Data.MIMETYPE };
    293 
    294         int _ID = 0;
    295         int RAW_CONTACT_ID = 1;
    296         int MIMETYPE = 2;
    297     }
    298 
    299 
    300     private interface RawContactsQuery {
    301         String TABLE = Tables.RAW_CONTACTS;
    302 
    303         String[] COLUMNS = new String[] {
    304                 RawContacts.DELETED,
    305                 RawContacts.ACCOUNT_TYPE,
    306                 RawContacts.ACCOUNT_NAME,
    307         };
    308 
    309         int DELETED = 0;
    310         int ACCOUNT_TYPE = 1;
    311         int ACCOUNT_NAME = 2;
    312     }
    313 
    314     public static final String DEFAULT_ACCOUNT_TYPE = "com.google";
    315     public static final String FEATURE_LEGACY_HOSTED_OR_GOOGLE = "legacy_hosted_or_google";
    316 
    317     /** Sql where statement for filtering on groups. */
    318     private static final String CONTACTS_IN_GROUP_SELECT =
    319             Contacts._ID + " IN "
    320                     + "(SELECT " + RawContacts.CONTACT_ID
    321                     + " FROM " + Tables.RAW_CONTACTS
    322                     + " WHERE " + RawContactsColumns.CONCRETE_ID + " IN "
    323                             + "(SELECT " + DataColumns.CONCRETE_RAW_CONTACT_ID
    324                             + " FROM " + Tables.DATA_JOIN_MIMETYPES
    325                             + " WHERE " + Data.MIMETYPE + "='" + GroupMembership.CONTENT_ITEM_TYPE
    326                                     + "' AND " + GroupMembership.GROUP_ROW_ID + "="
    327                                     + "(SELECT " + Tables.GROUPS + "." + Groups._ID
    328                                     + " FROM " + Tables.GROUPS
    329                                     + " WHERE " + Groups.TITLE + "=?)))";
    330 
    331     /** Sql for updating DIRTY flag on multiple raw contacts */
    332     private static final String UPDATE_RAW_CONTACT_SET_DIRTY_SQL =
    333             "UPDATE " + Tables.RAW_CONTACTS +
    334             " SET " + RawContacts.DIRTY + "=1" +
    335             " WHERE " + RawContacts._ID + " IN (";
    336 
    337     /** Sql for updating VERSION on multiple raw contacts */
    338     private static final String UPDATE_RAW_CONTACT_SET_VERSION_SQL =
    339             "UPDATE " + Tables.RAW_CONTACTS +
    340             " SET " + RawContacts.VERSION + " = " + RawContacts.VERSION + " + 1" +
    341             " WHERE " + RawContacts._ID + " IN (";
    342 
    343     /** Name lookup types used for contact filtering */
    344     private static final String CONTACT_LOOKUP_NAME_TYPES =
    345             NameLookupType.NAME_COLLATION_KEY + "," +
    346             NameLookupType.EMAIL_BASED_NICKNAME + "," +
    347             NameLookupType.NICKNAME + "," +
    348             NameLookupType.NAME_SHORTHAND + "," +
    349             NameLookupType.ORGANIZATION + "," +
    350             NameLookupType.NAME_CONSONANTS;
    351 
    352 
    353     /** Contains just BaseColumns._COUNT */
    354     private static final HashMap<String, String> sCountProjectionMap;
    355     /** Contains just the contacts columns */
    356     private static final HashMap<String, String> sContactsProjectionMap;
    357     /** Contains just the contacts columns */
    358     private static final HashMap<String, String> sContactsProjectionWithSnippetMap;
    359 
    360     /** Used for pushing starred contacts to the top of a times contacted list **/
    361     private static final HashMap<String, String> sStrequentStarredProjectionMap;
    362     private static final HashMap<String, String> sStrequentFrequentProjectionMap;
    363     /** Contains just the contacts vCard columns */
    364     private static final HashMap<String, String> sContactsVCardProjectionMap;
    365     /** Contains just the raw contacts columns */
    366     private static final HashMap<String, String> sRawContactsProjectionMap;
    367     /** Contains the columns from the raw contacts entity view*/
    368     private static final HashMap<String, String> sRawContactsEntityProjectionMap;
    369     /** Contains columns from the data view */
    370     private static final HashMap<String, String> sDataProjectionMap;
    371     /** Contains columns from the data view */
    372     private static final HashMap<String, String> sDistinctDataProjectionMap;
    373     /** Contains the data and contacts columns, for joined tables */
    374     private static final HashMap<String, String> sPhoneLookupProjectionMap;
    375     /** Contains the just the {@link Groups} columns */
    376     private static final HashMap<String, String> sGroupsProjectionMap;
    377     /** Contains {@link Groups} columns along with summary details */
    378     private static final HashMap<String, String> sGroupsSummaryProjectionMap;
    379     /** Contains the agg_exceptions columns */
    380     private static final HashMap<String, String> sAggregationExceptionsProjectionMap;
    381     /** Contains the agg_exceptions columns */
    382     private static final HashMap<String, String> sSettingsProjectionMap;
    383     /** Contains StatusUpdates columns */
    384     private static final HashMap<String, String> sStatusUpdatesProjectionMap;
    385     /** Contains Live Folders columns */
    386     private static final HashMap<String, String> sLiveFoldersProjectionMap;
    387 
    388     // where clause to update the status_updates table
    389     private static final String WHERE_CLAUSE_FOR_STATUS_UPDATES_TABLE =
    390             StatusUpdatesColumns.DATA_ID + " IN (SELECT Distinct " + StatusUpdates.DATA_ID +
    391             " FROM " + Tables.STATUS_UPDATES + " LEFT OUTER JOIN " + Tables.PRESENCE +
    392             " ON " + StatusUpdatesColumns.DATA_ID + " = " + StatusUpdates.DATA_ID + " WHERE ";
    393 
    394     private static final String[] EMPTY_STRING_ARRAY = new String[0];
    395 
    396     /**
    397      * Notification ID for failure to import contacts.
    398      */
    399     private static final int LEGACY_IMPORT_FAILED_NOTIFICATION = 1;
    400 
    401     /** Precompiled sql statement for setting a data record to the primary. */
    402     private SQLiteStatement mSetPrimaryStatement;
    403     /** Precompiled sql statement for setting a data record to the super primary. */
    404     private SQLiteStatement mSetSuperPrimaryStatement;
    405     /** Precompiled sql statement for updating a contact display name */
    406     private SQLiteStatement mRawContactDisplayNameUpdate;
    407     /** Precompiled sql statement for updating an aggregated status update */
    408     private SQLiteStatement mLastStatusUpdate;
    409     private SQLiteStatement mNameLookupInsert;
    410     private SQLiteStatement mNameLookupDelete;
    411     private SQLiteStatement mStatusUpdateAutoTimestamp;
    412     private SQLiteStatement mStatusUpdateInsert;
    413     private SQLiteStatement mStatusUpdateReplace;
    414     private SQLiteStatement mStatusAttributionUpdate;
    415     private SQLiteStatement mStatusUpdateDelete;
    416     private SQLiteStatement mResetNameVerifiedForOtherRawContacts;
    417 
    418     private long mMimeTypeIdEmail;
    419     private long mMimeTypeIdIm;
    420     private long mMimeTypeIdStructuredName;
    421     private long mMimeTypeIdOrganization;
    422     private long mMimeTypeIdNickname;
    423     private long mMimeTypeIdPhone;
    424     private StringBuilder mSb = new StringBuilder();
    425     private String[] mSelectionArgs1 = new String[1];
    426     private String[] mSelectionArgs2 = new String[2];
    427     private ArrayList<String> mSelectionArgs = Lists.newArrayList();
    428 
    429     private Account mAccount;
    430 
    431     static {
    432         // Contacts URI matching table
    433         final UriMatcher matcher = sUriMatcher;
    434         matcher.addURI(ContactsContract.AUTHORITY, "contacts", CONTACTS);
    435         matcher.addURI(ContactsContract.AUTHORITY, "contacts/#", CONTACTS_ID);
    436         matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/data", CONTACTS_DATA);
    437         matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/suggestions",
    438                 AGGREGATION_SUGGESTIONS);
    439         matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/suggestions/*",
    440                 AGGREGATION_SUGGESTIONS);
    441         matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/photo", CONTACTS_PHOTO);
    442         matcher.addURI(ContactsContract.AUTHORITY, "contacts/filter/*", CONTACTS_FILTER);
    443         matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*", CONTACTS_LOOKUP);
    444         matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/#", CONTACTS_LOOKUP_ID);
    445         matcher.addURI(ContactsContract.AUTHORITY, "contacts/as_vcard/*", CONTACTS_AS_VCARD);
    446         matcher.addURI(ContactsContract.AUTHORITY, "contacts/as_multi_vcard/*",
    447                 CONTACTS_AS_MULTI_VCARD);
    448         matcher.addURI(ContactsContract.AUTHORITY, "contacts/strequent/", CONTACTS_STREQUENT);
    449         matcher.addURI(ContactsContract.AUTHORITY, "contacts/strequent/filter/*",
    450                 CONTACTS_STREQUENT_FILTER);
    451         matcher.addURI(ContactsContract.AUTHORITY, "contacts/group/*", CONTACTS_GROUP);
    452 
    453         matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts", RAW_CONTACTS);
    454         matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#", RAW_CONTACTS_ID);
    455         matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#/data", RAW_CONTACTS_DATA);
    456         matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#/entity", RAW_CONTACT_ENTITY_ID);
    457 
    458         matcher.addURI(ContactsContract.AUTHORITY, "raw_contact_entities", RAW_CONTACT_ENTITIES);
    459 
    460         matcher.addURI(ContactsContract.AUTHORITY, "data", DATA);
    461         matcher.addURI(ContactsContract.AUTHORITY, "data/#", DATA_ID);
    462         matcher.addURI(ContactsContract.AUTHORITY, "data/phones", PHONES);
    463         matcher.addURI(ContactsContract.AUTHORITY, "data/phones/#", PHONES_ID);
    464         matcher.addURI(ContactsContract.AUTHORITY, "data/phones/filter", PHONES_FILTER);
    465         matcher.addURI(ContactsContract.AUTHORITY, "data/phones/filter/*", PHONES_FILTER);
    466         matcher.addURI(ContactsContract.AUTHORITY, "data/emails", EMAILS);
    467         matcher.addURI(ContactsContract.AUTHORITY, "data/emails/#", EMAILS_ID);
    468         matcher.addURI(ContactsContract.AUTHORITY, "data/emails/lookup/*", EMAILS_LOOKUP);
    469         matcher.addURI(ContactsContract.AUTHORITY, "data/emails/filter", EMAILS_FILTER);
    470         matcher.addURI(ContactsContract.AUTHORITY, "data/emails/filter/*", EMAILS_FILTER);
    471         matcher.addURI(ContactsContract.AUTHORITY, "data/postals", POSTALS);
    472         matcher.addURI(ContactsContract.AUTHORITY, "data/postals/#", POSTALS_ID);
    473 
    474         matcher.addURI(ContactsContract.AUTHORITY, "groups", GROUPS);
    475         matcher.addURI(ContactsContract.AUTHORITY, "groups/#", GROUPS_ID);
    476         matcher.addURI(ContactsContract.AUTHORITY, "groups_summary", GROUPS_SUMMARY);
    477 
    478         matcher.addURI(ContactsContract.AUTHORITY, SyncStateContentProviderHelper.PATH, SYNCSTATE);
    479         matcher.addURI(ContactsContract.AUTHORITY, SyncStateContentProviderHelper.PATH + "/#",
    480                 SYNCSTATE_ID);
    481 
    482         matcher.addURI(ContactsContract.AUTHORITY, "phone_lookup/*", PHONE_LOOKUP);
    483         matcher.addURI(ContactsContract.AUTHORITY, "aggregation_exceptions",
    484                 AGGREGATION_EXCEPTIONS);
    485         matcher.addURI(ContactsContract.AUTHORITY, "aggregation_exceptions/*",
    486                 AGGREGATION_EXCEPTION_ID);
    487 
    488         matcher.addURI(ContactsContract.AUTHORITY, "settings", SETTINGS);
    489 
    490         matcher.addURI(ContactsContract.AUTHORITY, "status_updates", STATUS_UPDATES);
    491         matcher.addURI(ContactsContract.AUTHORITY, "status_updates/#", STATUS_UPDATES_ID);
    492 
    493         matcher.addURI(ContactsContract.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY,
    494                 SEARCH_SUGGESTIONS);
    495         matcher.addURI(ContactsContract.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY + "/*",
    496                 SEARCH_SUGGESTIONS);
    497         matcher.addURI(ContactsContract.AUTHORITY, SearchManager.SUGGEST_URI_PATH_SHORTCUT + "/*",
    498                 SEARCH_SHORTCUT);
    499 
    500         matcher.addURI(ContactsContract.AUTHORITY, "live_folders/contacts",
    501                 LIVE_FOLDERS_CONTACTS);
    502         matcher.addURI(ContactsContract.AUTHORITY, "live_folders/contacts/*",
    503                 LIVE_FOLDERS_CONTACTS_GROUP_NAME);
    504         matcher.addURI(ContactsContract.AUTHORITY, "live_folders/contacts_with_phones",
    505                 LIVE_FOLDERS_CONTACTS_WITH_PHONES);
    506         matcher.addURI(ContactsContract.AUTHORITY, "live_folders/favorites",
    507                 LIVE_FOLDERS_CONTACTS_FAVORITES);
    508 
    509         matcher.addURI(ContactsContract.AUTHORITY, "provider_status", PROVIDER_STATUS);
    510     }
    511 
    512     static {
    513         sCountProjectionMap = new HashMap<String, String>();
    514         sCountProjectionMap.put(BaseColumns._COUNT, "COUNT(*)");
    515 
    516         sContactsProjectionMap = new HashMap<String, String>();
    517         sContactsProjectionMap.put(Contacts._ID, Contacts._ID);
    518         sContactsProjectionMap.put(Contacts.DISPLAY_NAME, Contacts.DISPLAY_NAME_PRIMARY);
    519         sContactsProjectionMap.put(Contacts.DISPLAY_NAME_ALTERNATIVE,
    520                 Contacts.DISPLAY_NAME_ALTERNATIVE);
    521         sContactsProjectionMap.put(Contacts.DISPLAY_NAME_SOURCE, Contacts.DISPLAY_NAME_SOURCE);
    522         sContactsProjectionMap.put(Contacts.PHONETIC_NAME, Contacts.PHONETIC_NAME);
    523         sContactsProjectionMap.put(Contacts.PHONETIC_NAME_STYLE, Contacts.PHONETIC_NAME_STYLE);
    524         sContactsProjectionMap.put(Contacts.SORT_KEY_PRIMARY, Contacts.SORT_KEY_PRIMARY);
    525         sContactsProjectionMap.put(Contacts.SORT_KEY_ALTERNATIVE, Contacts.SORT_KEY_ALTERNATIVE);
    526         sContactsProjectionMap.put(Contacts.LAST_TIME_CONTACTED, Contacts.LAST_TIME_CONTACTED);
    527         sContactsProjectionMap.put(Contacts.TIMES_CONTACTED, Contacts.TIMES_CONTACTED);
    528         sContactsProjectionMap.put(Contacts.STARRED, Contacts.STARRED);
    529         sContactsProjectionMap.put(Contacts.IN_VISIBLE_GROUP, Contacts.IN_VISIBLE_GROUP);
    530         sContactsProjectionMap.put(Contacts.PHOTO_ID, Contacts.PHOTO_ID);
    531         sContactsProjectionMap.put(Contacts.CUSTOM_RINGTONE, Contacts.CUSTOM_RINGTONE);
    532         sContactsProjectionMap.put(Contacts.HAS_PHONE_NUMBER, Contacts.HAS_PHONE_NUMBER);
    533         sContactsProjectionMap.put(Contacts.SEND_TO_VOICEMAIL, Contacts.SEND_TO_VOICEMAIL);
    534         sContactsProjectionMap.put(Contacts.LOOKUP_KEY, Contacts.LOOKUP_KEY);
    535 
    536         // Handle projections for Contacts-level statuses
    537         addProjection(sContactsProjectionMap, Contacts.CONTACT_PRESENCE,
    538                 Tables.AGGREGATED_PRESENCE + "." + StatusUpdates.PRESENCE);
    539         addProjection(sContactsProjectionMap, Contacts.CONTACT_CHAT_CAPABILITY,
    540                 Tables.AGGREGATED_PRESENCE + "." + StatusUpdates.CHAT_CAPABILITY);
    541         addProjection(sContactsProjectionMap, Contacts.CONTACT_STATUS,
    542                 ContactsStatusUpdatesColumns.CONCRETE_STATUS);
    543         addProjection(sContactsProjectionMap, Contacts.CONTACT_STATUS_TIMESTAMP,
    544                 ContactsStatusUpdatesColumns.CONCRETE_STATUS_TIMESTAMP);
    545         addProjection(sContactsProjectionMap, Contacts.CONTACT_STATUS_RES_PACKAGE,
    546                 ContactsStatusUpdatesColumns.CONCRETE_STATUS_RES_PACKAGE);
    547         addProjection(sContactsProjectionMap, Contacts.CONTACT_STATUS_LABEL,
    548                 ContactsStatusUpdatesColumns.CONCRETE_STATUS_LABEL);
    549         addProjection(sContactsProjectionMap, Contacts.CONTACT_STATUS_ICON,
    550                 ContactsStatusUpdatesColumns.CONCRETE_STATUS_ICON);
    551 
    552         sContactsProjectionWithSnippetMap = new HashMap<String, String>();
    553         sContactsProjectionWithSnippetMap.putAll(sContactsProjectionMap);
    554         sContactsProjectionWithSnippetMap.put(SearchSnippetColumns.SNIPPET_MIMETYPE,
    555                 SearchSnippetColumns.SNIPPET_MIMETYPE);
    556         sContactsProjectionWithSnippetMap.put(SearchSnippetColumns.SNIPPET_DATA_ID,
    557                 SearchSnippetColumns.SNIPPET_DATA_ID);
    558         sContactsProjectionWithSnippetMap.put(SearchSnippetColumns.SNIPPET_DATA1,
    559                 SearchSnippetColumns.SNIPPET_DATA1);
    560         sContactsProjectionWithSnippetMap.put(SearchSnippetColumns.SNIPPET_DATA2,
    561                 SearchSnippetColumns.SNIPPET_DATA2);
    562         sContactsProjectionWithSnippetMap.put(SearchSnippetColumns.SNIPPET_DATA3,
    563                 SearchSnippetColumns.SNIPPET_DATA3);
    564         sContactsProjectionWithSnippetMap.put(SearchSnippetColumns.SNIPPET_DATA4,
    565                 SearchSnippetColumns.SNIPPET_DATA4);
    566 
    567         sStrequentStarredProjectionMap = new HashMap<String, String>(sContactsProjectionMap);
    568         sStrequentStarredProjectionMap.put(TIMES_CONTACED_SORT_COLUMN,
    569                   Long.MAX_VALUE + " AS " + TIMES_CONTACED_SORT_COLUMN);
    570 
    571         sStrequentFrequentProjectionMap = new HashMap<String, String>(sContactsProjectionMap);
    572         sStrequentFrequentProjectionMap.put(TIMES_CONTACED_SORT_COLUMN,
    573                   Contacts.TIMES_CONTACTED + " AS " + TIMES_CONTACED_SORT_COLUMN);
    574 
    575         sContactsVCardProjectionMap = Maps.newHashMap();
    576         sContactsVCardProjectionMap.put(OpenableColumns.DISPLAY_NAME, Contacts.DISPLAY_NAME
    577                 + " || '.vcf' AS " + OpenableColumns.DISPLAY_NAME);
    578         sContactsVCardProjectionMap.put(OpenableColumns.SIZE, "NULL AS " + OpenableColumns.SIZE);
    579 
    580         sRawContactsProjectionMap = new HashMap<String, String>();
    581         sRawContactsProjectionMap.put(RawContacts._ID, RawContacts._ID);
    582         sRawContactsProjectionMap.put(RawContacts.CONTACT_ID, RawContacts.CONTACT_ID);
    583         sRawContactsProjectionMap.put(RawContacts.ACCOUNT_NAME, RawContacts.ACCOUNT_NAME);
    584         sRawContactsProjectionMap.put(RawContacts.ACCOUNT_TYPE, RawContacts.ACCOUNT_TYPE);
    585         sRawContactsProjectionMap.put(RawContacts.SOURCE_ID, RawContacts.SOURCE_ID);
    586         sRawContactsProjectionMap.put(RawContacts.VERSION, RawContacts.VERSION);
    587         sRawContactsProjectionMap.put(RawContacts.DIRTY, RawContacts.DIRTY);
    588         sRawContactsProjectionMap.put(RawContacts.DELETED, RawContacts.DELETED);
    589         sRawContactsProjectionMap.put(RawContacts.DISPLAY_NAME_PRIMARY,
    590                 RawContacts.DISPLAY_NAME_PRIMARY);
    591         sRawContactsProjectionMap.put(RawContacts.DISPLAY_NAME_ALTERNATIVE,
    592                 RawContacts.DISPLAY_NAME_ALTERNATIVE);
    593         sRawContactsProjectionMap.put(RawContacts.DISPLAY_NAME_SOURCE,
    594                 RawContacts.DISPLAY_NAME_SOURCE);
    595         sRawContactsProjectionMap.put(RawContacts.PHONETIC_NAME,
    596                 RawContacts.PHONETIC_NAME);
    597         sRawContactsProjectionMap.put(RawContacts.PHONETIC_NAME_STYLE,
    598                 RawContacts.PHONETIC_NAME_STYLE);
    599         sRawContactsProjectionMap.put(RawContacts.NAME_VERIFIED,
    600                 RawContacts.NAME_VERIFIED);
    601         sRawContactsProjectionMap.put(RawContacts.SORT_KEY_PRIMARY,
    602                 RawContacts.SORT_KEY_PRIMARY);
    603         sRawContactsProjectionMap.put(RawContacts.SORT_KEY_ALTERNATIVE,
    604                 RawContacts.SORT_KEY_ALTERNATIVE);
    605         sRawContactsProjectionMap.put(RawContacts.TIMES_CONTACTED, RawContacts.TIMES_CONTACTED);
    606         sRawContactsProjectionMap.put(RawContacts.LAST_TIME_CONTACTED,
    607                 RawContacts.LAST_TIME_CONTACTED);
    608         sRawContactsProjectionMap.put(RawContacts.CUSTOM_RINGTONE, RawContacts.CUSTOM_RINGTONE);
    609         sRawContactsProjectionMap.put(RawContacts.SEND_TO_VOICEMAIL, RawContacts.SEND_TO_VOICEMAIL);
    610         sRawContactsProjectionMap.put(RawContacts.STARRED, RawContacts.STARRED);
    611         sRawContactsProjectionMap.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE);
    612         sRawContactsProjectionMap.put(RawContacts.SYNC1, RawContacts.SYNC1);
    613         sRawContactsProjectionMap.put(RawContacts.SYNC2, RawContacts.SYNC2);
    614         sRawContactsProjectionMap.put(RawContacts.SYNC3, RawContacts.SYNC3);
    615         sRawContactsProjectionMap.put(RawContacts.SYNC4, RawContacts.SYNC4);
    616 
    617         sDataProjectionMap = new HashMap<String, String>();
    618         sDataProjectionMap.put(Data._ID, Data._ID);
    619         sDataProjectionMap.put(Data.RAW_CONTACT_ID, Data.RAW_CONTACT_ID);
    620         sDataProjectionMap.put(Data.DATA_VERSION, Data.DATA_VERSION);
    621         sDataProjectionMap.put(Data.IS_PRIMARY, Data.IS_PRIMARY);
    622         sDataProjectionMap.put(Data.IS_SUPER_PRIMARY, Data.IS_SUPER_PRIMARY);
    623         sDataProjectionMap.put(Data.RES_PACKAGE, Data.RES_PACKAGE);
    624         sDataProjectionMap.put(Data.MIMETYPE, Data.MIMETYPE);
    625         sDataProjectionMap.put(Data.DATA1, Data.DATA1);
    626         sDataProjectionMap.put(Data.DATA2, Data.DATA2);
    627         sDataProjectionMap.put(Data.DATA3, Data.DATA3);
    628         sDataProjectionMap.put(Data.DATA4, Data.DATA4);
    629         sDataProjectionMap.put(Data.DATA5, Data.DATA5);
    630         sDataProjectionMap.put(Data.DATA6, Data.DATA6);
    631         sDataProjectionMap.put(Data.DATA7, Data.DATA7);
    632         sDataProjectionMap.put(Data.DATA8, Data.DATA8);
    633         sDataProjectionMap.put(Data.DATA9, Data.DATA9);
    634         sDataProjectionMap.put(Data.DATA10, Data.DATA10);
    635         sDataProjectionMap.put(Data.DATA11, Data.DATA11);
    636         sDataProjectionMap.put(Data.DATA12, Data.DATA12);
    637         sDataProjectionMap.put(Data.DATA13, Data.DATA13);
    638         sDataProjectionMap.put(Data.DATA14, Data.DATA14);
    639         sDataProjectionMap.put(Data.DATA15, Data.DATA15);
    640         sDataProjectionMap.put(Data.SYNC1, Data.SYNC1);
    641         sDataProjectionMap.put(Data.SYNC2, Data.SYNC2);
    642         sDataProjectionMap.put(Data.SYNC3, Data.SYNC3);
    643         sDataProjectionMap.put(Data.SYNC4, Data.SYNC4);
    644         sDataProjectionMap.put(Data.CONTACT_ID, Data.CONTACT_ID);
    645         sDataProjectionMap.put(RawContacts.ACCOUNT_NAME, RawContacts.ACCOUNT_NAME);
    646         sDataProjectionMap.put(RawContacts.ACCOUNT_TYPE, RawContacts.ACCOUNT_TYPE);
    647         sDataProjectionMap.put(RawContacts.SOURCE_ID, RawContacts.SOURCE_ID);
    648         sDataProjectionMap.put(RawContacts.VERSION, RawContacts.VERSION);
    649         sDataProjectionMap.put(RawContacts.DIRTY, RawContacts.DIRTY);
    650         sDataProjectionMap.put(RawContacts.NAME_VERIFIED, RawContacts.NAME_VERIFIED);
    651         sDataProjectionMap.put(Contacts.LOOKUP_KEY, Contacts.LOOKUP_KEY);
    652         sDataProjectionMap.put(Contacts.DISPLAY_NAME, Contacts.DISPLAY_NAME);
    653         sDataProjectionMap.put(Contacts.DISPLAY_NAME_ALTERNATIVE,
    654                 Contacts.DISPLAY_NAME_ALTERNATIVE);
    655         sDataProjectionMap.put(Contacts.DISPLAY_NAME_SOURCE, Contacts.DISPLAY_NAME_SOURCE);
    656         sDataProjectionMap.put(Contacts.PHONETIC_NAME, Contacts.PHONETIC_NAME);
    657         sDataProjectionMap.put(Contacts.PHONETIC_NAME_STYLE, Contacts.PHONETIC_NAME_STYLE);
    658         sDataProjectionMap.put(Contacts.SORT_KEY_PRIMARY, Contacts.SORT_KEY_PRIMARY);
    659         sDataProjectionMap.put(Contacts.SORT_KEY_ALTERNATIVE, Contacts.SORT_KEY_ALTERNATIVE);
    660         sDataProjectionMap.put(Contacts.CUSTOM_RINGTONE, Contacts.CUSTOM_RINGTONE);
    661         sDataProjectionMap.put(Contacts.SEND_TO_VOICEMAIL, Contacts.SEND_TO_VOICEMAIL);
    662         sDataProjectionMap.put(Contacts.LAST_TIME_CONTACTED, Contacts.LAST_TIME_CONTACTED);
    663         sDataProjectionMap.put(Contacts.TIMES_CONTACTED, Contacts.TIMES_CONTACTED);
    664         sDataProjectionMap.put(Contacts.STARRED, Contacts.STARRED);
    665         sDataProjectionMap.put(Contacts.PHOTO_ID, Contacts.PHOTO_ID);
    666         sDataProjectionMap.put(Contacts.IN_VISIBLE_GROUP, Contacts.IN_VISIBLE_GROUP);
    667         sDataProjectionMap.put(Contacts.NAME_RAW_CONTACT_ID, Contacts.NAME_RAW_CONTACT_ID);
    668         sDataProjectionMap.put(GroupMembership.GROUP_SOURCE_ID, GroupMembership.GROUP_SOURCE_ID);
    669 
    670         HashMap<String, String> columns;
    671         columns = new HashMap<String, String>();
    672         columns.put(RawContacts._ID, RawContacts._ID);
    673         columns.put(RawContacts.CONTACT_ID, RawContacts.CONTACT_ID);
    674         columns.put(RawContacts.ACCOUNT_NAME, RawContacts.ACCOUNT_NAME);
    675         columns.put(RawContacts.ACCOUNT_TYPE, RawContacts.ACCOUNT_TYPE);
    676         columns.put(RawContacts.SOURCE_ID, RawContacts.SOURCE_ID);
    677         columns.put(RawContacts.VERSION, RawContacts.VERSION);
    678         columns.put(RawContacts.DIRTY, RawContacts.DIRTY);
    679         columns.put(RawContacts.DELETED, RawContacts.DELETED);
    680         columns.put(RawContacts.IS_RESTRICTED, RawContacts.IS_RESTRICTED);
    681         columns.put(RawContacts.SYNC1, RawContacts.SYNC1);
    682         columns.put(RawContacts.SYNC2, RawContacts.SYNC2);
    683         columns.put(RawContacts.SYNC3, RawContacts.SYNC3);
    684         columns.put(RawContacts.SYNC4, RawContacts.SYNC4);
    685         columns.put(RawContacts.NAME_VERIFIED, RawContacts.NAME_VERIFIED);
    686         columns.put(Data.RES_PACKAGE, Data.RES_PACKAGE);
    687         columns.put(Data.MIMETYPE, Data.MIMETYPE);
    688         columns.put(Data.DATA1, Data.DATA1);
    689         columns.put(Data.DATA2, Data.DATA2);
    690         columns.put(Data.DATA3, Data.DATA3);
    691         columns.put(Data.DATA4, Data.DATA4);
    692         columns.put(Data.DATA5, Data.DATA5);
    693         columns.put(Data.DATA6, Data.DATA6);
    694         columns.put(Data.DATA7, Data.DATA7);
    695         columns.put(Data.DATA8, Data.DATA8);
    696         columns.put(Data.DATA9, Data.DATA9);
    697         columns.put(Data.DATA10, Data.DATA10);
    698         columns.put(Data.DATA11, Data.DATA11);
    699         columns.put(Data.DATA12, Data.DATA12);
    700         columns.put(Data.DATA13, Data.DATA13);
    701         columns.put(Data.DATA14, Data.DATA14);
    702         columns.put(Data.DATA15, Data.DATA15);
    703         columns.put(Data.SYNC1, Data.SYNC1);
    704         columns.put(Data.SYNC2, Data.SYNC2);
    705         columns.put(Data.SYNC3, Data.SYNC3);
    706         columns.put(Data.SYNC4, Data.SYNC4);
    707         columns.put(RawContacts.Entity.DATA_ID, RawContacts.Entity.DATA_ID);
    708         columns.put(Data.STARRED, Data.STARRED);
    709         columns.put(Data.DATA_VERSION, Data.DATA_VERSION);
    710         columns.put(Data.IS_PRIMARY, Data.IS_PRIMARY);
    711         columns.put(Data.IS_SUPER_PRIMARY, Data.IS_SUPER_PRIMARY);
    712         columns.put(GroupMembership.GROUP_SOURCE_ID, GroupMembership.GROUP_SOURCE_ID);
    713         sRawContactsEntityProjectionMap = columns;
    714 
    715         // Handle projections for Contacts-level statuses
    716         addProjection(sDataProjectionMap, Contacts.CONTACT_PRESENCE,
    717                 Tables.AGGREGATED_PRESENCE + "." + StatusUpdates.PRESENCE);
    718         addProjection(sContactsProjectionMap, Contacts.CONTACT_CHAT_CAPABILITY,
    719                 Tables.AGGREGATED_PRESENCE + "." + StatusUpdates.CHAT_CAPABILITY);
    720         addProjection(sDataProjectionMap, Contacts.CONTACT_STATUS,
    721                 ContactsStatusUpdatesColumns.CONCRETE_STATUS);
    722         addProjection(sDataProjectionMap, Contacts.CONTACT_STATUS_TIMESTAMP,
    723                 ContactsStatusUpdatesColumns.CONCRETE_STATUS_TIMESTAMP);
    724         addProjection(sDataProjectionMap, Contacts.CONTACT_STATUS_RES_PACKAGE,
    725                 ContactsStatusUpdatesColumns.CONCRETE_STATUS_RES_PACKAGE);
    726         addProjection(sDataProjectionMap, Contacts.CONTACT_STATUS_LABEL,
    727                 ContactsStatusUpdatesColumns.CONCRETE_STATUS_LABEL);
    728         addProjection(sDataProjectionMap, Contacts.CONTACT_STATUS_ICON,
    729                 ContactsStatusUpdatesColumns.CONCRETE_STATUS_ICON);
    730 
    731         // Handle projections for Data-level statuses
    732         addProjection(sDataProjectionMap, Data.PRESENCE,
    733                 Tables.PRESENCE + "." + StatusUpdates.PRESENCE);
    734         addProjection(sDataProjectionMap, Data.CONTACT_CHAT_CAPABILITY,
    735                 Tables.AGGREGATED_PRESENCE + "." + StatusUpdates.CHAT_CAPABILITY);
    736         addProjection(sDataProjectionMap, Data.STATUS,
    737                 StatusUpdatesColumns.CONCRETE_STATUS);
    738         addProjection(sDataProjectionMap, Data.STATUS_TIMESTAMP,
    739                 StatusUpdatesColumns.CONCRETE_STATUS_TIMESTAMP);
    740         addProjection(sDataProjectionMap, Data.STATUS_RES_PACKAGE,
    741                 StatusUpdatesColumns.CONCRETE_STATUS_RES_PACKAGE);
    742         addProjection(sDataProjectionMap, Data.STATUS_LABEL,
    743                 StatusUpdatesColumns.CONCRETE_STATUS_LABEL);
    744         addProjection(sDataProjectionMap, Data.STATUS_ICON,
    745                 StatusUpdatesColumns.CONCRETE_STATUS_ICON);
    746 
    747         // Projection map for data grouped by contact (not raw contact) and some data field(s)
    748         sDistinctDataProjectionMap = new HashMap<String, String>();
    749         sDistinctDataProjectionMap.put(Data._ID,
    750                 "MIN(" + Data._ID + ") AS " + Data._ID);
    751         sDistinctDataProjectionMap.put(Data.DATA_VERSION, Data.DATA_VERSION);
    752         sDistinctDataProjectionMap.put(Data.IS_PRIMARY, Data.IS_PRIMARY);
    753         sDistinctDataProjectionMap.put(Data.IS_SUPER_PRIMARY, Data.IS_SUPER_PRIMARY);
    754         sDistinctDataProjectionMap.put(Data.RES_PACKAGE, Data.RES_PACKAGE);
    755         sDistinctDataProjectionMap.put(Data.MIMETYPE, Data.MIMETYPE);
    756         sDistinctDataProjectionMap.put(Data.DATA1, Data.DATA1);
    757         sDistinctDataProjectionMap.put(Data.DATA2, Data.DATA2);
    758         sDistinctDataProjectionMap.put(Data.DATA3, Data.DATA3);
    759         sDistinctDataProjectionMap.put(Data.DATA4, Data.DATA4);
    760         sDistinctDataProjectionMap.put(Data.DATA5, Data.DATA5);
    761         sDistinctDataProjectionMap.put(Data.DATA6, Data.DATA6);
    762         sDistinctDataProjectionMap.put(Data.DATA7, Data.DATA7);
    763         sDistinctDataProjectionMap.put(Data.DATA8, Data.DATA8);
    764         sDistinctDataProjectionMap.put(Data.DATA9, Data.DATA9);
    765         sDistinctDataProjectionMap.put(Data.DATA10, Data.DATA10);
    766         sDistinctDataProjectionMap.put(Data.DATA11, Data.DATA11);
    767         sDistinctDataProjectionMap.put(Data.DATA12, Data.DATA12);
    768         sDistinctDataProjectionMap.put(Data.DATA13, Data.DATA13);
    769         sDistinctDataProjectionMap.put(Data.DATA14, Data.DATA14);
    770         sDistinctDataProjectionMap.put(Data.DATA15, Data.DATA15);
    771         sDistinctDataProjectionMap.put(Data.SYNC1, Data.SYNC1);
    772         sDistinctDataProjectionMap.put(Data.SYNC2, Data.SYNC2);
    773         sDistinctDataProjectionMap.put(Data.SYNC3, Data.SYNC3);
    774         sDistinctDataProjectionMap.put(Data.SYNC4, Data.SYNC4);
    775         sDistinctDataProjectionMap.put(RawContacts.CONTACT_ID, RawContacts.CONTACT_ID);
    776         sDistinctDataProjectionMap.put(Contacts.LOOKUP_KEY, Contacts.LOOKUP_KEY);
    777         sDistinctDataProjectionMap.put(Contacts.DISPLAY_NAME, Contacts.DISPLAY_NAME);
    778         sDistinctDataProjectionMap.put(Contacts.DISPLAY_NAME_ALTERNATIVE,
    779                 Contacts.DISPLAY_NAME_ALTERNATIVE);
    780         sDistinctDataProjectionMap.put(Contacts.DISPLAY_NAME_SOURCE, Contacts.DISPLAY_NAME_SOURCE);
    781         sDistinctDataProjectionMap.put(Contacts.PHONETIC_NAME, Contacts.PHONETIC_NAME);
    782         sDistinctDataProjectionMap.put(Contacts.PHONETIC_NAME_STYLE, Contacts.PHONETIC_NAME_STYLE);
    783         sDistinctDataProjectionMap.put(Contacts.SORT_KEY_PRIMARY, Contacts.SORT_KEY_PRIMARY);
    784         sDistinctDataProjectionMap.put(Contacts.SORT_KEY_ALTERNATIVE,
    785                 Contacts.SORT_KEY_ALTERNATIVE);
    786         sDistinctDataProjectionMap.put(Contacts.CUSTOM_RINGTONE, Contacts.CUSTOM_RINGTONE);
    787         sDistinctDataProjectionMap.put(Contacts.SEND_TO_VOICEMAIL, Contacts.SEND_TO_VOICEMAIL);
    788         sDistinctDataProjectionMap.put(Contacts.LAST_TIME_CONTACTED, Contacts.LAST_TIME_CONTACTED);
    789         sDistinctDataProjectionMap.put(Contacts.TIMES_CONTACTED, Contacts.TIMES_CONTACTED);
    790         sDistinctDataProjectionMap.put(Contacts.STARRED, Contacts.STARRED);
    791         sDistinctDataProjectionMap.put(Contacts.PHOTO_ID, Contacts.PHOTO_ID);
    792         sDistinctDataProjectionMap.put(Contacts.IN_VISIBLE_GROUP, Contacts.IN_VISIBLE_GROUP);
    793         sDistinctDataProjectionMap.put(GroupMembership.GROUP_SOURCE_ID,
    794                 GroupMembership.GROUP_SOURCE_ID);
    795 
    796         // Handle projections for Contacts-level statuses
    797         addProjection(sDistinctDataProjectionMap, Contacts.CONTACT_PRESENCE,
    798                 Tables.AGGREGATED_PRESENCE + "." + StatusUpdates.PRESENCE);
    799         addProjection(sDistinctDataProjectionMap, Contacts.CONTACT_CHAT_CAPABILITY,
    800                 Tables.AGGREGATED_PRESENCE + "." + StatusUpdates.CHAT_CAPABILITY);
    801         addProjection(sDistinctDataProjectionMap, Contacts.CONTACT_STATUS,
    802                 ContactsStatusUpdatesColumns.CONCRETE_STATUS);
    803         addProjection(sDistinctDataProjectionMap, Contacts.CONTACT_STATUS_TIMESTAMP,
    804                 ContactsStatusUpdatesColumns.CONCRETE_STATUS_TIMESTAMP);
    805         addProjection(sDistinctDataProjectionMap, Contacts.CONTACT_STATUS_RES_PACKAGE,
    806                 ContactsStatusUpdatesColumns.CONCRETE_STATUS_RES_PACKAGE);
    807         addProjection(sDistinctDataProjectionMap, Contacts.CONTACT_STATUS_LABEL,
    808                 ContactsStatusUpdatesColumns.CONCRETE_STATUS_LABEL);
    809         addProjection(sDistinctDataProjectionMap, Contacts.CONTACT_STATUS_ICON,
    810                 ContactsStatusUpdatesColumns.CONCRETE_STATUS_ICON);
    811 
    812         // Handle projections for Data-level statuses
    813         addProjection(sDistinctDataProjectionMap, Data.PRESENCE,
    814                 Tables.PRESENCE + "." + StatusUpdates.PRESENCE);
    815         addProjection(sDistinctDataProjectionMap, Data.CHAT_CAPABILITY,
    816                 Tables.PRESENCE + "." + StatusUpdates.CHAT_CAPABILITY);
    817         addProjection(sDistinctDataProjectionMap, Data.STATUS,
    818                 StatusUpdatesColumns.CONCRETE_STATUS);
    819         addProjection(sDistinctDataProjectionMap, Data.STATUS_TIMESTAMP,
    820                 StatusUpdatesColumns.CONCRETE_STATUS_TIMESTAMP);
    821         addProjection(sDistinctDataProjectionMap, Data.STATUS_RES_PACKAGE,
    822                 StatusUpdatesColumns.CONCRETE_STATUS_RES_PACKAGE);
    823         addProjection(sDistinctDataProjectionMap, Data.STATUS_LABEL,
    824                 StatusUpdatesColumns.CONCRETE_STATUS_LABEL);
    825         addProjection(sDistinctDataProjectionMap, Data.STATUS_ICON,
    826                 StatusUpdatesColumns.CONCRETE_STATUS_ICON);
    827 
    828         sPhoneLookupProjectionMap = new HashMap<String, String>();
    829         sPhoneLookupProjectionMap.put(PhoneLookup._ID,
    830                 "contacts_view." + Contacts._ID
    831                         + " AS " + PhoneLookup._ID);
    832         sPhoneLookupProjectionMap.put(PhoneLookup.LOOKUP_KEY,
    833                 "contacts_view." + Contacts.LOOKUP_KEY
    834                         + " AS " + PhoneLookup.LOOKUP_KEY);
    835         sPhoneLookupProjectionMap.put(PhoneLookup.DISPLAY_NAME,
    836                 "contacts_view." + Contacts.DISPLAY_NAME
    837                         + " AS " + PhoneLookup.DISPLAY_NAME);
    838         sPhoneLookupProjectionMap.put(PhoneLookup.LAST_TIME_CONTACTED,
    839                 "contacts_view." + Contacts.LAST_TIME_CONTACTED
    840                         + " AS " + PhoneLookup.LAST_TIME_CONTACTED);
    841         sPhoneLookupProjectionMap.put(PhoneLookup.TIMES_CONTACTED,
    842                 "contacts_view." + Contacts.TIMES_CONTACTED
    843                         + " AS " + PhoneLookup.TIMES_CONTACTED);
    844         sPhoneLookupProjectionMap.put(PhoneLookup.STARRED,
    845                 "contacts_view." + Contacts.STARRED
    846                         + " AS " + PhoneLookup.STARRED);
    847         sPhoneLookupProjectionMap.put(PhoneLookup.IN_VISIBLE_GROUP,
    848                 "contacts_view." + Contacts.IN_VISIBLE_GROUP
    849                         + " AS " + PhoneLookup.IN_VISIBLE_GROUP);
    850         sPhoneLookupProjectionMap.put(PhoneLookup.PHOTO_ID,
    851                 "contacts_view." + Contacts.PHOTO_ID
    852                         + " AS " + PhoneLookup.PHOTO_ID);
    853         sPhoneLookupProjectionMap.put(PhoneLookup.CUSTOM_RINGTONE,
    854                 "contacts_view." + Contacts.CUSTOM_RINGTONE
    855                         + " AS " + PhoneLookup.CUSTOM_RINGTONE);
    856         sPhoneLookupProjectionMap.put(PhoneLookup.HAS_PHONE_NUMBER,
    857                 "contacts_view." + Contacts.HAS_PHONE_NUMBER
    858                         + " AS " + PhoneLookup.HAS_PHONE_NUMBER);
    859         sPhoneLookupProjectionMap.put(PhoneLookup.SEND_TO_VOICEMAIL,
    860                 "contacts_view." + Contacts.SEND_TO_VOICEMAIL
    861                         + " AS " + PhoneLookup.SEND_TO_VOICEMAIL);
    862         sPhoneLookupProjectionMap.put(PhoneLookup.NUMBER,
    863                 Phone.NUMBER + " AS " + PhoneLookup.NUMBER);
    864         sPhoneLookupProjectionMap.put(PhoneLookup.TYPE,
    865                 Phone.TYPE + " AS " + PhoneLookup.TYPE);
    866         sPhoneLookupProjectionMap.put(PhoneLookup.LABEL,
    867                 Phone.LABEL + " AS " + PhoneLookup.LABEL);
    868 
    869         // Groups projection map
    870         columns = new HashMap<String, String>();
    871         columns.put(Groups._ID, Groups._ID);
    872         columns.put(Groups.ACCOUNT_NAME, Groups.ACCOUNT_NAME);
    873         columns.put(Groups.ACCOUNT_TYPE, Groups.ACCOUNT_TYPE);
    874         columns.put(Groups.SOURCE_ID, Groups.SOURCE_ID);
    875         columns.put(Groups.DIRTY, Groups.DIRTY);
    876         columns.put(Groups.VERSION, Groups.VERSION);
    877         columns.put(Groups.RES_PACKAGE, Groups.RES_PACKAGE);
    878         columns.put(Groups.TITLE, Groups.TITLE);
    879         columns.put(Groups.TITLE_RES, Groups.TITLE_RES);
    880         columns.put(Groups.GROUP_VISIBLE, Groups.GROUP_VISIBLE);
    881         columns.put(Groups.SYSTEM_ID, Groups.SYSTEM_ID);
    882         columns.put(Groups.DELETED, Groups.DELETED);
    883         columns.put(Groups.NOTES, Groups.NOTES);
    884         columns.put(Groups.SHOULD_SYNC, Groups.SHOULD_SYNC);
    885         columns.put(Groups.SYNC1, Groups.SYNC1);
    886         columns.put(Groups.SYNC2, Groups.SYNC2);
    887         columns.put(Groups.SYNC3, Groups.SYNC3);
    888         columns.put(Groups.SYNC4, Groups.SYNC4);
    889         sGroupsProjectionMap = columns;
    890 
    891         // RawContacts and groups projection map
    892         columns = new HashMap<String, String>();
    893         columns.putAll(sGroupsProjectionMap);
    894         columns.put(Groups.SUMMARY_COUNT, "(SELECT COUNT(DISTINCT " + ContactsColumns.CONCRETE_ID
    895                 + ") FROM " + Tables.DATA_JOIN_MIMETYPES_RAW_CONTACTS_CONTACTS + " WHERE "
    896                 + Clauses.MIMETYPE_IS_GROUP_MEMBERSHIP + " AND " + Clauses.BELONGS_TO_GROUP
    897                 + ") AS " + Groups.SUMMARY_COUNT);
    898         columns.put(Groups.SUMMARY_WITH_PHONES, "(SELECT COUNT(DISTINCT "
    899                 + ContactsColumns.CONCRETE_ID + ") FROM "
    900                 + Tables.DATA_JOIN_MIMETYPES_RAW_CONTACTS_CONTACTS + " WHERE "
    901                 + Clauses.MIMETYPE_IS_GROUP_MEMBERSHIP + " AND " + Clauses.BELONGS_TO_GROUP
    902                 + " AND " + Contacts.HAS_PHONE_NUMBER + ") AS " + Groups.SUMMARY_WITH_PHONES);
    903         sGroupsSummaryProjectionMap = columns;
    904 
    905         // Aggregate exception projection map
    906         columns = new HashMap<String, String>();
    907         columns.put(AggregationExceptionColumns._ID, Tables.AGGREGATION_EXCEPTIONS + "._id AS _id");
    908         columns.put(AggregationExceptions.TYPE, AggregationExceptions.TYPE);
    909         columns.put(AggregationExceptions.RAW_CONTACT_ID1, AggregationExceptions.RAW_CONTACT_ID1);
    910         columns.put(AggregationExceptions.RAW_CONTACT_ID2, AggregationExceptions.RAW_CONTACT_ID2);
    911         sAggregationExceptionsProjectionMap = columns;
    912 
    913         // Settings projection map
    914         columns = new HashMap<String, String>();
    915         columns.put(Settings.ACCOUNT_NAME, Settings.ACCOUNT_NAME);
    916         columns.put(Settings.ACCOUNT_TYPE, Settings.ACCOUNT_TYPE);
    917         columns.put(Settings.UNGROUPED_VISIBLE, Settings.UNGROUPED_VISIBLE);
    918         columns.put(Settings.SHOULD_SYNC, Settings.SHOULD_SYNC);
    919         columns.put(Settings.ANY_UNSYNCED, "(CASE WHEN MIN(" + Settings.SHOULD_SYNC
    920                 + ",(SELECT (CASE WHEN MIN(" + Groups.SHOULD_SYNC + ") IS NULL THEN 1 ELSE MIN("
    921                 + Groups.SHOULD_SYNC + ") END) FROM " + Tables.GROUPS + " WHERE "
    922                 + GroupsColumns.CONCRETE_ACCOUNT_NAME + "=" + SettingsColumns.CONCRETE_ACCOUNT_NAME
    923                 + " AND " + GroupsColumns.CONCRETE_ACCOUNT_TYPE + "="
    924                 + SettingsColumns.CONCRETE_ACCOUNT_TYPE + "))=0 THEN 1 ELSE 0 END) AS "
    925                 + Settings.ANY_UNSYNCED);
    926         columns.put(Settings.UNGROUPED_COUNT, "(SELECT COUNT(*) FROM (SELECT 1 FROM "
    927                 + Tables.SETTINGS_JOIN_RAW_CONTACTS_DATA_MIMETYPES_CONTACTS + " GROUP BY "
    928                 + Clauses.GROUP_BY_ACCOUNT_CONTACT_ID + " HAVING " + Clauses.HAVING_NO_GROUPS
    929                 + ")) AS " + Settings.UNGROUPED_COUNT);
    930         columns.put(Settings.UNGROUPED_WITH_PHONES, "(SELECT COUNT(*) FROM (SELECT 1 FROM "
    931                 + Tables.SETTINGS_JOIN_RAW_CONTACTS_DATA_MIMETYPES_CONTACTS + " WHERE "
    932                 + Contacts.HAS_PHONE_NUMBER + " GROUP BY " + Clauses.GROUP_BY_ACCOUNT_CONTACT_ID
    933                 + " HAVING " + Clauses.HAVING_NO_GROUPS + ")) AS "
    934                 + Settings.UNGROUPED_WITH_PHONES);
    935         sSettingsProjectionMap = columns;
    936 
    937         columns = new HashMap<String, String>();
    938         columns.put(PresenceColumns.RAW_CONTACT_ID, PresenceColumns.RAW_CONTACT_ID);
    939         columns.put(StatusUpdates.DATA_ID,
    940                 DataColumns.CONCRETE_ID + " AS " + StatusUpdates.DATA_ID);
    941         columns.put(StatusUpdates.IM_ACCOUNT, StatusUpdates.IM_ACCOUNT);
    942         columns.put(StatusUpdates.IM_HANDLE, StatusUpdates.IM_HANDLE);
    943         columns.put(StatusUpdates.PROTOCOL, StatusUpdates.PROTOCOL);
    944         // We cannot allow a null in the custom protocol field, because SQLite3 does not
    945         // properly enforce uniqueness of null values
    946         columns.put(StatusUpdates.CUSTOM_PROTOCOL, "(CASE WHEN " + StatusUpdates.CUSTOM_PROTOCOL
    947                 + "='' THEN NULL ELSE " + StatusUpdates.CUSTOM_PROTOCOL + " END) AS "
    948                 + StatusUpdates.CUSTOM_PROTOCOL);
    949         columns.put(StatusUpdates.PRESENCE, StatusUpdates.PRESENCE);
    950         columns.put(StatusUpdates.CHAT_CAPABILITY, StatusUpdates.CHAT_CAPABILITY);
    951         columns.put(StatusUpdates.STATUS, StatusUpdates.STATUS);
    952         columns.put(StatusUpdates.STATUS_TIMESTAMP, StatusUpdates.STATUS_TIMESTAMP);
    953         columns.put(StatusUpdates.STATUS_RES_PACKAGE, StatusUpdates.STATUS_RES_PACKAGE);
    954         columns.put(StatusUpdates.STATUS_ICON, StatusUpdates.STATUS_ICON);
    955         columns.put(StatusUpdates.STATUS_LABEL, StatusUpdates.STATUS_LABEL);
    956         sStatusUpdatesProjectionMap = columns;
    957 
    958         // Live folder projection
    959         sLiveFoldersProjectionMap = new HashMap<String, String>();
    960         sLiveFoldersProjectionMap.put(LiveFolders._ID,
    961                 Contacts._ID + " AS " + LiveFolders._ID);
    962         sLiveFoldersProjectionMap.put(LiveFolders.NAME,
    963                 Contacts.DISPLAY_NAME + " AS " + LiveFolders.NAME);
    964         // TODO: Put contact photo back when we have a way to display a default icon
    965         // for contacts without a photo
    966         // sLiveFoldersProjectionMap.put(LiveFolders.ICON_BITMAP,
    967         //      Photos.DATA + " AS " + LiveFolders.ICON_BITMAP);
    968     }
    969 
    970     private static void addProjection(HashMap<String, String> map, String toField, String fromField) {
    971         map.put(toField, fromField + " AS " + toField);
    972     }
    973 
    974     /**
    975      * Handles inserts and update for a specific Data type.
    976      */
    977     private abstract class DataRowHandler {
    978 
    979         protected final String mMimetype;
    980         protected long mMimetypeId;
    981 
    982         @SuppressWarnings("all")
    983         public DataRowHandler(String mimetype) {
    984             mMimetype = mimetype;
    985 
    986             // To ensure the data column position. This is dead code if properly configured.
    987             if (StructuredName.DISPLAY_NAME != Data.DATA1 || Nickname.NAME != Data.DATA1
    988                     || Organization.COMPANY != Data.DATA1 || Phone.NUMBER != Data.DATA1
    989                     || Email.DATA != Data.DATA1) {
    990                 throw new AssertionError("Some of ContactsContract.CommonDataKinds class primary"
    991                         + " data is not in DATA1 column");
    992             }
    993         }
    994 
    995         protected long getMimeTypeId() {
    996             if (mMimetypeId == 0) {
    997                 mMimetypeId = mDbHelper.getMimeTypeId(mMimetype);
    998             }
    999             return mMimetypeId;
   1000         }
   1001 
   1002         /**
   1003          * Inserts a row into the {@link Data} table.
   1004          */
   1005         public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) {
   1006             final long dataId = db.insert(Tables.DATA, null, values);
   1007 
   1008             Integer primary = values.getAsInteger(Data.IS_PRIMARY);
   1009             if (primary != null && primary != 0) {
   1010                 setIsPrimary(rawContactId, dataId, getMimeTypeId());
   1011             }
   1012 
   1013             return dataId;
   1014         }
   1015 
   1016         /**
   1017          * Validates data and updates a {@link Data} row using the cursor, which contains
   1018          * the current data.
   1019          *
   1020          * @return true if update changed something
   1021          */
   1022         public boolean update(SQLiteDatabase db, ContentValues values, Cursor c,
   1023                 boolean callerIsSyncAdapter) {
   1024             long dataId = c.getLong(DataUpdateQuery._ID);
   1025             long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID);
   1026 
   1027             if (values.containsKey(Data.IS_SUPER_PRIMARY)) {
   1028                 long mimeTypeId = getMimeTypeId();
   1029                 setIsSuperPrimary(rawContactId, dataId, mimeTypeId);
   1030                 setIsPrimary(rawContactId, dataId, mimeTypeId);
   1031 
   1032                 // Now that we've taken care of setting these, remove them from "values".
   1033                 values.remove(Data.IS_SUPER_PRIMARY);
   1034                 values.remove(Data.IS_PRIMARY);
   1035             } else if (values.containsKey(Data.IS_PRIMARY)) {
   1036                 setIsPrimary(rawContactId, dataId, getMimeTypeId());
   1037 
   1038                 // Now that we've taken care of setting this, remove it from "values".
   1039                 values.remove(Data.IS_PRIMARY);
   1040             }
   1041 
   1042             if (values.size() > 0) {
   1043                 mSelectionArgs1[0] = String.valueOf(dataId);
   1044                 mDb.update(Tables.DATA, values, Data._ID + " =?", mSelectionArgs1);
   1045             }
   1046 
   1047             if (!callerIsSyncAdapter) {
   1048                 setRawContactDirty(rawContactId);
   1049             }
   1050 
   1051             return true;
   1052         }
   1053 
   1054         public int delete(SQLiteDatabase db, Cursor c) {
   1055             long dataId = c.getLong(DataDeleteQuery._ID);
   1056             long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID);
   1057             boolean primary = c.getInt(DataDeleteQuery.IS_PRIMARY) != 0;
   1058             mSelectionArgs1[0] = String.valueOf(dataId);
   1059             int count = db.delete(Tables.DATA, Data._ID + "=?", mSelectionArgs1);
   1060             mSelectionArgs1[0] = String.valueOf(rawContactId);
   1061             db.delete(Tables.PRESENCE, PresenceColumns.RAW_CONTACT_ID + "=?", mSelectionArgs1);
   1062             if (count != 0 && primary) {
   1063                 fixPrimary(db, rawContactId);
   1064             }
   1065             return count;
   1066         }
   1067 
   1068         private void fixPrimary(SQLiteDatabase db, long rawContactId) {
   1069             long mimeTypeId = getMimeTypeId();
   1070             long primaryId = -1;
   1071             int primaryType = -1;
   1072             mSelectionArgs1[0] = String.valueOf(rawContactId);
   1073             Cursor c = db.query(DataDeleteQuery.TABLE,
   1074                     DataDeleteQuery.CONCRETE_COLUMNS,
   1075                     Data.RAW_CONTACT_ID + "=?" +
   1076                         " AND " + DataColumns.MIMETYPE_ID + "=" + mimeTypeId,
   1077                     mSelectionArgs1, null, null, null);
   1078             try {
   1079                 while (c.moveToNext()) {
   1080                     long dataId = c.getLong(DataDeleteQuery._ID);
   1081                     int type = c.getInt(DataDeleteQuery.DATA1);
   1082                     if (primaryType == -1 || getTypeRank(type) < getTypeRank(primaryType)) {
   1083                         primaryId = dataId;
   1084                         primaryType = type;
   1085                     }
   1086                 }
   1087             } finally {
   1088                 c.close();
   1089             }
   1090             if (primaryId != -1) {
   1091                 setIsPrimary(rawContactId, primaryId, mimeTypeId);
   1092             }
   1093         }
   1094 
   1095         /**
   1096          * Returns the rank of a specific record type to be used in determining the primary
   1097          * row. Lower number represents higher priority.
   1098          */
   1099         protected int getTypeRank(int type) {
   1100             return 0;
   1101         }
   1102 
   1103         protected void fixRawContactDisplayName(SQLiteDatabase db, long rawContactId) {
   1104             if (!isNewRawContact(rawContactId)) {
   1105                 updateRawContactDisplayName(db, rawContactId);
   1106                 mContactAggregator.updateDisplayNameForRawContact(db, rawContactId);
   1107             }
   1108         }
   1109 
   1110         /**
   1111          * Return set of values, using current values at given {@link Data#_ID}
   1112          * as baseline, but augmented with any updates.  Returns null if there is
   1113          * no change.
   1114          */
   1115         public ContentValues getAugmentedValues(SQLiteDatabase db, long dataId,
   1116                 ContentValues update) {
   1117             boolean changing = false;
   1118             final ContentValues values = new ContentValues();
   1119             mSelectionArgs1[0] = String.valueOf(dataId);
   1120             final Cursor cursor = db.query(Tables.DATA, null, Data._ID + "=?",
   1121                     mSelectionArgs1, null, null, null);
   1122             try {
   1123                 if (cursor.moveToFirst()) {
   1124                     for (int i = 0; i < cursor.getColumnCount(); i++) {
   1125                         final String key = cursor.getColumnName(i);
   1126                         final String value = cursor.getString(i);
   1127                         if (!changing && update.containsKey(key)) {
   1128                             Object newValue = update.get(key);
   1129                             String newString = newValue == null ? null : newValue.toString();
   1130                             changing |= !TextUtils.equals(newString, value);
   1131                         }
   1132                         values.put(key, value);
   1133                     }
   1134                 }
   1135             } finally {
   1136                 cursor.close();
   1137             }
   1138             if (!changing) {
   1139                 return null;
   1140             }
   1141 
   1142             values.putAll(update);
   1143             return values;
   1144         }
   1145     }
   1146 
   1147     public class CustomDataRowHandler extends DataRowHandler {
   1148 
   1149         public CustomDataRowHandler(String mimetype) {
   1150             super(mimetype);
   1151         }
   1152     }
   1153 
   1154     public class StructuredNameRowHandler extends DataRowHandler {
   1155         private final NameSplitter mSplitter;
   1156 
   1157         public StructuredNameRowHandler(NameSplitter splitter) {
   1158             super(StructuredName.CONTENT_ITEM_TYPE);
   1159             mSplitter = splitter;
   1160         }
   1161 
   1162         @Override
   1163         public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) {
   1164             fixStructuredNameComponents(values, values);
   1165 
   1166             long dataId = super.insert(db, rawContactId, values);
   1167 
   1168             String name = values.getAsString(StructuredName.DISPLAY_NAME);
   1169             Integer fullNameStyle = values.getAsInteger(StructuredName.FULL_NAME_STYLE);
   1170             insertNameLookupForStructuredName(rawContactId, dataId, name,
   1171                     fullNameStyle != null
   1172                             ? mNameSplitter.getAdjustedFullNameStyle(fullNameStyle)
   1173                             : FullNameStyle.UNDEFINED);
   1174             insertNameLookupForPhoneticName(rawContactId, dataId, values);
   1175             fixRawContactDisplayName(db, rawContactId);
   1176             triggerAggregation(rawContactId);
   1177             return dataId;
   1178         }
   1179 
   1180         @Override
   1181         public boolean update(SQLiteDatabase db, ContentValues values, Cursor c,
   1182                 boolean callerIsSyncAdapter) {
   1183             final long dataId = c.getLong(DataUpdateQuery._ID);
   1184             final long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID);
   1185 
   1186             final ContentValues augmented = getAugmentedValues(db, dataId, values);
   1187             if (augmented == null) {  // No change
   1188                 return false;
   1189             }
   1190 
   1191             fixStructuredNameComponents(augmented, values);
   1192 
   1193             super.update(db, values, c, callerIsSyncAdapter);
   1194             if (values.containsKey(StructuredName.DISPLAY_NAME) ||
   1195                     values.containsKey(StructuredName.PHONETIC_FAMILY_NAME) ||
   1196                     values.containsKey(StructuredName.PHONETIC_MIDDLE_NAME) ||
   1197                     values.containsKey(StructuredName.PHONETIC_GIVEN_NAME)) {
   1198                 augmented.putAll(values);
   1199                 String name = augmented.getAsString(StructuredName.DISPLAY_NAME);
   1200                 deleteNameLookup(dataId);
   1201                 Integer fullNameStyle = augmented.getAsInteger(StructuredName.FULL_NAME_STYLE);
   1202                 insertNameLookupForStructuredName(rawContactId, dataId, name,
   1203                         fullNameStyle != null
   1204                                 ? mNameSplitter.getAdjustedFullNameStyle(fullNameStyle)
   1205                                 : FullNameStyle.UNDEFINED);
   1206                 insertNameLookupForPhoneticName(rawContactId, dataId, augmented);
   1207             }
   1208             fixRawContactDisplayName(db, rawContactId);
   1209             triggerAggregation(rawContactId);
   1210             return true;
   1211         }
   1212 
   1213         @Override
   1214         public int delete(SQLiteDatabase db, Cursor c) {
   1215             long dataId = c.getLong(DataDeleteQuery._ID);
   1216             long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID);
   1217 
   1218             int count = super.delete(db, c);
   1219 
   1220             deleteNameLookup(dataId);
   1221             fixRawContactDisplayName(db, rawContactId);
   1222             triggerAggregation(rawContactId);
   1223             return count;
   1224         }
   1225 
   1226         /**
   1227          * Specific list of structured fields.
   1228          */
   1229         private final String[] STRUCTURED_FIELDS = new String[] {
   1230                 StructuredName.PREFIX, StructuredName.GIVEN_NAME, StructuredName.MIDDLE_NAME,
   1231                 StructuredName.FAMILY_NAME, StructuredName.SUFFIX
   1232         };
   1233 
   1234         /**
   1235          * Parses the supplied display name, but only if the incoming values do
   1236          * not already contain structured name parts. Also, if the display name
   1237          * is not provided, generate one by concatenating first name and last
   1238          * name.
   1239          */
   1240         private void fixStructuredNameComponents(ContentValues augmented, ContentValues update) {
   1241             final String unstruct = update.getAsString(StructuredName.DISPLAY_NAME);
   1242 
   1243             final boolean touchedUnstruct = !TextUtils.isEmpty(unstruct);
   1244             final boolean touchedStruct = !areAllEmpty(update, STRUCTURED_FIELDS);
   1245 
   1246             if (touchedUnstruct && !touchedStruct) {
   1247                 NameSplitter.Name name = new NameSplitter.Name();
   1248                 mSplitter.split(name, unstruct);
   1249                 name.toValues(update);
   1250             } else if (!touchedUnstruct
   1251                     && (touchedStruct || areAnySpecified(update, STRUCTURED_FIELDS))) {
   1252                 // We need to update the display name when any structured components
   1253                 // are specified, even when they are null, which is why we are checking
   1254                 // areAnySpecified.  The touchedStruct in the condition is an optimization:
   1255                 // if there are non-null values, we know for a fact that some values are present.
   1256                 NameSplitter.Name name = new NameSplitter.Name();
   1257                 name.fromValues(augmented);
   1258                 // As the name could be changed, let's guess the name style again.
   1259                 name.fullNameStyle = FullNameStyle.UNDEFINED;
   1260                 mSplitter.guessNameStyle(name);
   1261                 int unadjustedFullNameStyle = name.fullNameStyle;
   1262                 name.fullNameStyle = mSplitter.getAdjustedFullNameStyle(name.fullNameStyle);
   1263                 final String joined = mSplitter.join(name, true);
   1264                 update.put(StructuredName.DISPLAY_NAME, joined);
   1265 
   1266                 update.put(StructuredName.FULL_NAME_STYLE, unadjustedFullNameStyle);
   1267                 update.put(StructuredName.PHONETIC_NAME_STYLE, name.phoneticNameStyle);
   1268             } else if (touchedUnstruct && touchedStruct){
   1269                 if (!update.containsKey(StructuredName.FULL_NAME_STYLE)) {
   1270                     update.put(StructuredName.FULL_NAME_STYLE,
   1271                             mSplitter.guessFullNameStyle(unstruct));
   1272                 }
   1273                 if (!update.containsKey(StructuredName.PHONETIC_NAME_STYLE)) {
   1274                     update.put(StructuredName.PHONETIC_NAME_STYLE,
   1275                             mSplitter.guessPhoneticNameStyle(unstruct));
   1276                 }
   1277             }
   1278         }
   1279     }
   1280 
   1281     public class StructuredPostalRowHandler extends DataRowHandler {
   1282         private PostalSplitter mSplitter;
   1283 
   1284         public StructuredPostalRowHandler(PostalSplitter splitter) {
   1285             super(StructuredPostal.CONTENT_ITEM_TYPE);
   1286             mSplitter = splitter;
   1287         }
   1288 
   1289         @Override
   1290         public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) {
   1291             fixStructuredPostalComponents(values, values);
   1292             return super.insert(db, rawContactId, values);
   1293         }
   1294 
   1295         @Override
   1296         public boolean update(SQLiteDatabase db, ContentValues values, Cursor c,
   1297                 boolean callerIsSyncAdapter) {
   1298             final long dataId = c.getLong(DataUpdateQuery._ID);
   1299             final ContentValues augmented = getAugmentedValues(db, dataId, values);
   1300             if (augmented == null) {    // No change
   1301                 return false;
   1302             }
   1303 
   1304             fixStructuredPostalComponents(augmented, values);
   1305             super.update(db, values, c, callerIsSyncAdapter);
   1306             return true;
   1307         }
   1308 
   1309         /**
   1310          * Specific list of structured fields.
   1311          */
   1312         private final String[] STRUCTURED_FIELDS = new String[] {
   1313                 StructuredPostal.STREET, StructuredPostal.POBOX, StructuredPostal.NEIGHBORHOOD,
   1314                 StructuredPostal.CITY, StructuredPostal.REGION, StructuredPostal.POSTCODE,
   1315                 StructuredPostal.COUNTRY,
   1316         };
   1317 
   1318         /**
   1319          * Prepares the given {@link StructuredPostal} row, building
   1320          * {@link StructuredPostal#FORMATTED_ADDRESS} to match the structured
   1321          * values when missing. When structured components are missing, the
   1322          * unstructured value is assigned to {@link StructuredPostal#STREET}.
   1323          */
   1324         private void fixStructuredPostalComponents(ContentValues augmented, ContentValues update) {
   1325             final String unstruct = update.getAsString(StructuredPostal.FORMATTED_ADDRESS);
   1326 
   1327             final boolean touchedUnstruct = !TextUtils.isEmpty(unstruct);
   1328             final boolean touchedStruct = !areAllEmpty(update, STRUCTURED_FIELDS);
   1329 
   1330             final PostalSplitter.Postal postal = new PostalSplitter.Postal();
   1331 
   1332             if (touchedUnstruct && !touchedStruct) {
   1333                 mSplitter.split(postal, unstruct);
   1334                 postal.toValues(update);
   1335             } else if (!touchedUnstruct
   1336                     && (touchedStruct || areAnySpecified(update, STRUCTURED_FIELDS))) {
   1337                 // See comment in
   1338                 postal.fromValues(augmented);
   1339                 final String joined = mSplitter.join(postal);
   1340                 update.put(StructuredPostal.FORMATTED_ADDRESS, joined);
   1341             }
   1342         }
   1343     }
   1344 
   1345     public class CommonDataRowHandler extends DataRowHandler {
   1346 
   1347         private final String mTypeColumn;
   1348         private final String mLabelColumn;
   1349 
   1350         public CommonDataRowHandler(String mimetype, String typeColumn, String labelColumn) {
   1351             super(mimetype);
   1352             mTypeColumn = typeColumn;
   1353             mLabelColumn = labelColumn;
   1354         }
   1355 
   1356         @Override
   1357         public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) {
   1358             enforceTypeAndLabel(values, values);
   1359             return super.insert(db, rawContactId, values);
   1360         }
   1361 
   1362         @Override
   1363         public boolean update(SQLiteDatabase db, ContentValues values, Cursor c,
   1364                 boolean callerIsSyncAdapter) {
   1365             final long dataId = c.getLong(DataUpdateQuery._ID);
   1366             final ContentValues augmented = getAugmentedValues(db, dataId, values);
   1367             if (augmented == null) {        // No change
   1368                 return false;
   1369             }
   1370             enforceTypeAndLabel(augmented, values);
   1371             return super.update(db, values, c, callerIsSyncAdapter);
   1372         }
   1373 
   1374         /**
   1375          * If the given {@link ContentValues} defines {@link #mTypeColumn},
   1376          * enforce that {@link #mLabelColumn} only appears when type is
   1377          * {@link BaseTypes#TYPE_CUSTOM}. Exception is thrown otherwise.
   1378          */
   1379         private void enforceTypeAndLabel(ContentValues augmented, ContentValues update) {
   1380             final boolean hasType = !TextUtils.isEmpty(augmented.getAsString(mTypeColumn));
   1381             final boolean hasLabel = !TextUtils.isEmpty(augmented.getAsString(mLabelColumn));
   1382 
   1383             if (hasLabel && !hasType) {
   1384                 // When label exists, assert that some type is defined
   1385                 throw new IllegalArgumentException(mTypeColumn + " must be specified when "
   1386                         + mLabelColumn + " is defined.");
   1387             }
   1388         }
   1389     }
   1390 
   1391     public class OrganizationDataRowHandler extends CommonDataRowHandler {
   1392 
   1393         public OrganizationDataRowHandler() {
   1394             super(Organization.CONTENT_ITEM_TYPE, Organization.TYPE, Organization.LABEL);
   1395         }
   1396 
   1397         @Override
   1398         public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) {
   1399             String company = values.getAsString(Organization.COMPANY);
   1400             String title = values.getAsString(Organization.TITLE);
   1401 
   1402             long dataId = super.insert(db, rawContactId, values);
   1403 
   1404             fixRawContactDisplayName(db, rawContactId);
   1405             insertNameLookupForOrganization(rawContactId, dataId, company, title);
   1406             return dataId;
   1407         }
   1408 
   1409         @Override
   1410         public boolean update(SQLiteDatabase db, ContentValues values, Cursor c,
   1411                 boolean callerIsSyncAdapter) {
   1412             if (!super.update(db, values, c, callerIsSyncAdapter)) {
   1413                 return false;
   1414             }
   1415 
   1416             boolean containsCompany = values.containsKey(Organization.COMPANY);
   1417             boolean containsTitle = values.containsKey(Organization.TITLE);
   1418             if (containsCompany || containsTitle) {
   1419                 long dataId = c.getLong(DataUpdateQuery._ID);
   1420                 long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID);
   1421 
   1422                 String company;
   1423 
   1424                 if (containsCompany) {
   1425                     company = values.getAsString(Organization.COMPANY);
   1426                 } else {
   1427                     mSelectionArgs1[0] = String.valueOf(dataId);
   1428                     company = DatabaseUtils.stringForQuery(db,
   1429                             "SELECT " + Organization.COMPANY +
   1430                             " FROM " + Tables.DATA +
   1431                             " WHERE " + Data._ID + "=?", mSelectionArgs1);
   1432                 }
   1433 
   1434                 String title;
   1435                 if (containsTitle) {
   1436                     title = values.getAsString(Organization.TITLE);
   1437                 } else {
   1438                     mSelectionArgs1[0] = String.valueOf(dataId);
   1439                     title = DatabaseUtils.stringForQuery(db,
   1440                             "SELECT " + Organization.TITLE +
   1441                             " FROM " + Tables.DATA +
   1442                             " WHERE " + Data._ID + "=?", mSelectionArgs1);
   1443                 }
   1444 
   1445                 deleteNameLookup(dataId);
   1446                 insertNameLookupForOrganization(rawContactId, dataId, company, title);
   1447 
   1448                 fixRawContactDisplayName(db, rawContactId);
   1449             }
   1450             return true;
   1451         }
   1452 
   1453         @Override
   1454         public int delete(SQLiteDatabase db, Cursor c) {
   1455             long dataId = c.getLong(DataUpdateQuery._ID);
   1456             long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID);
   1457 
   1458             int count = super.delete(db, c);
   1459             fixRawContactDisplayName(db, rawContactId);
   1460             deleteNameLookup(dataId);
   1461             return count;
   1462         }
   1463 
   1464         @Override
   1465         protected int getTypeRank(int type) {
   1466             switch (type) {
   1467                 case Organization.TYPE_WORK: return 0;
   1468                 case Organization.TYPE_CUSTOM: return 1;
   1469                 case Organization.TYPE_OTHER: return 2;
   1470                 default: return 1000;
   1471             }
   1472         }
   1473     }
   1474 
   1475     public class EmailDataRowHandler extends CommonDataRowHandler {
   1476 
   1477         public EmailDataRowHandler() {
   1478             super(Email.CONTENT_ITEM_TYPE, Email.TYPE, Email.LABEL);
   1479         }
   1480 
   1481         @Override
   1482         public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) {
   1483             String email = values.getAsString(Email.DATA);
   1484 
   1485             long dataId = super.insert(db, rawContactId, values);
   1486 
   1487             fixRawContactDisplayName(db, rawContactId);
   1488             String address = insertNameLookupForEmail(rawContactId, dataId, email);
   1489             if (address != null) {
   1490                 triggerAggregation(rawContactId);
   1491             }
   1492             return dataId;
   1493         }
   1494 
   1495         @Override
   1496         public boolean update(SQLiteDatabase db, ContentValues values, Cursor c,
   1497                 boolean callerIsSyncAdapter) {
   1498             if (!super.update(db, values, c, callerIsSyncAdapter)) {
   1499                 return false;
   1500             }
   1501 
   1502             if (values.containsKey(Email.DATA)) {
   1503                 long dataId = c.getLong(DataUpdateQuery._ID);
   1504                 long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID);
   1505 
   1506                 String address = values.getAsString(Email.DATA);
   1507                 deleteNameLookup(dataId);
   1508                 insertNameLookupForEmail(rawContactId, dataId, address);
   1509                 fixRawContactDisplayName(db, rawContactId);
   1510                 triggerAggregation(rawContactId);
   1511             }
   1512 
   1513             return true;
   1514         }
   1515 
   1516         @Override
   1517         public int delete(SQLiteDatabase db, Cursor c) {
   1518             long dataId = c.getLong(DataDeleteQuery._ID);
   1519             long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID);
   1520 
   1521             int count = super.delete(db, c);
   1522 
   1523             deleteNameLookup(dataId);
   1524             fixRawContactDisplayName(db, rawContactId);
   1525             triggerAggregation(rawContactId);
   1526             return count;
   1527         }
   1528 
   1529         @Override
   1530         protected int getTypeRank(int type) {
   1531             switch (type) {
   1532                 case Email.TYPE_HOME: return 0;
   1533                 case Email.TYPE_WORK: return 1;
   1534                 case Email.TYPE_CUSTOM: return 2;
   1535                 case Email.TYPE_OTHER: return 3;
   1536                 default: return 1000;
   1537             }
   1538         }
   1539     }
   1540 
   1541     public class NicknameDataRowHandler extends CommonDataRowHandler {
   1542 
   1543         public NicknameDataRowHandler() {
   1544             super(Nickname.CONTENT_ITEM_TYPE, Nickname.TYPE, Nickname.LABEL);
   1545         }
   1546 
   1547         @Override
   1548         public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) {
   1549             String nickname = values.getAsString(Nickname.NAME);
   1550 
   1551             long dataId = super.insert(db, rawContactId, values);
   1552 
   1553             if (!TextUtils.isEmpty(nickname)) {
   1554                 fixRawContactDisplayName(db, rawContactId);
   1555                 insertNameLookupForNickname(rawContactId, dataId, nickname);
   1556                 triggerAggregation(rawContactId);
   1557             }
   1558             return dataId;
   1559         }
   1560 
   1561         @Override
   1562         public boolean update(SQLiteDatabase db, ContentValues values, Cursor c,
   1563                 boolean callerIsSyncAdapter) {
   1564             long dataId = c.getLong(DataUpdateQuery._ID);
   1565             long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID);
   1566 
   1567             if (!super.update(db, values, c, callerIsSyncAdapter)) {
   1568                 return false;
   1569             }
   1570 
   1571             if (values.containsKey(Nickname.NAME)) {
   1572                 String nickname = values.getAsString(Nickname.NAME);
   1573                 deleteNameLookup(dataId);
   1574                 insertNameLookupForNickname(rawContactId, dataId, nickname);
   1575                 fixRawContactDisplayName(db, rawContactId);
   1576                 triggerAggregation(rawContactId);
   1577             }
   1578 
   1579             return true;
   1580         }
   1581 
   1582         @Override
   1583         public int delete(SQLiteDatabase db, Cursor c) {
   1584             long dataId = c.getLong(DataDeleteQuery._ID);
   1585             long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID);
   1586 
   1587             int count = super.delete(db, c);
   1588 
   1589             deleteNameLookup(dataId);
   1590             fixRawContactDisplayName(db, rawContactId);
   1591             triggerAggregation(rawContactId);
   1592             return count;
   1593         }
   1594     }
   1595 
   1596     public class PhoneDataRowHandler extends CommonDataRowHandler {
   1597 
   1598         public PhoneDataRowHandler() {
   1599             super(Phone.CONTENT_ITEM_TYPE, Phone.TYPE, Phone.LABEL);
   1600         }
   1601 
   1602         @Override
   1603         public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) {
   1604             long dataId;
   1605             if (values.containsKey(Phone.NUMBER)) {
   1606                 String number = values.getAsString(Phone.NUMBER);
   1607                 String normalizedNumber = computeNormalizedNumber(number);
   1608                 values.put(PhoneColumns.NORMALIZED_NUMBER, normalizedNumber);
   1609                 dataId = super.insert(db, rawContactId, values);
   1610 
   1611                 updatePhoneLookup(db, rawContactId, dataId, number, normalizedNumber);
   1612                 mContactAggregator.updateHasPhoneNumber(db, rawContactId);
   1613                 fixRawContactDisplayName(db, rawContactId);
   1614                 if (normalizedNumber != null) {
   1615                     triggerAggregation(rawContactId);
   1616                 }
   1617             } else {
   1618                 dataId = super.insert(db, rawContactId, values);
   1619             }
   1620             return dataId;
   1621         }
   1622 
   1623         @Override
   1624         public boolean update(SQLiteDatabase db, ContentValues values, Cursor c,
   1625                 boolean callerIsSyncAdapter) {
   1626             String number = null;
   1627             String normalizedNumber = null;
   1628             if (values.containsKey(Phone.NUMBER)) {
   1629                 number = values.getAsString(Phone.NUMBER);
   1630                 normalizedNumber = computeNormalizedNumber(number);
   1631                 values.put(PhoneColumns.NORMALIZED_NUMBER, normalizedNumber);
   1632             }
   1633 
   1634             if (!super.update(db, values, c, callerIsSyncAdapter)) {
   1635                 return false;
   1636             }
   1637 
   1638             if (values.containsKey(Phone.NUMBER)) {
   1639                 long dataId = c.getLong(DataUpdateQuery._ID);
   1640                 long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID);
   1641                 updatePhoneLookup(db, rawContactId, dataId, number, normalizedNumber);
   1642                 mContactAggregator.updateHasPhoneNumber(db, rawContactId);
   1643                 fixRawContactDisplayName(db, rawContactId);
   1644                 triggerAggregation(rawContactId);
   1645             }
   1646             return true;
   1647         }
   1648 
   1649         @Override
   1650         public int delete(SQLiteDatabase db, Cursor c) {
   1651             long dataId = c.getLong(DataDeleteQuery._ID);
   1652             long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID);
   1653 
   1654             int count = super.delete(db, c);
   1655 
   1656             updatePhoneLookup(db, rawContactId, dataId, null, null);
   1657             mContactAggregator.updateHasPhoneNumber(db, rawContactId);
   1658             fixRawContactDisplayName(db, rawContactId);
   1659             triggerAggregation(rawContactId);
   1660             return count;
   1661         }
   1662 
   1663         private String computeNormalizedNumber(String number) {
   1664             String normalizedNumber = null;
   1665             if (number != null) {
   1666                 normalizedNumber = PhoneNumberUtils.getStrippedReversed(number);
   1667             }
   1668             return normalizedNumber;
   1669         }
   1670 
   1671         private void updatePhoneLookup(SQLiteDatabase db, long rawContactId, long dataId,
   1672                 String number, String normalizedNumber) {
   1673             if (number != null) {
   1674                 ContentValues phoneValues = new ContentValues();
   1675                 phoneValues.put(PhoneLookupColumns.RAW_CONTACT_ID, rawContactId);
   1676                 phoneValues.put(PhoneLookupColumns.DATA_ID, dataId);
   1677                 phoneValues.put(PhoneLookupColumns.NORMALIZED_NUMBER, normalizedNumber);
   1678                 phoneValues.put(PhoneLookupColumns.MIN_MATCH,
   1679                         PhoneNumberUtils.toCallerIDMinMatch(number));
   1680 
   1681                 db.replace(Tables.PHONE_LOOKUP, null, phoneValues);
   1682             } else {
   1683                 mSelectionArgs1[0] = String.valueOf(dataId);
   1684                 db.delete(Tables.PHONE_LOOKUP, PhoneLookupColumns.DATA_ID + "=?", mSelectionArgs1);
   1685             }
   1686         }
   1687 
   1688         @Override
   1689         protected int getTypeRank(int type) {
   1690             switch (type) {
   1691                 case Phone.TYPE_MOBILE: return 0;
   1692                 case Phone.TYPE_WORK: return 1;
   1693                 case Phone.TYPE_HOME: return 2;
   1694                 case Phone.TYPE_PAGER: return 3;
   1695                 case Phone.TYPE_CUSTOM: return 4;
   1696                 case Phone.TYPE_OTHER: return 5;
   1697                 case Phone.TYPE_FAX_WORK: return 6;
   1698                 case Phone.TYPE_FAX_HOME: return 7;
   1699                 default: return 1000;
   1700             }
   1701         }
   1702     }
   1703 
   1704     public class GroupMembershipRowHandler extends DataRowHandler {
   1705 
   1706         public GroupMembershipRowHandler() {
   1707             super(GroupMembership.CONTENT_ITEM_TYPE);
   1708         }
   1709 
   1710         @Override
   1711         public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) {
   1712             resolveGroupSourceIdInValues(rawContactId, db, values, true);
   1713             long dataId = super.insert(db, rawContactId, values);
   1714             updateVisibility(rawContactId);
   1715             return dataId;
   1716         }
   1717 
   1718         @Override
   1719         public boolean update(SQLiteDatabase db, ContentValues values, Cursor c,
   1720                 boolean callerIsSyncAdapter) {
   1721             long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID);
   1722             resolveGroupSourceIdInValues(rawContactId, db, values, false);
   1723             if (!super.update(db, values, c, callerIsSyncAdapter)) {
   1724                 return false;
   1725             }
   1726             updateVisibility(rawContactId);
   1727             return true;
   1728         }
   1729 
   1730         @Override
   1731         public int delete(SQLiteDatabase db, Cursor c) {
   1732             long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID);
   1733             int count = super.delete(db, c);
   1734             updateVisibility(rawContactId);
   1735             return count;
   1736         }
   1737 
   1738         private void updateVisibility(long rawContactId) {
   1739             long contactId = mDbHelper.getContactId(rawContactId);
   1740             if (contactId != 0) {
   1741                 mDbHelper.updateContactVisible(contactId);
   1742             }
   1743         }
   1744 
   1745         private void resolveGroupSourceIdInValues(long rawContactId, SQLiteDatabase db,
   1746                 ContentValues values, boolean isInsert) {
   1747             boolean containsGroupSourceId = values.containsKey(GroupMembership.GROUP_SOURCE_ID);
   1748             boolean containsGroupId = values.containsKey(GroupMembership.GROUP_ROW_ID);
   1749             if (containsGroupSourceId && containsGroupId) {
   1750                 throw new IllegalArgumentException(
   1751                         "you are not allowed to set both the GroupMembership.GROUP_SOURCE_ID "
   1752                                 + "and GroupMembership.GROUP_ROW_ID");
   1753             }
   1754 
   1755             if (!containsGroupSourceId && !containsGroupId) {
   1756                 if (isInsert) {
   1757                     throw new IllegalArgumentException(
   1758                             "you must set exactly one of GroupMembership.GROUP_SOURCE_ID "
   1759                                     + "and GroupMembership.GROUP_ROW_ID");
   1760                 } else {
   1761                     return;
   1762                 }
   1763             }
   1764 
   1765             if (containsGroupSourceId) {
   1766                 final String sourceId = values.getAsString(GroupMembership.GROUP_SOURCE_ID);
   1767                 final long groupId = getOrMakeGroup(db, rawContactId, sourceId,
   1768                         mInsertedRawContacts.get(rawContactId));
   1769                 values.remove(GroupMembership.GROUP_SOURCE_ID);
   1770                 values.put(GroupMembership.GROUP_ROW_ID, groupId);
   1771             }
   1772         }
   1773     }
   1774 
   1775     public class PhotoDataRowHandler extends DataRowHandler {
   1776 
   1777         public PhotoDataRowHandler() {
   1778             super(Photo.CONTENT_ITEM_TYPE);
   1779         }
   1780 
   1781         @Override
   1782         public long insert(SQLiteDatabase db, long rawContactId, ContentValues values) {
   1783             long dataId = super.insert(db, rawContactId, values);
   1784             if (!isNewRawContact(rawContactId)) {
   1785                 mContactAggregator.updatePhotoId(db, rawContactId);
   1786             }
   1787             return dataId;
   1788         }
   1789 
   1790         @Override
   1791         public boolean update(SQLiteDatabase db, ContentValues values, Cursor c,
   1792                 boolean callerIsSyncAdapter) {
   1793             long rawContactId = c.getLong(DataUpdateQuery.RAW_CONTACT_ID);
   1794             if (!super.update(db, values, c, callerIsSyncAdapter)) {
   1795                 return false;
   1796             }
   1797 
   1798             mContactAggregator.updatePhotoId(db, rawContactId);
   1799             return true;
   1800         }
   1801 
   1802         @Override
   1803         public int delete(SQLiteDatabase db, Cursor c) {
   1804             long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID);
   1805             int count = super.delete(db, c);
   1806             mContactAggregator.updatePhotoId(db, rawContactId);
   1807             return count;
   1808         }
   1809     }
   1810 
   1811     /**
   1812      * An entry in group id cache. It maps the combination of (account type, account name
   1813      * and source id) to group row id.
   1814      */
   1815     public class GroupIdCacheEntry {
   1816         String accountType;
   1817         String accountName;
   1818         String sourceId;
   1819         long groupId;
   1820     }
   1821 
   1822     private HashMap<String, DataRowHandler> mDataRowHandlers;
   1823     private ContactsDatabaseHelper mDbHelper;
   1824 
   1825     private NameSplitter mNameSplitter;
   1826     private NameLookupBuilder mNameLookupBuilder;
   1827 
   1828     private PostalSplitter mPostalSplitter;
   1829 
   1830     // We don't need a soft cache for groups - the assumption is that there will only
   1831     // be a small number of contact groups. The cache is keyed off source id.  The value
   1832     // is a list of groups with this group id.
   1833     private HashMap<String, ArrayList<GroupIdCacheEntry>> mGroupIdCache = Maps.newHashMap();
   1834 
   1835     private ContactAggregator mContactAggregator;
   1836     private LegacyApiSupport mLegacyApiSupport;
   1837     private GlobalSearchSupport mGlobalSearchSupport;
   1838     private CommonNicknameCache mCommonNicknameCache;
   1839 
   1840     private ContentValues mValues = new ContentValues();
   1841     private CharArrayBuffer mCharArrayBuffer = new CharArrayBuffer(128);
   1842     private NameSplitter.Name mName = new NameSplitter.Name();
   1843     private HashMap<String, Boolean> mAccountWritability = Maps.newHashMap();
   1844 
   1845     private int mProviderStatus = ProviderStatus.STATUS_NORMAL;
   1846     private long mEstimatedStorageRequirement = 0;
   1847     private volatile CountDownLatch mAccessLatch;
   1848 
   1849     private HashMap<Long, Account> mInsertedRawContacts = Maps.newHashMap();
   1850     private HashSet<Long> mUpdatedRawContacts = Sets.newHashSet();
   1851     private HashSet<Long> mDirtyRawContacts = Sets.newHashSet();
   1852     private HashMap<Long, Object> mUpdatedSyncStates = Maps.newHashMap();
   1853 
   1854     private boolean mVisibleTouched = false;
   1855 
   1856     private boolean mSyncToNetwork;
   1857 
   1858     private Locale mCurrentLocale;
   1859 
   1860 
   1861     @Override
   1862     public boolean onCreate() {
   1863         super.onCreate();
   1864         try {
   1865             return initialize();
   1866         } catch (RuntimeException e) {
   1867             Log.e(TAG, "Cannot start provider", e);
   1868             return false;
   1869         }
   1870     }
   1871 
   1872     private boolean initialize() {
   1873         final Context context = getContext();
   1874         mDbHelper = (ContactsDatabaseHelper)getDatabaseHelper();
   1875         mGlobalSearchSupport = new GlobalSearchSupport(this);
   1876         mLegacyApiSupport = new LegacyApiSupport(context, mDbHelper, this, mGlobalSearchSupport);
   1877         mContactAggregator = new ContactAggregator(this, mDbHelper,
   1878                 createPhotoPriorityResolver(context));
   1879         mContactAggregator.setEnabled(SystemProperties.getBoolean(AGGREGATE_CONTACTS, true));
   1880 
   1881         mDb = mDbHelper.getWritableDatabase();
   1882 
   1883         initForDefaultLocale();
   1884 
   1885         mSetPrimaryStatement = mDb.compileStatement(
   1886                 "UPDATE " + Tables.DATA +
   1887                 " SET " + Data.IS_PRIMARY + "=(_id=?)" +
   1888                 " WHERE " + DataColumns.MIMETYPE_ID + "=?" +
   1889                 "   AND " + Data.RAW_CONTACT_ID + "=?");
   1890 
   1891         mSetSuperPrimaryStatement = mDb.compileStatement(
   1892                 "UPDATE " + Tables.DATA +
   1893                 " SET " + Data.IS_SUPER_PRIMARY + "=(" + Data._ID + "=?)" +
   1894                 " WHERE " + DataColumns.MIMETYPE_ID + "=?" +
   1895                 "   AND " + Data.RAW_CONTACT_ID + " IN (" +
   1896                         "SELECT " + RawContacts._ID +
   1897                         " FROM " + Tables.RAW_CONTACTS +
   1898                         " WHERE " + RawContacts.CONTACT_ID + " =(" +
   1899                                 "SELECT " + RawContacts.CONTACT_ID +
   1900                                 " FROM " + Tables.RAW_CONTACTS +
   1901                                 " WHERE " + RawContacts._ID + "=?))");
   1902 
   1903         mRawContactDisplayNameUpdate = mDb.compileStatement(
   1904                 "UPDATE " + Tables.RAW_CONTACTS +
   1905                 " SET " +
   1906                         RawContacts.DISPLAY_NAME_SOURCE + "=?," +
   1907                         RawContacts.DISPLAY_NAME_PRIMARY + "=?," +
   1908                         RawContacts.DISPLAY_NAME_ALTERNATIVE + "=?," +
   1909                         RawContacts.PHONETIC_NAME + "=?," +
   1910                         RawContacts.PHONETIC_NAME_STYLE + "=?," +
   1911                         RawContacts.SORT_KEY_PRIMARY + "=?," +
   1912                         RawContacts.SORT_KEY_ALTERNATIVE + "=?" +
   1913                 " WHERE " + RawContacts._ID + "=?");
   1914 
   1915         mLastStatusUpdate = mDb.compileStatement(
   1916                 "UPDATE " + Tables.CONTACTS +
   1917                 " SET " + ContactsColumns.LAST_STATUS_UPDATE_ID + "=" +
   1918                         "(SELECT " + DataColumns.CONCRETE_ID +
   1919                         " FROM " + Tables.STATUS_UPDATES +
   1920                         " JOIN " + Tables.DATA +
   1921                         "   ON (" + StatusUpdatesColumns.DATA_ID + "="
   1922                                 + DataColumns.CONCRETE_ID + ")" +
   1923                         " JOIN " + Tables.RAW_CONTACTS +
   1924                         "   ON (" + DataColumns.CONCRETE_RAW_CONTACT_ID + "="
   1925                                 + RawContactsColumns.CONCRETE_ID + ")" +
   1926                         " WHERE " + RawContacts.CONTACT_ID + "=?" +
   1927                         " ORDER BY " + StatusUpdates.STATUS_TIMESTAMP + " DESC,"
   1928                                 + StatusUpdates.STATUS +
   1929                         " LIMIT 1)" +
   1930                 " WHERE " + ContactsColumns.CONCRETE_ID + "=?");
   1931 
   1932         mNameLookupInsert = mDb.compileStatement("INSERT OR IGNORE INTO " + Tables.NAME_LOOKUP + "("
   1933                 + NameLookupColumns.RAW_CONTACT_ID + "," + NameLookupColumns.DATA_ID + ","
   1934                 + NameLookupColumns.NAME_TYPE + "," + NameLookupColumns.NORMALIZED_NAME
   1935                 + ") VALUES (?,?,?,?)");
   1936         mNameLookupDelete = mDb.compileStatement("DELETE FROM " + Tables.NAME_LOOKUP + " WHERE "
   1937                 + NameLookupColumns.DATA_ID + "=?");
   1938 
   1939         mStatusUpdateInsert = mDb.compileStatement(
   1940                 "INSERT INTO " + Tables.STATUS_UPDATES + "("
   1941                         + StatusUpdatesColumns.DATA_ID + ", "
   1942                         + StatusUpdates.STATUS + ","
   1943                         + StatusUpdates.STATUS_RES_PACKAGE + ","
   1944                         + StatusUpdates.STATUS_ICON + ","
   1945                         + StatusUpdates.STATUS_LABEL + ")" +
   1946                 " VALUES (?,?,?,?,?)");
   1947 
   1948         mStatusUpdateReplace = mDb.compileStatement(
   1949                 "INSERT OR REPLACE INTO " + Tables.STATUS_UPDATES + "("
   1950                         + StatusUpdatesColumns.DATA_ID + ", "
   1951                         + StatusUpdates.STATUS_TIMESTAMP + ","
   1952                         + StatusUpdates.STATUS + ","
   1953                         + StatusUpdates.STATUS_RES_PACKAGE + ","
   1954                         + StatusUpdates.STATUS_ICON + ","
   1955                         + StatusUpdates.STATUS_LABEL + ")" +
   1956                 " VALUES (?,?,?,?,?,?)");
   1957 
   1958         mStatusUpdateAutoTimestamp = mDb.compileStatement(
   1959                 "UPDATE " + Tables.STATUS_UPDATES +
   1960                 " SET " + StatusUpdates.STATUS_TIMESTAMP + "=?,"
   1961                         + StatusUpdates.STATUS + "=?" +
   1962                 " WHERE " + StatusUpdatesColumns.DATA_ID + "=?"
   1963                         + " AND " + StatusUpdates.STATUS + "!=?");
   1964 
   1965         mStatusAttributionUpdate = mDb.compileStatement(
   1966                 "UPDATE " + Tables.STATUS_UPDATES +
   1967                 " SET " + StatusUpdates.STATUS_RES_PACKAGE + "=?,"
   1968                         + StatusUpdates.STATUS_ICON + "=?,"
   1969                         + StatusUpdates.STATUS_LABEL + "=?" +
   1970                 " WHERE " + StatusUpdatesColumns.DATA_ID + "=?");
   1971 
   1972         mStatusUpdateDelete = mDb.compileStatement(
   1973                 "DELETE FROM " + Tables.STATUS_UPDATES +
   1974                 " WHERE " + StatusUpdatesColumns.DATA_ID + "=?");
   1975 
   1976         // When setting NAME_VERIFIED to 1 on a raw contact, reset it to 0
   1977         // on all other raw contacts in the same aggregate
   1978         mResetNameVerifiedForOtherRawContacts = mDb.compileStatement(
   1979                 "UPDATE " + Tables.RAW_CONTACTS +
   1980                 " SET " + RawContacts.NAME_VERIFIED + "=0" +
   1981                 " WHERE " + RawContacts.CONTACT_ID + "=(" +
   1982                         "SELECT " + RawContacts.CONTACT_ID +
   1983                         " FROM " + Tables.RAW_CONTACTS +
   1984                         " WHERE " + RawContacts._ID + "=?)" +
   1985                 " AND " + RawContacts._ID + "!=?");
   1986 
   1987         mMimeTypeIdEmail = mDbHelper.getMimeTypeId(Email.CONTENT_ITEM_TYPE);
   1988         mMimeTypeIdIm = mDbHelper.getMimeTypeId(Im.CONTENT_ITEM_TYPE);
   1989         mMimeTypeIdStructuredName = mDbHelper.getMimeTypeId(StructuredName.CONTENT_ITEM_TYPE);
   1990         mMimeTypeIdOrganization = mDbHelper.getMimeTypeId(Organization.CONTENT_ITEM_TYPE);
   1991         mMimeTypeIdNickname = mDbHelper.getMimeTypeId(Nickname.CONTENT_ITEM_TYPE);
   1992         mMimeTypeIdPhone = mDbHelper.getMimeTypeId(Phone.CONTENT_ITEM_TYPE);
   1993 
   1994         verifyAccounts();
   1995 
   1996         if (isLegacyContactImportNeeded()) {
   1997             importLegacyContactsAsync();
   1998         } else {
   1999             verifyLocale();
   2000         }
   2001 
   2002         if (isAggregationUpgradeNeeded()) {
   2003             upgradeAggregationAlgorithm();
   2004         }
   2005 
   2006         return (mDb != null);
   2007     }
   2008 
   2009     private void initDataRowHandlers() {
   2010       mDataRowHandlers = new HashMap<String, DataRowHandler>();
   2011 
   2012       mDataRowHandlers.put(Email.CONTENT_ITEM_TYPE, new EmailDataRowHandler());
   2013       mDataRowHandlers.put(Im.CONTENT_ITEM_TYPE,
   2014               new CommonDataRowHandler(Im.CONTENT_ITEM_TYPE, Im.TYPE, Im.LABEL));
   2015       mDataRowHandlers.put(Nickname.CONTENT_ITEM_TYPE, new CommonDataRowHandler(
   2016               StructuredPostal.CONTENT_ITEM_TYPE, StructuredPostal.TYPE, StructuredPostal.LABEL));
   2017       mDataRowHandlers.put(Organization.CONTENT_ITEM_TYPE, new OrganizationDataRowHandler());
   2018       mDataRowHandlers.put(Phone.CONTENT_ITEM_TYPE, new PhoneDataRowHandler());
   2019       mDataRowHandlers.put(Nickname.CONTENT_ITEM_TYPE, new NicknameDataRowHandler());
   2020       mDataRowHandlers.put(StructuredName.CONTENT_ITEM_TYPE,
   2021               new StructuredNameRowHandler(mNameSplitter));
   2022       mDataRowHandlers.put(StructuredPostal.CONTENT_ITEM_TYPE,
   2023               new StructuredPostalRowHandler(mPostalSplitter));
   2024       mDataRowHandlers.put(GroupMembership.CONTENT_ITEM_TYPE, new GroupMembershipRowHandler());
   2025       mDataRowHandlers.put(Photo.CONTENT_ITEM_TYPE, new PhotoDataRowHandler());
   2026     }
   2027     /**
   2028      * Visible for testing.
   2029      */
   2030     /* package */ PhotoPriorityResolver createPhotoPriorityResolver(Context context) {
   2031         return new PhotoPriorityResolver(context);
   2032     }
   2033 
   2034     /**
   2035      * (Re)allocates all locale-sensitive structures.
   2036      */
   2037     private void initForDefaultLocale() {
   2038         mCurrentLocale = getLocale();
   2039         mNameSplitter = mDbHelper.createNameSplitter();
   2040         mNameLookupBuilder = new StructuredNameLookupBuilder(mNameSplitter);
   2041         mPostalSplitter = new PostalSplitter(mCurrentLocale);
   2042         mCommonNicknameCache = new CommonNicknameCache(mDbHelper.getReadableDatabase());
   2043         ContactLocaleUtils.getIntance().setLocale(mCurrentLocale);
   2044         initDataRowHandlers();
   2045     }
   2046 
   2047     @Override
   2048     public void onConfigurationChanged(Configuration newConfig) {
   2049         if (mProviderStatus != ProviderStatus.STATUS_NORMAL) {
   2050             return;
   2051         }
   2052 
   2053         initForDefaultLocale();
   2054         verifyLocale();
   2055     }
   2056 
   2057     protected void verifyAccounts() {
   2058         AccountManager.get(getContext()).addOnAccountsUpdatedListener(this, null, false);
   2059         onAccountsUpdated(AccountManager.get(getContext()).getAccounts());
   2060     }
   2061 
   2062     /**
   2063      * Verifies that the contacts database is properly configured for the current locale.
   2064      * If not, changes the database locale to the current locale using an asynchronous task.
   2065      * This needs to be done asynchronously because the process involves rebuilding
   2066      * large data structures (name lookup, sort keys), which can take minutes on
   2067      * a large set of contacts.
   2068      */
   2069     protected void verifyLocale() {
   2070 
   2071         // The process is already running - postpone the change
   2072         if (mProviderStatus == ProviderStatus.STATUS_CHANGING_LOCALE) {
   2073             return;
   2074         }
   2075 
   2076         final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
   2077         final String providerLocale = prefs.getString(PREF_LOCALE, null);
   2078         final Locale currentLocale = mCurrentLocale;
   2079         if (currentLocale.toString().equals(providerLocale)) {
   2080             return;
   2081         }
   2082 
   2083         int providerStatus = mProviderStatus;
   2084         setProviderStatus(ProviderStatus.STATUS_CHANGING_LOCALE);
   2085 
   2086         AsyncTask<Integer, Void, Void> task = new AsyncTask<Integer, Void, Void>() {
   2087 
   2088             int savedProviderStatus;
   2089 
   2090             @Override
   2091             protected Void doInBackground(Integer... params) {
   2092                 savedProviderStatus = params[0];
   2093                 mDbHelper.setLocale(ContactsProvider2.this, currentLocale);
   2094                 return null;
   2095             }
   2096 
   2097             @Override
   2098             protected void onPostExecute(Void result) {
   2099                 prefs.edit().putString(PREF_LOCALE, currentLocale.toString()).apply();
   2100                 setProviderStatus(savedProviderStatus);
   2101 
   2102                 // Recursive invocation, needed to cover the case where locale
   2103                 // changes once and then changes again before the db upgrade is completed.
   2104                 verifyLocale();
   2105             }
   2106         };
   2107 
   2108         task.execute(providerStatus);
   2109     }
   2110 
   2111     /* Visible for testing */
   2112     @Override
   2113     protected ContactsDatabaseHelper getDatabaseHelper(final Context context) {
   2114         return ContactsDatabaseHelper.getInstance(context);
   2115     }
   2116 
   2117     /* package */ NameSplitter getNameSplitter() {
   2118         return mNameSplitter;
   2119     }
   2120 
   2121     /* Visible for testing */
   2122     protected Locale getLocale() {
   2123         return Locale.getDefault();
   2124     }
   2125 
   2126     protected boolean isLegacyContactImportNeeded() {
   2127         int version = Integer.parseInt(mDbHelper.getProperty(PROPERTY_CONTACTS_IMPORTED, "0"));
   2128         return version < PROPERTY_CONTACTS_IMPORT_VERSION;
   2129     }
   2130 
   2131     protected LegacyContactImporter getLegacyContactImporter() {
   2132         return new LegacyContactImporter(getContext(), this);
   2133     }
   2134 
   2135     /**
   2136      * Imports legacy contacts in a separate thread.  As long as the import process is running
   2137      * all other access to the contacts is blocked.
   2138      */
   2139     private void importLegacyContactsAsync() {
   2140         Log.v(TAG, "Importing legacy contacts");
   2141         setProviderStatus(ProviderStatus.STATUS_UPGRADING);
   2142         if (mAccessLatch == null) {
   2143             mAccessLatch = new CountDownLatch(1);
   2144         }
   2145 
   2146         Thread importThread = new Thread("LegacyContactImport") {
   2147             @Override
   2148             public void run() {
   2149                 final SharedPreferences prefs =
   2150                     PreferenceManager.getDefaultSharedPreferences(getContext());
   2151                 mDbHelper.setLocale(ContactsProvider2.this, mCurrentLocale);
   2152                 prefs.edit().putString(PREF_LOCALE, mCurrentLocale.toString()).commit();
   2153 
   2154                 LegacyContactImporter importer = getLegacyContactImporter();
   2155                 if (importLegacyContacts(importer)) {
   2156                     onLegacyContactImportSuccess();
   2157                 } else {
   2158                     onLegacyContactImportFailure();
   2159                 }
   2160             }
   2161         };
   2162 
   2163         importThread.start();
   2164     }
   2165 
   2166     /**
   2167      * Unlocks the provider and declares that the import process is complete.
   2168      */
   2169     private void onLegacyContactImportSuccess() {
   2170         NotificationManager nm =
   2171             (NotificationManager)getContext().getSystemService(Context.NOTIFICATION_SERVICE);
   2172         nm.cancel(LEGACY_IMPORT_FAILED_NOTIFICATION);
   2173 
   2174         // Store a property in the database indicating that the conversion process succeeded
   2175         mDbHelper.setProperty(PROPERTY_CONTACTS_IMPORTED,
   2176                 String.valueOf(PROPERTY_CONTACTS_IMPORT_VERSION));
   2177         setProviderStatus(ProviderStatus.STATUS_NORMAL);
   2178         mAccessLatch.countDown();
   2179         mAccessLatch = null;
   2180         Log.v(TAG, "Completed import of legacy contacts");
   2181     }
   2182 
   2183     /**
   2184      * Announces the provider status and keeps the provider locked.
   2185      */
   2186     private void onLegacyContactImportFailure() {
   2187         Context context = getContext();
   2188         NotificationManager nm =
   2189             (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE);
   2190 
   2191         // Show a notification
   2192         Notification n = new Notification(android.R.drawable.stat_notify_error,
   2193                 context.getString(R.string.upgrade_out_of_memory_notification_ticker),
   2194                 System.currentTimeMillis());
   2195         n.setLatestEventInfo(context,
   2196                 context.getString(R.string.upgrade_out_of_memory_notification_title),
   2197                 context.getString(R.string.upgrade_out_of_memory_notification_text),
   2198                 PendingIntent.getActivity(context, 0, new Intent(Intents.UI.LIST_DEFAULT), 0));
   2199         n.flags |= Notification.FLAG_NO_CLEAR | Notification.FLAG_ONGOING_EVENT;
   2200 
   2201         nm.notify(LEGACY_IMPORT_FAILED_NOTIFICATION, n);
   2202 
   2203         setProviderStatus(ProviderStatus.STATUS_UPGRADE_OUT_OF_MEMORY);
   2204         Log.v(TAG, "Failed to import legacy contacts");
   2205     }
   2206 
   2207     /* Visible for testing */
   2208     /* package */ boolean importLegacyContacts(LegacyContactImporter importer) {
   2209         boolean aggregatorEnabled = mContactAggregator.isEnabled();
   2210         mContactAggregator.setEnabled(false);
   2211         try {
   2212             if (importer.importContacts()) {
   2213 
   2214                 // TODO aggregate all newly added raw contacts
   2215                 mContactAggregator.setEnabled(aggregatorEnabled);
   2216                 return true;
   2217             }
   2218         } catch (Throwable e) {
   2219            Log.e(TAG, "Legacy contact import failed", e);
   2220         }
   2221         mEstimatedStorageRequirement = importer.getEstimatedStorageRequirement();
   2222         return false;
   2223     }
   2224 
   2225     /**
   2226      * Wipes all data from the contacts database.
   2227      */
   2228     /* package */ void wipeData() {
   2229         mDbHelper.wipeData();
   2230     }
   2231 
   2232     /**
   2233      * While importing and aggregating contacts, this content provider will
   2234      * block all attempts to change contacts data. In particular, it will hold
   2235      * up all contact syncs. As soon as the import process is complete, all
   2236      * processes waiting to write to the provider are unblocked and can proceed
   2237      * to compete for the database transaction monitor.
   2238      */
   2239     private void waitForAccess() {
   2240         CountDownLatch latch = mAccessLatch;
   2241         if (latch != null) {
   2242             while (true) {
   2243                 try {
   2244                     latch.await();
   2245                     mAccessLatch = null;
   2246                     return;
   2247                 } catch (InterruptedException e) {
   2248                     Thread.currentThread().interrupt();
   2249                 }
   2250             }
   2251         }
   2252     }
   2253 
   2254     @Override
   2255     public Uri insert(Uri uri, ContentValues values) {
   2256         waitForAccess();
   2257         return super.insert(uri, values);
   2258     }
   2259 
   2260     @Override
   2261     public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
   2262         if (mAccessLatch != null) {
   2263             // We are stuck trying to upgrade contacts db.  The only update request
   2264             // allowed in this case is an update of provider status, which will trigger
   2265             // an attempt to upgrade contacts again.
   2266             int match = sUriMatcher.match(uri);
   2267             if (match == PROVIDER_STATUS && isLegacyContactImportNeeded()) {
   2268                 Integer newStatus = values.getAsInteger(ProviderStatus.STATUS);
   2269                 if (newStatus != null && newStatus == ProviderStatus.STATUS_UPGRADING) {
   2270                     importLegacyContactsAsync();
   2271                     return 1;
   2272                 } else {
   2273                     return 0;
   2274                 }
   2275             }
   2276         }
   2277         waitForAccess();
   2278         return super.update(uri, values, selection, selectionArgs);
   2279     }
   2280 
   2281     @Override
   2282     public int delete(Uri uri, String selection, String[] selectionArgs) {
   2283         waitForAccess();
   2284         return super.delete(uri, selection, selectionArgs);
   2285     }
   2286 
   2287     @Override
   2288     public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
   2289             throws OperationApplicationException {
   2290         waitForAccess();
   2291         return super.applyBatch(operations);
   2292     }
   2293 
   2294     @Override
   2295     protected void onBeginTransaction() {
   2296         if (VERBOSE_LOGGING) {
   2297             Log.v(TAG, "onBeginTransaction");
   2298         }
   2299         super.onBeginTransaction();
   2300         mContactAggregator.clearPendingAggregations();
   2301         clearTransactionalChanges();
   2302     }
   2303 
   2304     private void clearTransactionalChanges() {
   2305         mInsertedRawContacts.clear();
   2306         mUpdatedRawContacts.clear();
   2307         mUpdatedSyncStates.clear();
   2308         mDirtyRawContacts.clear();
   2309     }
   2310 
   2311     @Override
   2312     protected void beforeTransactionCommit() {
   2313 
   2314         if (VERBOSE_LOGGING) {
   2315             Log.v(TAG, "beforeTransactionCommit");
   2316         }
   2317         super.beforeTransactionCommit();
   2318         flushTransactionalChanges();
   2319         mContactAggregator.aggregateInTransaction(mDb);
   2320         if (mVisibleTouched) {
   2321             mVisibleTouched = false;
   2322             mDbHelper.updateAllVisible();
   2323         }
   2324     }
   2325 
   2326     private void flushTransactionalChanges() {
   2327         if (VERBOSE_LOGGING) {
   2328             Log.v(TAG, "flushTransactionChanges");
   2329         }
   2330 
   2331         for (long rawContactId : mInsertedRawContacts.keySet()) {
   2332             updateRawContactDisplayName(mDb, rawContactId);
   2333             mContactAggregator.onRawContactInsert(mDb, rawContactId);
   2334         }
   2335 
   2336         if (!mDirtyRawContacts.isEmpty()) {
   2337             mSb.setLength(0);
   2338             mSb.append(UPDATE_RAW_CONTACT_SET_DIRTY_SQL);
   2339             appendIds(mSb, mDirtyRawContacts);
   2340             mSb.append(")");
   2341             mDb.execSQL(mSb.toString());
   2342         }
   2343 
   2344         if (!mUpdatedRawContacts.isEmpty()) {
   2345             mSb.setLength(0);
   2346             mSb.append(UPDATE_RAW_CONTACT_SET_VERSION_SQL);
   2347             appendIds(mSb, mUpdatedRawContacts);
   2348             mSb.append(")");
   2349             mDb.execSQL(mSb.toString());
   2350         }
   2351 
   2352         for (Map.Entry<Long, Object> entry : mUpdatedSyncStates.entrySet()) {
   2353             long id = entry.getKey();
   2354             if (mDbHelper.getSyncState().update(mDb, id, entry.getValue()) <= 0) {
   2355                 throw new IllegalStateException(
   2356                         "unable to update sync state, does it still exist?");
   2357             }
   2358         }
   2359 
   2360         clearTransactionalChanges();
   2361     }
   2362 
   2363     /**
   2364      * Appends comma separated ids.
   2365      * @param ids Should not be empty
   2366      */
   2367     private void appendIds(StringBuilder sb, HashSet<Long> ids) {
   2368         for (long id : ids) {
   2369             sb.append(id).append(',');
   2370         }
   2371 
   2372         sb.setLength(sb.length() - 1); // Yank the last comma
   2373     }
   2374 
   2375     @Override
   2376     protected void notifyChange() {
   2377         notifyChange(mSyncToNetwork);
   2378         mSyncToNetwork = false;
   2379     }
   2380 
   2381     protected void notifyChange(boolean syncToNetwork) {
   2382         getContext().getContentResolver().notifyChange(ContactsContract.AUTHORITY_URI, null,
   2383                 syncToNetwork);
   2384     }
   2385 
   2386     protected void setProviderStatus(int status) {
   2387         mProviderStatus = status;
   2388         getContext().getContentResolver().notifyChange(ContactsContract.ProviderStatus.CONTENT_URI,
   2389                 null, false);
   2390     }
   2391 
   2392     private boolean isNewRawContact(long rawContactId) {
   2393         return mInsertedRawContacts.containsKey(rawContactId);
   2394     }
   2395 
   2396     private DataRowHandler getDataRowHandler(final String mimeType) {
   2397         DataRowHandler handler = mDataRowHandlers.get(mimeType);
   2398         if (handler == null) {
   2399             handler = new CustomDataRowHandler(mimeType);
   2400             mDataRowHandlers.put(mimeType, handler);
   2401         }
   2402         return handler;
   2403     }
   2404 
   2405     @Override
   2406     protected Uri insertInTransaction(Uri uri, ContentValues values) {
   2407         if (VERBOSE_LOGGING) {
   2408             Log.v(TAG, "insertInTransaction: " + uri + " " + values);
   2409         }
   2410 
   2411         final boolean callerIsSyncAdapter =
   2412                 readBooleanQueryParameter(uri, ContactsContract.CALLER_IS_SYNCADAPTER, false);
   2413 
   2414         final int match = sUriMatcher.match(uri);
   2415         long id = 0;
   2416 
   2417         switch (match) {
   2418             case SYNCSTATE:
   2419                 id = mDbHelper.getSyncState().insert(mDb, values);
   2420                 break;
   2421 
   2422             case CONTACTS: {
   2423                 insertContact(values);
   2424                 break;
   2425             }
   2426 
   2427             case RAW_CONTACTS: {
   2428                 id = insertRawContact(uri, values);
   2429                 mSyncToNetwork |= !callerIsSyncAdapter;
   2430                 break;
   2431             }
   2432 
   2433             case RAW_CONTACTS_DATA: {
   2434                 values.put(Data.RAW_CONTACT_ID, uri.getPathSegments().get(1));
   2435                 id = insertData(values, callerIsSyncAdapter);
   2436                 mSyncToNetwork |= !callerIsSyncAdapter;
   2437                 break;
   2438             }
   2439 
   2440             case DATA: {
   2441                 id = insertData(values, callerIsSyncAdapter);
   2442                 mSyncToNetwork |= !callerIsSyncAdapter;
   2443                 break;
   2444             }
   2445 
   2446             case GROUPS: {
   2447                 id = insertGroup(uri, values, callerIsSyncAdapter);
   2448                 mSyncToNetwork |= !callerIsSyncAdapter;
   2449                 break;
   2450             }
   2451 
   2452             case SETTINGS: {
   2453                 id = insertSettings(uri, values);
   2454                 mSyncToNetwork |= !callerIsSyncAdapter;
   2455                 break;
   2456             }
   2457 
   2458             case STATUS_UPDATES: {
   2459                 id = insertStatusUpdate(values);
   2460                 break;
   2461             }
   2462 
   2463             default:
   2464                 mSyncToNetwork = true;
   2465                 return mLegacyApiSupport.insert(uri, values);
   2466         }
   2467 
   2468         if (id < 0) {
   2469             return null;
   2470         }
   2471 
   2472         return ContentUris.withAppendedId(uri, id);
   2473     }
   2474 
   2475     /**
   2476      * If account is non-null then store it in the values. If the account is
   2477      * already specified in the values then it must be consistent with the
   2478      * account, if it is non-null.
   2479      *
   2480      * @param uri Current {@link Uri} being operated on.
   2481      * @param values {@link ContentValues} to read and possibly update.
   2482      * @throws IllegalArgumentException when only one of
   2483      *             {@link RawContacts#ACCOUNT_NAME} or
   2484      *             {@link RawContacts#ACCOUNT_TYPE} is specified, leaving the
   2485      *             other undefined.
   2486      * @throws IllegalArgumentException when {@link RawContacts#ACCOUNT_NAME}
   2487      *             and {@link RawContacts#ACCOUNT_TYPE} are inconsistent between
   2488      *             the given {@link Uri} and {@link ContentValues}.
   2489      */
   2490     private Account resolveAccount(Uri uri, ContentValues values) throws IllegalArgumentException {
   2491         String accountName = getQueryParameter(uri, RawContacts.ACCOUNT_NAME);
   2492         String accountType = getQueryParameter(uri, RawContacts.ACCOUNT_TYPE);
   2493         final boolean partialUri = TextUtils.isEmpty(accountName) ^ TextUtils.isEmpty(accountType);
   2494 
   2495         String valueAccountName = values.getAsString(RawContacts.ACCOUNT_NAME);
   2496         String valueAccountType = values.getAsString(RawContacts.ACCOUNT_TYPE);
   2497         final boolean partialValues = TextUtils.isEmpty(valueAccountName)
   2498                 ^ TextUtils.isEmpty(valueAccountType);
   2499 
   2500         if (partialUri || partialValues) {
   2501             // Throw when either account is incomplete
   2502             throw new IllegalArgumentException(mDbHelper.exceptionMessage(
   2503                     "Must specify both or neither of ACCOUNT_NAME and ACCOUNT_TYPE", uri));
   2504         }
   2505 
   2506         // Accounts are valid by only checking one parameter, since we've
   2507         // already ruled out partial accounts.
   2508         final boolean validUri = !TextUtils.isEmpty(accountName);
   2509         final boolean validValues = !TextUtils.isEmpty(valueAccountName);
   2510 
   2511         if (validValues && validUri) {
   2512             // Check that accounts match when both present
   2513             final boolean accountMatch = TextUtils.equals(accountName, valueAccountName)
   2514                     && TextUtils.equals(accountType, valueAccountType);
   2515             if (!accountMatch) {
   2516                 throw new IllegalArgumentException(mDbHelper.exceptionMessage(
   2517                         "When both specified, ACCOUNT_NAME and ACCOUNT_TYPE must match", uri));
   2518             }
   2519         } else if (validUri) {
   2520             // Fill values from Uri when not present
   2521             values.put(RawContacts.ACCOUNT_NAME, accountName);
   2522             values.put(RawContacts.ACCOUNT_TYPE, accountType);
   2523         } else if (validValues) {
   2524             accountName = valueAccountName;
   2525             accountType = valueAccountType;
   2526         } else {
   2527             return null;
   2528         }
   2529 
   2530         // Use cached Account object when matches, otherwise create
   2531         if (mAccount == null
   2532                 || !mAccount.name.equals(accountName)
   2533                 || !mAccount.type.equals(accountType)) {
   2534             mAccount = new Account(accountName, accountType);
   2535         }
   2536 
   2537         return mAccount;
   2538     }
   2539 
   2540     /**
   2541      * Inserts an item in the contacts table
   2542      *
   2543      * @param values the values for the new row
   2544      * @return the row ID of the newly created row
   2545      */
   2546     private long insertContact(ContentValues values) {
   2547         throw new UnsupportedOperationException("Aggregate contacts are created automatically");
   2548     }
   2549 
   2550     /**
   2551      * Inserts an item in the contacts table
   2552      *
   2553      * @param uri the values for the new row
   2554      * @param values the account this contact should be associated with. may be null.
   2555      * @return the row ID of the newly created row
   2556      */
   2557     private long insertRawContact(Uri uri, ContentValues values) {
   2558         mValues.clear();
   2559         mValues.putAll(values);
   2560         mValues.putNull(RawContacts.CONTACT_ID);
   2561 
   2562         final Account account = resolveAccount(uri, mValues);
   2563 
   2564         if (values.containsKey(RawContacts.DELETED)
   2565                 && values.getAsInteger(RawContacts.DELETED) != 0) {
   2566             mValues.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DISABLED);
   2567         }
   2568 
   2569         long rawContactId = mDb.insert(Tables.RAW_CONTACTS, RawContacts.CONTACT_ID, mValues);
   2570         int aggregationMode = RawContacts.AGGREGATION_MODE_DEFAULT;
   2571         if (mValues.containsKey(RawContacts.AGGREGATION_MODE)) {
   2572             aggregationMode = mValues.getAsInteger(RawContacts.AGGREGATION_MODE);
   2573         }
   2574         mContactAggregator.markNewForAggregation(rawContactId, aggregationMode);
   2575 
   2576         // Trigger creation of a Contact based on this RawContact at the end of transaction
   2577         mInsertedRawContacts.put(rawContactId, account);
   2578 
   2579         return rawContactId;
   2580     }
   2581 
   2582     /**
   2583      * Inserts an item in the data table
   2584      *
   2585      * @param values the values for the new row
   2586      * @return the row ID of the newly created row
   2587      */
   2588     private long insertData(ContentValues values, boolean callerIsSyncAdapter) {
   2589         long id = 0;
   2590         mValues.clear();
   2591         mValues.putAll(values);
   2592 
   2593         long rawContactId = mValues.getAsLong(Data.RAW_CONTACT_ID);
   2594 
   2595         // Replace package with internal mapping
   2596         final String packageName = mValues.getAsString(Data.RES_PACKAGE);
   2597         if (packageName != null) {
   2598             mValues.put(DataColumns.PACKAGE_ID, mDbHelper.getPackageId(packageName));
   2599         }
   2600         mValues.remove(Data.RES_PACKAGE);
   2601 
   2602         // Replace mimetype with internal mapping
   2603         final String mimeType = mValues.getAsString(Data.MIMETYPE);
   2604         if (TextUtils.isEmpty(mimeType)) {
   2605             throw new IllegalArgumentException(Data.MIMETYPE + " is required");
   2606         }
   2607 
   2608         mValues.put(DataColumns.MIMETYPE_ID, mDbHelper.getMimeTypeId(mimeType));
   2609         mValues.remove(Data.MIMETYPE);
   2610 
   2611         DataRowHandler rowHandler = getDataRowHandler(mimeType);
   2612         id = rowHandler.insert(mDb, rawContactId, mValues);
   2613         if (!callerIsSyncAdapter) {
   2614             setRawContactDirty(rawContactId);
   2615         }
   2616         mUpdatedRawContacts.add(rawContactId);
   2617         return id;
   2618     }
   2619 
   2620     private void triggerAggregation(long rawContactId) {
   2621         if (!mContactAggregator.isEnabled()) {
   2622             return;
   2623         }
   2624 
   2625         int aggregationMode = mDbHelper.getAggregationMode(rawContactId);
   2626         switch (aggregationMode) {
   2627             case RawContacts.AGGREGATION_MODE_DISABLED:
   2628                 break;
   2629 
   2630             case RawContacts.AGGREGATION_MODE_DEFAULT: {
   2631                 mContactAggregator.markForAggregation(rawContactId, aggregationMode, false);
   2632                 break;
   2633             }
   2634 
   2635             case RawContacts.AGGREGATION_MODE_SUSPENDED: {
   2636                 long contactId = mDbHelper.getContactId(rawContactId);
   2637 
   2638                 if (contactId != 0) {
   2639                     mContactAggregator.updateAggregateData(contactId);
   2640                 }
   2641                 break;
   2642             }
   2643 
   2644             case RawContacts.AGGREGATION_MODE_IMMEDIATE: {
   2645                 mContactAggregator.aggregateContact(mDb, rawContactId);
   2646                 break;
   2647             }
   2648         }
   2649     }
   2650 
   2651     /**
   2652      * Returns the group id of the group with sourceId and the same account as rawContactId.
   2653      * If the group doesn't already exist then it is first created,
   2654      * @param db SQLiteDatabase to use for this operation
   2655      * @param rawContactId the contact this group is associated with
   2656      * @param sourceId the sourceIf of the group to query or create
   2657      * @return the group id of the existing or created group
   2658      * @throws IllegalArgumentException if the contact is not associated with an account
   2659      * @throws IllegalStateException if a group needs to be created but the creation failed
   2660      */
   2661     private long getOrMakeGroup(SQLiteDatabase db, long rawContactId, String sourceId,
   2662             Account account) {
   2663 
   2664         if (account == null) {
   2665             mSelectionArgs1[0] = String.valueOf(rawContactId);
   2666             Cursor c = db.query(RawContactsQuery.TABLE, RawContactsQuery.COLUMNS,
   2667                     RawContacts._ID + "=?", mSelectionArgs1, null, null, null);
   2668             try {
   2669                 if (c.moveToFirst()) {
   2670                     String accountName = c.getString(RawContactsQuery.ACCOUNT_NAME);
   2671                     String accountType = c.getString(RawContactsQuery.ACCOUNT_TYPE);
   2672                     if (!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(accountType)) {
   2673                         account = new Account(accountName, accountType);
   2674                     }
   2675                 }
   2676             } finally {
   2677                 c.close();
   2678             }
   2679         }
   2680 
   2681         if (account == null) {
   2682             throw new IllegalArgumentException("if the groupmembership only "
   2683                     + "has a sourceid the the contact must be associated with "
   2684                     + "an account");
   2685         }
   2686 
   2687         ArrayList<GroupIdCacheEntry> entries = mGroupIdCache.get(sourceId);
   2688         if (entries == null) {
   2689             entries = new ArrayList<GroupIdCacheEntry>(1);
   2690             mGroupIdCache.put(sourceId, entries);
   2691         }
   2692 
   2693         int count = entries.size();
   2694         for (int i = 0; i < count; i++) {
   2695             GroupIdCacheEntry entry = entries.get(i);
   2696             if (entry.accountName.equals(account.name) && entry.accountType.equals(account.type)) {
   2697                 return entry.groupId;
   2698             }
   2699         }
   2700 
   2701         GroupIdCacheEntry entry = new GroupIdCacheEntry();
   2702         entry.accountName = account.name;
   2703         entry.accountType = account.type;
   2704         entry.sourceId = sourceId;
   2705         entries.add(0, entry);
   2706 
   2707         // look up the group that contains this sourceId and has the same account name and type
   2708         // as the contact refered to by rawContactId
   2709         Cursor c = db.query(Tables.GROUPS, new String[]{RawContacts._ID},
   2710                 Clauses.GROUP_HAS_ACCOUNT_AND_SOURCE_ID,
   2711                 new String[]{sourceId, account.name, account.type}, null, null, null);
   2712         try {
   2713             if (c.moveToFirst()) {
   2714                 entry.groupId = c.getLong(0);
   2715             } else {
   2716                 ContentValues groupValues = new ContentValues();
   2717                 groupValues.put(Groups.ACCOUNT_NAME, account.name);
   2718                 groupValues.put(Groups.ACCOUNT_TYPE, account.type);
   2719                 groupValues.put(Groups.SOURCE_ID, sourceId);
   2720                 long groupId = db.insert(Tables.GROUPS, Groups.ACCOUNT_NAME, groupValues);
   2721                 if (groupId < 0) {
   2722                     throw new IllegalStateException("unable to create a new group with "
   2723                             + "this sourceid: " + groupValues);
   2724                 }
   2725                 entry.groupId = groupId;
   2726             }
   2727         } finally {
   2728             c.close();
   2729         }
   2730 
   2731         return entry.groupId;
   2732     }
   2733 
   2734     private interface DisplayNameQuery {
   2735         public static final String RAW_SQL =
   2736                 "SELECT "
   2737                         + DataColumns.MIMETYPE_ID + ","
   2738                         + Data.IS_PRIMARY + ","
   2739                         + Data.DATA1 + ","
   2740                         + Data.DATA2 + ","
   2741                         + Data.DATA3 + ","
   2742                         + Data.DATA4 + ","
   2743                         + Data.DATA5 + ","
   2744                         + Data.DATA6 + ","
   2745                         + Data.DATA7 + ","
   2746                         + Data.DATA8 + ","
   2747                         + Data.DATA9 + ","
   2748                         + Data.DATA10 + ","
   2749                         + Data.DATA11 +
   2750                 " FROM " + Tables.DATA +
   2751                 " WHERE " + Data.RAW_CONTACT_ID + "=?" +
   2752                         " AND (" + Data.DATA1 + " NOT NULL OR " +
   2753                                 Organization.TITLE + " NOT NULL)";
   2754 
   2755         public static final int MIMETYPE = 0;
   2756         public static final int IS_PRIMARY = 1;
   2757         public static final int DATA1 = 2;
   2758         public static final int GIVEN_NAME = 3;                         // data2
   2759         public static final int FAMILY_NAME = 4;                        // data3
   2760         public static final int PREFIX = 5;                             // data4
   2761         public static final int TITLE = 5;                              // data4
   2762         public static final int MIDDLE_NAME = 6;                        // data5
   2763         public static final int SUFFIX = 7;                             // data6
   2764         public static final int PHONETIC_GIVEN_NAME = 8;                // data7
   2765         public static final int PHONETIC_MIDDLE_NAME = 9;               // data8
   2766         public static final int ORGANIZATION_PHONETIC_NAME = 9;         // data8
   2767         public static final int PHONETIC_FAMILY_NAME = 10;              // data9
   2768         public static final int FULL_NAME_STYLE = 11;                   // data10
   2769         public static final int ORGANIZATION_PHONETIC_NAME_STYLE = 11;  // data10
   2770         public static final int PHONETIC_NAME_STYLE = 12;               // data11
   2771     }
   2772 
   2773     /**
   2774      * Updates a raw contact display name based on data rows, e.g. structured name,
   2775      * organization, email etc.
   2776      */
   2777     public void updateRawContactDisplayName(SQLiteDatabase db, long rawContactId) {
   2778         int bestDisplayNameSource = DisplayNameSources.UNDEFINED;
   2779         NameSplitter.Name bestName = null;
   2780         String bestDisplayName = null;
   2781         String bestPhoneticName = null;
   2782         int bestPhoneticNameStyle = PhoneticNameStyle.UNDEFINED;
   2783 
   2784         mSelectionArgs1[0] = String.valueOf(rawContactId);
   2785         Cursor c = db.rawQuery(DisplayNameQuery.RAW_SQL, mSelectionArgs1);
   2786         try {
   2787             while (c.moveToNext()) {
   2788                 int mimeType = c.getInt(DisplayNameQuery.MIMETYPE);
   2789                 int source = getDisplayNameSource(mimeType);
   2790                 if (source < bestDisplayNameSource || source == DisplayNameSources.UNDEFINED) {
   2791                     continue;
   2792                 }
   2793 
   2794                 if (source == bestDisplayNameSource && c.getInt(DisplayNameQuery.IS_PRIMARY) == 0) {
   2795                     continue;
   2796                 }
   2797 
   2798                 if (mimeType == mMimeTypeIdStructuredName) {
   2799                     NameSplitter.Name name;
   2800                     if (bestName != null) {
   2801                         name = new NameSplitter.Name();
   2802                     } else {
   2803                         name = mName;
   2804                         name.clear();
   2805                     }
   2806                     name.prefix = c.getString(DisplayNameQuery.PREFIX);
   2807                     name.givenNames = c.getString(DisplayNameQuery.GIVEN_NAME);
   2808                     name.middleName = c.getString(DisplayNameQuery.MIDDLE_NAME);
   2809                     name.familyName = c.getString(DisplayNameQuery.FAMILY_NAME);
   2810                     name.suffix = c.getString(DisplayNameQuery.SUFFIX);
   2811                     name.fullNameStyle = c.isNull(DisplayNameQuery.FULL_NAME_STYLE)
   2812                             ? FullNameStyle.UNDEFINED
   2813                             : c.getInt(DisplayNameQuery.FULL_NAME_STYLE);
   2814                     name.phoneticFamilyName = c.getString(DisplayNameQuery.PHONETIC_FAMILY_NAME);
   2815                     name.phoneticMiddleName = c.getString(DisplayNameQuery.PHONETIC_MIDDLE_NAME);
   2816                     name.phoneticGivenName = c.getString(DisplayNameQuery.PHONETIC_GIVEN_NAME);
   2817                     name.phoneticNameStyle = c.isNull(DisplayNameQuery.PHONETIC_NAME_STYLE)
   2818                             ? PhoneticNameStyle.UNDEFINED
   2819                             : c.getInt(DisplayNameQuery.PHONETIC_NAME_STYLE);
   2820                     if (!name.isEmpty()) {
   2821                         bestDisplayNameSource = source;
   2822                         bestName = name;
   2823                     }
   2824                 } else if (mimeType == mMimeTypeIdOrganization) {
   2825                     mCharArrayBuffer.sizeCopied = 0;
   2826                     c.copyStringToBuffer(DisplayNameQuery.DATA1, mCharArrayBuffer);
   2827                     if (mCharArrayBuffer.sizeCopied != 0) {
   2828                         bestDisplayNameSource = source;
   2829                         bestDisplayName = new String(mCharArrayBuffer.data, 0,
   2830                                 mCharArrayBuffer.sizeCopied);
   2831                         bestPhoneticName = c.getString(DisplayNameQuery.ORGANIZATION_PHONETIC_NAME);
   2832                         bestPhoneticNameStyle =
   2833                                 c.isNull(DisplayNameQuery.ORGANIZATION_PHONETIC_NAME_STYLE)
   2834                                     ? PhoneticNameStyle.UNDEFINED
   2835                                     : c.getInt(DisplayNameQuery.ORGANIZATION_PHONETIC_NAME_STYLE);
   2836                     } else {
   2837                         c.copyStringToBuffer(DisplayNameQuery.TITLE, mCharArrayBuffer);
   2838                         if (mCharArrayBuffer.sizeCopied != 0) {
   2839                             bestDisplayNameSource = source;
   2840                             bestDisplayName = new String(mCharArrayBuffer.data, 0,
   2841                                     mCharArrayBuffer.sizeCopied);
   2842                             bestPhoneticName = null;
   2843                             bestPhoneticNameStyle = PhoneticNameStyle.UNDEFINED;
   2844                         }
   2845                     }
   2846                 } else {
   2847                     // Display name is at DATA1 in all other types.
   2848                     // This is ensured in the constructor.
   2849 
   2850                     mCharArrayBuffer.sizeCopied = 0;
   2851                     c.copyStringToBuffer(DisplayNameQuery.DATA1, mCharArrayBuffer);
   2852                     if (mCharArrayBuffer.sizeCopied != 0) {
   2853                         bestDisplayNameSource = source;
   2854                         bestDisplayName = new String(mCharArrayBuffer.data, 0,
   2855                                 mCharArrayBuffer.sizeCopied);
   2856                         bestPhoneticName = null;
   2857                         bestPhoneticNameStyle = PhoneticNameStyle.UNDEFINED;
   2858                     }
   2859                 }
   2860             }
   2861 
   2862         } finally {
   2863             c.close();
   2864         }
   2865 
   2866         String displayNamePrimary;
   2867         String displayNameAlternative;
   2868         String sortKeyPrimary = null;
   2869         String sortKeyAlternative = null;
   2870         int displayNameStyle = FullNameStyle.UNDEFINED;
   2871 
   2872         if (bestDisplayNameSource == DisplayNameSources.STRUCTURED_NAME) {
   2873             displayNameStyle = bestName.fullNameStyle;
   2874             if (displayNameStyle == FullNameStyle.CJK
   2875                     || displayNameStyle == FullNameStyle.UNDEFINED) {
   2876                 displayNameStyle = mNameSplitter.getAdjustedFullNameStyle(displayNameStyle);
   2877                 bestName.fullNameStyle = displayNameStyle;
   2878             }
   2879 
   2880             displayNamePrimary = mNameSplitter.join(bestName, true);
   2881             displayNameAlternative = mNameSplitter.join(bestName, false);
   2882 
   2883             bestPhoneticName = mNameSplitter.joinPhoneticName(bestName);
   2884             bestPhoneticNameStyle = bestName.phoneticNameStyle;
   2885         } else {
   2886             displayNamePrimary = displayNameAlternative = bestDisplayName;
   2887         }
   2888 
   2889         if (bestPhoneticName != null) {
   2890             sortKeyPrimary = sortKeyAlternative = bestPhoneticName;
   2891             if (bestPhoneticNameStyle == PhoneticNameStyle.UNDEFINED) {
   2892                 bestPhoneticNameStyle = mNameSplitter.guessPhoneticNameStyle(bestPhoneticName);
   2893             }
   2894         } else {
   2895             if (displayNameStyle == FullNameStyle.UNDEFINED) {
   2896                 displayNameStyle = mNameSplitter.guessFullNameStyle(bestDisplayName);
   2897                 if (displayNameStyle == FullNameStyle.UNDEFINED
   2898                         || displayNameStyle == FullNameStyle.CJK) {
   2899                     displayNameStyle = mNameSplitter.getAdjustedNameStyleBasedOnPhoneticNameStyle(
   2900                             displayNameStyle, bestPhoneticNameStyle);
   2901                 }
   2902                 displayNameStyle = mNameSplitter.getAdjustedFullNameStyle(displayNameStyle);
   2903             }
   2904             if (displayNameStyle == FullNameStyle.CHINESE ||
   2905                     displayNameStyle == FullNameStyle.CJK) {
   2906                 sortKeyPrimary = sortKeyAlternative =
   2907                         ContactLocaleUtils.getIntance().getSortKey(
   2908                                 displayNamePrimary, displayNameStyle);
   2909             }
   2910         }
   2911 
   2912         if (sortKeyPrimary == null) {
   2913             sortKeyPrimary = displayNamePrimary;
   2914             sortKeyAlternative = displayNameAlternative;
   2915         }
   2916 
   2917         setDisplayName(rawContactId, bestDisplayNameSource, displayNamePrimary,
   2918                 displayNameAlternative, bestPhoneticName, bestPhoneticNameStyle,
   2919                 sortKeyPrimary, sortKeyAlternative);
   2920     }
   2921 
   2922     private int getDisplayNameSource(int mimeTypeId) {
   2923         if (mimeTypeId == mMimeTypeIdStructuredName) {
   2924             return DisplayNameSources.STRUCTURED_NAME;
   2925         } else if (mimeTypeId == mMimeTypeIdEmail) {
   2926             return DisplayNameSources.EMAIL;
   2927         } else if (mimeTypeId == mMimeTypeIdPhone) {
   2928             return DisplayNameSources.PHONE;
   2929         } else if (mimeTypeId == mMimeTypeIdOrganization) {
   2930             return DisplayNameSources.ORGANIZATION;
   2931         } else if (mimeTypeId == mMimeTypeIdNickname) {
   2932             return DisplayNameSources.NICKNAME;
   2933         } else {
   2934             return DisplayNameSources.UNDEFINED;
   2935         }
   2936     }
   2937 
   2938     /**
   2939      * Delete data row by row so that fixing of primaries etc work correctly.
   2940      */
   2941     private int deleteData(String selection, String[] selectionArgs, boolean callerIsSyncAdapter) {
   2942         int count = 0;
   2943 
   2944         // Note that the query will return data according to the access restrictions,
   2945         // so we don't need to worry about deleting data we don't have permission to read.
   2946         Cursor c = query(Data.CONTENT_URI, DataDeleteQuery.COLUMNS, selection, selectionArgs, null);
   2947         try {
   2948             while(c.moveToNext()) {
   2949                 long rawContactId = c.getLong(DataDeleteQuery.RAW_CONTACT_ID);
   2950                 String mimeType = c.getString(DataDeleteQuery.MIMETYPE);
   2951                 DataRowHandler rowHandler = getDataRowHandler(mimeType);
   2952                 count += rowHandler.delete(mDb, c);
   2953                 if (!callerIsSyncAdapter) {
   2954                     setRawContactDirty(rawContactId);
   2955                 }
   2956             }
   2957         } finally {
   2958             c.close();
   2959         }
   2960 
   2961         return count;
   2962     }
   2963 
   2964     /**
   2965      * Delete a data row provided that it is one of the allowed mime types.
   2966      */
   2967     public int deleteData(long dataId, String[] allowedMimeTypes) {
   2968 
   2969         // Note that the query will return data according to the access restrictions,
   2970         // so we don't need to worry about deleting data we don't have permission to read.
   2971         mSelectionArgs1[0] = String.valueOf(dataId);
   2972         Cursor c = query(Data.CONTENT_URI, DataDeleteQuery.COLUMNS, Data._ID + "=?",
   2973                 mSelectionArgs1, null);
   2974 
   2975         try {
   2976             if (!c.moveToFirst()) {
   2977                 return 0;
   2978             }
   2979 
   2980             String mimeType = c.getString(DataDeleteQuery.MIMETYPE);
   2981             boolean valid = false;
   2982             for (int i = 0; i < allowedMimeTypes.length; i++) {
   2983                 if (TextUtils.equals(mimeType, allowedMimeTypes[i])) {
   2984                     valid = true;
   2985                     break;
   2986                 }
   2987             }
   2988 
   2989             if (!valid) {
   2990                 throw new IllegalArgumentException("Data type mismatch: expected "
   2991                         + Lists.newArrayList(allowedMimeTypes));
   2992             }
   2993 
   2994             DataRowHandler rowHandler = getDataRowHandler(mimeType);
   2995             return rowHandler.delete(mDb, c);
   2996         } finally {
   2997             c.close();
   2998         }
   2999     }
   3000 
   3001     /**
   3002      * Inserts an item in the groups table
   3003      */
   3004     private long insertGroup(Uri uri, ContentValues values, boolean callerIsSyncAdapter) {
   3005         mValues.clear();
   3006         mValues.putAll(values);
   3007 
   3008         final Account account = resolveAccount(uri, mValues);
   3009 
   3010         // Replace package with internal mapping
   3011         final String packageName = mValues.getAsString(Groups.RES_PACKAGE);
   3012         if (packageName != null) {
   3013             mValues.put(GroupsColumns.PACKAGE_ID, mDbHelper.getPackageId(packageName));
   3014         }
   3015         mValues.remove(Groups.RES_PACKAGE);
   3016 
   3017         if (!callerIsSyncAdapter) {
   3018             mValues.put(Groups.DIRTY, 1);
   3019         }
   3020 
   3021         long result = mDb.insert(Tables.GROUPS, Groups.TITLE, mValues);
   3022 
   3023         if (mValues.containsKey(Groups.GROUP_VISIBLE)) {
   3024             mVisibleTouched = true;
   3025         }
   3026 
   3027         return result;
   3028     }
   3029 
   3030     private long insertSettings(Uri uri, ContentValues values) {
   3031         final long id = mDb.insert(Tables.SETTINGS, null, values);
   3032 
   3033         if (values.containsKey(Settings.UNGROUPED_VISIBLE)) {
   3034             mVisibleTouched = true;
   3035         }
   3036 
   3037         return id;
   3038     }
   3039 
   3040     /**
   3041      * Inserts a status update.
   3042      */
   3043     public long insertStatusUpdate(ContentValues values) {
   3044         final String handle = values.getAsString(StatusUpdates.IM_HANDLE);
   3045         final Integer protocol = values.getAsInteger(StatusUpdates.PROTOCOL);
   3046         String customProtocol = null;
   3047 
   3048         if (protocol != null && protocol == Im.PROTOCOL_CUSTOM) {
   3049             customProtocol = values.getAsString(StatusUpdates.CUSTOM_PROTOCOL);
   3050             if (TextUtils.isEmpty(customProtocol)) {
   3051                 throw new IllegalArgumentException(
   3052                         "CUSTOM_PROTOCOL is required when PROTOCOL=PROTOCOL_CUSTOM");
   3053             }
   3054         }
   3055 
   3056         long rawContactId = -1;
   3057         long contactId = -1;
   3058         Long dataId = values.getAsLong(StatusUpdates.DATA_ID);
   3059         mSb.setLength(0);
   3060         mSelectionArgs.clear();
   3061         if (dataId != null) {
   3062             // Lookup the contact info for the given data row.
   3063 
   3064             mSb.append(Tables.DATA + "." + Data._ID + "=?");
   3065             mSelectionArgs.add(String.valueOf(dataId));
   3066         } else {
   3067             // Lookup the data row to attach this presence update to
   3068 
   3069             if (TextUtils.isEmpty(handle) || protocol == null) {
   3070                 throw new IllegalArgumentException("PROTOCOL and IM_HANDLE are required");
   3071             }
   3072 
   3073             // TODO: generalize to allow other providers to match against email
   3074             boolean matchEmail = Im.PROTOCOL_GOOGLE_TALK == protocol;
   3075 
   3076             String mimeTypeIdIm = String.valueOf(mMimeTypeIdIm);
   3077             if (matchEmail) {
   3078                 String mimeTypeIdEmail = String.valueOf(mMimeTypeIdEmail);
   3079 
   3080                 // The following hack forces SQLite to use the (mimetype_id,data1) index, otherwise
   3081                 // the "OR" conjunction confuses it and it switches to a full scan of
   3082                 // the raw_contacts table.
   3083 
   3084                 // This code relies on the fact that Im.DATA and Email.DATA are in fact the same
   3085                 // column - Data.DATA1
   3086                 mSb.append(DataColumns.MIMETYPE_ID + " IN (?,?)" +
   3087                         " AND " + Data.DATA1 + "=?" +
   3088                         " AND ((" + DataColumns.MIMETYPE_ID + "=? AND " + Im.PROTOCOL + "=?");
   3089                 mSelectionArgs.add(mimeTypeIdEmail);
   3090                 mSelectionArgs.add(mimeTypeIdIm);
   3091                 mSelectionArgs.add(handle);
   3092                 mSelectionArgs.add(mimeTypeIdIm);
   3093                 mSelectionArgs.add(String.valueOf(protocol));
   3094                 if (customProtocol != null) {
   3095                     mSb.append(" AND " + Im.CUSTOM_PROTOCOL + "=?");
   3096                     mSelectionArgs.add(customProtocol);
   3097                 }
   3098                 mSb.append(") OR (" + DataColumns.MIMETYPE_ID + "=?))");
   3099                 mSelectionArgs.add(mimeTypeIdEmail);
   3100             } else {
   3101                 mSb.append(DataColumns.MIMETYPE_ID + "=?" +
   3102                         " AND " + Im.PROTOCOL + "=?" +
   3103                         " AND " + Im.DATA + "=?");
   3104                 mSelectionArgs.add(mimeTypeIdIm);
   3105                 mSelectionArgs.add(String.valueOf(protocol));
   3106                 mSelectionArgs.add(handle);
   3107                 if (customProtocol != null) {
   3108                     mSb.append(" AND " + Im.CUSTOM_PROTOCOL + "=?");
   3109                     mSelectionArgs.add(customProtocol);
   3110                 }
   3111             }
   3112 
   3113             if (values.containsKey(StatusUpdates.DATA_ID)) {
   3114                 mSb.append(" AND " + DataColumns.CONCRETE_ID + "=?");
   3115                 mSelectionArgs.add(values.getAsString(StatusUpdates.DATA_ID));
   3116             }
   3117         }
   3118         mSb.append(" AND ").append(getContactsRestrictions());
   3119 
   3120         Cursor cursor = null;
   3121         try {
   3122             cursor = mDb.query(DataContactsQuery.TABLE, DataContactsQuery.PROJECTION,
   3123                     mSb.toString(), mSelectionArgs.toArray(EMPTY_STRING_ARRAY), null, null,
   3124                     Contacts.IN_VISIBLE_GROUP + " DESC, " + Data.RAW_CONTACT_ID);
   3125             if (cursor.moveToFirst()) {
   3126                 dataId = cursor.getLong(DataContactsQuery.DATA_ID);
   3127                 rawContactId = cursor.getLong(DataContactsQuery.RAW_CONTACT_ID);
   3128                 contactId = cursor.getLong(DataContactsQuery.CONTACT_ID);
   3129             } else {
   3130                 // No contact found, return a null URI
   3131                 return -1;
   3132             }
   3133         } finally {
   3134             if (cursor != null) {
   3135                 cursor.close();
   3136             }
   3137         }
   3138 
   3139         if (values.containsKey(StatusUpdates.PRESENCE)) {
   3140             if (customProtocol == null) {
   3141                 // We cannot allow a null in the custom protocol field, because SQLite3 does not
   3142                 // properly enforce uniqueness of null values
   3143                 customProtocol = "";
   3144             }
   3145 
   3146             mValues.clear();
   3147             mValues.put(StatusUpdates.DATA_ID, dataId);
   3148             mValues.put(PresenceColumns.RAW_CONTACT_ID, rawContactId);
   3149             mValues.put(PresenceColumns.CONTACT_ID, contactId);
   3150             mValues.put(StatusUpdates.PROTOCOL, protocol);
   3151             mValues.put(StatusUpdates.CUSTOM_PROTOCOL, customProtocol);
   3152             mValues.put(StatusUpdates.IM_HANDLE, handle);
   3153             if (values.containsKey(StatusUpdates.IM_ACCOUNT)) {
   3154                 mValues.put(StatusUpdates.IM_ACCOUNT, values.getAsString(StatusUpdates.IM_ACCOUNT));
   3155             }
   3156             mValues.put(StatusUpdates.PRESENCE,
   3157                     values.getAsString(StatusUpdates.PRESENCE));
   3158             mValues.put(StatusUpdates.CHAT_CAPABILITY,
   3159                     values.getAsString(StatusUpdates.CHAT_CAPABILITY));
   3160 
   3161             // Insert the presence update
   3162             mDb.replace(Tables.PRESENCE, null, mValues);
   3163         }
   3164 
   3165 
   3166         if (values.containsKey(StatusUpdates.STATUS)) {
   3167             String status = values.getAsString(StatusUpdates.STATUS);
   3168             String resPackage = values.getAsString(StatusUpdates.STATUS_RES_PACKAGE);
   3169             Integer labelResource = values.getAsInteger(StatusUpdates.STATUS_LABEL);
   3170 
   3171             if (TextUtils.isEmpty(resPackage)
   3172                     && (labelResource == null || labelResource == 0)
   3173                     && protocol != null) {
   3174                 labelResource = Im.getProtocolLabelResource(protocol);
   3175             }
   3176 
   3177             Long iconResource = values.getAsLong(StatusUpdates.STATUS_ICON);
   3178             // TODO compute the default icon based on the protocol
   3179 
   3180             if (TextUtils.isEmpty(status)) {
   3181                 mStatusUpdateDelete.bindLong(1, dataId);
   3182                 mStatusUpdateDelete.execute();
   3183             } else if (values.containsKey(StatusUpdates.STATUS_TIMESTAMP)) {
   3184                 long timestamp = values.getAsLong(StatusUpdates.STATUS_TIMESTAMP);
   3185                 mStatusUpdateReplace.bindLong(1, dataId);
   3186                 mStatusUpdateReplace.bindLong(2, timestamp);
   3187                 bindString(mStatusUpdateReplace, 3, status);
   3188                 bindString(mStatusUpdateReplace, 4, resPackage);
   3189                 bindLong(mStatusUpdateReplace, 5, iconResource);
   3190                 bindLong(mStatusUpdateReplace, 6, labelResource);
   3191                 mStatusUpdateReplace.execute();
   3192             } else {
   3193 
   3194                 try {
   3195                     mStatusUpdateInsert.bindLong(1, dataId);
   3196                     bindString(mStatusUpdateInsert, 2, status);
   3197                     bindString(mStatusUpdateInsert, 3, resPackage);
   3198                     bindLong(mStatusUpdateInsert, 4, iconResource);
   3199                     bindLong(mStatusUpdateInsert, 5, labelResource);
   3200                     mStatusUpdateInsert.executeInsert();
   3201                 } catch (SQLiteConstraintException e) {
   3202                     // The row already exists - update it
   3203                     long timestamp = System.currentTimeMillis();
   3204                     mStatusUpdateAutoTimestamp.bindLong(1, timestamp);
   3205                     bindString(mStatusUpdateAutoTimestamp, 2, status);
   3206                     mStatusUpdateAutoTimestamp.bindLong(3, dataId);
   3207                     bindString(mStatusUpdateAutoTimestamp, 4, status);
   3208                     mStatusUpdateAutoTimestamp.execute();
   3209 
   3210                     bindString(mStatusAttributionUpdate, 1, resPackage);
   3211                     bindLong(mStatusAttributionUpdate, 2, iconResource);
   3212                     bindLong(mStatusAttributionUpdate, 3, labelResource);
   3213                     mStatusAttributionUpdate.bindLong(4, dataId);
   3214                     mStatusAttributionUpdate.execute();
   3215                 }
   3216             }
   3217         }
   3218 
   3219         if (contactId != -1) {
   3220             mLastStatusUpdate.bindLong(1, contactId);
   3221             mLastStatusUpdate.bindLong(2, contactId);
   3222             mLastStatusUpdate.execute();
   3223         }
   3224 
   3225         return dataId;
   3226     }
   3227 
   3228     @Override
   3229     protected int deleteInTransaction(Uri uri, String selection, String[] selectionArgs) {
   3230         if (VERBOSE_LOGGING) {
   3231             Log.v(TAG, "deleteInTransaction: " + uri);
   3232         }
   3233         flushTransactionalChanges();
   3234         final boolean callerIsSyncAdapter =
   3235                 readBooleanQueryParameter(uri, ContactsContract.CALLER_IS_SYNCADAPTER, false);
   3236         final int match = sUriMatcher.match(uri);
   3237         switch (match) {
   3238             case SYNCSTATE:
   3239                 return mDbHelper.getSyncState().delete(mDb, selection, selectionArgs);
   3240 
   3241             case SYNCSTATE_ID:
   3242                 String selectionWithId =
   3243                         (SyncStateContract.Columns._ID + "=" + ContentUris.parseId(uri) + " ")
   3244                         + (selection == null ? "" : " AND (" + selection + ")");
   3245                 return mDbHelper.getSyncState().delete(mDb, selectionWithId, selectionArgs);
   3246 
   3247             case CONTACTS: {
   3248                 // TODO
   3249                 return 0;
   3250             }
   3251 
   3252             case CONTACTS_ID: {
   3253                 long contactId = ContentUris.parseId(uri);
   3254                 return deleteContact(contactId);
   3255             }
   3256 
   3257             case CONTACTS_LOOKUP: {
   3258                 final List<String> pathSegments = uri.getPathSegments();
   3259                 final int segmentCount = pathSegments.size();
   3260                 if (segmentCount < 3) {
   3261                     throw new IllegalArgumentException(mDbHelper.exceptionMessage(
   3262                             "Missing a lookup key", uri));
   3263                 }
   3264                 final String lookupKey = pathSegments.get(2);
   3265                 final long contactId = lookupContactIdByLookupKey(mDb, lookupKey);
   3266                 return deleteContact(contactId);
   3267             }
   3268 
   3269             case CONTACTS_LOOKUP_ID: {
   3270                 // lookup contact by id and lookup key to see if they still match the actual record
   3271                 long contactId = ContentUris.parseId(uri);
   3272                 final List<String> pathSegments = uri.getPathSegments();
   3273                 final String lookupKey = pathSegments.get(2);
   3274                 SQLiteQueryBuilder lookupQb = new SQLiteQueryBuilder();
   3275                 setTablesAndProjectionMapForContacts(lookupQb, uri, null);
   3276                 String[] args;
   3277                 if (selectionArgs == null) {
   3278                     args = new String[2];
   3279                 } else {
   3280                     args = new String[selectionArgs.length + 2];
   3281                     System.arraycopy(selectionArgs, 0, args, 2, selectionArgs.length);
   3282                 }
   3283                 args[0] = String.valueOf(contactId);
   3284                 args[1] = Uri.encode(lookupKey);
   3285                 lookupQb.appendWhere(Contacts._ID + "=? AND " + Contacts.LOOKUP_KEY + "=?");
   3286                 final SQLiteDatabase db = mDbHelper.getReadableDatabase();
   3287                 Cursor c = query(db, lookupQb, null, selection, args, null, null, null);
   3288                 try {
   3289                     if (c.getCount() == 1) {
   3290                         // contact was unmodified so go ahead and delete it
   3291                         return deleteContact(contactId);
   3292                     } else {
   3293                         // row was changed (e.g. the merging might have changed), we got multiple
   3294                         // rows or the supplied selection filtered the record out
   3295                         return 0;
   3296                     }
   3297                 } finally {
   3298                     c.close();
   3299                 }
   3300             }
   3301 
   3302             case RAW_CONTACTS: {
   3303                 int numDeletes = 0;
   3304                 Cursor c = mDb.query(Tables.RAW_CONTACTS,
   3305                         new String[]{RawContacts._ID, RawContacts.CONTACT_ID},
   3306                         appendAccountToSelection(uri, selection), selectionArgs, null, null, null);
   3307                 try {
   3308                     while (c.moveToNext()) {
   3309                         final long rawContactId = c.getLong(0);
   3310                         long contactId = c.getLong(1);
   3311                         numDeletes += deleteRawContact(rawContactId, contactId,
   3312                                 callerIsSyncAdapter);
   3313                     }
   3314                 } finally {
   3315                     c.close();
   3316                 }
   3317                 return numDeletes;
   3318             }
   3319 
   3320             case RAW_CONTACTS_ID: {
   3321                 final long rawContactId = ContentUris.parseId(uri);
   3322                 return deleteRawContact(rawContactId, mDbHelper.getContactId(rawContactId),
   3323                         callerIsSyncAdapter);
   3324             }
   3325 
   3326             case DATA: {
   3327                 mSyncToNetwork |= !callerIsSyncAdapter;
   3328                 return deleteData(appendAccountToSelection(uri, selection), selectionArgs,
   3329                         callerIsSyncAdapter);
   3330             }
   3331 
   3332             case DATA_ID:
   3333             case PHONES_ID:
   3334             case EMAILS_ID:
   3335             case POSTALS_ID: {
   3336                 long dataId = ContentUris.parseId(uri);
   3337                 mSyncToNetwork |= !callerIsSyncAdapter;
   3338                 mSelectionArgs1[0] = String.valueOf(dataId);
   3339                 return deleteData(Data._ID + "=?", mSelectionArgs1, callerIsSyncAdapter);
   3340             }
   3341 
   3342             case GROUPS_ID: {
   3343                 mSyncToNetwork |= !callerIsSyncAdapter;
   3344                 return deleteGroup(uri, ContentUris.parseId(uri), callerIsSyncAdapter);
   3345             }
   3346 
   3347             case GROUPS: {
   3348                 int numDeletes = 0;
   3349                 Cursor c = mDb.query(Tables.GROUPS, new String[]{Groups._ID},
   3350                         appendAccountToSelection(uri, selection), selectionArgs, null, null, null);
   3351                 try {
   3352                     while (c.moveToNext()) {
   3353                         numDeletes += deleteGroup(uri, c.getLong(0), callerIsSyncAdapter);
   3354                     }
   3355                 } finally {
   3356                     c.close();
   3357                 }
   3358                 if (numDeletes > 0) {
   3359                     mSyncToNetwork |= !callerIsSyncAdapter;
   3360                 }
   3361                 return numDeletes;
   3362             }
   3363 
   3364             case SETTINGS: {
   3365                 mSyncToNetwork |= !callerIsSyncAdapter;
   3366                 return deleteSettings(uri, appendAccountToSelection(uri, selection), selectionArgs);
   3367             }
   3368 
   3369             case STATUS_UPDATES: {
   3370                 return deleteStatusUpdates(selection, selectionArgs);
   3371             }
   3372 
   3373             default: {
   3374                 mSyncToNetwork = true;
   3375                 return mLegacyApiSupport.delete(uri, selection, selectionArgs);
   3376             }
   3377         }
   3378     }
   3379 
   3380     public int deleteGroup(Uri uri, long groupId, boolean callerIsSyncAdapter) {
   3381         mGroupIdCache.clear();
   3382         final long groupMembershipMimetypeId = mDbHelper
   3383                 .getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE);
   3384         mDb.delete(Tables.DATA, DataColumns.MIMETYPE_ID + "="
   3385                 + groupMembershipMimetypeId + " AND " + GroupMembership.GROUP_ROW_ID + "="
   3386                 + groupId, null);
   3387 
   3388         try {
   3389             if (callerIsSyncAdapter) {
   3390                 return mDb.delete(Tables.GROUPS, Groups._ID + "=" + groupId, null);
   3391             } else {
   3392                 mValues.clear();
   3393                 mValues.put(Groups.DELETED, 1);
   3394                 mValues.put(Groups.DIRTY, 1);
   3395                 return mDb.update(Tables.GROUPS, mValues, Groups._ID + "=" + groupId, null);
   3396             }
   3397         } finally {
   3398             mVisibleTouched = true;
   3399         }
   3400     }
   3401 
   3402     private int deleteSettings(Uri uri, String selection, String[] selectionArgs) {
   3403         final int count = mDb.delete(Tables.SETTINGS, selection, selectionArgs);
   3404         mVisibleTouched = true;
   3405         return count;
   3406     }
   3407 
   3408     private int deleteContact(long contactId) {
   3409         mSelectionArgs1[0] = Long.toString(contactId);
   3410         Cursor c = mDb.query(Tables.RAW_CONTACTS, new String[]{RawContacts._ID},
   3411                 RawContacts.CONTACT_ID + "=?", mSelectionArgs1,
   3412                 null, null, null);
   3413         try {
   3414             while (c.moveToNext()) {
   3415                 long rawContactId = c.getLong(0);
   3416                 markRawContactAsDeleted(rawContactId);
   3417             }
   3418         } finally {
   3419             c.close();
   3420         }
   3421 
   3422         return mDb.delete(Tables.CONTACTS, Contacts._ID + "=" + contactId, null);
   3423     }
   3424 
   3425     public int deleteRawContact(long rawContactId, long contactId, boolean callerIsSyncAdapter) {
   3426         mContactAggregator.invalidateAggregationExceptionCache();
   3427         if (callerIsSyncAdapter) {
   3428             mDb.delete(Tables.PRESENCE, PresenceColumns.RAW_CONTACT_ID + "=" + rawContactId, null);
   3429             int count = mDb.delete(Tables.RAW_CONTACTS, RawContacts._ID + "=" + rawContactId, null);
   3430             mContactAggregator.updateDisplayNameForContact(mDb, contactId);
   3431             return count;
   3432         } else {
   3433             mDbHelper.removeContactIfSingleton(rawContactId);
   3434             return markRawContactAsDeleted(rawContactId);
   3435         }
   3436     }
   3437 
   3438     private int deleteStatusUpdates(String selection, String[] selectionArgs) {
   3439       // delete from both tables: presence and status_updates
   3440       // TODO should account type/name be appended to the where clause?
   3441       if (VERBOSE_LOGGING) {
   3442           Log.v(TAG, "deleting data from status_updates for " + selection);
   3443       }
   3444       mDb.delete(Tables.STATUS_UPDATES, getWhereClauseForStatusUpdatesTable(selection),
   3445           selectionArgs);
   3446       return mDb.delete(Tables.PRESENCE, selection, selectionArgs);
   3447     }
   3448 
   3449     private int markRawContactAsDeleted(long rawContactId) {
   3450         mSyncToNetwork = true;
   3451 
   3452         mValues.clear();
   3453         mValues.put(RawContacts.DELETED, 1);
   3454         mValues.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DISABLED);
   3455         mValues.put(RawContactsColumns.AGGREGATION_NEEDED, 1);
   3456         mValues.putNull(RawContacts.CONTACT_ID);
   3457         mValues.put(RawContacts.DIRTY, 1);
   3458         return updateRawContact(rawContactId, mValues);
   3459     }
   3460 
   3461     @Override
   3462     protected int updateInTransaction(Uri uri, ContentValues values, String selection,
   3463             String[] selectionArgs) {
   3464         if (VERBOSE_LOGGING) {
   3465             Log.v(TAG, "updateInTransaction: " + uri);
   3466         }
   3467 
   3468         int count = 0;
   3469 
   3470         final int match = sUriMatcher.match(uri);
   3471         if (match == SYNCSTATE_ID && selection == null) {
   3472             long rowId = ContentUris.parseId(uri);
   3473             Object data = values.get(ContactsContract.SyncState.DATA);
   3474             mUpdatedSyncStates.put(rowId, data);
   3475             return 1;
   3476         }
   3477         flushTransactionalChanges();
   3478         final boolean callerIsSyncAdapter =
   3479                 readBooleanQueryParameter(uri, ContactsContract.CALLER_IS_SYNCADAPTER, false);
   3480         switch(match) {
   3481             case SYNCSTATE:
   3482                 return mDbHelper.getSyncState().update(mDb, values,
   3483                         appendAccountToSelection(uri, selection), selectionArgs);
   3484 
   3485             case SYNCSTATE_ID: {
   3486                 selection = appendAccountToSelection(uri, selection);
   3487                 String selectionWithId =
   3488                         (SyncStateContract.Columns._ID + "=" + ContentUris.parseId(uri) + " ")
   3489                         + (selection == null ? "" : " AND (" + selection + ")");
   3490                 return mDbHelper.getSyncState().update(mDb, values,
   3491                         selectionWithId, selectionArgs);
   3492             }
   3493 
   3494             case CONTACTS: {
   3495                 count = updateContactOptions(values, selection, selectionArgs);
   3496                 break;
   3497             }
   3498 
   3499             case CONTACTS_ID: {
   3500                 count = updateContactOptions(ContentUris.parseId(uri), values);
   3501                 break;
   3502             }
   3503 
   3504             case CONTACTS_LOOKUP:
   3505             case CONTACTS_LOOKUP_ID: {
   3506                 final List<String> pathSegments = uri.getPathSegments();
   3507                 final int segmentCount = pathSegments.size();
   3508                 if (segmentCount < 3) {
   3509                     throw new IllegalArgumentException(mDbHelper.exceptionMessage(
   3510                             "Missing a lookup key", uri));
   3511                 }
   3512                 final String lookupKey = pathSegments.get(2);
   3513                 final long contactId = lookupContactIdByLookupKey(mDb, lookupKey);
   3514                 count = updateContactOptions(contactId, values);
   3515                 break;
   3516             }
   3517 
   3518             case RAW_CONTACTS_DATA: {
   3519                 final String rawContactId = uri.getPathSegments().get(1);
   3520                 String selectionWithId = (Data.RAW_CONTACT_ID + "=" + rawContactId + " ")
   3521                     + (selection == null ? "" : " AND " + selection);
   3522 
   3523                 count = updateData(uri, values, selectionWithId, selectionArgs, callerIsSyncAdapter);
   3524 
   3525                 break;
   3526             }
   3527 
   3528             case DATA: {
   3529                 count = updateData(uri, values, appendAccountToSelection(uri, selection),
   3530                         selectionArgs, callerIsSyncAdapter);
   3531                 if (count > 0) {
   3532                     mSyncToNetwork |= !callerIsSyncAdapter;
   3533                 }
   3534                 break;
   3535             }
   3536 
   3537             case DATA_ID:
   3538             case PHONES_ID:
   3539             case EMAILS_ID:
   3540             case POSTALS_ID: {
   3541                 count = updateData(uri, values, selection, selectionArgs, callerIsSyncAdapter);
   3542                 if (count > 0) {
   3543                     mSyncToNetwork |= !callerIsSyncAdapter;
   3544                 }
   3545                 break;
   3546             }
   3547 
   3548             case RAW_CONTACTS: {
   3549                 selection = appendAccountToSelection(uri, selection);
   3550                 count = updateRawContacts(values, selection, selectionArgs);
   3551                 break;
   3552             }
   3553 
   3554             case RAW_CONTACTS_ID: {
   3555                 long rawContactId = ContentUris.parseId(uri);
   3556                 if (selection != null) {
   3557                     selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId));
   3558                     count = updateRawContacts(values, RawContacts._ID + "=?"
   3559                                     + " AND(" + selection + ")", selectionArgs);
   3560                 } else {
   3561                     mSelectionArgs1[0] = String.valueOf(rawContactId);
   3562                     count = updateRawContacts(values, RawContacts._ID + "=?", mSelectionArgs1);
   3563                 }
   3564                 break;
   3565             }
   3566 
   3567             case GROUPS: {
   3568                 count = updateGroups(uri, values, appendAccountToSelection(uri, selection),
   3569                         selectionArgs, callerIsSyncAdapter);
   3570                 if (count > 0) {
   3571                     mSyncToNetwork |= !callerIsSyncAdapter;
   3572                 }
   3573                 break;
   3574             }
   3575 
   3576             case GROUPS_ID: {
   3577                 long groupId = ContentUris.parseId(uri);
   3578                 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(groupId));
   3579                 String selectionWithId = Groups._ID + "=? "
   3580                         + (selection == null ? "" : " AND " + selection);
   3581                 count = updateGroups(uri, values, selectionWithId, selectionArgs,
   3582                         callerIsSyncAdapter);
   3583                 if (count > 0) {
   3584                     mSyncToNetwork |= !callerIsSyncAdapter;
   3585                 }
   3586                 break;
   3587             }
   3588 
   3589             case AGGREGATION_EXCEPTIONS: {
   3590                 count = updateAggregationException(mDb, values);
   3591                 break;
   3592             }
   3593 
   3594             case SETTINGS: {
   3595                 count = updateSettings(uri, values, appendAccountToSelection(uri, selection),
   3596                         selectionArgs);
   3597                 mSyncToNetwork |= !callerIsSyncAdapter;
   3598                 break;
   3599             }
   3600 
   3601             case STATUS_UPDATES: {
   3602                 count = updateStatusUpdate(uri, values, selection, selectionArgs);
   3603                 break;
   3604             }
   3605 
   3606             default: {
   3607                 mSyncToNetwork = true;
   3608                 return mLegacyApiSupport.update(uri, values, selection, selectionArgs);
   3609             }
   3610         }
   3611 
   3612         return count;
   3613     }
   3614 
   3615     private int updateStatusUpdate(Uri uri, ContentValues values, String selection,
   3616         String[] selectionArgs) {
   3617         // update status_updates table, if status is provided
   3618         // TODO should account type/name be appended to the where clause?
   3619         int updateCount = 0;
   3620         ContentValues settableValues = getSettableColumnsForStatusUpdatesTable(values);
   3621         if (settableValues.size() > 0) {
   3622           updateCount = mDb.update(Tables.STATUS_UPDATES,
   3623                     settableValues,
   3624                     getWhereClauseForStatusUpdatesTable(selection),
   3625                     selectionArgs);
   3626         }
   3627 
   3628         // now update the Presence table
   3629         settableValues = getSettableColumnsForPresenceTable(values);
   3630         if (settableValues.size() > 0) {
   3631           updateCount = mDb.update(Tables.PRESENCE, settableValues,
   3632                     selection, selectionArgs);
   3633         }
   3634         // TODO updateCount is not entirely a valid count of updated rows because 2 tables could
   3635         // potentially get updated in this method.
   3636         return updateCount;
   3637     }
   3638 
   3639     /**
   3640      * Build a where clause to select the rows to be updated in status_updates table.
   3641      */
   3642     private String getWhereClauseForStatusUpdatesTable(String selection) {
   3643         mSb.setLength(0);
   3644         mSb.append(WHERE_CLAUSE_FOR_STATUS_UPDATES_TABLE);
   3645         mSb.append(selection);
   3646         mSb.append(")");
   3647         return mSb.toString();
   3648     }
   3649 
   3650     private ContentValues getSettableColumnsForStatusUpdatesTable(ContentValues values) {
   3651         mValues.clear();
   3652         ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS, values,
   3653             StatusUpdates.STATUS);
   3654         ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS_TIMESTAMP, values,
   3655             StatusUpdates.STATUS_TIMESTAMP);
   3656         ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS_RES_PACKAGE, values,
   3657             StatusUpdates.STATUS_RES_PACKAGE);
   3658         ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS_LABEL, values,
   3659             StatusUpdates.STATUS_LABEL);
   3660         ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS_ICON, values,
   3661             StatusUpdates.STATUS_ICON);
   3662         return mValues;
   3663     }
   3664 
   3665     private ContentValues getSettableColumnsForPresenceTable(ContentValues values) {
   3666         mValues.clear();
   3667         ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.PRESENCE, values,
   3668             StatusUpdates.PRESENCE);
   3669         ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.CHAT_CAPABILITY, values,
   3670                 StatusUpdates.CHAT_CAPABILITY);
   3671         return mValues;
   3672     }
   3673 
   3674     private int updateGroups(Uri uri, ContentValues values, String selectionWithId,
   3675             String[] selectionArgs, boolean callerIsSyncAdapter) {
   3676 
   3677         mGroupIdCache.clear();
   3678 
   3679         ContentValues updatedValues;
   3680         if (!callerIsSyncAdapter && !values.containsKey(Groups.DIRTY)) {
   3681             updatedValues = mValues;
   3682             updatedValues.clear();
   3683             updatedValues.putAll(values);
   3684             updatedValues.put(Groups.DIRTY, 1);
   3685         } else {
   3686             updatedValues = values;
   3687         }
   3688 
   3689         int count = mDb.update(Tables.GROUPS, updatedValues, selectionWithId, selectionArgs);
   3690         if (updatedValues.containsKey(Groups.GROUP_VISIBLE)) {
   3691             mVisibleTouched = true;
   3692         }
   3693         if (updatedValues.containsKey(Groups.SHOULD_SYNC)
   3694                 && updatedValues.getAsInteger(Groups.SHOULD_SYNC) != 0) {
   3695             Cursor c = mDb.query(Tables.GROUPS, new String[]{Groups.ACCOUNT_NAME,
   3696                     Groups.ACCOUNT_TYPE}, selectionWithId, selectionArgs, null,
   3697                     null, null);
   3698             String accountName;
   3699             String accountType;
   3700             try {
   3701                 while (c.moveToNext()) {
   3702                     accountName = c.getString(0);
   3703                     accountType = c.getString(1);
   3704                     if(!TextUtils.isEmpty(accountName) && !TextUtils.isEmpty(accountType)) {
   3705                         Account account = new Account(accountName, accountType);
   3706                         ContentResolver.requestSync(account, ContactsContract.AUTHORITY,
   3707                                 new Bundle());
   3708                         break;
   3709                     }
   3710                 }
   3711             } finally {
   3712                 c.close();
   3713             }
   3714         }
   3715         return count;
   3716     }
   3717 
   3718     private int updateSettings(Uri uri, ContentValues values, String selection,
   3719             String[] selectionArgs) {
   3720         final int count = mDb.update(Tables.SETTINGS, values, selection, selectionArgs);
   3721         if (values.containsKey(Settings.UNGROUPED_VISIBLE)) {
   3722             mVisibleTouched = true;
   3723         }
   3724         return count;
   3725     }
   3726 
   3727     private int updateRawContacts(ContentValues values, String selection, String[] selectionArgs) {
   3728         if (values.containsKey(RawContacts.CONTACT_ID)) {
   3729             throw new IllegalArgumentException(RawContacts.CONTACT_ID + " should not be included " +
   3730                     "in content values. Contact IDs are assigned automatically");
   3731         }
   3732 
   3733         int count = 0;
   3734         Cursor cursor = mDb.query(mDbHelper.getRawContactView(),
   3735                 new String[] { RawContacts._ID }, selection,
   3736                 selectionArgs, null, null, null);
   3737         try {
   3738             while (cursor.moveToNext()) {
   3739                 long rawContactId = cursor.getLong(0);
   3740                 updateRawContact(rawContactId, values);
   3741                 count++;
   3742             }
   3743         } finally {
   3744             cursor.close();
   3745         }
   3746 
   3747         return count;
   3748     }
   3749 
   3750     private int updateRawContact(long rawContactId, ContentValues values) {
   3751         final String selection = RawContacts._ID + " = ?";
   3752         mSelectionArgs1[0] = Long.toString(rawContactId);
   3753         final boolean requestUndoDelete = (values.containsKey(RawContacts.DELETED)
   3754                 && values.getAsInteger(RawContacts.DELETED) == 0);
   3755         int previousDeleted = 0;
   3756         String accountType = null;
   3757         String accountName = null;
   3758         if (requestUndoDelete) {
   3759             Cursor cursor = mDb.query(RawContactsQuery.TABLE, RawContactsQuery.COLUMNS, selection,
   3760                     mSelectionArgs1, null, null, null);
   3761             try {
   3762                 if (cursor.moveToFirst()) {
   3763                     previousDeleted = cursor.getInt(RawContactsQuery.DELETED);
   3764                     accountType = cursor.getString(RawContactsQuery.ACCOUNT_TYPE);
   3765                     accountName = cursor.getString(RawContactsQuery.ACCOUNT_NAME);
   3766                 }
   3767             } finally {
   3768                 cursor.close();
   3769             }
   3770             values.put(ContactsContract.RawContacts.AGGREGATION_MODE,
   3771                     ContactsContract.RawContacts.AGGREGATION_MODE_DEFAULT);
   3772         }
   3773 
   3774         int count = mDb.update(Tables.RAW_CONTACTS, values, selection, mSelectionArgs1);
   3775         if (count != 0) {
   3776             if (values.containsKey(RawContacts.AGGREGATION_MODE)) {
   3777                 int aggregationMode = values.getAsInteger(RawContacts.AGGREGATION_MODE);
   3778 
   3779                 // As per ContactsContract documentation, changing aggregation mode
   3780                 // to DEFAULT should not trigger aggregation
   3781                 if (aggregationMode != RawContacts.AGGREGATION_MODE_DEFAULT) {
   3782                     mContactAggregator.markForAggregation(rawContactId, aggregationMode, false);
   3783                 }
   3784             }
   3785             if (values.containsKey(RawContacts.STARRED)) {
   3786                 mContactAggregator.updateStarred(rawContactId);
   3787             }
   3788             if (values.containsKey(RawContacts.SOURCE_ID)) {
   3789                 mContactAggregator.updateLookupKeyForRawContact(mDb, rawContactId);
   3790             }
   3791             if (values.containsKey(RawContacts.NAME_VERIFIED)) {
   3792 
   3793                 // If setting NAME_VERIFIED for this raw contact, reset it for all
   3794                 // other raw contacts in the same aggregate
   3795                 if (values.getAsInteger(RawContacts.NAME_VERIFIED) != 0) {
   3796                     mResetNameVerifiedForOtherRawContacts.bindLong(1, rawContactId);
   3797                     mResetNameVerifiedForOtherRawContacts.bindLong(2, rawContactId);
   3798                     mResetNameVerifiedForOtherRawContacts.execute();
   3799                 }
   3800                 mContactAggregator.updateDisplayNameForRawContact(mDb, rawContactId);
   3801             }
   3802             if (requestUndoDelete && previousDeleted == 1) {
   3803                 // undo delete, needs aggregation again.
   3804                 mInsertedRawContacts.put(rawContactId, new Account(accountName, accountType));
   3805             }
   3806         }
   3807         return count;
   3808     }
   3809 
   3810     private int updateData(Uri uri, ContentValues values, String selection,
   3811             String[] selectionArgs, boolean callerIsSyncAdapter) {
   3812         mValues.clear();
   3813         mValues.putAll(values);
   3814         mValues.remove(Data._ID);
   3815         mValues.remove(Data.RAW_CONTACT_ID);
   3816         mValues.remove(Data.MIMETYPE);
   3817 
   3818         String packageName = values.getAsString(Data.RES_PACKAGE);
   3819         if (packageName != null) {
   3820             mValues.remove(Data.RES_PACKAGE);
   3821             mValues.put(DataColumns.PACKAGE_ID, mDbHelper.getPackageId(packageName));
   3822         }
   3823 
   3824         boolean containsIsSuperPrimary = mValues.containsKey(Data.IS_SUPER_PRIMARY);
   3825         boolean containsIsPrimary = mValues.containsKey(Data.IS_PRIMARY);
   3826 
   3827         // Remove primary or super primary values being set to 0. This is disallowed by the
   3828         // content provider.
   3829         if (containsIsSuperPrimary && mValues.getAsInteger(Data.IS_SUPER_PRIMARY) == 0) {
   3830             containsIsSuperPrimary = false;
   3831             mValues.remove(Data.IS_SUPER_PRIMARY);
   3832         }
   3833         if (containsIsPrimary && mValues.getAsInteger(Data.IS_PRIMARY) == 0) {
   3834             containsIsPrimary = false;
   3835             mValues.remove(Data.IS_PRIMARY);
   3836         }
   3837 
   3838         int count = 0;
   3839 
   3840         // Note that the query will return data according to the access restrictions,
   3841         // so we don't need to worry about updating data we don't have permission to read.
   3842         Cursor c = query(uri, DataUpdateQuery.COLUMNS, selection, selectionArgs, null);
   3843         try {
   3844             while(c.moveToNext()) {
   3845                 count += updateData(mValues, c, callerIsSyncAdapter);
   3846             }
   3847         } finally {
   3848             c.close();
   3849         }
   3850 
   3851         return count;
   3852     }
   3853 
   3854     private int updateData(ContentValues values, Cursor c, boolean callerIsSyncAdapter) {
   3855         if (values.size() == 0) {
   3856             return 0;
   3857         }
   3858 
   3859         final String mimeType = c.getString(DataUpdateQuery.MIMETYPE);
   3860         DataRowHandler rowHandler = getDataRowHandler(mimeType);
   3861         if (rowHandler.update(mDb, values, c, callerIsSyncAdapter)) {
   3862             return 1;
   3863         } else {
   3864             return 0;
   3865         }
   3866     }
   3867 
   3868     private int updateContactOptions(ContentValues values, String selection,
   3869             String[] selectionArgs) {
   3870         int count = 0;
   3871         Cursor cursor = mDb.query(mDbHelper.getContactView(),
   3872                 new String[] { Contacts._ID }, selection,
   3873                 selectionArgs, null, null, null);
   3874         try {
   3875             while (cursor.moveToNext()) {
   3876                 long contactId = cursor.getLong(0);
   3877                 updateContactOptions(contactId, values);
   3878                 count++;
   3879             }
   3880         } finally {
   3881             cursor.close();
   3882         }
   3883 
   3884         return count;
   3885     }
   3886 
   3887     private int updateContactOptions(long contactId, ContentValues values) {
   3888 
   3889         mValues.clear();
   3890         ContactsDatabaseHelper.copyStringValue(mValues, RawContacts.CUSTOM_RINGTONE,
   3891                 values, Contacts.CUSTOM_RINGTONE);
   3892         ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.SEND_TO_VOICEMAIL,
   3893                 values, Contacts.SEND_TO_VOICEMAIL);
   3894         ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.LAST_TIME_CONTACTED,
   3895                 values, Contacts.LAST_TIME_CONTACTED);
   3896         ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.TIMES_CONTACTED,
   3897                 values, Contacts.TIMES_CONTACTED);
   3898         ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.STARRED,
   3899                 values, Contacts.STARRED);
   3900 
   3901         // Nothing to update - just return
   3902         if (mValues.size() == 0) {
   3903             return 0;
   3904         }
   3905 
   3906         if (mValues.containsKey(RawContacts.STARRED)) {
   3907             // Mark dirty when changing starred to trigger sync
   3908             mValues.put(RawContacts.DIRTY, 1);
   3909         }
   3910 
   3911         mSelectionArgs1[0] = String.valueOf(contactId);
   3912         mDb.update(Tables.RAW_CONTACTS, mValues, RawContacts.CONTACT_ID + "=?", mSelectionArgs1);
   3913 
   3914         // Copy changeable values to prevent automatically managed fields from
   3915         // being explicitly updated by clients.
   3916         mValues.clear();
   3917         ContactsDatabaseHelper.copyStringValue(mValues, RawContacts.CUSTOM_RINGTONE,
   3918                 values, Contacts.CUSTOM_RINGTONE);
   3919         ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.SEND_TO_VOICEMAIL,
   3920                 values, Contacts.SEND_TO_VOICEMAIL);
   3921         ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.LAST_TIME_CONTACTED,
   3922                 values, Contacts.LAST_TIME_CONTACTED);
   3923         ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.TIMES_CONTACTED,
   3924                 values, Contacts.TIMES_CONTACTED);
   3925         ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.STARRED,
   3926                 values, Contacts.STARRED);
   3927 
   3928         int rslt = mDb.update(Tables.CONTACTS, mValues, Contacts._ID + "=?", mSelectionArgs1);
   3929 
   3930         if (values.containsKey(Contacts.LAST_TIME_CONTACTED) &&
   3931                 !values.containsKey(Contacts.TIMES_CONTACTED)) {
   3932             mDb.execSQL(UPDATE_TIMES_CONTACTED_CONTACTS_TABLE, mSelectionArgs1);
   3933             mDb.execSQL(UPDATE_TIMES_CONTACTED_RAWCONTACTS_TABLE, mSelectionArgs1);
   3934         }
   3935         return rslt;
   3936     }
   3937 
   3938     private int updateAggregationException(SQLiteDatabase db, ContentValues values) {
   3939         int exceptionType = values.getAsInteger(AggregationExceptions.TYPE);
   3940         long rcId1 = values.getAsInteger(AggregationExceptions.RAW_CONTACT_ID1);
   3941         long rcId2 = values.getAsInteger(AggregationExceptions.RAW_CONTACT_ID2);
   3942 
   3943         long rawContactId1, rawContactId2;
   3944         if (rcId1 < rcId2) {
   3945             rawContactId1 = rcId1;
   3946             rawContactId2 = rcId2;
   3947         } else {
   3948             rawContactId2 = rcId1;
   3949             rawContactId1 = rcId2;
   3950         }
   3951 
   3952         if (exceptionType == AggregationExceptions.TYPE_AUTOMATIC) {
   3953             mSelectionArgs2[0] = String.valueOf(rawContactId1);
   3954             mSelectionArgs2[1] = String.valueOf(rawContactId2);
   3955             db.delete(Tables.AGGREGATION_EXCEPTIONS,
   3956                     AggregationExceptions.RAW_CONTACT_ID1 + "=? AND "
   3957                     + AggregationExceptions.RAW_CONTACT_ID2 + "=?", mSelectionArgs2);
   3958         } else {
   3959             ContentValues exceptionValues = new ContentValues(3);
   3960             exceptionValues.put(AggregationExceptions.TYPE, exceptionType);
   3961             exceptionValues.put(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
   3962             exceptionValues.put(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
   3963             db.replace(Tables.AGGREGATION_EXCEPTIONS, AggregationExceptions._ID,
   3964                     exceptionValues);
   3965         }
   3966 
   3967         mContactAggregator.invalidateAggregationExceptionCache();
   3968         mContactAggregator.markForAggregation(rawContactId1,
   3969                 RawContacts.AGGREGATION_MODE_DEFAULT, true);
   3970         mContactAggregator.markForAggregation(rawContactId2,
   3971                 RawContacts.AGGREGATION_MODE_DEFAULT, true);
   3972 
   3973         mContactAggregator.aggregateContact(db, rawContactId1);
   3974         mContactAggregator.aggregateContact(db, rawContactId2);
   3975 
   3976         // The return value is fake - we just confirm that we made a change, not count actual
   3977         // rows changed.
   3978         return 1;
   3979     }
   3980 
   3981     /**
   3982      * Check whether GOOGLE_MY_CONTACTS_GROUP exists, otherwise create it.
   3983      *
   3984      * @return the group id
   3985      */
   3986     private long getOrCreateMyContactsGroupInTransaction(String accountName, String accountType) {
   3987         Cursor cursor = mDb.query(Tables.GROUPS, new String[] {"_id"},
   3988                 Groups.ACCOUNT_NAME + " =? AND " + Groups.ACCOUNT_TYPE + " =? AND "
   3989                     + Groups.TITLE + " =?",
   3990                 new String[] {accountName, accountType, GOOGLE_MY_CONTACTS_GROUP_TITLE},
   3991                 null, null, null);
   3992         try {
   3993             if(cursor.moveToNext()) {
   3994                 return cursor.getLong(0);
   3995             }
   3996         } finally {
   3997             cursor.close();
   3998         }
   3999 
   4000         ContentValues values = new ContentValues();
   4001         values.put(Groups.TITLE, GOOGLE_MY_CONTACTS_GROUP_TITLE);
   4002         values.put(Groups.ACCOUNT_NAME, accountName);
   4003         values.put(Groups.ACCOUNT_TYPE, accountType);
   4004         values.put(Groups.GROUP_VISIBLE, "1");
   4005         return mDb.insert(Tables.GROUPS, null, values);
   4006     }
   4007 
   4008     public void onAccountsUpdated(Account[] accounts) {
   4009         // TODO : Check the unit test.
   4010         HashSet<Account> existingAccounts = new HashSet<Account>();
   4011         boolean hasUnassignedContacts[] = new boolean[]{false};
   4012         mDb.beginTransaction();
   4013         try {
   4014             findValidAccounts(existingAccounts, hasUnassignedContacts);
   4015 
   4016             // Add a row to the ACCOUNTS table for each new account
   4017             for (Account account : accounts) {
   4018                 if (!existingAccounts.contains(account)) {
   4019                     mDb.execSQL("INSERT INTO " + Tables.ACCOUNTS + " (" + RawContacts.ACCOUNT_NAME
   4020                             + ", " + RawContacts.ACCOUNT_TYPE + ") VALUES (?, ?)",
   4021                             new String[] {account.name, account.type});
   4022                 }
   4023             }
   4024 
   4025             // Remove all valid accounts from the existing account set. What is left
   4026             // in the accountsToDelete set will be extra accounts whose data must be deleted.
   4027             HashSet<Account> accountsToDelete = new HashSet<Account>(existingAccounts);
   4028             for (Account account : accounts) {
   4029                 accountsToDelete.remove(account);
   4030             }
   4031 
   4032             for (Account account : accountsToDelete) {
   4033                 Log.d(TAG, "removing data for removed account " + account);
   4034                 String[] params = new String[] {account.name, account.type};
   4035                 mDb.execSQL(
   4036                         "DELETE FROM " + Tables.GROUPS +
   4037                         " WHERE " + Groups.ACCOUNT_NAME + " = ?" +
   4038                                 " AND " + Groups.ACCOUNT_TYPE + " = ?", params);
   4039                 mDb.execSQL(
   4040                         "DELETE FROM " + Tables.PRESENCE +
   4041                         " WHERE " + PresenceColumns.RAW_CONTACT_ID + " IN (" +
   4042                                 "SELECT " + RawContacts._ID +
   4043                                 " FROM " + Tables.RAW_CONTACTS +
   4044                                 " WHERE " + RawContacts.ACCOUNT_NAME + " = ?" +
   4045                                 " AND " + RawContacts.ACCOUNT_TYPE + " = ?)", params);
   4046                 mDb.execSQL(
   4047                         "DELETE FROM " + Tables.RAW_CONTACTS +
   4048                         " WHERE " + RawContacts.ACCOUNT_NAME + " = ?" +
   4049                         " AND " + RawContacts.ACCOUNT_TYPE + " = ?", params);
   4050                 mDb.execSQL(
   4051                         "DELETE FROM " + Tables.SETTINGS +
   4052                         " WHERE " + Settings.ACCOUNT_NAME + " = ?" +
   4053                         " AND " + Settings.ACCOUNT_TYPE + " = ?", params);
   4054                 mDb.execSQL(
   4055                         "DELETE FROM " + Tables.ACCOUNTS +
   4056                         " WHERE " + RawContacts.ACCOUNT_NAME + "=?" +
   4057                         " AND " + RawContacts.ACCOUNT_TYPE + "=?", params);
   4058             }
   4059 
   4060             if (!accountsToDelete.isEmpty()) {
   4061                 // Find all aggregated contacts that used to contain the raw contacts
   4062                 // we have just deleted and see if they are still referencing the deleted
   4063                 // names of photos.  If so, fix up those contacts.
   4064                 HashSet<Long> orphanContactIds = Sets.newHashSet();
   4065                 Cursor cursor = mDb.rawQuery("SELECT " + Contacts._ID +
   4066                         " FROM " + Tables.CONTACTS +
   4067                         " WHERE (" + Contacts.NAME_RAW_CONTACT_ID + " NOT NULL AND " +
   4068                                 Contacts.NAME_RAW_CONTACT_ID + " NOT IN " +
   4069                                         "(SELECT " + RawContacts._ID +
   4070                                         " FROM " + Tables.RAW_CONTACTS + "))" +
   4071                         " OR (" + Contacts.PHOTO_ID + " NOT NULL AND " +
   4072                                 Contacts.PHOTO_ID + " NOT IN " +
   4073                                         "(SELECT " + Data._ID +
   4074                                         " FROM " + Tables.DATA + "))", null);
   4075                 try {
   4076                     while (cursor.moveToNext()) {
   4077                         orphanContactIds.add(cursor.getLong(0));
   4078                     }
   4079                 } finally {
   4080                     cursor.close();
   4081                 }
   4082 
   4083                 for (Long contactId : orphanContactIds) {
   4084                     mContactAggregator.updateAggregateData(contactId);
   4085                 }
   4086             }
   4087 
   4088             if (hasUnassignedContacts[0]) {
   4089 
   4090                 Account primaryAccount = null;
   4091                 for (Account account : accounts) {
   4092                     if (isWritableAccount(account.type)) {
   4093                         primaryAccount = account;
   4094                         break;
   4095                     }
   4096                 }
   4097 
   4098                 if (primaryAccount != null) {
   4099                     String[] params = new String[] {primaryAccount.name, primaryAccount.type};
   4100                     if (primaryAccount.type.equals(DEFAULT_ACCOUNT_TYPE)) {
   4101                         long groupId = getOrCreateMyContactsGroupInTransaction(
   4102                                 primaryAccount.name, primaryAccount.type);
   4103                         if (groupId != -1) {
   4104                             long mimeTypeId = mDbHelper.getMimeTypeId(
   4105                                     GroupMembership.CONTENT_ITEM_TYPE);
   4106                             mDb.execSQL(
   4107                                     "INSERT INTO " + Tables.DATA + "(" + DataColumns.MIMETYPE_ID +
   4108                                         ", " + Data.RAW_CONTACT_ID + ", "
   4109                                         + GroupMembership.GROUP_ROW_ID + ") " +
   4110                                     "SELECT " + mimeTypeId + ", "
   4111                                             + RawContacts._ID + ", " + groupId +
   4112                                     " FROM " + Tables.RAW_CONTACTS +
   4113                                     " WHERE " + RawContacts.ACCOUNT_NAME + " IS NULL" +
   4114                                     " AND " + RawContacts.ACCOUNT_TYPE + " IS NULL"
   4115                             );
   4116                         }
   4117                     }
   4118                     mDb.execSQL(
   4119                             "UPDATE " + Tables.RAW_CONTACTS +
   4120                             " SET " + RawContacts.ACCOUNT_NAME + "=?,"
   4121                                     + RawContacts.ACCOUNT_TYPE + "=?" +
   4122                             " WHERE " + RawContacts.ACCOUNT_NAME + " IS NULL" +
   4123                             " AND " + RawContacts.ACCOUNT_TYPE + " IS NULL", params);
   4124 
   4125                     // We don't currently support groups for unsynced accounts, so this is for
   4126                     // the future
   4127                     mDb.execSQL(
   4128                             "UPDATE " + Tables.GROUPS +
   4129                             " SET " + Groups.ACCOUNT_NAME + "=?,"
   4130                                     + Groups.ACCOUNT_TYPE + "=?" +
   4131                             " WHERE " + Groups.ACCOUNT_NAME + " IS NULL" +
   4132                             " AND " + Groups.ACCOUNT_TYPE + " IS NULL", params);
   4133 
   4134                     mDb.execSQL(
   4135                             "DELETE FROM " + Tables.ACCOUNTS +
   4136                             " WHERE " + RawContacts.ACCOUNT_NAME + " IS NULL" +
   4137                             " AND " + RawContacts.ACCOUNT_TYPE + " IS NULL");
   4138                 }
   4139             }
   4140 
   4141             mDbHelper.updateAllVisible();
   4142 
   4143             mDbHelper.getSyncState().onAccountsChanged(mDb, accounts);
   4144             mDb.setTransactionSuccessful();
   4145         } finally {
   4146             mDb.endTransaction();
   4147         }
   4148         mAccountWritability.clear();
   4149     }
   4150 
   4151     /**
   4152      * Finds all distinct accounts present in the specified table.
   4153      */
   4154     private void findValidAccounts(Set<Account> validAccounts, boolean[] hasUnassignedContacts) {
   4155         Cursor c = mDb.rawQuery(
   4156                 "SELECT " + RawContacts.ACCOUNT_NAME + "," + RawContacts.ACCOUNT_TYPE +
   4157                 " FROM " + Tables.ACCOUNTS, null);
   4158         try {
   4159             while (c.moveToNext()) {
   4160                 if (c.isNull(0) && c.isNull(1)) {
   4161                     hasUnassignedContacts[0] = true;
   4162                 } else {
   4163                     validAccounts.add(new Account(c.getString(0), c.getString(1)));
   4164                 }
   4165             }
   4166         } finally {
   4167             c.close();
   4168         }
   4169     }
   4170 
   4171     /**
   4172      * Test all against {@link TextUtils#isEmpty(CharSequence)}.
   4173      */
   4174     private static boolean areAllEmpty(ContentValues values, String[] keys) {
   4175         for (String key : keys) {
   4176             if (!TextUtils.isEmpty(values.getAsString(key))) {
   4177                 return false;
   4178             }
   4179         }
   4180         return true;
   4181     }
   4182 
   4183     /**
   4184      * Returns true if a value (possibly null) is specified for at least one of the supplied keys.
   4185      */
   4186     private static boolean areAnySpecified(ContentValues values, String[] keys) {
   4187         for (String key : keys) {
   4188             if (values.containsKey(key)) {
   4189                 return true;
   4190             }
   4191         }
   4192         return false;
   4193     }
   4194 
   4195     @Override
   4196     public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
   4197             String sortOrder) {
   4198         if (VERBOSE_LOGGING) {
   4199             Log.v(TAG, "query: " + uri);
   4200         }
   4201 
   4202         final SQLiteDatabase db = mDbHelper.getReadableDatabase();
   4203 
   4204         SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
   4205         String groupBy = null;
   4206         String limit = getLimit(uri);
   4207 
   4208         // TODO: Consider writing a test case for RestrictionExceptions when you
   4209         // write a new query() block to make sure it protects restricted data.
   4210         final int match = sUriMatcher.match(uri);
   4211         switch (match) {
   4212             case SYNCSTATE:
   4213                 return mDbHelper.getSyncState().query(db, projection, selection,  selectionArgs,
   4214                         sortOrder);
   4215 
   4216             case CONTACTS: {
   4217                 setTablesAndProjectionMapForContacts(qb, uri, projection);
   4218                 break;
   4219             }
   4220 
   4221             case CONTACTS_ID: {
   4222                 long contactId = ContentUris.parseId(uri);
   4223                 setTablesAndProjectionMapForContacts(qb, uri, projection);
   4224                 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(contactId));
   4225                 qb.appendWhere(Contacts._ID + "=?");
   4226                 break;
   4227             }
   4228 
   4229             case CONTACTS_LOOKUP:
   4230             case CONTACTS_LOOKUP_ID: {
   4231                 List<String> pathSegments = uri.getPathSegments();
   4232                 int segmentCount = pathSegments.size();
   4233                 if (segmentCount < 3) {
   4234                     throw new IllegalArgumentException(mDbHelper.exceptionMessage(
   4235                             "Missing a lookup key", uri));
   4236                 }
   4237                 String lookupKey = pathSegments.get(2);
   4238                 if (segmentCount == 4) {
   4239                     // TODO: pull this out into a method and generalize to not require contactId
   4240                     long contactId = Long.parseLong(pathSegments.get(3));
   4241                     SQLiteQueryBuilder lookupQb = new SQLiteQueryBuilder();
   4242                     setTablesAndProjectionMapForContacts(lookupQb, uri, projection);
   4243                     String[] args;
   4244                     if (selectionArgs == null) {
   4245                         args = new String[2];
   4246                     } else {
   4247                         args = new String[selectionArgs.length + 2];
   4248                         System.arraycopy(selectionArgs, 0, args, 2, selectionArgs.length);
   4249                     }
   4250                     args[0] = String.valueOf(contactId);
   4251                     args[1] = Uri.encode(lookupKey);
   4252                     lookupQb.appendWhere(Contacts._ID + "=? AND " + Contacts.LOOKUP_KEY + "=?");
   4253                     Cursor c = query(db, lookupQb, projection, selection, args, sortOrder,
   4254                             groupBy, limit);
   4255                     if (c.getCount() != 0) {
   4256                         return c;
   4257                     }
   4258 
   4259                     c.close();
   4260                 }
   4261 
   4262                 setTablesAndProjectionMapForContacts(qb, uri, projection);
   4263                 selectionArgs = insertSelectionArg(selectionArgs,
   4264                         String.valueOf(lookupContactIdByLookupKey(db, lookupKey)));
   4265                 qb.appendWhere(Contacts._ID + "=?");
   4266                 break;
   4267             }
   4268 
   4269             case CONTACTS_AS_VCARD: {
   4270                 // When reading as vCard always use restricted view
   4271                 final String lookupKey = Uri.encode(uri.getPathSegments().get(2));
   4272                 qb.setTables(mDbHelper.getContactView(true /* require restricted */));
   4273                 qb.setProjectionMap(sContactsVCardProjectionMap);
   4274                 selectionArgs = insertSelectionArg(selectionArgs,
   4275                         String.valueOf(lookupContactIdByLookupKey(db, lookupKey)));
   4276                 qb.appendWhere(Contacts._ID + "=?");
   4277                 break;
   4278             }
   4279 
   4280             case CONTACTS_AS_MULTI_VCARD: {
   4281                 SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd_HHmmss");
   4282                 String currentDateString = dateFormat.format(new Date()).toString();
   4283                 return db.rawQuery(
   4284                     "SELECT" +
   4285                     " 'vcards_' || ? || '.vcf' AS " + OpenableColumns.DISPLAY_NAME + "," +
   4286                     " NULL AS " + OpenableColumns.SIZE,
   4287                     new String[] { currentDateString });
   4288             }
   4289 
   4290             case CONTACTS_FILTER: {
   4291                 String filterParam = "";
   4292                 if (uri.getPathSegments().size() > 2) {
   4293                     filterParam = uri.getLastPathSegment();
   4294                 }
   4295                 setTablesAndProjectionMapForContactsWithSnippet(qb, uri, projection, filterParam);
   4296                 break;
   4297             }
   4298 
   4299             case CONTACTS_STREQUENT_FILTER:
   4300             case CONTACTS_STREQUENT: {
   4301                 String filterSql = null;
   4302                 if (match == CONTACTS_STREQUENT_FILTER
   4303                         && uri.getPathSegments().size() > 3) {
   4304                     String filterParam = uri.getLastPathSegment();
   4305                     StringBuilder sb = new StringBuilder();
   4306                     sb.append(Contacts._ID + " IN ");
   4307                     appendContactFilterAsNestedQuery(sb, filterParam);
   4308                     filterSql = sb.toString();
   4309                 }
   4310 
   4311                 setTablesAndProjectionMapForContacts(qb, uri, projection);
   4312 
   4313                 String[] starredProjection = null;
   4314                 String[] frequentProjection = null;
   4315                 if (projection != null) {
   4316                     starredProjection = appendProjectionArg(projection, TIMES_CONTACED_SORT_COLUMN);
   4317                     frequentProjection = appendProjectionArg(projection, TIMES_CONTACED_SORT_COLUMN);
   4318                 }
   4319 
   4320                 // Build the first query for starred
   4321                 if (filterSql != null) {
   4322                     qb.appendWhere(filterSql);
   4323                 }
   4324                 qb.setProjectionMap(sStrequentStarredProjectionMap);
   4325                 final String starredQuery = qb.buildQuery(starredProjection, Contacts.STARRED + "=1",
   4326                         null, Contacts._ID, null, null, null);
   4327 
   4328                 // Build the second query for frequent
   4329                 qb = new SQLiteQueryBuilder();
   4330                 setTablesAndProjectionMapForContacts(qb, uri, projection);
   4331                 if (filterSql != null) {
   4332                     qb.appendWhere(filterSql);
   4333                 }
   4334                 qb.setProjectionMap(sStrequentFrequentProjectionMap);
   4335                 final String frequentQuery = qb.buildQuery(frequentProjection,
   4336                         Contacts.TIMES_CONTACTED + " > 0 AND (" + Contacts.STARRED
   4337                         + " = 0 OR " + Contacts.STARRED + " IS NULL)",
   4338                         null, Contacts._ID, null, null, null);
   4339 
   4340                 // Put them together
   4341                 final String query = qb.buildUnionQuery(new String[] {starredQuery, frequentQuery},
   4342                         STREQUENT_ORDER_BY, STREQUENT_LIMIT);
   4343                 Cursor c = db.rawQuery(query, null);
   4344                 if (c != null) {
   4345                     c.setNotificationUri(getContext().getContentResolver(),
   4346                             ContactsContract.AUTHORITY_URI);
   4347                 }
   4348                 return c;
   4349             }
   4350 
   4351             case CONTACTS_GROUP: {
   4352                 setTablesAndProjectionMapForContacts(qb, uri, projection);
   4353                 if (uri.getPathSegments().size() > 2) {
   4354                     qb.appendWhere(CONTACTS_IN_GROUP_SELECT);
   4355                     selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
   4356                 }
   4357                 break;
   4358             }
   4359 
   4360             case CONTACTS_DATA: {
   4361                 long contactId = Long.parseLong(uri.getPathSegments().get(1));
   4362                 setTablesAndProjectionMapForData(qb, uri, projection, false);
   4363                 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(contactId));
   4364                 qb.appendWhere(" AND " + RawContacts.CONTACT_ID + "=?");
   4365                 break;
   4366             }
   4367 
   4368             case CONTACTS_PHOTO: {
   4369                 long contactId = Long.parseLong(uri.getPathSegments().get(1));
   4370                 setTablesAndProjectionMapForData(qb, uri, projection, false);
   4371                 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(contactId));
   4372                 qb.appendWhere(" AND " + RawContacts.CONTACT_ID + "=?");
   4373                 qb.appendWhere(" AND " + Data._ID + "=" + Contacts.PHOTO_ID);
   4374                 break;
   4375             }
   4376 
   4377             case PHONES: {
   4378                 setTablesAndProjectionMapForData(qb, uri, projection, false);
   4379                 qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Phone.CONTENT_ITEM_TYPE + "'");
   4380                 break;
   4381             }
   4382 
   4383             case PHONES_ID: {
   4384                 setTablesAndProjectionMapForData(qb, uri, projection, false);
   4385                 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
   4386                 qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Phone.CONTENT_ITEM_TYPE + "'");
   4387                 qb.appendWhere(" AND " + Data._ID + "=?");
   4388                 break;
   4389             }
   4390 
   4391             case PHONES_FILTER: {
   4392                 setTablesAndProjectionMapForData(qb, uri, projection, true);
   4393                 qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Phone.CONTENT_ITEM_TYPE + "'");
   4394                 if (uri.getPathSegments().size() > 2) {
   4395                     String filterParam = uri.getLastPathSegment();
   4396                     StringBuilder sb = new StringBuilder();
   4397                     sb.append(" AND (");
   4398 
   4399                     boolean hasCondition = false;
   4400                     boolean orNeeded = false;
   4401                     String normalizedName = NameNormalizer.normalize(filterParam);
   4402                     if (normalizedName.length() > 0) {
   4403                         sb.append(Data.RAW_CONTACT_ID + " IN ");
   4404                         appendRawContactsByNormalizedNameFilter(sb, normalizedName, false);
   4405                         orNeeded = true;
   4406                         hasCondition = true;
   4407                     }
   4408 
   4409                     if (isPhoneNumber(filterParam)) {
   4410                         if (orNeeded) {
   4411                             sb.append(" OR ");
   4412                         }
   4413                         String number = PhoneNumberUtils.convertKeypadLettersToDigits(filterParam);
   4414                         String reversed = PhoneNumberUtils.getStrippedReversed(number);
   4415                         sb.append(Data._ID +
   4416                                 " IN (SELECT " + PhoneLookupColumns.DATA_ID
   4417                                   + " FROM " + Tables.PHONE_LOOKUP
   4418                                   + " WHERE " + PhoneLookupColumns.NORMALIZED_NUMBER + " LIKE '%");
   4419                         sb.append(reversed);
   4420                         sb.append("')");
   4421                         hasCondition = true;
   4422                     }
   4423 
   4424                     if (!hasCondition) {
   4425                         // If it is neither a phone number nor a name, the query should return
   4426                         // an empty cursor.  Let's ensure that.
   4427                         sb.append("0");
   4428                     }
   4429                     sb.append(")");
   4430                     qb.appendWhere(sb);
   4431                 }
   4432                 groupBy = PhoneColumns.NORMALIZED_NUMBER + "," + RawContacts.CONTACT_ID;
   4433                 if (sortOrder == null) {
   4434                     sortOrder = Contacts.IN_VISIBLE_GROUP + " DESC, " + RawContacts.CONTACT_ID;
   4435                 }
   4436                 break;
   4437             }
   4438 
   4439             case EMAILS: {
   4440                 setTablesAndProjectionMapForData(qb, uri, projection, false);
   4441                 qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Email.CONTENT_ITEM_TYPE + "'");
   4442                 break;
   4443             }
   4444 
   4445             case EMAILS_ID: {
   4446                 setTablesAndProjectionMapForData(qb, uri, projection, false);
   4447                 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
   4448                 qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Email.CONTENT_ITEM_TYPE + "'"
   4449                         + " AND " + Data._ID + "=?");
   4450                 break;
   4451             }
   4452 
   4453             case EMAILS_LOOKUP: {
   4454                 setTablesAndProjectionMapForData(qb, uri, projection, false);
   4455                 qb.appendWhere(" AND " + Data.MIMETYPE + " = '" + Email.CONTENT_ITEM_TYPE + "'");
   4456                 if (uri.getPathSegments().size() > 2) {
   4457                     String email = uri.getLastPathSegment();
   4458                     String address = mDbHelper.extractAddressFromEmailAddress(email);
   4459                     selectionArgs = insertSelectionArg(selectionArgs, address);
   4460                     qb.appendWhere(" AND UPPER(" + Email.DATA + ")=UPPER(?)");
   4461                 }
   4462                 break;
   4463             }
   4464 
   4465             case EMAILS_FILTER: {
   4466                 setTablesAndProjectionMapForData(qb, uri, projection, true);
   4467                 String filterParam = null;
   4468                 if (uri.getPathSegments().size() > 3) {
   4469                     filterParam = uri.getLastPathSegment();
   4470                     if (TextUtils.isEmpty(filterParam)) {
   4471                         filterParam = null;
   4472                     }
   4473                 }
   4474 
   4475                 if (filterParam == null) {
   4476                     // If the filter is unspecified, return nothing
   4477                     qb.appendWhere(" AND 0");
   4478                 } else {
   4479                     StringBuilder sb = new StringBuilder();
   4480                     sb.append(" AND " + Data._ID + " IN (");
   4481                     sb.append(
   4482                             "SELECT " + Data._ID +
   4483                             " FROM " + Tables.DATA +
   4484                             " WHERE " + DataColumns.MIMETYPE_ID + "=" + mMimeTypeIdEmail +
   4485                             " AND " + Data.DATA1 + " LIKE ");
   4486                     DatabaseUtils.appendEscapedSQLString(sb, filterParam + '%');
   4487                     if (!filterParam.contains("@")) {
   4488                         String normalizedName = NameNormalizer.normalize(filterParam);
   4489                         if (normalizedName.length() > 0) {
   4490 
   4491                             /*
   4492                              * Using a UNION instead of an "OR" to make SQLite use the right
   4493                              * indexes. We need it to use the (mimetype,data1) index for the
   4494                              * email lookup (see above), but not for the name lookup.
   4495                              * SQLite is not smart enough to use the index on one side of an OR
   4496                              * but not on the other. Using two separate nested queries
   4497                              * and a UNION between them does the job.
   4498                              */
   4499                             sb.append(
   4500                                     " UNION SELECT " + Data._ID +
   4501                                     " FROM " + Tables.DATA +
   4502                                     " WHERE +" + DataColumns.MIMETYPE_ID + "=" + mMimeTypeIdEmail +
   4503                                     " AND " + Data.RAW_CONTACT_ID + " IN ");
   4504                             appendRawContactsByNormalizedNameFilter(sb, normalizedName, false);
   4505                         }
   4506                     }
   4507                     sb.append(")");
   4508                     qb.appendWhere(sb);
   4509                 }
   4510                 groupBy = Email.DATA + "," + RawContacts.CONTACT_ID;
   4511                 if (sortOrder == null) {
   4512                     sortOrder = Contacts.IN_VISIBLE_GROUP + " DESC, " + RawContacts.CONTACT_ID;
   4513                 }
   4514                 break;
   4515             }
   4516 
   4517             case POSTALS: {
   4518                 setTablesAndProjectionMapForData(qb, uri, projection, false);
   4519                 qb.appendWhere(" AND " + Data.MIMETYPE + " = '"
   4520                         + StructuredPostal.CONTENT_ITEM_TYPE + "'");
   4521                 break;
   4522             }
   4523 
   4524             case POSTALS_ID: {
   4525                 setTablesAndProjectionMapForData(qb, uri, projection, false);
   4526                 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
   4527                 qb.appendWhere(" AND " + Data.MIMETYPE + " = '"
   4528                         + StructuredPostal.CONTENT_ITEM_TYPE + "'");
   4529                 qb.appendWhere(" AND " + Data._ID + "=?");
   4530                 break;
   4531             }
   4532 
   4533             case RAW_CONTACTS: {
   4534                 setTablesAndProjectionMapForRawContacts(qb, uri);
   4535                 break;
   4536             }
   4537 
   4538             case RAW_CONTACTS_ID: {
   4539                 long rawContactId = ContentUris.parseId(uri);
   4540                 setTablesAndProjectionMapForRawContacts(qb, uri);
   4541                 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId));
   4542                 qb.appendWhere(" AND " + RawContacts._ID + "=?");
   4543                 break;
   4544             }
   4545 
   4546             case RAW_CONTACTS_DATA: {
   4547                 long rawContactId = Long.parseLong(uri.getPathSegments().get(1));
   4548                 setTablesAndProjectionMapForData(qb, uri, projection, false);
   4549                 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId));
   4550                 qb.appendWhere(" AND " + Data.RAW_CONTACT_ID + "=?");
   4551                 break;
   4552             }
   4553 
   4554             case DATA: {
   4555                 setTablesAndProjectionMapForData(qb, uri, projection, false);
   4556                 break;
   4557             }
   4558 
   4559             case DATA_ID: {
   4560                 setTablesAndProjectionMapForData(qb, uri, projection, false);
   4561                 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
   4562                 qb.appendWhere(" AND " + Data._ID + "=?");
   4563                 break;
   4564             }
   4565 
   4566             case PHONE_LOOKUP: {
   4567 
   4568                 if (TextUtils.isEmpty(sortOrder)) {
   4569                     // Default the sort order to something reasonable so we get consistent
   4570                     // results when callers don't request an ordering
   4571                     sortOrder = RawContactsColumns.CONCRETE_ID;
   4572                 }
   4573 
   4574                 String number = uri.getPathSegments().size() > 1 ? uri.getLastPathSegment() : "";
   4575                 mDbHelper.buildPhoneLookupAndContactQuery(qb, number);
   4576                 qb.setProjectionMap(sPhoneLookupProjectionMap);
   4577 
   4578                 // Phone lookup cannot be combined with a selection
   4579                 selection = null;
   4580                 selectionArgs = null;
   4581                 break;
   4582             }
   4583 
   4584             case GROUPS: {
   4585                 qb.setTables(mDbHelper.getGroupView());
   4586                 qb.setProjectionMap(sGroupsProjectionMap);
   4587                 appendAccountFromParameter(qb, uri);
   4588                 break;
   4589             }
   4590 
   4591             case GROUPS_ID: {
   4592                 qb.setTables(mDbHelper.getGroupView());
   4593                 qb.setProjectionMap(sGroupsProjectionMap);
   4594                 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
   4595                 qb.appendWhere(Groups._ID + "=?");
   4596                 break;
   4597             }
   4598 
   4599             case GROUPS_SUMMARY: {
   4600                 qb.setTables(mDbHelper.getGroupView() + " AS groups");
   4601                 qb.setProjectionMap(sGroupsSummaryProjectionMap);
   4602                 appendAccountFromParameter(qb, uri);
   4603                 groupBy = Groups._ID;
   4604                 break;
   4605             }
   4606 
   4607             case AGGREGATION_EXCEPTIONS: {
   4608                 qb.setTables(Tables.AGGREGATION_EXCEPTIONS);
   4609                 qb.setProjectionMap(sAggregationExceptionsProjectionMap);
   4610                 break;
   4611             }
   4612 
   4613             case AGGREGATION_SUGGESTIONS: {
   4614                 long contactId = Long.parseLong(uri.getPathSegments().get(1));
   4615                 String filter = null;
   4616                 if (uri.getPathSegments().size() > 3) {
   4617                     filter = uri.getPathSegments().get(3);
   4618                 }
   4619                 final int maxSuggestions;
   4620                 if (limit != null) {
   4621                     maxSuggestions = Integer.parseInt(limit);
   4622                 } else {
   4623                     maxSuggestions = DEFAULT_MAX_SUGGESTIONS;
   4624                 }
   4625 
   4626                 setTablesAndProjectionMapForContacts(qb, uri, projection);
   4627 
   4628                 return mContactAggregator.queryAggregationSuggestions(qb, projection, contactId,
   4629                         maxSuggestions, filter);
   4630             }
   4631 
   4632             case SETTINGS: {
   4633                 qb.setTables(Tables.SETTINGS);
   4634                 qb.setProjectionMap(sSettingsProjectionMap);
   4635                 appendAccountFromParameter(qb, uri);
   4636 
   4637                 // When requesting specific columns, this query requires
   4638                 // late-binding of the GroupMembership MIME-type.
   4639                 final String groupMembershipMimetypeId = Long.toString(mDbHelper
   4640                         .getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE));
   4641                 if (projection != null && projection.length != 0 &&
   4642                         mDbHelper.isInProjection(projection, Settings.UNGROUPED_COUNT)) {
   4643                     selectionArgs = insertSelectionArg(selectionArgs, groupMembershipMimetypeId);
   4644                 }
   4645                 if (projection != null && projection.length != 0 &&
   4646                         mDbHelper.isInProjection(projection, Settings.UNGROUPED_WITH_PHONES)) {
   4647                     selectionArgs = insertSelectionArg(selectionArgs, groupMembershipMimetypeId);
   4648                 }
   4649 
   4650                 break;
   4651             }
   4652 
   4653             case STATUS_UPDATES: {
   4654                 setTableAndProjectionMapForStatusUpdates(qb, projection);
   4655                 break;
   4656             }
   4657 
   4658             case STATUS_UPDATES_ID: {
   4659                 setTableAndProjectionMapForStatusUpdates(qb, projection);
   4660                 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
   4661                 qb.appendWhere(DataColumns.CONCRETE_ID + "=?");
   4662                 break;
   4663             }
   4664 
   4665             case SEARCH_SUGGESTIONS: {
   4666                 return mGlobalSearchSupport.handleSearchSuggestionsQuery(db, uri, limit);
   4667             }
   4668 
   4669             case SEARCH_SHORTCUT: {
   4670                 String lookupKey = uri.getLastPathSegment();
   4671                 return mGlobalSearchSupport.handleSearchShortcutRefresh(db, lookupKey, projection);
   4672             }
   4673 
   4674             case LIVE_FOLDERS_CONTACTS:
   4675                 qb.setTables(mDbHelper.getContactView());
   4676                 qb.setProjectionMap(sLiveFoldersProjectionMap);
   4677                 break;
   4678 
   4679             case LIVE_FOLDERS_CONTACTS_WITH_PHONES:
   4680                 qb.setTables(mDbHelper.getContactView());
   4681                 qb.setProjectionMap(sLiveFoldersProjectionMap);
   4682                 qb.appendWhere(Contacts.HAS_PHONE_NUMBER + "=1");
   4683                 break;
   4684 
   4685             case LIVE_FOLDERS_CONTACTS_FAVORITES:
   4686                 qb.setTables(mDbHelper.getContactView());
   4687                 qb.setProjectionMap(sLiveFoldersProjectionMap);
   4688                 qb.appendWhere(Contacts.STARRED + "=1");
   4689                 break;
   4690 
   4691             case LIVE_FOLDERS_CONTACTS_GROUP_NAME:
   4692                 qb.setTables(mDbHelper.getContactView());
   4693                 qb.setProjectionMap(sLiveFoldersProjectionMap);
   4694                 qb.appendWhere(CONTACTS_IN_GROUP_SELECT);
   4695                 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
   4696                 break;
   4697 
   4698             case RAW_CONTACT_ENTITIES: {
   4699                 setTablesAndProjectionMapForRawContactsEntities(qb, uri);
   4700                 break;
   4701             }
   4702 
   4703             case RAW_CONTACT_ENTITY_ID: {
   4704                 long rawContactId = Long.parseLong(uri.getPathSegments().get(1));
   4705                 setTablesAndProjectionMapForRawContactsEntities(qb, uri);
   4706                 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId));
   4707                 qb.appendWhere(" AND " + RawContacts._ID + "=?");
   4708                 break;
   4709             }
   4710 
   4711             case PROVIDER_STATUS: {
   4712                 return queryProviderStatus(uri, projection);
   4713             }
   4714 
   4715             default:
   4716                 return mLegacyApiSupport.query(uri, projection, selection, selectionArgs,
   4717                         sortOrder, limit);
   4718         }
   4719 
   4720         qb.setStrictProjectionMap(true);
   4721 
   4722         Cursor cursor =
   4723                 query(db, qb, projection, selection, selectionArgs, sortOrder, groupBy, limit);
   4724         if (readBooleanQueryParameter(uri, ContactCounts.ADDRESS_BOOK_INDEX_EXTRAS, false)) {
   4725             cursor = bundleLetterCountExtras(cursor, db, qb, selection, selectionArgs, sortOrder);
   4726         }
   4727         return cursor;
   4728     }
   4729 
   4730     private Cursor query(final SQLiteDatabase db, SQLiteQueryBuilder qb, String[] projection,
   4731             String selection, String[] selectionArgs, String sortOrder, String groupBy,
   4732             String limit) {
   4733         if (projection != null && projection.length == 1
   4734                 && BaseColumns._COUNT.equals(projection[0])) {
   4735             qb.setProjectionMap(sCountProjectionMap);
   4736         }
   4737         final Cursor c = qb.query(db, projection, selection, selectionArgs, groupBy, null,
   4738                 sortOrder, limit);
   4739         if (c != null) {
   4740             c.setNotificationUri(getContext().getContentResolver(), ContactsContract.AUTHORITY_URI);
   4741         }
   4742         return c;
   4743     }
   4744 
   4745     /**
   4746      * Creates a single-row cursor containing the current status of the provider.
   4747      */
   4748     private Cursor queryProviderStatus(Uri uri, String[] projection) {
   4749         MatrixCursor cursor = new MatrixCursor(projection);
   4750         RowBuilder row = cursor.newRow();
   4751         for (int i = 0; i < projection.length; i++) {
   4752             if (ProviderStatus.STATUS.equals(projection[i])) {
   4753                 row.add(mProviderStatus);
   4754             } else if (ProviderStatus.DATA1.equals(projection[i])) {
   4755                 row.add(mEstimatedStorageRequirement);
   4756             }
   4757         }
   4758         return cursor;
   4759     }
   4760 
   4761 
   4762     private static final class AddressBookIndexQuery {
   4763         public static final String LETTER = "letter";
   4764         public static final String TITLE = "title";
   4765         public static final String COUNT = "count";
   4766 
   4767         public static final String[] COLUMNS = new String[] {
   4768                 LETTER, TITLE, COUNT
   4769         };
   4770 
   4771         public static final int COLUMN_LETTER = 0;
   4772         public static final int COLUMN_TITLE = 1;
   4773         public static final int COLUMN_COUNT = 2;
   4774 
   4775         public static final String ORDER_BY = LETTER + " COLLATE " + PHONEBOOK_COLLATOR_NAME;
   4776     }
   4777 
   4778     /**
   4779      * Computes counts by the address book index titles and adds the resulting tally
   4780      * to the returned cursor as a bundle of extras.
   4781      */
   4782     private Cursor bundleLetterCountExtras(Cursor cursor, final SQLiteDatabase db,
   4783             SQLiteQueryBuilder qb, String selection, String[] selectionArgs, String sortOrder) {
   4784         String sortKey;
   4785 
   4786         // The sort order suffix could be something like "DESC".
   4787         // We want to preserve it in the query even though we will change
   4788         // the sort column itself.
   4789         String sortOrderSuffix = "";
   4790         if (sortOrder != null) {
   4791             int spaceIndex = sortOrder.indexOf(' ');
   4792             if (spaceIndex != -1) {
   4793                 sortKey = sortOrder.substring(0, spaceIndex);
   4794                 sortOrderSuffix = sortOrder.substring(spaceIndex);
   4795             } else {
   4796                 sortKey = sortOrder;
   4797             }
   4798         } else {
   4799             sortKey = Contacts.SORT_KEY_PRIMARY;
   4800         }
   4801 
   4802         String locale = getLocale().toString();
   4803         HashMap<String, String> projectionMap = Maps.newHashMap();
   4804         projectionMap.put(AddressBookIndexQuery.LETTER,
   4805                 "SUBSTR(" + sortKey + ",1,1) AS " + AddressBookIndexQuery.LETTER);
   4806 
   4807         /**
   4808          * Use the GET_PHONEBOOK_INDEX function, which is an android extension for SQLite3,
   4809          * to map the first letter of the sort key to a character that is traditionally
   4810          * used in phonebooks to represent that letter.  For example, in Korean it will
   4811          * be the first consonant in the letter; for Japanese it will be Hiragana rather
   4812          * than Katakana.
   4813          */
   4814         projectionMap.put(AddressBookIndexQuery.TITLE,
   4815                 "GET_PHONEBOOK_INDEX(SUBSTR(" + sortKey + ",1,1),'" + locale + "')"
   4816                         + " AS " + AddressBookIndexQuery.TITLE);
   4817         projectionMap.put(AddressBookIndexQuery.COUNT,
   4818                 "COUNT(" + Contacts._ID + ") AS " + AddressBookIndexQuery.COUNT);
   4819         qb.setProjectionMap(projectionMap);
   4820 
   4821         Cursor indexCursor = qb.query(db, AddressBookIndexQuery.COLUMNS, selection, selectionArgs,
   4822                 AddressBookIndexQuery.ORDER_BY, null /* having */,
   4823                 AddressBookIndexQuery.ORDER_BY + sortOrderSuffix);
   4824 
   4825         try {
   4826             int groupCount = indexCursor.getCount();
   4827             String titles[] = new String[groupCount];
   4828             int counts[] = new int[groupCount];
   4829             int indexCount = 0;
   4830             String currentTitle = null;
   4831 
   4832             // Since GET_PHONEBOOK_INDEX is a many-to-1 function, we may end up
   4833             // with multiple entries for the same title.  The following code
   4834             // collapses those duplicates.
   4835             for (int i = 0; i < groupCount; i++) {
   4836                 indexCursor.moveToNext();
   4837                 String title = indexCursor.getString(AddressBookIndexQuery.COLUMN_TITLE);
   4838                 int count = indexCursor.getInt(AddressBookIndexQuery.COLUMN_COUNT);
   4839                 if (indexCount == 0 || !TextUtils.equals(title, currentTitle)) {
   4840                     titles[indexCount] = currentTitle = title;
   4841                     counts[indexCount] = count;
   4842                     indexCount++;
   4843                 } else {
   4844                     counts[indexCount - 1] += count;
   4845                 }
   4846             }
   4847 
   4848             if (indexCount < groupCount) {
   4849                 String[] newTitles = new String[indexCount];
   4850                 System.arraycopy(titles, 0, newTitles, 0, indexCount);
   4851                 titles = newTitles;
   4852 
   4853                 int[] newCounts = new int[indexCount];
   4854                 System.arraycopy(counts, 0, newCounts, 0, indexCount);
   4855                 counts = newCounts;
   4856             }
   4857 
   4858             final Bundle bundle = new Bundle();
   4859             bundle.putStringArray(ContactCounts.EXTRA_ADDRESS_BOOK_INDEX_TITLES, titles);
   4860             bundle.putIntArray(ContactCounts.EXTRA_ADDRESS_BOOK_INDEX_COUNTS, counts);
   4861             return new CursorWrapper(cursor) {
   4862 
   4863                 @Override
   4864                 public Bundle getExtras() {
   4865                     return bundle;
   4866                 }
   4867             };
   4868         } finally {
   4869             indexCursor.close();
   4870         }
   4871     }
   4872 
   4873     /**
   4874      * Returns the contact Id for the contact identified by the lookupKey.
   4875      * Robust against changes in the lookup key: if the key has changed, will
   4876      * look up the contact by the raw contact IDs or name encoded in the lookup
   4877      * key.
   4878      */
   4879     public long lookupContactIdByLookupKey(SQLiteDatabase db, String lookupKey) {
   4880         ContactLookupKey key = new ContactLookupKey();
   4881         ArrayList<LookupKeySegment> segments = key.parse(lookupKey);
   4882 
   4883         long contactId = -1;
   4884         if (lookupKeyContainsType(segments, ContactLookupKey.LOOKUP_TYPE_SOURCE_ID)) {
   4885             contactId = lookupContactIdBySourceIds(db, segments);
   4886             if (contactId != -1) {
   4887                 return contactId;
   4888             }
   4889         }
   4890 
   4891         boolean hasRawContactIds =
   4892                 lookupKeyContainsType(segments, ContactLookupKey.LOOKUP_TYPE_RAW_CONTACT_ID);
   4893         if (hasRawContactIds) {
   4894             contactId = lookupContactIdByRawContactIds(db, segments);
   4895             if (contactId != -1) {
   4896                 return contactId;
   4897             }
   4898         }
   4899 
   4900         if (hasRawContactIds
   4901                 || lookupKeyContainsType(segments, ContactLookupKey.LOOKUP_TYPE_DISPLAY_NAME)) {
   4902             contactId = lookupContactIdByDisplayNames(db, segments);
   4903         }
   4904 
   4905         return contactId;
   4906     }
   4907 
   4908     private interface LookupBySourceIdQuery {
   4909         String TABLE = Tables.RAW_CONTACTS;
   4910 
   4911         String COLUMNS[] = {
   4912                 RawContacts.CONTACT_ID,
   4913                 RawContacts.ACCOUNT_TYPE,
   4914                 RawContacts.ACCOUNT_NAME,
   4915                 RawContacts.SOURCE_ID
   4916         };
   4917 
   4918         int CONTACT_ID = 0;
   4919         int ACCOUNT_TYPE = 1;
   4920         int ACCOUNT_NAME = 2;
   4921         int SOURCE_ID = 3;
   4922     }
   4923 
   4924     private long lookupContactIdBySourceIds(SQLiteDatabase db,
   4925                 ArrayList<LookupKeySegment> segments) {
   4926         StringBuilder sb = new StringBuilder();
   4927         sb.append(RawContacts.SOURCE_ID + " IN (");
   4928         for (int i = 0; i < segments.size(); i++) {
   4929             LookupKeySegment segment = segments.get(i);
   4930             if (segment.lookupType == ContactLookupKey.LOOKUP_TYPE_SOURCE_ID) {
   4931                 DatabaseUtils.appendEscapedSQLString(sb, segment.key);
   4932                 sb.append(",");
   4933             }
   4934         }
   4935         sb.setLength(sb.length() - 1);      // Last comma
   4936         sb.append(") AND " + RawContacts.CONTACT_ID + " NOT NULL");
   4937 
   4938         Cursor c = db.query(LookupBySourceIdQuery.TABLE, LookupBySourceIdQuery.COLUMNS,
   4939                  sb.toString(), null, null, null, null);
   4940         try {
   4941             while (c.moveToNext()) {
   4942                 String accountType = c.getString(LookupBySourceIdQuery.ACCOUNT_TYPE);
   4943                 String accountName = c.getString(LookupBySourceIdQuery.ACCOUNT_NAME);
   4944                 int accountHashCode =
   4945                         ContactLookupKey.getAccountHashCode(accountType, accountName);
   4946                 String sourceId = c.getString(LookupBySourceIdQuery.SOURCE_ID);
   4947                 for (int i = 0; i < segments.size(); i++) {
   4948                     LookupKeySegment segment = segments.get(i);
   4949                     if (segment.lookupType == ContactLookupKey.LOOKUP_TYPE_SOURCE_ID
   4950                             && accountHashCode == segment.accountHashCode
   4951                             && segment.key.equals(sourceId)) {
   4952                         segment.contactId = c.getLong(LookupBySourceIdQuery.CONTACT_ID);
   4953                         break;
   4954                     }
   4955                 }
   4956             }
   4957         } finally {
   4958             c.close();
   4959         }
   4960 
   4961         return getMostReferencedContactId(segments);
   4962     }
   4963 
   4964     private interface LookupByRawContactIdQuery {
   4965         String TABLE = Tables.RAW_CONTACTS;
   4966 
   4967         String COLUMNS[] = {
   4968                 RawContacts.CONTACT_ID,
   4969                 RawContacts.ACCOUNT_TYPE,
   4970                 RawContacts.ACCOUNT_NAME,
   4971                 RawContacts._ID,
   4972         };
   4973 
   4974         int CONTACT_ID = 0;
   4975         int ACCOUNT_TYPE = 1;
   4976         int ACCOUNT_NAME = 2;
   4977         int ID = 3;
   4978     }
   4979 
   4980     private long lookupContactIdByRawContactIds(SQLiteDatabase db,
   4981             ArrayList<LookupKeySegment> segments) {
   4982         StringBuilder sb = new StringBuilder();
   4983         sb.append(RawContacts._ID + " IN (");
   4984         for (int i = 0; i < segments.size(); i++) {
   4985             LookupKeySegment segment = segments.get(i);
   4986             if (segment.lookupType == ContactLookupKey.LOOKUP_TYPE_RAW_CONTACT_ID) {
   4987                 sb.append(segment.rawContactId);
   4988                 sb.append(",");
   4989             }
   4990         }
   4991         sb.setLength(sb.length() - 1);      // Last comma
   4992         sb.append(") AND " + RawContacts.CONTACT_ID + " NOT NULL");
   4993 
   4994         Cursor c = db.query(LookupByRawContactIdQuery.TABLE, LookupByRawContactIdQuery.COLUMNS,
   4995                  sb.toString(), null, null, null, null);
   4996         try {
   4997             while (c.moveToNext()) {
   4998                 String accountType = c.getString(LookupByRawContactIdQuery.ACCOUNT_TYPE);
   4999                 String accountName = c.getString(LookupByRawContactIdQuery.ACCOUNT_NAME);
   5000                 int accountHashCode =
   5001                         ContactLookupKey.getAccountHashCode(accountType, accountName);
   5002                 String rawContactId = c.getString(LookupByRawContactIdQuery.ID);
   5003                 for (int i = 0; i < segments.size(); i++) {
   5004                     LookupKeySegment segment = segments.get(i);
   5005                     if (segment.lookupType == ContactLookupKey.LOOKUP_TYPE_RAW_CONTACT_ID
   5006                             && accountHashCode == segment.accountHashCode
   5007                             && segment.rawContactId.equals(rawContactId)) {
   5008                         segment.contactId = c.getLong(LookupByRawContactIdQuery.CONTACT_ID);
   5009                         break;
   5010                     }
   5011                 }
   5012             }
   5013         } finally {
   5014             c.close();
   5015         }
   5016 
   5017         return getMostReferencedContactId(segments);
   5018     }
   5019 
   5020     private interface LookupByDisplayNameQuery {
   5021         String TABLE = Tables.NAME_LOOKUP_JOIN_RAW_CONTACTS;
   5022 
   5023         String COLUMNS[] = {
   5024                 RawContacts.CONTACT_ID,
   5025                 RawContacts.ACCOUNT_TYPE,
   5026                 RawContacts.ACCOUNT_NAME,
   5027                 NameLookupColumns.NORMALIZED_NAME
   5028         };
   5029 
   5030         int CONTACT_ID = 0;
   5031         int ACCOUNT_TYPE = 1;
   5032         int ACCOUNT_NAME = 2;
   5033         int NORMALIZED_NAME = 3;
   5034     }
   5035 
   5036     private long lookupContactIdByDisplayNames(SQLiteDatabase db,
   5037                 ArrayList<LookupKeySegment> segments) {
   5038         StringBuilder sb = new StringBuilder();
   5039         sb.append(NameLookupColumns.NORMALIZED_NAME + " IN (");
   5040         for (int i = 0; i < segments.size(); i++) {
   5041             LookupKeySegment segment = segments.get(i);
   5042             if (segment.lookupType == ContactLookupKey.LOOKUP_TYPE_DISPLAY_NAME
   5043                     || segment.lookupType == ContactLookupKey.LOOKUP_TYPE_RAW_CONTACT_ID) {
   5044                 DatabaseUtils.appendEscapedSQLString(sb, segment.key);
   5045                 sb.append(",");
   5046             }
   5047         }
   5048         sb.setLength(sb.length() - 1);      // Last comma
   5049         sb.append(") AND " + NameLookupColumns.NAME_TYPE + "=" + NameLookupType.NAME_COLLATION_KEY
   5050                 + " AND " + RawContacts.CONTACT_ID + " NOT NULL");
   5051 
   5052         Cursor c = db.query(LookupByDisplayNameQuery.TABLE, LookupByDisplayNameQuery.COLUMNS,
   5053                  sb.toString(), null, null, null, null);
   5054         try {
   5055             while (c.moveToNext()) {
   5056                 String accountType = c.getString(LookupByDisplayNameQuery.ACCOUNT_TYPE);
   5057                 String accountName = c.getString(LookupByDisplayNameQuery.ACCOUNT_NAME);
   5058                 int accountHashCode =
   5059                         ContactLookupKey.getAccountHashCode(accountType, accountName);
   5060                 String name = c.getString(LookupByDisplayNameQuery.NORMALIZED_NAME);
   5061                 for (int i = 0; i < segments.size(); i++) {
   5062                     LookupKeySegment segment = segments.get(i);
   5063                     if ((segment.lookupType == ContactLookupKey.LOOKUP_TYPE_DISPLAY_NAME
   5064                             || segment.lookupType == ContactLookupKey.LOOKUP_TYPE_RAW_CONTACT_ID)
   5065                             && accountHashCode == segment.accountHashCode
   5066                             && segment.key.equals(name)) {
   5067                         segment.contactId = c.getLong(LookupByDisplayNameQuery.CONTACT_ID);
   5068                         break;
   5069                     }
   5070                 }
   5071             }
   5072         } finally {
   5073             c.close();
   5074         }
   5075 
   5076         return getMostReferencedContactId(segments);
   5077     }
   5078 
   5079     private boolean lookupKeyContainsType(ArrayList<LookupKeySegment> segments, int lookupType) {
   5080         for (int i = 0; i < segments.size(); i++) {
   5081             LookupKeySegment segment = segments.get(i);
   5082             if (segment.lookupType == lookupType) {
   5083                 return true;
   5084             }
   5085         }
   5086 
   5087         return false;
   5088     }
   5089 
   5090     public void updateLookupKeyForRawContact(SQLiteDatabase db, long rawContactId) {
   5091         mContactAggregator.updateLookupKeyForRawContact(db, rawContactId);
   5092     }
   5093 
   5094     /**
   5095      * Returns the contact ID that is mentioned the highest number of times.
   5096      */
   5097     private long getMostReferencedContactId(ArrayList<LookupKeySegment> segments) {
   5098         Collections.sort(segments);
   5099 
   5100         long bestContactId = -1;
   5101         int bestRefCount = 0;
   5102 
   5103         long contactId = -1;
   5104         int count = 0;
   5105 
   5106         int segmentCount = segments.size();
   5107         for (int i = 0; i < segmentCount; i++) {
   5108             LookupKeySegment segment = segments.get(i);
   5109             if (segment.contactId != -1) {
   5110                 if (segment.contactId == contactId) {
   5111                     count++;
   5112                 } else {
   5113                     if (count > bestRefCount) {
   5114                         bestContactId = contactId;
   5115                         bestRefCount = count;
   5116                     }
   5117                     contactId = segment.contactId;
   5118                     count = 1;
   5119                 }
   5120             }
   5121         }
   5122         if (count > bestRefCount) {
   5123             return contactId;
   5124         } else {
   5125             return bestContactId;
   5126         }
   5127     }
   5128 
   5129     private void setTablesAndProjectionMapForContacts(SQLiteQueryBuilder qb, Uri uri,
   5130             String[] projection) {
   5131         StringBuilder sb = new StringBuilder();
   5132         appendContactsTables(sb, uri, projection);
   5133         qb.setTables(sb.toString());
   5134         qb.setProjectionMap(sContactsProjectionMap);
   5135     }
   5136 
   5137     /**
   5138      * Finds name lookup records matching the supplied filter, picks one arbitrary match per
   5139      * contact and joins that with other contacts tables.
   5140      */
   5141     private void setTablesAndProjectionMapForContactsWithSnippet(SQLiteQueryBuilder qb, Uri uri,
   5142             String[] projection, String filter) {
   5143 
   5144         StringBuilder sb = new StringBuilder();
   5145         appendContactsTables(sb, uri, projection);
   5146 
   5147         sb.append(" JOIN (SELECT " +
   5148                 RawContacts.CONTACT_ID + " AS snippet_contact_id");
   5149 
   5150         if (mDbHelper.isInProjection(projection, SearchSnippetColumns.SNIPPET_DATA_ID)) {
   5151             sb.append(", " + DataColumns.CONCRETE_ID + " AS "
   5152                     + SearchSnippetColumns.SNIPPET_DATA_ID);
   5153         }
   5154 
   5155         if (mDbHelper.isInProjection(projection, SearchSnippetColumns.SNIPPET_DATA1)) {
   5156             sb.append(", " + Data.DATA1 + " AS " + SearchSnippetColumns.SNIPPET_DATA1);
   5157         }
   5158 
   5159         if (mDbHelper.isInProjection(projection, SearchSnippetColumns.SNIPPET_DATA2)) {
   5160             sb.append(", " + Data.DATA2 + " AS " + SearchSnippetColumns.SNIPPET_DATA2);
   5161         }
   5162 
   5163         if (mDbHelper.isInProjection(projection, SearchSnippetColumns.SNIPPET_DATA3)) {
   5164             sb.append(", " + Data.DATA3 + " AS " + SearchSnippetColumns.SNIPPET_DATA3);
   5165         }
   5166 
   5167         if (mDbHelper.isInProjection(projection, SearchSnippetColumns.SNIPPET_DATA4)) {
   5168             sb.append(", " + Data.DATA4 + " AS " + SearchSnippetColumns.SNIPPET_DATA4);
   5169         }
   5170 
   5171         if (mDbHelper.isInProjection(projection, SearchSnippetColumns.SNIPPET_MIMETYPE)) {
   5172             sb.append(", (" +
   5173                     "SELECT " + MimetypesColumns.MIMETYPE +
   5174                     " FROM " + Tables.MIMETYPES +
   5175                     " WHERE " + MimetypesColumns._ID + "=" + DataColumns.MIMETYPE_ID +
   5176                     ") AS " + SearchSnippetColumns.SNIPPET_MIMETYPE);
   5177         }
   5178 
   5179         sb.append(" FROM " + Tables.DATA_JOIN_RAW_CONTACTS +
   5180                 " WHERE " + DataColumns.CONCRETE_ID +
   5181                 " IN (");
   5182 
   5183         // Construct a query that gives us exactly one data _id per matching contact.
   5184         // MIN stands in for ANY in this context.
   5185         sb.append(
   5186                 "SELECT MIN(" + Tables.NAME_LOOKUP + "." + NameLookupColumns.DATA_ID + ")" +
   5187                 " FROM " + Tables.NAME_LOOKUP +
   5188                 " JOIN " + Tables.RAW_CONTACTS +
   5189                 " ON (" + RawContactsColumns.CONCRETE_ID
   5190                         + "=" + Tables.NAME_LOOKUP + "." + NameLookupColumns.RAW_CONTACT_ID + ")" +
   5191                 " WHERE " + NameLookupColumns.NORMALIZED_NAME + " GLOB '");
   5192         sb.append(NameNormalizer.normalize(filter));
   5193         sb.append("*' AND " + NameLookupColumns.NAME_TYPE +
   5194                     " IN(" + CONTACT_LOOKUP_NAME_TYPES + ")" +
   5195                 " GROUP BY " + RawContactsColumns.CONCRETE_CONTACT_ID);
   5196 
   5197         sb.append(")) ON (" + Contacts._ID + "=snippet_contact_id)");
   5198 
   5199         qb.setTables(sb.toString());
   5200         qb.setProjectionMap(sContactsProjectionWithSnippetMap);
   5201     }
   5202 
   5203     private void appendContactsTables(StringBuilder sb, Uri uri, String[] projection) {
   5204         boolean excludeRestrictedData = false;
   5205         String requestingPackage = getQueryParameter(uri,
   5206                 ContactsContract.REQUESTING_PACKAGE_PARAM_KEY);
   5207         if (requestingPackage != null) {
   5208             excludeRestrictedData = !mDbHelper.hasAccessToRestrictedData(requestingPackage);
   5209         }
   5210         sb.append(mDbHelper.getContactView(excludeRestrictedData));
   5211         if (mDbHelper.isInProjection(projection,
   5212                 Contacts.CONTACT_PRESENCE)) {
   5213             sb.append(" LEFT OUTER JOIN " + Tables.AGGREGATED_PRESENCE +
   5214                     " ON (" + Contacts._ID + " = " + AggregatedPresenceColumns.CONTACT_ID + ")");
   5215         }
   5216         if (mDbHelper.isInProjection(projection,
   5217                 Contacts.CONTACT_STATUS,
   5218                 Contacts.CONTACT_STATUS_RES_PACKAGE,
   5219                 Contacts.CONTACT_STATUS_ICON,
   5220                 Contacts.CONTACT_STATUS_LABEL,
   5221                 Contacts.CONTACT_STATUS_TIMESTAMP)) {
   5222             sb.append(" LEFT OUTER JOIN " + Tables.STATUS_UPDATES + " "
   5223                     + ContactsStatusUpdatesColumns.ALIAS +
   5224                     " ON (" + ContactsColumns.LAST_STATUS_UPDATE_ID + "="
   5225                             + ContactsStatusUpdatesColumns.CONCRETE_DATA_ID + ")");
   5226         }
   5227     }
   5228 
   5229     private void setTablesAndProjectionMapForRawContacts(SQLiteQueryBuilder qb, Uri uri) {
   5230         StringBuilder sb = new StringBuilder();
   5231         boolean excludeRestrictedData = false;
   5232         String requestingPackage = getQueryParameter(uri,
   5233                 ContactsContract.REQUESTING_PACKAGE_PARAM_KEY);
   5234         if (requestingPackage != null) {
   5235             excludeRestrictedData = !mDbHelper.hasAccessToRestrictedData(requestingPackage);
   5236         }
   5237         sb.append(mDbHelper.getRawContactView(excludeRestrictedData));
   5238         qb.setTables(sb.toString());
   5239         qb.setProjectionMap(sRawContactsProjectionMap);
   5240         appendAccountFromParameter(qb, uri);
   5241     }
   5242 
   5243     private void setTablesAndProjectionMapForRawContactsEntities(SQLiteQueryBuilder qb, Uri uri) {
   5244         // Note: currently, "export only" equals to "restricted", but may not in the future.
   5245         boolean excludeRestrictedData = readBooleanQueryParameter(uri,
   5246                 Data.FOR_EXPORT_ONLY, false);
   5247 
   5248         String requestingPackage = getQueryParameter(uri,
   5249                 ContactsContract.REQUESTING_PACKAGE_PARAM_KEY);
   5250         if (requestingPackage != null) {
   5251             excludeRestrictedData = excludeRestrictedData
   5252                     || !mDbHelper.hasAccessToRestrictedData(requestingPackage);
   5253         }
   5254         qb.setTables(mDbHelper.getContactEntitiesView(excludeRestrictedData));
   5255         qb.setProjectionMap(sRawContactsEntityProjectionMap);
   5256         appendAccountFromParameter(qb, uri);
   5257     }
   5258 
   5259     private void setTablesAndProjectionMapForData(SQLiteQueryBuilder qb, Uri uri,
   5260             String[] projection, boolean distinct) {
   5261         StringBuilder sb = new StringBuilder();
   5262         // Note: currently, "export only" equals to "restricted", but may not in the future.
   5263         boolean excludeRestrictedData = readBooleanQueryParameter(uri,
   5264                 Data.FOR_EXPORT_ONLY, false);
   5265 
   5266         String requestingPackage = getQueryParameter(uri,
   5267                 ContactsContract.REQUESTING_PACKAGE_PARAM_KEY);
   5268         if (requestingPackage != null) {
   5269             excludeRestrictedData = excludeRestrictedData
   5270                     || !mDbHelper.hasAccessToRestrictedData(requestingPackage);
   5271         }
   5272 
   5273         sb.append(mDbHelper.getDataView(excludeRestrictedData));
   5274         sb.append(" data");
   5275 
   5276         // Include aggregated presence when requested
   5277         if (mDbHelper.isInProjection(projection, Data.CONTACT_PRESENCE)) {
   5278             sb.append(" LEFT OUTER JOIN " + Tables.AGGREGATED_PRESENCE +
   5279                     " ON (" + AggregatedPresenceColumns.CONCRETE_CONTACT_ID + "="
   5280                     + RawContacts.CONTACT_ID + ")");
   5281         }
   5282 
   5283         // Include aggregated status updates when requested
   5284         if (mDbHelper.isInProjection(projection,
   5285                 Data.CONTACT_STATUS,
   5286                 Data.CONTACT_STATUS_RES_PACKAGE,
   5287                 Data.CONTACT_STATUS_ICON,
   5288                 Data.CONTACT_STATUS_LABEL,
   5289                 Data.CONTACT_STATUS_TIMESTAMP)) {
   5290             sb.append(" LEFT OUTER JOIN " + Tables.STATUS_UPDATES + " "
   5291                     + ContactsStatusUpdatesColumns.ALIAS +
   5292                     " ON (" + ContactsColumns.LAST_STATUS_UPDATE_ID + "="
   5293                             + ContactsStatusUpdatesColumns.CONCRETE_DATA_ID + ")");
   5294         }
   5295 
   5296         // Include individual presence when requested
   5297         if (mDbHelper.isInProjection(projection, Data.PRESENCE)) {
   5298             sb.append(" LEFT OUTER JOIN " + Tables.PRESENCE +
   5299                     " ON (" + StatusUpdates.DATA_ID + "="
   5300                     + DataColumns.CONCRETE_ID + ")");
   5301         }
   5302 
   5303         // Include individual status updates when requested
   5304         if (mDbHelper.isInProjection(projection,
   5305                 Data.STATUS,
   5306                 Data.STATUS_RES_PACKAGE,
   5307                 Data.STATUS_ICON,
   5308                 Data.STATUS_LABEL,
   5309                 Data.STATUS_TIMESTAMP)) {
   5310             sb.append(" LEFT OUTER JOIN " + Tables.STATUS_UPDATES +
   5311                     " ON (" + StatusUpdatesColumns.CONCRETE_DATA_ID + "="
   5312                             + DataColumns.CONCRETE_ID + ")");
   5313         }
   5314 
   5315         qb.setTables(sb.toString());
   5316         qb.setProjectionMap(distinct ? sDistinctDataProjectionMap : sDataProjectionMap);
   5317         appendAccountFromParameter(qb, uri);
   5318     }
   5319 
   5320     private void setTableAndProjectionMapForStatusUpdates(SQLiteQueryBuilder qb,
   5321             String[] projection) {
   5322         StringBuilder sb = new StringBuilder();
   5323         sb.append(mDbHelper.getDataView());
   5324         sb.append(" data");
   5325 
   5326         if (mDbHelper.isInProjection(projection, StatusUpdates.PRESENCE)) {
   5327             sb.append(" LEFT OUTER JOIN " + Tables.PRESENCE +
   5328                     " ON(" + Tables.PRESENCE + "." + StatusUpdates.DATA_ID
   5329                     + "=" + DataColumns.CONCRETE_ID + ")");
   5330         }
   5331 
   5332         if (mDbHelper.isInProjection(projection,
   5333                 StatusUpdates.STATUS,
   5334                 StatusUpdates.STATUS_RES_PACKAGE,
   5335                 StatusUpdates.STATUS_ICON,
   5336                 StatusUpdates.STATUS_LABEL,
   5337                 StatusUpdates.STATUS_TIMESTAMP)) {
   5338             sb.append(" LEFT OUTER JOIN " + Tables.STATUS_UPDATES +
   5339                     " ON(" + Tables.STATUS_UPDATES + "." + StatusUpdatesColumns.DATA_ID
   5340                     + "=" + DataColumns.CONCRETE_ID + ")");
   5341         }
   5342         qb.setTables(sb.toString());
   5343         qb.setProjectionMap(sStatusUpdatesProjectionMap);
   5344     }
   5345 
   5346     private void appendAccountFromParameter(SQLiteQueryBuilder qb, Uri uri) {
   5347         final String accountName = getQueryParameter(uri, RawContacts.ACCOUNT_NAME);
   5348         final String accountType = getQueryParameter(uri, RawContacts.ACCOUNT_TYPE);
   5349 
   5350         final boolean partialUri = TextUtils.isEmpty(accountName) ^ TextUtils.isEmpty(accountType);
   5351         if (partialUri) {
   5352             // Throw when either account is incomplete
   5353             throw new IllegalArgumentException(mDbHelper.exceptionMessage(
   5354                     "Must specify both or neither of ACCOUNT_NAME and ACCOUNT_TYPE", uri));
   5355         }
   5356 
   5357         // Accounts are valid by only checking one parameter, since we've
   5358         // already ruled out partial accounts.
   5359         final boolean validAccount = !TextUtils.isEmpty(accountName);
   5360         if (validAccount) {
   5361             qb.appendWhere(RawContacts.ACCOUNT_NAME + "="
   5362                     + DatabaseUtils.sqlEscapeString(accountName) + " AND "
   5363                     + RawContacts.ACCOUNT_TYPE + "="
   5364                     + DatabaseUtils.sqlEscapeString(accountType));
   5365         } else {
   5366             qb.appendWhere("1");
   5367         }
   5368     }
   5369 
   5370     private String appendAccountToSelection(Uri uri, String selection) {
   5371         final String accountName = getQueryParameter(uri, RawContacts.ACCOUNT_NAME);
   5372         final String accountType = getQueryParameter(uri, RawContacts.ACCOUNT_TYPE);
   5373 
   5374         final boolean partialUri = TextUtils.isEmpty(accountName) ^ TextUtils.isEmpty(accountType);
   5375         if (partialUri) {
   5376             // Throw when either account is incomplete
   5377             throw new IllegalArgumentException(mDbHelper.exceptionMessage(
   5378                     "Must specify both or neither of ACCOUNT_NAME and ACCOUNT_TYPE", uri));
   5379         }
   5380 
   5381         // Accounts are valid by only checking one parameter, since we've
   5382         // already ruled out partial accounts.
   5383         final boolean validAccount = !TextUtils.isEmpty(accountName);
   5384         if (validAccount) {
   5385             StringBuilder selectionSb = new StringBuilder(RawContacts.ACCOUNT_NAME + "="
   5386                     + DatabaseUtils.sqlEscapeString(accountName) + " AND "
   5387                     + RawContacts.ACCOUNT_TYPE + "="
   5388                     + DatabaseUtils.sqlEscapeString(accountType));
   5389             if (!TextUtils.isEmpty(selection)) {
   5390                 selectionSb.append(" AND (");
   5391                 selectionSb.append(selection);
   5392                 selectionSb.append(')');
   5393             }
   5394             return selectionSb.toString();
   5395         } else {
   5396             return selection;
   5397         }
   5398     }
   5399 
   5400     /**
   5401      * Gets the value of the "limit" URI query parameter.
   5402      *
   5403      * @return A string containing a non-negative integer, or <code>null</code> if
   5404      *         the parameter is not set, or is set to an invalid value.
   5405      */
   5406     private String getLimit(Uri uri) {
   5407         String limitParam = getQueryParameter(uri, "limit");
   5408         if (limitParam == null) {
   5409             return null;
   5410         }
   5411         // make sure that the limit is a non-negative integer
   5412         try {
   5413             int l = Integer.parseInt(limitParam);
   5414             if (l < 0) {
   5415                 Log.w(TAG, "Invalid limit parameter: " + limitParam);
   5416                 return null;
   5417             }
   5418             return String.valueOf(l);
   5419         } catch (NumberFormatException ex) {
   5420             Log.w(TAG, "Invalid limit parameter: " + limitParam);
   5421             return null;
   5422         }
   5423     }
   5424 
   5425     /**
   5426      * Returns true if all the characters are meaningful as digits
   5427      * in a phone number -- letters, digits, and a few punctuation marks.
   5428      */
   5429     private boolean isPhoneNumber(CharSequence cons) {
   5430         int len = cons.length();
   5431 
   5432         for (int i = 0; i < len; i++) {
   5433             char c = cons.charAt(i);
   5434 
   5435             if ((c >= '0') && (c <= '9')) {
   5436                 continue;
   5437             }
   5438             if ((c == ' ') || (c == '-') || (c == '(') || (c == ')') || (c == '.') || (c == '+')
   5439                     || (c == '#') || (c == '*')) {
   5440                 continue;
   5441             }
   5442             if ((c >= 'A') && (c <= 'Z')) {
   5443                 continue;
   5444             }
   5445             if ((c >= 'a') && (c <= 'z')) {
   5446                 continue;
   5447             }
   5448 
   5449             return false;
   5450         }
   5451 
   5452         return true;
   5453     }
   5454 
   5455     String getContactsRestrictions() {
   5456         if (mDbHelper.hasAccessToRestrictedData()) {
   5457             return "1";
   5458         } else {
   5459             return RawContactsColumns.CONCRETE_IS_RESTRICTED + "=0";
   5460         }
   5461     }
   5462 
   5463     public String getContactsRestrictionExceptionAsNestedQuery(String contactIdColumn) {
   5464         if (mDbHelper.hasAccessToRestrictedData()) {
   5465             return "1";
   5466         } else {
   5467             return "(SELECT " + RawContacts.IS_RESTRICTED + " FROM " + Tables.RAW_CONTACTS
   5468                     + " WHERE " + RawContactsColumns.CONCRETE_ID + "=" + contactIdColumn + ")=0";
   5469         }
   5470     }
   5471 
   5472     @Override
   5473     public AssetFileDescriptor openAssetFile(Uri uri, String mode) throws FileNotFoundException {
   5474         int match = sUriMatcher.match(uri);
   5475         switch (match) {
   5476             case CONTACTS_PHOTO: {
   5477                 return openPhotoAssetFile(uri, mode,
   5478                         Data._ID + "=" + Contacts.PHOTO_ID + " AND " + RawContacts.CONTACT_ID + "=?",
   5479                         new String[]{uri.getPathSegments().get(1)});
   5480             }
   5481 
   5482             case DATA_ID: {
   5483                 return openPhotoAssetFile(uri, mode,
   5484                         Data._ID + "=? AND " + Data.MIMETYPE + "='" + Photo.CONTENT_ITEM_TYPE + "'",
   5485                         new String[]{uri.getPathSegments().get(1)});
   5486             }
   5487 
   5488             case CONTACTS_AS_VCARD: {
   5489                 final String lookupKey = Uri.encode(uri.getPathSegments().get(2));
   5490                 mSelectionArgs1[0] = String.valueOf(lookupContactIdByLookupKey(mDb, lookupKey));
   5491                 final String selection = Contacts._ID + "=?";
   5492 
   5493                 // When opening a contact as file, we pass back contents as a
   5494                 // vCard-encoded stream. We build into a local buffer first,
   5495                 // then pipe into MemoryFile once the exact size is known.
   5496                 final ByteArrayOutputStream localStream = new ByteArrayOutputStream();
   5497                 outputRawContactsAsVCard(localStream, selection, mSelectionArgs1);
   5498                 return buildAssetFileDescriptor(localStream);
   5499             }
   5500 
   5501             case CONTACTS_AS_MULTI_VCARD: {
   5502                 final String lookupKeys = uri.getPathSegments().get(2);
   5503                 final String[] loopupKeyList = lookupKeys.split(":");
   5504                 final StringBuilder inBuilder = new StringBuilder();
   5505                 int index = 0;
   5506                 // SQLite has limits on how many parameters can be used
   5507                 // so the IDs are concatenated to a query string here instead
   5508                 for (String lookupKey : loopupKeyList) {
   5509                     if (index == 0) {
   5510                         inBuilder.append("(");
   5511                     } else {
   5512                         inBuilder.append(",");
   5513                     }
   5514                     inBuilder.append(lookupContactIdByLookupKey(mDb, lookupKey));
   5515                     index++;
   5516                 }
   5517                 inBuilder.append(')');
   5518                 final String selection = Contacts._ID + " IN " + inBuilder.toString();
   5519 
   5520                 // When opening a contact as file, we pass back contents as a
   5521                 // vCard-encoded stream. We build into a local buffer first,
   5522                 // then pipe into MemoryFile once the exact size is known.
   5523                 final ByteArrayOutputStream localStream = new ByteArrayOutputStream();
   5524                 outputRawContactsAsVCard(localStream, selection, null);
   5525                 return buildAssetFileDescriptor(localStream);
   5526             }
   5527 
   5528             default:
   5529                 throw new FileNotFoundException(mDbHelper.exceptionMessage("File does not exist",
   5530                         uri));
   5531         }
   5532     }
   5533 
   5534     private AssetFileDescriptor openPhotoAssetFile(Uri uri, String mode, String selection,
   5535             String[] selectionArgs)
   5536             throws FileNotFoundException {
   5537         if (!"r".equals(mode)) {
   5538             throw new FileNotFoundException(mDbHelper.exceptionMessage("Mode " + mode
   5539                     + " not supported.", uri));
   5540         }
   5541 
   5542         String sql =
   5543                 "SELECT " + Photo.PHOTO + " FROM " + mDbHelper.getDataView() +
   5544                 " WHERE " + selection;
   5545         SQLiteDatabase db = mDbHelper.getReadableDatabase();
   5546         return SQLiteContentHelper.getBlobColumnAsAssetFile(db, sql,
   5547                 selectionArgs);
   5548     }
   5549 
   5550     private static final String CONTACT_MEMORY_FILE_NAME = "contactAssetFile";
   5551 
   5552     /**
   5553      * Build a {@link AssetFileDescriptor} through a {@link MemoryFile} with the
   5554      * contents of the given {@link ByteArrayOutputStream}.
   5555      */
   5556     private AssetFileDescriptor buildAssetFileDescriptor(ByteArrayOutputStream stream) {
   5557         AssetFileDescriptor fd = null;
   5558         try {
   5559             stream.flush();
   5560 
   5561             final byte[] byteData = stream.toByteArray();
   5562             final int size = byteData.length;
   5563 
   5564             final MemoryFile memoryFile = new MemoryFile(CONTACT_MEMORY_FILE_NAME, size);
   5565             memoryFile.writeBytes(byteData, 0, 0, size);
   5566             memoryFile.deactivate();
   5567 
   5568             fd = AssetFileDescriptor.fromMemoryFile(memoryFile);
   5569         } catch (IOException e) {
   5570             Log.w(TAG, "Problem writing stream into an AssetFileDescriptor: " + e.toString());
   5571         }
   5572         return fd;
   5573     }
   5574 
   5575     /**
   5576      * Output {@link RawContacts} matching the requested selection in the vCard
   5577      * format to the given {@link OutputStream}. This method returns silently if
   5578      * any errors encountered.
   5579      */
   5580     private void outputRawContactsAsVCard(OutputStream stream, String selection,
   5581             String[] selectionArgs) {
   5582         final Context context = this.getContext();
   5583         final VCardComposer composer =
   5584                 new VCardComposer(context, VCardConfig.VCARD_TYPE_DEFAULT, false);
   5585         composer.addHandler(composer.new HandlerForOutputStream(stream));
   5586 
   5587         // No extra checks since composer always uses restricted views
   5588         if (!composer.init(selection, selectionArgs)) {
   5589             Log.w(TAG, "Failed to init VCardComposer");
   5590             return;
   5591         }
   5592 
   5593         while (!composer.isAfterLast()) {
   5594             if (!composer.createOneEntry()) {
   5595                 Log.w(TAG, "Failed to output a contact.");
   5596             }
   5597         }
   5598         composer.terminate();
   5599     }
   5600 
   5601     @Override
   5602     public String getType(Uri uri) {
   5603         final int match = sUriMatcher.match(uri);
   5604         switch (match) {
   5605             case CONTACTS:
   5606                 return Contacts.CONTENT_TYPE;
   5607             case CONTACTS_LOOKUP:
   5608             case CONTACTS_ID:
   5609             case CONTACTS_LOOKUP_ID:
   5610                 return Contacts.CONTENT_ITEM_TYPE;
   5611             case CONTACTS_AS_VCARD:
   5612             case CONTACTS_AS_MULTI_VCARD:
   5613                 return Contacts.CONTENT_VCARD_TYPE;
   5614             case RAW_CONTACTS:
   5615                 return RawContacts.CONTENT_TYPE;
   5616             case RAW_CONTACTS_ID:
   5617                 return RawContacts.CONTENT_ITEM_TYPE;
   5618             case DATA:
   5619                 return Data.CONTENT_TYPE;
   5620             case DATA_ID:
   5621                 return mDbHelper.getDataMimeType(ContentUris.parseId(uri));
   5622             case PHONES:
   5623                 return Phone.CONTENT_TYPE;
   5624             case PHONES_ID:
   5625                 return Phone.CONTENT_ITEM_TYPE;
   5626             case PHONE_LOOKUP:
   5627                 return PhoneLookup.CONTENT_TYPE;
   5628             case EMAILS:
   5629                 return Email.CONTENT_TYPE;
   5630             case EMAILS_ID:
   5631                 return Email.CONTENT_ITEM_TYPE;
   5632             case POSTALS:
   5633                 return StructuredPostal.CONTENT_TYPE;
   5634             case POSTALS_ID:
   5635                 return StructuredPostal.CONTENT_ITEM_TYPE;
   5636             case AGGREGATION_EXCEPTIONS:
   5637                 return AggregationExceptions.CONTENT_TYPE;
   5638             case AGGREGATION_EXCEPTION_ID:
   5639                 return AggregationExceptions.CONTENT_ITEM_TYPE;
   5640             case SETTINGS:
   5641                 return Settings.CONTENT_TYPE;
   5642             case AGGREGATION_SUGGESTIONS:
   5643                 return Contacts.CONTENT_TYPE;
   5644             case SEARCH_SUGGESTIONS:
   5645                 return SearchManager.SUGGEST_MIME_TYPE;
   5646             case SEARCH_SHORTCUT:
   5647                 return SearchManager.SHORTCUT_MIME_TYPE;
   5648 
   5649             default:
   5650                 return mLegacyApiSupport.getType(uri);
   5651         }
   5652     }
   5653 
   5654     private void setDisplayName(long rawContactId, int displayNameSource,
   5655             String displayNamePrimary, String displayNameAlternative, String phoneticName,
   5656             int phoneticNameStyle, String sortKeyPrimary, String sortKeyAlternative) {
   5657         mRawContactDisplayNameUpdate.bindLong(1, displayNameSource);
   5658         bindString(mRawContactDisplayNameUpdate, 2, displayNamePrimary);
   5659         bindString(mRawContactDisplayNameUpdate, 3, displayNameAlternative);
   5660         bindString(mRawContactDisplayNameUpdate, 4, phoneticName);
   5661         mRawContactDisplayNameUpdate.bindLong(5, phoneticNameStyle);
   5662         bindString(mRawContactDisplayNameUpdate, 6, sortKeyPrimary);
   5663         bindString(mRawContactDisplayNameUpdate, 7, sortKeyAlternative);
   5664         mRawContactDisplayNameUpdate.bindLong(8, rawContactId);
   5665         mRawContactDisplayNameUpdate.execute();
   5666     }
   5667 
   5668     /**
   5669      * Sets the {@link RawContacts#DIRTY} for the specified raw contact.
   5670      */
   5671     private void setRawContactDirty(long rawContactId) {
   5672         mDirtyRawContacts.add(rawContactId);
   5673     }
   5674 
   5675     /*
   5676      * Sets the given dataId record in the "data" table to primary, and resets all data records of
   5677      * the same mimetype and under the same contact to not be primary.
   5678      *
   5679      * @param dataId the id of the data record to be set to primary.
   5680      */
   5681     private void setIsPrimary(long rawContactId, long dataId, long mimeTypeId) {
   5682         mSetPrimaryStatement.bindLong(1, dataId);
   5683         mSetPrimaryStatement.bindLong(2, mimeTypeId);
   5684         mSetPrimaryStatement.bindLong(3, rawContactId);
   5685         mSetPrimaryStatement.execute();
   5686     }
   5687 
   5688     /*
   5689      * Sets the given dataId record in the "data" table to "super primary", and resets all data
   5690      * records of the same mimetype and under the same aggregate to not be "super primary".
   5691      *
   5692      * @param dataId the id of the data record to be set to primary.
   5693      */
   5694     private void setIsSuperPrimary(long rawContactId, long dataId, long mimeTypeId) {
   5695         mSetSuperPrimaryStatement.bindLong(1, dataId);
   5696         mSetSuperPrimaryStatement.bindLong(2, mimeTypeId);
   5697         mSetSuperPrimaryStatement.bindLong(3, rawContactId);
   5698         mSetSuperPrimaryStatement.execute();
   5699     }
   5700 
   5701     public String insertNameLookupForEmail(long rawContactId, long dataId, String email) {
   5702         if (TextUtils.isEmpty(email)) {
   5703             return null;
   5704         }
   5705 
   5706         String address = mDbHelper.extractHandleFromEmailAddress(email);
   5707         if (address == null) {
   5708             return null;
   5709         }
   5710 
   5711         insertNameLookup(rawContactId, dataId,
   5712                 NameLookupType.EMAIL_BASED_NICKNAME, NameNormalizer.normalize(address));
   5713         return address;
   5714     }
   5715 
   5716     /**
   5717      * Normalizes the nickname and inserts it in the name lookup table.
   5718      */
   5719     public void insertNameLookupForNickname(long rawContactId, long dataId, String nickname) {
   5720         if (TextUtils.isEmpty(nickname)) {
   5721             return;
   5722         }
   5723 
   5724         insertNameLookup(rawContactId, dataId,
   5725                 NameLookupType.NICKNAME, NameNormalizer.normalize(nickname));
   5726     }
   5727 
   5728     public void insertNameLookupForOrganization(long rawContactId, long dataId, String company,
   5729             String title) {
   5730         if (!TextUtils.isEmpty(company)) {
   5731             insertNameLookup(rawContactId, dataId,
   5732                     NameLookupType.ORGANIZATION, NameNormalizer.normalize(company));
   5733         }
   5734         if (!TextUtils.isEmpty(title)) {
   5735             insertNameLookup(rawContactId, dataId,
   5736                     NameLookupType.ORGANIZATION, NameNormalizer.normalize(title));
   5737         }
   5738     }
   5739 
   5740     public void insertNameLookupForStructuredName(long rawContactId, long dataId, String name,
   5741             int fullNameStyle) {
   5742         mNameLookupBuilder.insertNameLookup(rawContactId, dataId, name, fullNameStyle);
   5743     }
   5744 
   5745     private class StructuredNameLookupBuilder extends NameLookupBuilder {
   5746 
   5747         public StructuredNameLookupBuilder(NameSplitter splitter) {
   5748             super(splitter);
   5749         }
   5750 
   5751         @Override
   5752         protected void insertNameLookup(long rawContactId, long dataId, int lookupType,
   5753                 String name) {
   5754             ContactsProvider2.this.insertNameLookup(rawContactId, dataId, lookupType, name);
   5755         }
   5756 
   5757         @Override
   5758         protected String[] getCommonNicknameClusters(String normalizedName) {
   5759             return mCommonNicknameCache.getCommonNicknameClusters(normalizedName);
   5760         }
   5761     }
   5762 
   5763     public void insertNameLookupForPhoneticName(long rawContactId, long dataId,
   5764             ContentValues values) {
   5765         if (values.containsKey(StructuredName.PHONETIC_FAMILY_NAME)
   5766                 || values.containsKey(StructuredName.PHONETIC_GIVEN_NAME)
   5767                 || values.containsKey(StructuredName.PHONETIC_MIDDLE_NAME)) {
   5768             insertNameLookupForPhoneticName(rawContactId, dataId,
   5769                     values.getAsString(StructuredName.PHONETIC_FAMILY_NAME),
   5770                     values.getAsString(StructuredName.PHONETIC_MIDDLE_NAME),
   5771                     values.getAsString(StructuredName.PHONETIC_GIVEN_NAME));
   5772         }
   5773     }
   5774 
   5775     public void insertNameLookupForPhoneticName(long rawContactId, long dataId, String familyName,
   5776             String middleName, String givenName) {
   5777         mSb.setLength(0);
   5778         if (familyName != null) {
   5779             mSb.append(familyName.trim());
   5780         }
   5781         if (middleName != null) {
   5782             mSb.append(middleName.trim());
   5783         }
   5784         if (givenName != null) {
   5785             mSb.append(givenName.trim());
   5786         }
   5787 
   5788         if (mSb.length() > 0) {
   5789             insertNameLookup(rawContactId, dataId, NameLookupType.NAME_COLLATION_KEY,
   5790                     NameNormalizer.normalize(mSb.toString()));
   5791         }
   5792 
   5793         if (givenName != null) {
   5794             // We want the phonetic given name to be used for search, but not for aggregation,
   5795             // which is why we are using NAME_SHORTHAND rather than NAME_COLLATION_KEY
   5796             insertNameLookup(rawContactId, dataId, NameLookupType.NAME_SHORTHAND,
   5797                     NameNormalizer.normalize(givenName.trim()));
   5798         }
   5799     }
   5800 
   5801     /**
   5802      * Inserts a record in the {@link Tables#NAME_LOOKUP} table.
   5803      */
   5804     public void insertNameLookup(long rawContactId, long dataId, int lookupType, String name) {
   5805         mNameLookupInsert.bindLong(1, rawContactId);
   5806         mNameLookupInsert.bindLong(2, dataId);
   5807         mNameLookupInsert.bindLong(3, lookupType);
   5808         bindString(mNameLookupInsert, 4, name);
   5809         mNameLookupInsert.executeInsert();
   5810     }
   5811 
   5812     /**
   5813      * Deletes all {@link Tables#NAME_LOOKUP} table rows associated with the specified data element.
   5814      */
   5815     public void deleteNameLookup(long dataId) {
   5816         mNameLookupDelete.bindLong(1, dataId);
   5817         mNameLookupDelete.execute();
   5818     }
   5819 
   5820     public void appendContactFilterAsNestedQuery(StringBuilder sb, String filterParam) {
   5821         sb.append("(" +
   5822                 "SELECT DISTINCT " + RawContacts.CONTACT_ID +
   5823                 " FROM " + Tables.RAW_CONTACTS +
   5824                 " JOIN " + Tables.NAME_LOOKUP +
   5825                 " ON(" + RawContactsColumns.CONCRETE_ID + "="
   5826                         + NameLookupColumns.RAW_CONTACT_ID + ")" +
   5827                 " WHERE normalized_name GLOB '");
   5828         sb.append(NameNormalizer.normalize(filterParam));
   5829         sb.append("*' AND " + NameLookupColumns.NAME_TYPE +
   5830                     " IN(" + CONTACT_LOOKUP_NAME_TYPES + "))");
   5831     }
   5832 
   5833     public String getRawContactsByFilterAsNestedQuery(String filterParam) {
   5834         StringBuilder sb = new StringBuilder();
   5835         appendRawContactsByFilterAsNestedQuery(sb, filterParam);
   5836         return sb.toString();
   5837     }
   5838 
   5839     public void appendRawContactsByFilterAsNestedQuery(StringBuilder sb, String filterParam) {
   5840         appendRawContactsByNormalizedNameFilter(sb, NameNormalizer.normalize(filterParam), true);
   5841     }
   5842 
   5843     private void appendRawContactsByNormalizedNameFilter(StringBuilder sb, String normalizedName,
   5844             boolean allowEmailMatch) {
   5845         sb.append("(" +
   5846                 "SELECT " + NameLookupColumns.RAW_CONTACT_ID +
   5847                 " FROM " + Tables.NAME_LOOKUP +
   5848                 " WHERE " + NameLookupColumns.NORMALIZED_NAME +
   5849                 " GLOB '");
   5850         sb.append(normalizedName);
   5851         sb.append("*' AND " + NameLookupColumns.NAME_TYPE + " IN ("
   5852                 + NameLookupType.NAME_COLLATION_KEY + ","
   5853                 + NameLookupType.NICKNAME + ","
   5854                 + NameLookupType.NAME_SHORTHAND + ","
   5855                 + NameLookupType.ORGANIZATION + ","
   5856                 + NameLookupType.NAME_CONSONANTS);
   5857         if (allowEmailMatch) {
   5858             sb.append("," + NameLookupType.EMAIL_BASED_NICKNAME);
   5859         }
   5860         sb.append("))");
   5861     }
   5862 
   5863     /**
   5864      * Inserts an argument at the beginning of the selection arg list.
   5865      */
   5866     private String[] insertSelectionArg(String[] selectionArgs, String arg) {
   5867         if (selectionArgs == null) {
   5868             return new String[] {arg};
   5869         } else {
   5870             int newLength = selectionArgs.length + 1;
   5871             String[] newSelectionArgs = new String[newLength];
   5872             newSelectionArgs[0] = arg;
   5873             System.arraycopy(selectionArgs, 0, newSelectionArgs, 1, selectionArgs.length);
   5874             return newSelectionArgs;
   5875         }
   5876     }
   5877 
   5878     private String[] appendProjectionArg(String[] projection, String arg) {
   5879         if (projection == null) {
   5880             return null;
   5881         }
   5882         final int length = projection.length;
   5883         String[] newProjection = new String[length + 1];
   5884         System.arraycopy(projection, 0, newProjection, 0, length);
   5885         newProjection[length] = arg;
   5886         return newProjection;
   5887     }
   5888 
   5889     protected Account getDefaultAccount() {
   5890         AccountManager accountManager = AccountManager.get(getContext());
   5891         try {
   5892             Account[] accounts = accountManager.getAccountsByTypeAndFeatures(DEFAULT_ACCOUNT_TYPE,
   5893                     new String[] {FEATURE_LEGACY_HOSTED_OR_GOOGLE}, null, null).getResult();
   5894             if (accounts != null && accounts.length > 0) {
   5895                 return accounts[0];
   5896             }
   5897         } catch (Throwable e) {
   5898             Log.e(TAG, "Cannot determine the default account for contacts compatibility", e);
   5899         }
   5900         return null;
   5901     }
   5902 
   5903     /**
   5904      * Returns true if the specified account type is writable.
   5905      */
   5906     protected boolean isWritableAccount(String accountType) {
   5907         if (accountType == null) {
   5908             return true;
   5909         }
   5910 
   5911         Boolean writable = mAccountWritability.get(accountType);
   5912         if (writable != null) {
   5913             return writable;
   5914         }
   5915 
   5916         IContentService contentService = ContentResolver.getContentService();
   5917         try {
   5918             for (SyncAdapterType sync : contentService.getSyncAdapterTypes()) {
   5919                 if (ContactsContract.AUTHORITY.equals(sync.authority) &&
   5920                         accountType.equals(sync.accountType)) {
   5921                     writable = sync.supportsUploading();
   5922                     break;
   5923                 }
   5924             }
   5925         } catch (RemoteException e) {
   5926             Log.e(TAG, "Could not acquire sync adapter types");
   5927         }
   5928 
   5929         if (writable == null) {
   5930             writable = false;
   5931         }
   5932 
   5933         mAccountWritability.put(accountType, writable);
   5934         return writable;
   5935     }
   5936 
   5937     /* package */ static boolean readBooleanQueryParameter(Uri uri, String parameter,
   5938             boolean defaultValue) {
   5939 
   5940         // Manually parse the query, which is much faster than calling uri.getQueryParameter
   5941         String query = uri.getEncodedQuery();
   5942         if (query == null) {
   5943             return defaultValue;
   5944         }
   5945 
   5946         int index = query.indexOf(parameter);
   5947         if (index == -1) {
   5948             return defaultValue;
   5949         }
   5950 
   5951         index += parameter.length();
   5952 
   5953         return !matchQueryParameter(query, index, "=0", false)
   5954                 && !matchQueryParameter(query, index, "=false", true);
   5955     }
   5956 
   5957     private static boolean matchQueryParameter(String query, int index, String value,
   5958             boolean ignoreCase) {
   5959         int length = value.length();
   5960         return query.regionMatches(ignoreCase, index, value, 0, length)
   5961                 && (query.length() == index + length || query.charAt(index + length) == '&');
   5962     }
   5963 
   5964     /**
   5965      * A fast re-implementation of {@link Uri#getQueryParameter}
   5966      */
   5967     /* package */ static String getQueryParameter(Uri uri, String parameter) {
   5968         String query = uri.getEncodedQuery();
   5969         if (query == null) {
   5970             return null;
   5971         }
   5972 
   5973         int queryLength = query.length();
   5974         int parameterLength = parameter.length();
   5975 
   5976         String value;
   5977         int index = 0;
   5978         while (true) {
   5979             index = query.indexOf(parameter, index);
   5980             if (index == -1) {
   5981                 return null;
   5982             }
   5983 
   5984             index += parameterLength;
   5985 
   5986             if (queryLength == index) {
   5987                 return null;
   5988             }
   5989 
   5990             if (query.charAt(index) == '=') {
   5991                 index++;
   5992                 break;
   5993             }
   5994         }
   5995 
   5996         int ampIndex = query.indexOf('&', index);
   5997         if (ampIndex == -1) {
   5998             value = query.substring(index);
   5999         } else {
   6000             value = query.substring(index, ampIndex);
   6001         }
   6002 
   6003         return Uri.decode(value);
   6004     }
   6005 
   6006     private void bindString(SQLiteStatement stmt, int index, String value) {
   6007         if (value == null) {
   6008             stmt.bindNull(index);
   6009         } else {
   6010             stmt.bindString(index, value);
   6011         }
   6012     }
   6013 
   6014     private void bindLong(SQLiteStatement stmt, int index, Number value) {
   6015         if (value == null) {
   6016             stmt.bindNull(index);
   6017         } else {
   6018             stmt.bindLong(index, value.longValue());
   6019         }
   6020     }
   6021 
   6022     protected boolean isAggregationUpgradeNeeded() {
   6023         if (!mContactAggregator.isEnabled()) {
   6024             return false;
   6025         }
   6026 
   6027         int version = Integer.parseInt(mDbHelper.getProperty(PROPERTY_AGGREGATION_ALGORITHM, "2"));
   6028         return version < PROPERTY_AGGREGATION_ALGORITHM_VERSION;
   6029     }
   6030 
   6031     protected void upgradeAggregationAlgorithm() {
   6032         // This upgrade will affect very few contacts, so it can be performed on the
   6033         // main thread during the initial boot after an OTA
   6034 
   6035         Log.i(TAG, "Upgrading aggregation algorithm");
   6036         int count = 0;
   6037         long start = SystemClock.currentThreadTimeMillis();
   6038         try {
   6039             mDb.beginTransaction();
   6040             Cursor cursor = mDb.query(true,
   6041                     Tables.RAW_CONTACTS + " r1 JOIN " + Tables.RAW_CONTACTS + " r2",
   6042                     new String[]{"r1." + RawContacts._ID},
   6043                     "r1." + RawContacts._ID + "!=r2." + RawContacts._ID +
   6044                     " AND r1." + RawContacts.CONTACT_ID + "=r2." + RawContacts.CONTACT_ID +
   6045                     " AND r1." + RawContacts.ACCOUNT_NAME + "=r2." + RawContacts.ACCOUNT_NAME +
   6046                     " AND r1." + RawContacts.ACCOUNT_TYPE + "=r2." + RawContacts.ACCOUNT_TYPE,
   6047                     null, null, null, null, null);
   6048             try {
   6049                 while (cursor.moveToNext()) {
   6050                     long rawContactId = cursor.getLong(0);
   6051                     mContactAggregator.markForAggregation(rawContactId,
   6052                             RawContacts.AGGREGATION_MODE_DEFAULT, true);
   6053                     count++;
   6054                 }
   6055             } finally {
   6056                 cursor.close();
   6057             }
   6058             mContactAggregator.aggregateInTransaction(mDb);
   6059             mDb.setTransactionSuccessful();
   6060             mDbHelper.setProperty(PROPERTY_AGGREGATION_ALGORITHM,
   6061                     String.valueOf(PROPERTY_AGGREGATION_ALGORITHM_VERSION));
   6062         } finally {
   6063             mDb.endTransaction();
   6064             long end = SystemClock.currentThreadTimeMillis();
   6065             Log.i(TAG, "Aggregation algorithm upgraded for " + count
   6066                     + " contacts, in " + (end - start) + "ms");
   6067         }
   6068     }
   6069 }
   6070