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