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 android.accounts.Account;
     20 import android.accounts.AccountManager;
     21 import android.accounts.OnAccountsUpdateListener;
     22 import android.app.SearchManager;
     23 import android.content.ContentProviderOperation;
     24 import android.content.ContentProviderResult;
     25 import android.content.ContentResolver;
     26 import android.content.ContentUris;
     27 import android.content.ContentValues;
     28 import android.content.Context;
     29 import android.content.IContentService;
     30 import android.content.OperationApplicationException;
     31 import android.content.SharedPreferences;
     32 import android.content.SyncAdapterType;
     33 import android.content.UriMatcher;
     34 import android.content.pm.PackageManager;
     35 import android.content.pm.PackageManager.NameNotFoundException;
     36 import android.content.pm.ProviderInfo;
     37 import android.content.res.AssetFileDescriptor;
     38 import android.content.res.Resources;
     39 import android.content.res.Resources.NotFoundException;
     40 import android.database.AbstractCursor;
     41 import android.database.Cursor;
     42 import android.database.DatabaseUtils;
     43 import android.database.MatrixCursor;
     44 import android.database.MatrixCursor.RowBuilder;
     45 import android.database.sqlite.SQLiteDatabase;
     46 import android.database.sqlite.SQLiteDoneException;
     47 import android.database.sqlite.SQLiteQueryBuilder;
     48 import android.graphics.Bitmap;
     49 import android.graphics.BitmapFactory;
     50 import android.net.Uri;
     51 import android.net.Uri.Builder;
     52 import android.os.AsyncTask;
     53 import android.os.Binder;
     54 import android.os.Bundle;
     55 import android.os.CancellationSignal;
     56 import android.os.Handler;
     57 import android.os.HandlerThread;
     58 import android.os.Message;
     59 import android.os.ParcelFileDescriptor;
     60 import android.os.ParcelFileDescriptor.AutoCloseInputStream;
     61 import android.os.Process;
     62 import android.os.RemoteException;
     63 import android.os.StrictMode;
     64 import android.os.SystemClock;
     65 import android.os.SystemProperties;
     66 import android.preference.PreferenceManager;
     67 import android.provider.BaseColumns;
     68 import android.provider.ContactsContract;
     69 import android.provider.ContactsContract.AggregationExceptions;
     70 import android.provider.ContactsContract.Authorization;
     71 import android.provider.ContactsContract.CommonDataKinds.Email;
     72 import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
     73 import android.provider.ContactsContract.CommonDataKinds.Identity;
     74 import android.provider.ContactsContract.CommonDataKinds.Im;
     75 import android.provider.ContactsContract.CommonDataKinds.Nickname;
     76 import android.provider.ContactsContract.CommonDataKinds.Note;
     77 import android.provider.ContactsContract.CommonDataKinds.Organization;
     78 import android.provider.ContactsContract.CommonDataKinds.Phone;
     79 import android.provider.ContactsContract.CommonDataKinds.Photo;
     80 import android.provider.ContactsContract.CommonDataKinds.SipAddress;
     81 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
     82 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
     83 import android.provider.ContactsContract.ContactCounts;
     84 import android.provider.ContactsContract.Contacts;
     85 import android.provider.ContactsContract.Contacts.AggregationSuggestions;
     86 import android.provider.ContactsContract.Data;
     87 import android.provider.ContactsContract.DataUsageFeedback;
     88 import android.provider.ContactsContract.Directory;
     89 import android.provider.ContactsContract.DisplayPhoto;
     90 import android.provider.ContactsContract.Groups;
     91 import android.provider.ContactsContract.PhoneLookup;
     92 import android.provider.ContactsContract.PhotoFiles;
     93 import android.provider.ContactsContract.Profile;
     94 import android.provider.ContactsContract.ProviderStatus;
     95 import android.provider.ContactsContract.RawContacts;
     96 import android.provider.ContactsContract.RawContactsEntity;
     97 import android.provider.ContactsContract.SearchSnippetColumns;
     98 import android.provider.ContactsContract.Settings;
     99 import android.provider.ContactsContract.StatusUpdates;
    100 import android.provider.ContactsContract.StreamItemPhotos;
    101 import android.provider.ContactsContract.StreamItems;
    102 import android.provider.OpenableColumns;
    103 import android.provider.SyncStateContract;
    104 import android.telephony.PhoneNumberUtils;
    105 import android.telephony.TelephonyManager;
    106 import android.text.TextUtils;
    107 import android.util.Log;
    108 
    109 import com.android.common.content.ProjectionMap;
    110 import com.android.common.content.SyncStateContentProviderHelper;
    111 import com.android.providers.contacts.ContactLookupKey.LookupKeySegment;
    112 import com.android.providers.contacts.ContactsDatabaseHelper.AccountsColumns;
    113 import com.android.providers.contacts.ContactsDatabaseHelper.AggregatedPresenceColumns;
    114 import com.android.providers.contacts.ContactsDatabaseHelper.AggregationExceptionColumns;
    115 import com.android.providers.contacts.ContactsDatabaseHelper.Clauses;
    116 import com.android.providers.contacts.ContactsDatabaseHelper.ContactsColumns;
    117 import com.android.providers.contacts.ContactsDatabaseHelper.ContactsStatusUpdatesColumns;
    118 import com.android.providers.contacts.ContactsDatabaseHelper.DataColumns;
    119 import com.android.providers.contacts.ContactsDatabaseHelper.DataUsageStatColumns;
    120 import com.android.providers.contacts.ContactsDatabaseHelper.DbProperties;
    121 import com.android.providers.contacts.ContactsDatabaseHelper.GroupsColumns;
    122 import com.android.providers.contacts.ContactsDatabaseHelper.Joins;
    123 import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupColumns;
    124 import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupType;
    125 import com.android.providers.contacts.ContactsDatabaseHelper.PhoneLookupColumns;
    126 import com.android.providers.contacts.ContactsDatabaseHelper.PhotoFilesColumns;
    127 import com.android.providers.contacts.ContactsDatabaseHelper.PresenceColumns;
    128 import com.android.providers.contacts.ContactsDatabaseHelper.Projections;
    129 import com.android.providers.contacts.ContactsDatabaseHelper.RawContactsColumns;
    130 import com.android.providers.contacts.ContactsDatabaseHelper.SearchIndexColumns;
    131 import com.android.providers.contacts.ContactsDatabaseHelper.SettingsColumns;
    132 import com.android.providers.contacts.ContactsDatabaseHelper.StatusUpdatesColumns;
    133 import com.android.providers.contacts.ContactsDatabaseHelper.StreamItemPhotosColumns;
    134 import com.android.providers.contacts.ContactsDatabaseHelper.StreamItemsColumns;
    135 import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
    136 import com.android.providers.contacts.ContactsDatabaseHelper.ViewGroupsColumns;
    137 import com.android.providers.contacts.ContactsDatabaseHelper.Views;
    138 import com.android.providers.contacts.SearchIndexManager.FtsQueryBuilder;
    139 import com.android.providers.contacts.aggregation.ContactAggregator;
    140 import com.android.providers.contacts.aggregation.ContactAggregator.AggregationSuggestionParameter;
    141 import com.android.providers.contacts.aggregation.ProfileAggregator;
    142 import com.android.providers.contacts.aggregation.util.CommonNicknameCache;
    143 import com.android.providers.contacts.util.Clock;
    144 import com.android.providers.contacts.util.DbQueryUtils;
    145 import com.android.providers.contacts.util.NeededForTesting;
    146 import com.android.vcard.VCardComposer;
    147 import com.android.vcard.VCardConfig;
    148 import com.google.android.collect.Lists;
    149 import com.google.android.collect.Maps;
    150 import com.google.android.collect.Sets;
    151 import com.google.common.annotations.VisibleForTesting;
    152 import com.google.common.base.Preconditions;
    153 
    154 import java.io.BufferedWriter;
    155 import java.io.ByteArrayOutputStream;
    156 import java.io.File;
    157 import java.io.FileDescriptor;
    158 import java.io.FileNotFoundException;
    159 import java.io.IOException;
    160 import java.io.OutputStream;
    161 import java.io.OutputStreamWriter;
    162 import java.io.PrintWriter;
    163 import java.io.Writer;
    164 import java.security.SecureRandom;
    165 import java.text.SimpleDateFormat;
    166 import java.util.ArrayList;
    167 import java.util.Arrays;
    168 import java.util.Collections;
    169 import java.util.Date;
    170 import java.util.HashMap;
    171 import java.util.HashSet;
    172 import java.util.List;
    173 import java.util.Locale;
    174 import java.util.Map;
    175 import java.util.Set;
    176 import java.util.concurrent.CountDownLatch;
    177 
    178 /**
    179  * Contacts content provider. The contract between this provider and applications
    180  * is defined in {@link ContactsContract}.
    181  */
    182 public class ContactsProvider2 extends AbstractContactsProvider
    183         implements OnAccountsUpdateListener {
    184 
    185     private static final int BACKGROUND_TASK_INITIALIZE = 0;
    186     private static final int BACKGROUND_TASK_OPEN_WRITE_ACCESS = 1;
    187     private static final int BACKGROUND_TASK_UPDATE_ACCOUNTS = 3;
    188     private static final int BACKGROUND_TASK_UPDATE_LOCALE = 4;
    189     private static final int BACKGROUND_TASK_UPGRADE_AGGREGATION_ALGORITHM = 5;
    190     private static final int BACKGROUND_TASK_UPDATE_SEARCH_INDEX = 6;
    191     private static final int BACKGROUND_TASK_UPDATE_PROVIDER_STATUS = 7;
    192     private static final int BACKGROUND_TASK_UPDATE_DIRECTORIES = 8;
    193     private static final int BACKGROUND_TASK_CHANGE_LOCALE = 9;
    194     private static final int BACKGROUND_TASK_CLEANUP_PHOTOS = 10;
    195 
    196     /** Default for the maximum number of returned aggregation suggestions. */
    197     private static final int DEFAULT_MAX_SUGGESTIONS = 5;
    198 
    199     /** Limit for the maximum number of social stream items to store under a raw contact. */
    200     private static final int MAX_STREAM_ITEMS_PER_RAW_CONTACT = 5;
    201 
    202     /** Rate limit (in ms) for photo cleanup.  Do it at most once per day. */
    203     private static final int PHOTO_CLEANUP_RATE_LIMIT = 24 * 60 * 60 * 1000;
    204 
    205     /**
    206      * Default expiration duration for pre-authorized URIs.  May be overridden from a secure
    207      * setting.
    208      */
    209     private static final int DEFAULT_PREAUTHORIZED_URI_EXPIRATION = 5 * 60 * 1000;
    210 
    211     /**
    212      * Random URI parameter that will be appended to preauthorized URIs for uniqueness.
    213      */
    214     private static final String PREAUTHORIZED_URI_TOKEN = "perm_token";
    215 
    216     private static final String PREF_LOCALE = "locale";
    217 
    218     private static final int PROPERTY_AGGREGATION_ALGORITHM_VERSION = 3;
    219 
    220     private static final String AGGREGATE_CONTACTS = "sync.contacts.aggregate";
    221 
    222     /**
    223      * If set to "1", we don't remove account data when accounts have been removed.
    224      *
    225      * This should be used sparingly; even though there are data still available, the UI
    226      * don't know anything about them, so they won't show up in the contact filter screen, and
    227      * the contact card/editor may get confused to see unknown custom mimetypes.
    228      *
    229      * We can't spell it out because a property name must be less than 32 chars.
    230      */
    231     private static final String DEBUG_PROPERTY_KEEP_STALE_ACCOUNT_DATA =
    232             "debug.contacts.ksad";
    233 
    234     private static final ProfileAwareUriMatcher sUriMatcher =
    235             new ProfileAwareUriMatcher(UriMatcher.NO_MATCH);
    236 
    237     private static final String FREQUENT_ORDER_BY = DataUsageStatColumns.TIMES_USED + " DESC,"
    238             + Contacts.DISPLAY_NAME + " COLLATE LOCALIZED ASC";
    239 
    240     /* package */ static final String UPDATE_TIMES_CONTACTED_CONTACTS_TABLE =
    241             "UPDATE " + Tables.CONTACTS + " SET " + Contacts.TIMES_CONTACTED + "=" +
    242             " ifnull(" + Contacts.TIMES_CONTACTED + ",0)+1" +
    243             " WHERE " + Contacts._ID + "=?";
    244 
    245     /* package */ static final String UPDATE_TIMES_CONTACTED_RAWCONTACTS_TABLE =
    246             "UPDATE " + Tables.RAW_CONTACTS + " SET " + RawContacts.TIMES_CONTACTED + "=" +
    247             " ifnull(" + RawContacts.TIMES_CONTACTED + ",0)+1 " +
    248             " WHERE " + RawContacts.CONTACT_ID + "=?";
    249 
    250     /* package */ static final String PHONEBOOK_COLLATOR_NAME = "PHONEBOOK";
    251 
    252     // Regex for splitting query strings - we split on any group of non-alphanumeric characters,
    253     // excluding the @ symbol.
    254     /* package */ static final String QUERY_TOKENIZER_REGEX = "[^\\w@]+";
    255 
    256     private static final int CONTACTS = 1000;
    257     private static final int CONTACTS_ID = 1001;
    258     private static final int CONTACTS_LOOKUP = 1002;
    259     private static final int CONTACTS_LOOKUP_ID = 1003;
    260     private static final int CONTACTS_ID_DATA = 1004;
    261     private static final int CONTACTS_FILTER = 1005;
    262     private static final int CONTACTS_STREQUENT = 1006;
    263     private static final int CONTACTS_STREQUENT_FILTER = 1007;
    264     private static final int CONTACTS_GROUP = 1008;
    265     private static final int CONTACTS_ID_PHOTO = 1009;
    266     private static final int CONTACTS_LOOKUP_PHOTO = 1010;
    267     private static final int CONTACTS_LOOKUP_ID_PHOTO = 1011;
    268     private static final int CONTACTS_ID_DISPLAY_PHOTO = 1012;
    269     private static final int CONTACTS_LOOKUP_DISPLAY_PHOTO = 1013;
    270     private static final int CONTACTS_LOOKUP_ID_DISPLAY_PHOTO = 1014;
    271     private static final int CONTACTS_AS_VCARD = 1015;
    272     private static final int CONTACTS_AS_MULTI_VCARD = 1016;
    273     private static final int CONTACTS_LOOKUP_DATA = 1017;
    274     private static final int CONTACTS_LOOKUP_ID_DATA = 1018;
    275     private static final int CONTACTS_ID_ENTITIES = 1019;
    276     private static final int CONTACTS_LOOKUP_ENTITIES = 1020;
    277     private static final int CONTACTS_LOOKUP_ID_ENTITIES = 1021;
    278     private static final int CONTACTS_ID_STREAM_ITEMS = 1022;
    279     private static final int CONTACTS_LOOKUP_STREAM_ITEMS = 1023;
    280     private static final int CONTACTS_LOOKUP_ID_STREAM_ITEMS = 1024;
    281     private static final int CONTACTS_FREQUENT = 1025;
    282     private static final int CONTACTS_DELETE_USAGE = 1026;
    283 
    284     private static final int RAW_CONTACTS = 2002;
    285     private static final int RAW_CONTACTS_ID = 2003;
    286     private static final int RAW_CONTACTS_ID_DATA = 2004;
    287     private static final int RAW_CONTACT_ID_ENTITY = 2005;
    288     private static final int RAW_CONTACTS_ID_DISPLAY_PHOTO = 2006;
    289     private static final int RAW_CONTACTS_ID_STREAM_ITEMS = 2007;
    290     private static final int RAW_CONTACTS_ID_STREAM_ITEMS_ID = 2008;
    291 
    292     private static final int DATA = 3000;
    293     private static final int DATA_ID = 3001;
    294     private static final int PHONES = 3002;
    295     private static final int PHONES_ID = 3003;
    296     private static final int PHONES_FILTER = 3004;
    297     private static final int EMAILS = 3005;
    298     private static final int EMAILS_ID = 3006;
    299     private static final int EMAILS_LOOKUP = 3007;
    300     private static final int EMAILS_FILTER = 3008;
    301     private static final int POSTALS = 3009;
    302     private static final int POSTALS_ID = 3010;
    303     private static final int CALLABLES = 3011;
    304     private static final int CALLABLES_ID = 3012;
    305     private static final int CALLABLES_FILTER = 3013;
    306 
    307     private static final int PHONE_LOOKUP = 4000;
    308 
    309     private static final int AGGREGATION_EXCEPTIONS = 6000;
    310     private static final int AGGREGATION_EXCEPTION_ID = 6001;
    311 
    312     private static final int STATUS_UPDATES = 7000;
    313     private static final int STATUS_UPDATES_ID = 7001;
    314 
    315     private static final int AGGREGATION_SUGGESTIONS = 8000;
    316 
    317     private static final int SETTINGS = 9000;
    318 
    319     private static final int GROUPS = 10000;
    320     private static final int GROUPS_ID = 10001;
    321     private static final int GROUPS_SUMMARY = 10003;
    322 
    323     private static final int SYNCSTATE = 11000;
    324     private static final int SYNCSTATE_ID = 11001;
    325     private static final int PROFILE_SYNCSTATE = 11002;
    326     private static final int PROFILE_SYNCSTATE_ID = 11003;
    327 
    328     private static final int SEARCH_SUGGESTIONS = 12001;
    329     private static final int SEARCH_SHORTCUT = 12002;
    330 
    331     private static final int RAW_CONTACT_ENTITIES = 15001;
    332 
    333     private static final int PROVIDER_STATUS = 16001;
    334 
    335     private static final int DIRECTORIES = 17001;
    336     private static final int DIRECTORIES_ID = 17002;
    337 
    338     private static final int COMPLETE_NAME = 18000;
    339 
    340     private static final int PROFILE = 19000;
    341     private static final int PROFILE_ENTITIES = 19001;
    342     private static final int PROFILE_DATA = 19002;
    343     private static final int PROFILE_DATA_ID = 19003;
    344     private static final int PROFILE_AS_VCARD = 19004;
    345     private static final int PROFILE_RAW_CONTACTS = 19005;
    346     private static final int PROFILE_RAW_CONTACTS_ID = 19006;
    347     private static final int PROFILE_RAW_CONTACTS_ID_DATA = 19007;
    348     private static final int PROFILE_RAW_CONTACTS_ID_ENTITIES = 19008;
    349     private static final int PROFILE_STATUS_UPDATES = 19009;
    350     private static final int PROFILE_RAW_CONTACT_ENTITIES = 19010;
    351     private static final int PROFILE_PHOTO = 19011;
    352     private static final int PROFILE_DISPLAY_PHOTO = 19012;
    353 
    354     private static final int DATA_USAGE_FEEDBACK_ID = 20001;
    355 
    356     private static final int STREAM_ITEMS = 21000;
    357     private static final int STREAM_ITEMS_PHOTOS = 21001;
    358     private static final int STREAM_ITEMS_ID = 21002;
    359     private static final int STREAM_ITEMS_ID_PHOTOS = 21003;
    360     private static final int STREAM_ITEMS_ID_PHOTOS_ID = 21004;
    361     private static final int STREAM_ITEMS_LIMIT = 21005;
    362 
    363     private static final int DISPLAY_PHOTO_ID = 22000;
    364     private static final int PHOTO_DIMENSIONS = 22001;
    365 
    366     // Inserts into URIs in this map will direct to the profile database if the parent record's
    367     // value (looked up from the ContentValues object with the key specified by the value in this
    368     // map) is in the profile ID-space (see {@link ProfileDatabaseHelper#PROFILE_ID_SPACE}).
    369     private static final Map<Integer, String> INSERT_URI_ID_VALUE_MAP = Maps.newHashMap();
    370     static {
    371         INSERT_URI_ID_VALUE_MAP.put(DATA, Data.RAW_CONTACT_ID);
    372         INSERT_URI_ID_VALUE_MAP.put(RAW_CONTACTS_ID_DATA, Data.RAW_CONTACT_ID);
    373         INSERT_URI_ID_VALUE_MAP.put(STATUS_UPDATES, StatusUpdates.DATA_ID);
    374         INSERT_URI_ID_VALUE_MAP.put(STREAM_ITEMS, StreamItems.RAW_CONTACT_ID);
    375         INSERT_URI_ID_VALUE_MAP.put(RAW_CONTACTS_ID_STREAM_ITEMS, StreamItems.RAW_CONTACT_ID);
    376         INSERT_URI_ID_VALUE_MAP.put(STREAM_ITEMS_PHOTOS, StreamItemPhotos.STREAM_ITEM_ID);
    377         INSERT_URI_ID_VALUE_MAP.put(STREAM_ITEMS_ID_PHOTOS, StreamItemPhotos.STREAM_ITEM_ID);
    378     }
    379 
    380     // Any interactions that involve these URIs will also require the calling package to have either
    381     // android.permission.READ_SOCIAL_STREAM permission or android.permission.WRITE_SOCIAL_STREAM
    382     // permission, depending on the type of operation being performed.
    383     private static final List<Integer> SOCIAL_STREAM_URIS = Lists.newArrayList(
    384             CONTACTS_ID_STREAM_ITEMS,
    385             CONTACTS_LOOKUP_STREAM_ITEMS,
    386             CONTACTS_LOOKUP_ID_STREAM_ITEMS,
    387             RAW_CONTACTS_ID_STREAM_ITEMS,
    388             RAW_CONTACTS_ID_STREAM_ITEMS_ID,
    389             STREAM_ITEMS,
    390             STREAM_ITEMS_PHOTOS,
    391             STREAM_ITEMS_ID,
    392             STREAM_ITEMS_ID_PHOTOS,
    393             STREAM_ITEMS_ID_PHOTOS_ID
    394     );
    395 
    396     private static final String SELECTION_FAVORITES_GROUPS_BY_RAW_CONTACT_ID =
    397             RawContactsColumns.CONCRETE_ID + "=? AND "
    398                 + GroupsColumns.CONCRETE_ACCOUNT_ID + "=" + RawContactsColumns.CONCRETE_ACCOUNT_ID
    399                 + " AND " + Groups.FAVORITES + " != 0";
    400 
    401     private static final String SELECTION_AUTO_ADD_GROUPS_BY_RAW_CONTACT_ID =
    402             RawContactsColumns.CONCRETE_ID + "=? AND "
    403                 + GroupsColumns.CONCRETE_ACCOUNT_ID + "=" + RawContactsColumns.CONCRETE_ACCOUNT_ID
    404                 + " AND " + Groups.AUTO_ADD + " != 0";
    405 
    406     private static final String[] PROJECTION_GROUP_ID
    407             = new String[]{Tables.GROUPS + "." + Groups._ID};
    408 
    409     private static final String SELECTION_GROUPMEMBERSHIP_DATA = DataColumns.MIMETYPE_ID + "=? "
    410             + "AND " + GroupMembership.GROUP_ROW_ID + "=? "
    411             + "AND " + GroupMembership.RAW_CONTACT_ID + "=?";
    412 
    413     private static final String SELECTION_STARRED_FROM_RAW_CONTACTS =
    414             "SELECT " + RawContacts.STARRED
    415                     + " FROM " + Tables.RAW_CONTACTS + " WHERE " + RawContacts._ID + "=?";
    416 
    417     private interface DataContactsQuery {
    418         public static final String TABLE = "data "
    419                 + "JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id) "
    420                 + "JOIN " + Tables.ACCOUNTS + " ON ("
    421                     + AccountsColumns.CONCRETE_ID + "=" + RawContactsColumns.CONCRETE_ACCOUNT_ID
    422                     + ")"
    423                 + "JOIN contacts ON (raw_contacts.contact_id = contacts._id)";
    424 
    425         public static final String[] PROJECTION = new String[] {
    426             RawContactsColumns.CONCRETE_ID,
    427             AccountsColumns.CONCRETE_ACCOUNT_TYPE,
    428             AccountsColumns.CONCRETE_ACCOUNT_NAME,
    429             AccountsColumns.CONCRETE_DATA_SET,
    430             DataColumns.CONCRETE_ID,
    431             ContactsColumns.CONCRETE_ID
    432         };
    433 
    434         public static final int RAW_CONTACT_ID = 0;
    435         public static final int ACCOUNT_TYPE = 1;
    436         public static final int ACCOUNT_NAME = 2;
    437         public static final int DATA_SET = 3;
    438         public static final int DATA_ID = 4;
    439         public static final int CONTACT_ID = 5;
    440     }
    441 
    442     interface RawContactsQuery {
    443         String TABLE = Tables.RAW_CONTACTS_JOIN_ACCOUNTS;
    444 
    445         String[] COLUMNS = new String[] {
    446                 RawContacts.DELETED,
    447                 RawContactsColumns.ACCOUNT_ID,
    448                 AccountsColumns.CONCRETE_ACCOUNT_TYPE,
    449                 AccountsColumns.CONCRETE_ACCOUNT_NAME,
    450                 AccountsColumns.CONCRETE_DATA_SET,
    451         };
    452 
    453         int DELETED = 0;
    454         int ACCOUNT_ID = 1;
    455         int ACCOUNT_TYPE = 2;
    456         int ACCOUNT_NAME = 3;
    457         int DATA_SET = 4;
    458     }
    459 
    460     public static final String DEFAULT_ACCOUNT_TYPE = "com.google";
    461 
    462     /** Sql where statement for filtering on groups. */
    463     private static final String CONTACTS_IN_GROUP_SELECT =
    464             Contacts._ID + " IN "
    465                     + "(SELECT " + RawContacts.CONTACT_ID
    466                     + " FROM " + Tables.RAW_CONTACTS
    467                     + " WHERE " + RawContactsColumns.CONCRETE_ID + " IN "
    468                             + "(SELECT " + DataColumns.CONCRETE_RAW_CONTACT_ID
    469                             + " FROM " + Tables.DATA_JOIN_MIMETYPES
    470                             + " WHERE " + DataColumns.MIMETYPE_ID + "=?"
    471                                     + " AND " + GroupMembership.GROUP_ROW_ID + "="
    472                                     + "(SELECT " + Tables.GROUPS + "." + Groups._ID
    473                                     + " FROM " + Tables.GROUPS
    474                                     + " WHERE " + Groups.TITLE + "=?)))";
    475 
    476     /** Sql for updating DIRTY flag on multiple raw contacts */
    477     private static final String UPDATE_RAW_CONTACT_SET_DIRTY_SQL =
    478             "UPDATE " + Tables.RAW_CONTACTS +
    479             " SET " + RawContacts.DIRTY + "=1" +
    480             " WHERE " + RawContacts._ID + " IN (";
    481 
    482     /** Sql for updating VERSION on multiple raw contacts */
    483     private static final String UPDATE_RAW_CONTACT_SET_VERSION_SQL =
    484             "UPDATE " + Tables.RAW_CONTACTS +
    485             " SET " + RawContacts.VERSION + " = " + RawContacts.VERSION + " + 1" +
    486             " WHERE " + RawContacts._ID + " IN (";
    487 
    488     // Current contacts - those contacted within the last 3 days (in seconds)
    489     private static final long LAST_TIME_USED_CURRENT_SEC = 3 * 24 * 60 * 60;
    490 
    491     // Recent contacts - those contacted within the last 30 days (in seconds)
    492     private static final long LAST_TIME_USED_RECENT_SEC = 30 * 24 * 60 * 60;
    493 
    494     private static final String TIME_SINCE_LAST_USED_SEC =
    495             "(strftime('%s', 'now') - " + DataUsageStatColumns.LAST_TIME_USED + "/1000)";
    496 
    497     private static final String SORT_BY_DATA_USAGE =
    498             "(CASE WHEN " + TIME_SINCE_LAST_USED_SEC + " < " + LAST_TIME_USED_CURRENT_SEC +
    499             " THEN 0 " +
    500                     " WHEN " + TIME_SINCE_LAST_USED_SEC + " < " + LAST_TIME_USED_RECENT_SEC +
    501             " THEN 1 " +
    502             " ELSE 2 END), " +
    503             DataUsageStatColumns.TIMES_USED + " DESC";
    504 
    505     /*
    506      * Sorting order for email address suggestions: first starred, then the rest.
    507      * Within the two groups:
    508      * - three buckets: very recently contacted, then fairly recently contacted, then the rest.
    509      * Within each of the bucket - descending count of times contacted (both for data row and for
    510      * contact row).
    511      * If all else fails, in_visible_group, alphabetical.
    512      * (Super)primary email address is returned before other addresses for the same contact.
    513      */
    514     private static final String EMAIL_FILTER_SORT_ORDER =
    515         Contacts.STARRED + " DESC, "
    516         + SORT_BY_DATA_USAGE + ", "
    517         + Contacts.IN_VISIBLE_GROUP + " DESC, "
    518         + Contacts.DISPLAY_NAME + ", "
    519         + Data.CONTACT_ID + ", "
    520         + Data.IS_SUPER_PRIMARY + " DESC, "
    521         + Data.IS_PRIMARY + " DESC";
    522 
    523     /** Currently same as {@link #EMAIL_FILTER_SORT_ORDER} */
    524     private static final String PHONE_FILTER_SORT_ORDER = EMAIL_FILTER_SORT_ORDER;
    525 
    526     /** Name lookup types used for contact filtering */
    527     private static final String CONTACT_LOOKUP_NAME_TYPES =
    528             NameLookupType.NAME_COLLATION_KEY + "," +
    529             NameLookupType.EMAIL_BASED_NICKNAME + "," +
    530             NameLookupType.NICKNAME;
    531 
    532     /**
    533      * If any of these columns are used in a Data projection, there is no point in
    534      * using the DISTINCT keyword, which can negatively affect performance.
    535      */
    536     private static final String[] DISTINCT_DATA_PROHIBITING_COLUMNS = {
    537             Data._ID,
    538             Data.RAW_CONTACT_ID,
    539             Data.NAME_RAW_CONTACT_ID,
    540             RawContacts.ACCOUNT_NAME,
    541             RawContacts.ACCOUNT_TYPE,
    542             RawContacts.DATA_SET,
    543             RawContacts.ACCOUNT_TYPE_AND_DATA_SET,
    544             RawContacts.DIRTY,
    545             RawContacts.NAME_VERIFIED,
    546             RawContacts.SOURCE_ID,
    547             RawContacts.VERSION,
    548     };
    549 
    550     private static final ProjectionMap sContactsColumns = ProjectionMap.builder()
    551             .add(Contacts.CUSTOM_RINGTONE)
    552             .add(Contacts.DISPLAY_NAME)
    553             .add(Contacts.DISPLAY_NAME_ALTERNATIVE)
    554             .add(Contacts.DISPLAY_NAME_SOURCE)
    555             .add(Contacts.IN_VISIBLE_GROUP)
    556             .add(Contacts.LAST_TIME_CONTACTED)
    557             .add(Contacts.LOOKUP_KEY)
    558             .add(Contacts.PHONETIC_NAME)
    559             .add(Contacts.PHONETIC_NAME_STYLE)
    560             .add(Contacts.PHOTO_ID)
    561             .add(Contacts.PHOTO_FILE_ID)
    562             .add(Contacts.PHOTO_URI)
    563             .add(Contacts.PHOTO_THUMBNAIL_URI)
    564             .add(Contacts.SEND_TO_VOICEMAIL)
    565             .add(Contacts.SORT_KEY_ALTERNATIVE)
    566             .add(Contacts.SORT_KEY_PRIMARY)
    567             .add(Contacts.STARRED)
    568             .add(Contacts.TIMES_CONTACTED)
    569             .add(Contacts.HAS_PHONE_NUMBER)
    570             .build();
    571 
    572     private static final ProjectionMap sContactsPresenceColumns = ProjectionMap.builder()
    573             .add(Contacts.CONTACT_PRESENCE,
    574                     Tables.AGGREGATED_PRESENCE + "." + StatusUpdates.PRESENCE)
    575             .add(Contacts.CONTACT_CHAT_CAPABILITY,
    576                     Tables.AGGREGATED_PRESENCE + "." + StatusUpdates.CHAT_CAPABILITY)
    577             .add(Contacts.CONTACT_STATUS,
    578                     ContactsStatusUpdatesColumns.CONCRETE_STATUS)
    579             .add(Contacts.CONTACT_STATUS_TIMESTAMP,
    580                     ContactsStatusUpdatesColumns.CONCRETE_STATUS_TIMESTAMP)
    581             .add(Contacts.CONTACT_STATUS_RES_PACKAGE,
    582                     ContactsStatusUpdatesColumns.CONCRETE_STATUS_RES_PACKAGE)
    583             .add(Contacts.CONTACT_STATUS_LABEL,
    584                     ContactsStatusUpdatesColumns.CONCRETE_STATUS_LABEL)
    585             .add(Contacts.CONTACT_STATUS_ICON,
    586                     ContactsStatusUpdatesColumns.CONCRETE_STATUS_ICON)
    587             .build();
    588 
    589     private static final ProjectionMap sSnippetColumns = ProjectionMap.builder()
    590             .add(SearchSnippetColumns.SNIPPET)
    591             .build();
    592 
    593     private static final ProjectionMap sRawContactColumns = ProjectionMap.builder()
    594             .add(RawContacts.ACCOUNT_NAME)
    595             .add(RawContacts.ACCOUNT_TYPE)
    596             .add(RawContacts.DATA_SET)
    597             .add(RawContacts.ACCOUNT_TYPE_AND_DATA_SET)
    598             .add(RawContacts.DIRTY)
    599             .add(RawContacts.NAME_VERIFIED)
    600             .add(RawContacts.SOURCE_ID)
    601             .add(RawContacts.VERSION)
    602             .build();
    603 
    604     private static final ProjectionMap sRawContactSyncColumns = ProjectionMap.builder()
    605             .add(RawContacts.SYNC1)
    606             .add(RawContacts.SYNC2)
    607             .add(RawContacts.SYNC3)
    608             .add(RawContacts.SYNC4)
    609             .build();
    610 
    611     private static final ProjectionMap sDataColumns = ProjectionMap.builder()
    612             .add(Data.DATA1)
    613             .add(Data.DATA2)
    614             .add(Data.DATA3)
    615             .add(Data.DATA4)
    616             .add(Data.DATA5)
    617             .add(Data.DATA6)
    618             .add(Data.DATA7)
    619             .add(Data.DATA8)
    620             .add(Data.DATA9)
    621             .add(Data.DATA10)
    622             .add(Data.DATA11)
    623             .add(Data.DATA12)
    624             .add(Data.DATA13)
    625             .add(Data.DATA14)
    626             .add(Data.DATA15)
    627             .add(Data.DATA_VERSION)
    628             .add(Data.IS_PRIMARY)
    629             .add(Data.IS_SUPER_PRIMARY)
    630             .add(Data.MIMETYPE)
    631             .add(Data.RES_PACKAGE)
    632             .add(Data.SYNC1)
    633             .add(Data.SYNC2)
    634             .add(Data.SYNC3)
    635             .add(Data.SYNC4)
    636             .add(GroupMembership.GROUP_SOURCE_ID)
    637             .build();
    638 
    639     private static final ProjectionMap sContactPresenceColumns = ProjectionMap.builder()
    640             .add(Contacts.CONTACT_PRESENCE,
    641                     Tables.AGGREGATED_PRESENCE + '.' + StatusUpdates.PRESENCE)
    642             .add(Contacts.CONTACT_CHAT_CAPABILITY,
    643                     Tables.AGGREGATED_PRESENCE + '.' + StatusUpdates.CHAT_CAPABILITY)
    644             .add(Contacts.CONTACT_STATUS,
    645                     ContactsStatusUpdatesColumns.CONCRETE_STATUS)
    646             .add(Contacts.CONTACT_STATUS_TIMESTAMP,
    647                     ContactsStatusUpdatesColumns.CONCRETE_STATUS_TIMESTAMP)
    648             .add(Contacts.CONTACT_STATUS_RES_PACKAGE,
    649                     ContactsStatusUpdatesColumns.CONCRETE_STATUS_RES_PACKAGE)
    650             .add(Contacts.CONTACT_STATUS_LABEL,
    651                     ContactsStatusUpdatesColumns.CONCRETE_STATUS_LABEL)
    652             .add(Contacts.CONTACT_STATUS_ICON,
    653                     ContactsStatusUpdatesColumns.CONCRETE_STATUS_ICON)
    654             .build();
    655 
    656     private static final ProjectionMap sDataPresenceColumns = ProjectionMap.builder()
    657             .add(Data.PRESENCE, Tables.PRESENCE + "." + StatusUpdates.PRESENCE)
    658             .add(Data.CHAT_CAPABILITY, Tables.PRESENCE + "." + StatusUpdates.CHAT_CAPABILITY)
    659             .add(Data.STATUS, StatusUpdatesColumns.CONCRETE_STATUS)
    660             .add(Data.STATUS_TIMESTAMP, StatusUpdatesColumns.CONCRETE_STATUS_TIMESTAMP)
    661             .add(Data.STATUS_RES_PACKAGE, StatusUpdatesColumns.CONCRETE_STATUS_RES_PACKAGE)
    662             .add(Data.STATUS_LABEL, StatusUpdatesColumns.CONCRETE_STATUS_LABEL)
    663             .add(Data.STATUS_ICON, StatusUpdatesColumns.CONCRETE_STATUS_ICON)
    664             .build();
    665 
    666     /** Contains just BaseColumns._COUNT */
    667     private static final ProjectionMap sCountProjectionMap = ProjectionMap.builder()
    668             .add(BaseColumns._COUNT, "COUNT(*)")
    669             .build();
    670 
    671     /** Contains just the contacts columns */
    672     private static final ProjectionMap sContactsProjectionMap = ProjectionMap.builder()
    673             .add(Contacts._ID)
    674             .add(Contacts.HAS_PHONE_NUMBER)
    675             .add(Contacts.NAME_RAW_CONTACT_ID)
    676             .add(Contacts.IS_USER_PROFILE)
    677             .addAll(sContactsColumns)
    678             .addAll(sContactsPresenceColumns)
    679             .build();
    680 
    681     /** Contains just the contacts columns */
    682     private static final ProjectionMap sContactsProjectionWithSnippetMap = ProjectionMap.builder()
    683             .addAll(sContactsProjectionMap)
    684             .addAll(sSnippetColumns)
    685             .build();
    686 
    687     /** Used for pushing starred contacts to the top of a times contacted list **/
    688     private static final ProjectionMap sStrequentStarredProjectionMap = ProjectionMap.builder()
    689             .addAll(sContactsProjectionMap)
    690             .add(DataUsageStatColumns.TIMES_USED, String.valueOf(Long.MAX_VALUE))
    691             .add(DataUsageStatColumns.LAST_TIME_USED, String.valueOf(Long.MAX_VALUE))
    692             .build();
    693 
    694     private static final ProjectionMap sStrequentFrequentProjectionMap = ProjectionMap.builder()
    695             .addAll(sContactsProjectionMap)
    696             .add(DataUsageStatColumns.TIMES_USED,
    697                     "SUM(" + DataUsageStatColumns.CONCRETE_TIMES_USED + ")")
    698             .add(DataUsageStatColumns.LAST_TIME_USED,
    699                     "MAX(" + DataUsageStatColumns.CONCRETE_LAST_TIME_USED + ")")
    700             .build();
    701 
    702     /**
    703      * Used for Strequent Uri with {@link ContactsContract#STREQUENT_PHONE_ONLY}, which allows
    704      * users to obtain part of Data columns. Right now Starred part just returns NULL for
    705      * those data columns (frequent part should return real ones in data table).
    706      **/
    707     private static final ProjectionMap sStrequentPhoneOnlyStarredProjectionMap
    708             = ProjectionMap.builder()
    709             .addAll(sContactsProjectionMap)
    710             .add(DataUsageStatColumns.TIMES_USED,
    711                     String.valueOf(Long.MAX_VALUE))
    712             .add(DataUsageStatColumns.LAST_TIME_USED, String.valueOf(Long.MAX_VALUE))
    713             .add(Phone.NUMBER, "NULL")
    714             .add(Phone.TYPE, "NULL")
    715             .add(Phone.LABEL, "NULL")
    716             .build();
    717 
    718     /**
    719      * Used for Strequent Uri with {@link ContactsContract#STREQUENT_PHONE_ONLY}, which allows
    720      * users to obtain part of Data columns. We hard-code {@link Contacts#IS_USER_PROFILE} to NULL,
    721      * because sContactsProjectionMap specifies a field that doesn't exist in the view behind the
    722      * query that uses this projection map.
    723      **/
    724     private static final ProjectionMap sStrequentPhoneOnlyFrequentProjectionMap
    725             = ProjectionMap.builder()
    726             .addAll(sContactsProjectionMap)
    727             .add(DataUsageStatColumns.TIMES_USED, DataUsageStatColumns.CONCRETE_TIMES_USED)
    728             .add(DataUsageStatColumns.LAST_TIME_USED, DataUsageStatColumns.CONCRETE_LAST_TIME_USED)
    729             .add(Phone.NUMBER)
    730             .add(Phone.TYPE)
    731             .add(Phone.LABEL)
    732             .add(Contacts.IS_USER_PROFILE, "NULL")
    733             .build();
    734 
    735     /** Contains just the contacts vCard columns */
    736     private static final ProjectionMap sContactsVCardProjectionMap = ProjectionMap.builder()
    737             .add(Contacts._ID)
    738             .add(OpenableColumns.DISPLAY_NAME, Contacts.DISPLAY_NAME + " || '.vcf'")
    739             .add(OpenableColumns.SIZE, "NULL")
    740             .build();
    741 
    742     /** Contains just the raw contacts columns */
    743     private static final ProjectionMap sRawContactsProjectionMap = ProjectionMap.builder()
    744             .add(RawContacts._ID)
    745             .add(RawContacts.CONTACT_ID)
    746             .add(RawContacts.DELETED)
    747             .add(RawContacts.DISPLAY_NAME_PRIMARY)
    748             .add(RawContacts.DISPLAY_NAME_ALTERNATIVE)
    749             .add(RawContacts.DISPLAY_NAME_SOURCE)
    750             .add(RawContacts.PHONETIC_NAME)
    751             .add(RawContacts.PHONETIC_NAME_STYLE)
    752             .add(RawContacts.SORT_KEY_PRIMARY)
    753             .add(RawContacts.SORT_KEY_ALTERNATIVE)
    754             .add(RawContacts.TIMES_CONTACTED)
    755             .add(RawContacts.LAST_TIME_CONTACTED)
    756             .add(RawContacts.CUSTOM_RINGTONE)
    757             .add(RawContacts.SEND_TO_VOICEMAIL)
    758             .add(RawContacts.STARRED)
    759             .add(RawContacts.AGGREGATION_MODE)
    760             .add(RawContacts.RAW_CONTACT_IS_USER_PROFILE)
    761             .addAll(sRawContactColumns)
    762             .addAll(sRawContactSyncColumns)
    763             .build();
    764 
    765     /** Contains the columns from the raw entity view*/
    766     private static final ProjectionMap sRawEntityProjectionMap = ProjectionMap.builder()
    767             .add(RawContacts._ID)
    768             .add(RawContacts.CONTACT_ID)
    769             .add(RawContacts.Entity.DATA_ID)
    770             .add(RawContacts.DELETED)
    771             .add(RawContacts.STARRED)
    772             .add(RawContacts.RAW_CONTACT_IS_USER_PROFILE)
    773             .addAll(sRawContactColumns)
    774             .addAll(sRawContactSyncColumns)
    775             .addAll(sDataColumns)
    776             .build();
    777 
    778     /** Contains the columns from the contact entity view*/
    779     private static final ProjectionMap sEntityProjectionMap = ProjectionMap.builder()
    780             .add(Contacts.Entity._ID)
    781             .add(Contacts.Entity.CONTACT_ID)
    782             .add(Contacts.Entity.RAW_CONTACT_ID)
    783             .add(Contacts.Entity.DATA_ID)
    784             .add(Contacts.Entity.NAME_RAW_CONTACT_ID)
    785             .add(Contacts.Entity.DELETED)
    786             .add(Contacts.IS_USER_PROFILE)
    787             .addAll(sContactsColumns)
    788             .addAll(sContactPresenceColumns)
    789             .addAll(sRawContactColumns)
    790             .addAll(sRawContactSyncColumns)
    791             .addAll(sDataColumns)
    792             .addAll(sDataPresenceColumns)
    793             .build();
    794 
    795     /** Contains columns in PhoneLookup which are not contained in the data view. */
    796     private static final ProjectionMap sSipLookupColumns = ProjectionMap.builder()
    797             .add(PhoneLookup.NUMBER, SipAddress.SIP_ADDRESS)
    798             .add(PhoneLookup.TYPE, "0")
    799             .add(PhoneLookup.LABEL, "NULL")
    800             .add(PhoneLookup.NORMALIZED_NUMBER, "NULL")
    801             .build();
    802 
    803     /** Contains columns from the data view */
    804     private static final ProjectionMap sDataProjectionMap = ProjectionMap.builder()
    805             .add(Data._ID)
    806             .add(Data.RAW_CONTACT_ID)
    807             .add(Data.CONTACT_ID)
    808             .add(Data.NAME_RAW_CONTACT_ID)
    809             .add(RawContacts.RAW_CONTACT_IS_USER_PROFILE)
    810             .addAll(sDataColumns)
    811             .addAll(sDataPresenceColumns)
    812             .addAll(sRawContactColumns)
    813             .addAll(sContactsColumns)
    814             .addAll(sContactPresenceColumns)
    815             .build();
    816 
    817     /** Contains columns from the data view used for SIP address lookup. */
    818     private static final ProjectionMap sDataSipLookupProjectionMap = ProjectionMap.builder()
    819             .addAll(sDataProjectionMap)
    820             .addAll(sSipLookupColumns)
    821             .build();
    822 
    823     /** Contains columns from the data view */
    824     private static final ProjectionMap sDistinctDataProjectionMap = ProjectionMap.builder()
    825             .add(Data._ID, "MIN(" + Data._ID + ")")
    826             .add(RawContacts.CONTACT_ID)
    827             .add(RawContacts.RAW_CONTACT_IS_USER_PROFILE)
    828             .addAll(sDataColumns)
    829             .addAll(sDataPresenceColumns)
    830             .addAll(sContactsColumns)
    831             .addAll(sContactPresenceColumns)
    832             .build();
    833 
    834     /** Contains columns from the data view used for SIP address lookup. */
    835     private static final ProjectionMap sDistinctDataSipLookupProjectionMap = ProjectionMap.builder()
    836             .addAll(sDistinctDataProjectionMap)
    837             .addAll(sSipLookupColumns)
    838             .build();
    839 
    840     /** Contains the data and contacts columns, for joined tables */
    841     private static final ProjectionMap sPhoneLookupProjectionMap = ProjectionMap.builder()
    842             .add(PhoneLookup._ID, "contacts_view." + Contacts._ID)
    843             .add(PhoneLookup.LOOKUP_KEY, "contacts_view." + Contacts.LOOKUP_KEY)
    844             .add(PhoneLookup.DISPLAY_NAME, "contacts_view." + Contacts.DISPLAY_NAME)
    845             .add(PhoneLookup.LAST_TIME_CONTACTED, "contacts_view." + Contacts.LAST_TIME_CONTACTED)
    846             .add(PhoneLookup.TIMES_CONTACTED, "contacts_view." + Contacts.TIMES_CONTACTED)
    847             .add(PhoneLookup.STARRED, "contacts_view." + Contacts.STARRED)
    848             .add(PhoneLookup.IN_VISIBLE_GROUP, "contacts_view." + Contacts.IN_VISIBLE_GROUP)
    849             .add(PhoneLookup.PHOTO_ID, "contacts_view." + Contacts.PHOTO_ID)
    850             .add(PhoneLookup.PHOTO_URI, "contacts_view." + Contacts.PHOTO_URI)
    851             .add(PhoneLookup.PHOTO_THUMBNAIL_URI, "contacts_view." + Contacts.PHOTO_THUMBNAIL_URI)
    852             .add(PhoneLookup.CUSTOM_RINGTONE, "contacts_view." + Contacts.CUSTOM_RINGTONE)
    853             .add(PhoneLookup.HAS_PHONE_NUMBER, "contacts_view." + Contacts.HAS_PHONE_NUMBER)
    854             .add(PhoneLookup.SEND_TO_VOICEMAIL, "contacts_view." + Contacts.SEND_TO_VOICEMAIL)
    855             .add(PhoneLookup.NUMBER, Phone.NUMBER)
    856             .add(PhoneLookup.TYPE, Phone.TYPE)
    857             .add(PhoneLookup.LABEL, Phone.LABEL)
    858             .add(PhoneLookup.NORMALIZED_NUMBER, Phone.NORMALIZED_NUMBER)
    859             .build();
    860 
    861     /** Contains the just the {@link Groups} columns */
    862     private static final ProjectionMap sGroupsProjectionMap = ProjectionMap.builder()
    863             .add(Groups._ID)
    864             .add(Groups.ACCOUNT_NAME)
    865             .add(Groups.ACCOUNT_TYPE)
    866             .add(Groups.DATA_SET)
    867             .add(Groups.ACCOUNT_TYPE_AND_DATA_SET)
    868             .add(Groups.SOURCE_ID)
    869             .add(Groups.DIRTY)
    870             .add(Groups.VERSION)
    871             .add(Groups.RES_PACKAGE)
    872             .add(Groups.TITLE)
    873             .add(Groups.TITLE_RES)
    874             .add(Groups.GROUP_VISIBLE)
    875             .add(Groups.SYSTEM_ID)
    876             .add(Groups.DELETED)
    877             .add(Groups.NOTES)
    878             .add(Groups.SHOULD_SYNC)
    879             .add(Groups.FAVORITES)
    880             .add(Groups.AUTO_ADD)
    881             .add(Groups.GROUP_IS_READ_ONLY)
    882             .add(Groups.SYNC1)
    883             .add(Groups.SYNC2)
    884             .add(Groups.SYNC3)
    885             .add(Groups.SYNC4)
    886             .build();
    887 
    888     /**
    889      * Contains {@link Groups} columns along with summary details.
    890      *
    891      * Note {@link Groups#SUMMARY_COUNT} doesn't exist in groups/view_groups.
    892      * When we detect this column being requested, we join {@link Joins#GROUP_MEMBER_COUNT} to
    893      * generate it.
    894      *
    895      * TODO Support SUMMARY_GROUP_COUNT_PER_ACCOUNT too.  See also queryLocal().
    896      */
    897     private static final ProjectionMap sGroupsSummaryProjectionMap = ProjectionMap.builder()
    898             .addAll(sGroupsProjectionMap)
    899             .add(Groups.SUMMARY_COUNT, "ifnull(group_member_count, 0)")
    900             .add(Groups.SUMMARY_WITH_PHONES,
    901                     "(SELECT COUNT(" + ContactsColumns.CONCRETE_ID + ") FROM "
    902                         + Tables.CONTACTS_JOIN_RAW_CONTACTS_DATA_FILTERED_BY_GROUPMEMBERSHIP
    903                         + " WHERE " + Contacts.HAS_PHONE_NUMBER + ")")
    904             .add(Groups.SUMMARY_GROUP_COUNT_PER_ACCOUNT, "0") // Always returns 0 for now.
    905             .build();
    906 
    907     /** Contains the agg_exceptions columns */
    908     private static final ProjectionMap sAggregationExceptionsProjectionMap = ProjectionMap.builder()
    909             .add(AggregationExceptionColumns._ID, Tables.AGGREGATION_EXCEPTIONS + "._id")
    910             .add(AggregationExceptions.TYPE)
    911             .add(AggregationExceptions.RAW_CONTACT_ID1)
    912             .add(AggregationExceptions.RAW_CONTACT_ID2)
    913             .build();
    914 
    915     /** Contains the agg_exceptions columns */
    916     private static final ProjectionMap sSettingsProjectionMap = ProjectionMap.builder()
    917             .add(Settings.ACCOUNT_NAME)
    918             .add(Settings.ACCOUNT_TYPE)
    919             .add(Settings.DATA_SET)
    920             .add(Settings.UNGROUPED_VISIBLE)
    921             .add(Settings.SHOULD_SYNC)
    922             .add(Settings.ANY_UNSYNCED,
    923                     "(CASE WHEN MIN(" + Settings.SHOULD_SYNC
    924                         + ",(SELECT "
    925                                 + "(CASE WHEN MIN(" + Groups.SHOULD_SYNC + ") IS NULL"
    926                                 + " THEN 1"
    927                                 + " ELSE MIN(" + Groups.SHOULD_SYNC + ")"
    928                                 + " END)"
    929                             + " FROM " + Views.GROUPS
    930                             + " WHERE " + ViewGroupsColumns.CONCRETE_ACCOUNT_NAME + "="
    931                                     + SettingsColumns.CONCRETE_ACCOUNT_NAME
    932                                 + " AND " + ViewGroupsColumns.CONCRETE_ACCOUNT_TYPE + "="
    933                                     + SettingsColumns.CONCRETE_ACCOUNT_TYPE
    934                                 + " AND ((" + ViewGroupsColumns.CONCRETE_DATA_SET + " IS NULL AND "
    935                                     + SettingsColumns.CONCRETE_DATA_SET + " IS NULL) OR ("
    936                                     + ViewGroupsColumns.CONCRETE_DATA_SET + "="
    937                                     + SettingsColumns.CONCRETE_DATA_SET + "))))=0"
    938                     + " THEN 1"
    939                     + " ELSE 0"
    940                     + " END)")
    941             .add(Settings.UNGROUPED_COUNT,
    942                     "(SELECT COUNT(*)"
    943                     + " FROM (SELECT 1"
    944                             + " FROM " + Tables.SETTINGS_JOIN_RAW_CONTACTS_DATA_MIMETYPES_CONTACTS
    945                             + " GROUP BY " + Clauses.GROUP_BY_ACCOUNT_CONTACT_ID
    946                             + " HAVING " + Clauses.HAVING_NO_GROUPS
    947                     + "))")
    948             .add(Settings.UNGROUPED_WITH_PHONES,
    949                     "(SELECT COUNT(*)"
    950                     + " FROM (SELECT 1"
    951                             + " FROM " + Tables.SETTINGS_JOIN_RAW_CONTACTS_DATA_MIMETYPES_CONTACTS
    952                             + " WHERE " + Contacts.HAS_PHONE_NUMBER
    953                             + " GROUP BY " + Clauses.GROUP_BY_ACCOUNT_CONTACT_ID
    954                             + " HAVING " + Clauses.HAVING_NO_GROUPS
    955                     + "))")
    956             .build();
    957 
    958     /** Contains StatusUpdates columns */
    959     private static final ProjectionMap sStatusUpdatesProjectionMap = ProjectionMap.builder()
    960             .add(PresenceColumns.RAW_CONTACT_ID)
    961             .add(StatusUpdates.DATA_ID, DataColumns.CONCRETE_ID)
    962             .add(StatusUpdates.IM_ACCOUNT)
    963             .add(StatusUpdates.IM_HANDLE)
    964             .add(StatusUpdates.PROTOCOL)
    965             // We cannot allow a null in the custom protocol field, because SQLite3 does not
    966             // properly enforce uniqueness of null values
    967             .add(StatusUpdates.CUSTOM_PROTOCOL,
    968                     "(CASE WHEN " + StatusUpdates.CUSTOM_PROTOCOL + "=''"
    969                     + " THEN NULL"
    970                     + " ELSE " + StatusUpdates.CUSTOM_PROTOCOL + " END)")
    971             .add(StatusUpdates.PRESENCE)
    972             .add(StatusUpdates.CHAT_CAPABILITY)
    973             .add(StatusUpdates.STATUS)
    974             .add(StatusUpdates.STATUS_TIMESTAMP)
    975             .add(StatusUpdates.STATUS_RES_PACKAGE)
    976             .add(StatusUpdates.STATUS_ICON)
    977             .add(StatusUpdates.STATUS_LABEL)
    978             .build();
    979 
    980     /** Contains StreamItems columns */
    981     private static final ProjectionMap sStreamItemsProjectionMap = ProjectionMap.builder()
    982             .add(StreamItems._ID)
    983             .add(StreamItems.CONTACT_ID)
    984             .add(StreamItems.CONTACT_LOOKUP_KEY)
    985             .add(StreamItems.ACCOUNT_NAME)
    986             .add(StreamItems.ACCOUNT_TYPE)
    987             .add(StreamItems.DATA_SET)
    988             .add(StreamItems.RAW_CONTACT_ID)
    989             .add(StreamItems.RAW_CONTACT_SOURCE_ID)
    990             .add(StreamItems.RES_PACKAGE)
    991             .add(StreamItems.RES_ICON)
    992             .add(StreamItems.RES_LABEL)
    993             .add(StreamItems.TEXT)
    994             .add(StreamItems.TIMESTAMP)
    995             .add(StreamItems.COMMENTS)
    996             .add(StreamItems.SYNC1)
    997             .add(StreamItems.SYNC2)
    998             .add(StreamItems.SYNC3)
    999             .add(StreamItems.SYNC4)
   1000             .build();
   1001 
   1002     private static final ProjectionMap sStreamItemPhotosProjectionMap = ProjectionMap.builder()
   1003             .add(StreamItemPhotos._ID, StreamItemPhotosColumns.CONCRETE_ID)
   1004             .add(StreamItems.RAW_CONTACT_ID)
   1005             .add(StreamItems.RAW_CONTACT_SOURCE_ID, RawContactsColumns.CONCRETE_SOURCE_ID)
   1006             .add(StreamItemPhotos.STREAM_ITEM_ID)
   1007             .add(StreamItemPhotos.SORT_INDEX)
   1008             .add(StreamItemPhotos.PHOTO_FILE_ID)
   1009             .add(StreamItemPhotos.PHOTO_URI,
   1010                     "'" + DisplayPhoto.CONTENT_URI + "'||'/'||" + StreamItemPhotos.PHOTO_FILE_ID)
   1011             .add(PhotoFiles.HEIGHT)
   1012             .add(PhotoFiles.WIDTH)
   1013             .add(PhotoFiles.FILESIZE)
   1014             .add(StreamItemPhotos.SYNC1)
   1015             .add(StreamItemPhotos.SYNC2)
   1016             .add(StreamItemPhotos.SYNC3)
   1017             .add(StreamItemPhotos.SYNC4)
   1018             .build();
   1019 
   1020     /** Contains {@link Directory} columns */
   1021     private static final ProjectionMap sDirectoryProjectionMap = ProjectionMap.builder()
   1022             .add(Directory._ID)
   1023             .add(Directory.PACKAGE_NAME)
   1024             .add(Directory.TYPE_RESOURCE_ID)
   1025             .add(Directory.DISPLAY_NAME)
   1026             .add(Directory.DIRECTORY_AUTHORITY)
   1027             .add(Directory.ACCOUNT_TYPE)
   1028             .add(Directory.ACCOUNT_NAME)
   1029             .add(Directory.EXPORT_SUPPORT)
   1030             .add(Directory.SHORTCUT_SUPPORT)
   1031             .add(Directory.PHOTO_SUPPORT)
   1032             .build();
   1033 
   1034     // where clause to update the status_updates table
   1035     private static final String WHERE_CLAUSE_FOR_STATUS_UPDATES_TABLE =
   1036             StatusUpdatesColumns.DATA_ID + " IN (SELECT Distinct " + StatusUpdates.DATA_ID +
   1037             " FROM " + Tables.STATUS_UPDATES + " LEFT OUTER JOIN " + Tables.PRESENCE +
   1038             " ON " + StatusUpdatesColumns.DATA_ID + " = " + StatusUpdates.DATA_ID + " WHERE ";
   1039 
   1040     private static final String[] EMPTY_STRING_ARRAY = new String[0];
   1041 
   1042     /**
   1043      * Notification ID for failure to import contacts.
   1044      */
   1045     private static final int LEGACY_IMPORT_FAILED_NOTIFICATION = 1;
   1046 
   1047     private static final String DEFAULT_SNIPPET_ARG_START_MATCH = "[";
   1048     private static final String DEFAULT_SNIPPET_ARG_END_MATCH = "]";
   1049     private static final String DEFAULT_SNIPPET_ARG_ELLIPSIS = "...";
   1050     private static final int DEFAULT_SNIPPET_ARG_MAX_TOKENS = -10;
   1051 
   1052     private boolean mIsPhoneInitialized;
   1053     private boolean mIsPhone;
   1054 
   1055     private final StringBuilder mSb = new StringBuilder();
   1056     private final String[] mSelectionArgs1 = new String[1];
   1057     private final String[] mSelectionArgs2 = new String[2];
   1058     private final String[] mSelectionArgs3 = new String[3];
   1059     private final String[] mSelectionArgs4 = new String[4];
   1060     private final ArrayList<String> mSelectionArgs = Lists.newArrayList();
   1061 
   1062     private Account mAccount;
   1063 
   1064     static {
   1065         // Contacts URI matching table
   1066         final UriMatcher matcher = sUriMatcher;
   1067         matcher.addURI(ContactsContract.AUTHORITY, "contacts", CONTACTS);
   1068         matcher.addURI(ContactsContract.AUTHORITY, "contacts/#", CONTACTS_ID);
   1069         matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/data", CONTACTS_ID_DATA);
   1070         matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/entities", CONTACTS_ID_ENTITIES);
   1071         matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/suggestions",
   1072                 AGGREGATION_SUGGESTIONS);
   1073         matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/suggestions/*",
   1074                 AGGREGATION_SUGGESTIONS);
   1075         matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/photo", CONTACTS_ID_PHOTO);
   1076         matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/display_photo",
   1077                 CONTACTS_ID_DISPLAY_PHOTO);
   1078         matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/stream_items",
   1079                 CONTACTS_ID_STREAM_ITEMS);
   1080         matcher.addURI(ContactsContract.AUTHORITY, "contacts/filter", CONTACTS_FILTER);
   1081         matcher.addURI(ContactsContract.AUTHORITY, "contacts/filter/*", CONTACTS_FILTER);
   1082         matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*", CONTACTS_LOOKUP);
   1083         matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/data", CONTACTS_LOOKUP_DATA);
   1084         matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/photo",
   1085                 CONTACTS_LOOKUP_PHOTO);
   1086         matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/#", CONTACTS_LOOKUP_ID);
   1087         matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/#/data",
   1088                 CONTACTS_LOOKUP_ID_DATA);
   1089         matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/#/photo",
   1090                 CONTACTS_LOOKUP_ID_PHOTO);
   1091         matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/display_photo",
   1092                 CONTACTS_LOOKUP_DISPLAY_PHOTO);
   1093         matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/#/display_photo",
   1094                 CONTACTS_LOOKUP_ID_DISPLAY_PHOTO);
   1095         matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/entities",
   1096                 CONTACTS_LOOKUP_ENTITIES);
   1097         matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/#/entities",
   1098                 CONTACTS_LOOKUP_ID_ENTITIES);
   1099         matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/stream_items",
   1100                 CONTACTS_LOOKUP_STREAM_ITEMS);
   1101         matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/#/stream_items",
   1102                 CONTACTS_LOOKUP_ID_STREAM_ITEMS);
   1103         matcher.addURI(ContactsContract.AUTHORITY, "contacts/as_vcard/*", CONTACTS_AS_VCARD);
   1104         matcher.addURI(ContactsContract.AUTHORITY, "contacts/as_multi_vcard/*",
   1105                 CONTACTS_AS_MULTI_VCARD);
   1106         matcher.addURI(ContactsContract.AUTHORITY, "contacts/strequent/", CONTACTS_STREQUENT);
   1107         matcher.addURI(ContactsContract.AUTHORITY, "contacts/strequent/filter/*",
   1108                 CONTACTS_STREQUENT_FILTER);
   1109         matcher.addURI(ContactsContract.AUTHORITY, "contacts/group/*", CONTACTS_GROUP);
   1110         matcher.addURI(ContactsContract.AUTHORITY, "contacts/frequent", CONTACTS_FREQUENT);
   1111         matcher.addURI(ContactsContract.AUTHORITY, "contacts/delete_usage", CONTACTS_DELETE_USAGE);
   1112 
   1113         matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts", RAW_CONTACTS);
   1114         matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#", RAW_CONTACTS_ID);
   1115         matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#/data", RAW_CONTACTS_ID_DATA);
   1116         matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#/display_photo",
   1117                 RAW_CONTACTS_ID_DISPLAY_PHOTO);
   1118         matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#/entity", RAW_CONTACT_ID_ENTITY);
   1119         matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#/stream_items",
   1120                 RAW_CONTACTS_ID_STREAM_ITEMS);
   1121         matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#/stream_items/#",
   1122                 RAW_CONTACTS_ID_STREAM_ITEMS_ID);
   1123 
   1124         matcher.addURI(ContactsContract.AUTHORITY, "raw_contact_entities", RAW_CONTACT_ENTITIES);
   1125 
   1126         matcher.addURI(ContactsContract.AUTHORITY, "data", DATA);
   1127         matcher.addURI(ContactsContract.AUTHORITY, "data/#", DATA_ID);
   1128         matcher.addURI(ContactsContract.AUTHORITY, "data/phones", PHONES);
   1129         matcher.addURI(ContactsContract.AUTHORITY, "data/phones/#", PHONES_ID);
   1130         matcher.addURI(ContactsContract.AUTHORITY, "data/phones/filter", PHONES_FILTER);
   1131         matcher.addURI(ContactsContract.AUTHORITY, "data/phones/filter/*", PHONES_FILTER);
   1132         matcher.addURI(ContactsContract.AUTHORITY, "data/emails", EMAILS);
   1133         matcher.addURI(ContactsContract.AUTHORITY, "data/emails/#", EMAILS_ID);
   1134         matcher.addURI(ContactsContract.AUTHORITY, "data/emails/lookup", EMAILS_LOOKUP);
   1135         matcher.addURI(ContactsContract.AUTHORITY, "data/emails/lookup/*", EMAILS_LOOKUP);
   1136         matcher.addURI(ContactsContract.AUTHORITY, "data/emails/filter", EMAILS_FILTER);
   1137         matcher.addURI(ContactsContract.AUTHORITY, "data/emails/filter/*", EMAILS_FILTER);
   1138         matcher.addURI(ContactsContract.AUTHORITY, "data/postals", POSTALS);
   1139         matcher.addURI(ContactsContract.AUTHORITY, "data/postals/#", POSTALS_ID);
   1140         /** "*" is in CSV form with data ids ("123,456,789") */
   1141         matcher.addURI(ContactsContract.AUTHORITY, "data/usagefeedback/*", DATA_USAGE_FEEDBACK_ID);
   1142         matcher.addURI(ContactsContract.AUTHORITY, "data/callables/", CALLABLES);
   1143         matcher.addURI(ContactsContract.AUTHORITY, "data/callables/#", CALLABLES_ID);
   1144         matcher.addURI(ContactsContract.AUTHORITY, "data/callables/filter", CALLABLES_FILTER);
   1145         matcher.addURI(ContactsContract.AUTHORITY, "data/callables/filter/*", CALLABLES_FILTER);
   1146 
   1147         matcher.addURI(ContactsContract.AUTHORITY, "groups", GROUPS);
   1148         matcher.addURI(ContactsContract.AUTHORITY, "groups/#", GROUPS_ID);
   1149         matcher.addURI(ContactsContract.AUTHORITY, "groups_summary", GROUPS_SUMMARY);
   1150 
   1151         matcher.addURI(ContactsContract.AUTHORITY, SyncStateContentProviderHelper.PATH, SYNCSTATE);
   1152         matcher.addURI(ContactsContract.AUTHORITY, SyncStateContentProviderHelper.PATH + "/#",
   1153                 SYNCSTATE_ID);
   1154         matcher.addURI(ContactsContract.AUTHORITY, "profile/" + SyncStateContentProviderHelper.PATH,
   1155                 PROFILE_SYNCSTATE);
   1156         matcher.addURI(ContactsContract.AUTHORITY,
   1157                 "profile/" + SyncStateContentProviderHelper.PATH + "/#",
   1158                 PROFILE_SYNCSTATE_ID);
   1159 
   1160         matcher.addURI(ContactsContract.AUTHORITY, "phone_lookup/*", PHONE_LOOKUP);
   1161         matcher.addURI(ContactsContract.AUTHORITY, "aggregation_exceptions",
   1162                 AGGREGATION_EXCEPTIONS);
   1163         matcher.addURI(ContactsContract.AUTHORITY, "aggregation_exceptions/*",
   1164                 AGGREGATION_EXCEPTION_ID);
   1165 
   1166         matcher.addURI(ContactsContract.AUTHORITY, "settings", SETTINGS);
   1167 
   1168         matcher.addURI(ContactsContract.AUTHORITY, "status_updates", STATUS_UPDATES);
   1169         matcher.addURI(ContactsContract.AUTHORITY, "status_updates/#", STATUS_UPDATES_ID);
   1170 
   1171         matcher.addURI(ContactsContract.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY,
   1172                 SEARCH_SUGGESTIONS);
   1173         matcher.addURI(ContactsContract.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY + "/*",
   1174                 SEARCH_SUGGESTIONS);
   1175         matcher.addURI(ContactsContract.AUTHORITY, SearchManager.SUGGEST_URI_PATH_SHORTCUT + "/*",
   1176                 SEARCH_SHORTCUT);
   1177 
   1178         matcher.addURI(ContactsContract.AUTHORITY, "provider_status", PROVIDER_STATUS);
   1179 
   1180         matcher.addURI(ContactsContract.AUTHORITY, "directories", DIRECTORIES);
   1181         matcher.addURI(ContactsContract.AUTHORITY, "directories/#", DIRECTORIES_ID);
   1182 
   1183         matcher.addURI(ContactsContract.AUTHORITY, "complete_name", COMPLETE_NAME);
   1184 
   1185         matcher.addURI(ContactsContract.AUTHORITY, "profile", PROFILE);
   1186         matcher.addURI(ContactsContract.AUTHORITY, "profile/entities", PROFILE_ENTITIES);
   1187         matcher.addURI(ContactsContract.AUTHORITY, "profile/data", PROFILE_DATA);
   1188         matcher.addURI(ContactsContract.AUTHORITY, "profile/data/#", PROFILE_DATA_ID);
   1189         matcher.addURI(ContactsContract.AUTHORITY, "profile/photo", PROFILE_PHOTO);
   1190         matcher.addURI(ContactsContract.AUTHORITY, "profile/display_photo", PROFILE_DISPLAY_PHOTO);
   1191         matcher.addURI(ContactsContract.AUTHORITY, "profile/as_vcard", PROFILE_AS_VCARD);
   1192         matcher.addURI(ContactsContract.AUTHORITY, "profile/raw_contacts", PROFILE_RAW_CONTACTS);
   1193         matcher.addURI(ContactsContract.AUTHORITY, "profile/raw_contacts/#",
   1194                 PROFILE_RAW_CONTACTS_ID);
   1195         matcher.addURI(ContactsContract.AUTHORITY, "profile/raw_contacts/#/data",
   1196                 PROFILE_RAW_CONTACTS_ID_DATA);
   1197         matcher.addURI(ContactsContract.AUTHORITY, "profile/raw_contacts/#/entity",
   1198                 PROFILE_RAW_CONTACTS_ID_ENTITIES);
   1199         matcher.addURI(ContactsContract.AUTHORITY, "profile/status_updates",
   1200                 PROFILE_STATUS_UPDATES);
   1201         matcher.addURI(ContactsContract.AUTHORITY, "profile/raw_contact_entities",
   1202                 PROFILE_RAW_CONTACT_ENTITIES);
   1203 
   1204         matcher.addURI(ContactsContract.AUTHORITY, "stream_items", STREAM_ITEMS);
   1205         matcher.addURI(ContactsContract.AUTHORITY, "stream_items/photo", STREAM_ITEMS_PHOTOS);
   1206         matcher.addURI(ContactsContract.AUTHORITY, "stream_items/#", STREAM_ITEMS_ID);
   1207         matcher.addURI(ContactsContract.AUTHORITY, "stream_items/#/photo", STREAM_ITEMS_ID_PHOTOS);
   1208         matcher.addURI(ContactsContract.AUTHORITY, "stream_items/#/photo/#",
   1209                 STREAM_ITEMS_ID_PHOTOS_ID);
   1210         matcher.addURI(ContactsContract.AUTHORITY, "stream_items_limit", STREAM_ITEMS_LIMIT);
   1211 
   1212         matcher.addURI(ContactsContract.AUTHORITY, "display_photo/#", DISPLAY_PHOTO_ID);
   1213         matcher.addURI(ContactsContract.AUTHORITY, "photo_dimensions", PHOTO_DIMENSIONS);
   1214     }
   1215 
   1216     private static class DirectoryInfo {
   1217         String authority;
   1218         String accountName;
   1219         String accountType;
   1220     }
   1221 
   1222     /**
   1223      * Cached information about contact directories.
   1224      */
   1225     private HashMap<String, DirectoryInfo> mDirectoryCache = new HashMap<String, DirectoryInfo>();
   1226     private boolean mDirectoryCacheValid = false;
   1227 
   1228     /**
   1229      * An entry in group id cache.
   1230      *
   1231      * TODO: Move this and {@link #mGroupIdCache} to {@link DataRowHandlerForGroupMembership}.
   1232      */
   1233     public static class GroupIdCacheEntry {
   1234         long accountId;
   1235         String sourceId;
   1236         long groupId;
   1237     }
   1238 
   1239     /**
   1240      * Map from group source IDs to lists of {@link GroupIdCacheEntry}s.
   1241      *
   1242      * We don't need a soft cache for groups - the assumption is that there will only
   1243      * be a small number of contact groups. The cache is keyed off source id.  The value
   1244      * is a list of groups with this group id.
   1245      */
   1246     private HashMap<String, ArrayList<GroupIdCacheEntry>> mGroupIdCache = Maps.newHashMap();
   1247 
   1248     /**
   1249      * Sub-provider for handling profile requests against the profile database.
   1250      */
   1251     private ProfileProvider mProfileProvider;
   1252 
   1253     private NameSplitter mNameSplitter;
   1254     private NameLookupBuilder mNameLookupBuilder;
   1255 
   1256     private PostalSplitter mPostalSplitter;
   1257 
   1258     private ContactDirectoryManager mContactDirectoryManager;
   1259 
   1260     // The database tag to use for representing the contacts DB in contacts transactions.
   1261     /* package */ static final String CONTACTS_DB_TAG = "contacts";
   1262 
   1263     // The database tag to use for representing the profile DB in contacts transactions.
   1264     /* package */ static final String PROFILE_DB_TAG = "profile";
   1265 
   1266     /**
   1267      * The thread-local holder of the active transaction.  Shared between this and the profile
   1268      * provider, to keep transactions on both databases synchronized.
   1269      */
   1270     private final ThreadLocal<ContactsTransaction> mTransactionHolder =
   1271             new ThreadLocal<ContactsTransaction>();
   1272 
   1273     // This variable keeps track of whether the current operation is intended for the profile DB.
   1274     private final ThreadLocal<Boolean> mInProfileMode = new ThreadLocal<Boolean>();
   1275 
   1276     // Separate data row handler instances for contact data and profile data.
   1277     private HashMap<String, DataRowHandler> mDataRowHandlers;
   1278     private HashMap<String, DataRowHandler> mProfileDataRowHandlers;
   1279 
   1280     // Depending on whether the action being performed is for the profile, we will use one of two
   1281     // database helper instances.
   1282     private final ThreadLocal<ContactsDatabaseHelper> mDbHelper =
   1283             new ThreadLocal<ContactsDatabaseHelper>();
   1284     private ContactsDatabaseHelper mContactsHelper;
   1285     private ProfileDatabaseHelper mProfileHelper;
   1286 
   1287     // Depending on whether the action being performed is for the profile or not, we will use one of
   1288     // two aggregator instances.
   1289     private final ThreadLocal<ContactAggregator> mAggregator = new ThreadLocal<ContactAggregator>();
   1290     private ContactAggregator mContactAggregator;
   1291     private ContactAggregator mProfileAggregator;
   1292 
   1293     // Depending on whether the action being performed is for the profile or not, we will use one of
   1294     // two photo store instances (with their files stored in separate subdirectories).
   1295     private final ThreadLocal<PhotoStore> mPhotoStore = new ThreadLocal<PhotoStore>();
   1296     private PhotoStore mContactsPhotoStore;
   1297     private PhotoStore mProfilePhotoStore;
   1298 
   1299     // The active transaction context will switch depending on the operation being performed.
   1300     // Both transaction contexts will be cleared out when a batch transaction is started, and
   1301     // each will be processed separately when a batch transaction completes.
   1302     private final TransactionContext mContactTransactionContext = new TransactionContext(false);
   1303     private final TransactionContext mProfileTransactionContext = new TransactionContext(true);
   1304     private final ThreadLocal<TransactionContext> mTransactionContext =
   1305             new ThreadLocal<TransactionContext>();
   1306 
   1307     // Duration in milliseconds that pre-authorized URIs will remain valid.
   1308     private long mPreAuthorizedUriDuration;
   1309 
   1310     // Map of single-use pre-authorized URIs to expiration times.
   1311     private final Map<Uri, Long> mPreAuthorizedUris = Maps.newHashMap();
   1312 
   1313     // Random number generator.
   1314     private final SecureRandom mRandom = new SecureRandom();
   1315 
   1316     private LegacyApiSupport mLegacyApiSupport;
   1317     private GlobalSearchSupport mGlobalSearchSupport;
   1318     private CommonNicknameCache mCommonNicknameCache;
   1319     private SearchIndexManager mSearchIndexManager;
   1320 
   1321     private final ContentValues mValues = new ContentValues();
   1322     private final HashMap<String, Boolean> mAccountWritability = Maps.newHashMap();
   1323 
   1324     private int mProviderStatus = ProviderStatus.STATUS_NORMAL;
   1325     private boolean mProviderStatusUpdateNeeded;
   1326     private long mEstimatedStorageRequirement = 0;
   1327     private volatile CountDownLatch mReadAccessLatch;
   1328     private volatile CountDownLatch mWriteAccessLatch;
   1329     private boolean mAccountUpdateListenerRegistered;
   1330     private boolean mOkToOpenAccess = true;
   1331 
   1332     private boolean mVisibleTouched = false;
   1333 
   1334     private boolean mSyncToNetwork;
   1335 
   1336     private Locale mCurrentLocale;
   1337     private int mContactsAccountCount;
   1338 
   1339     private HandlerThread mBackgroundThread;
   1340     private Handler mBackgroundHandler;
   1341 
   1342     private long mLastPhotoCleanup = 0;
   1343 
   1344     private FastScrollingIndexCache mFastScrollingIndexCache;
   1345 
   1346     // Stats about FastScrollingIndex.
   1347     private int mFastScrollingIndexCacheRequestCount;
   1348     private int mFastScrollingIndexCacheMissCount;
   1349     private long mTotalTimeFastScrollingIndexGenerate;
   1350 
   1351     @Override
   1352     public boolean onCreate() {
   1353         if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) {
   1354             Log.d(Constants.PERFORMANCE_TAG, "ContactsProvider2.onCreate start");
   1355         }
   1356         super.onCreate();
   1357         try {
   1358             return initialize();
   1359         } catch (RuntimeException e) {
   1360             Log.e(TAG, "Cannot start provider", e);
   1361             // In production code we don't want to throw here, so that phone will still work
   1362             // in low storage situations.
   1363             // See I5c88a3024ff1c5a06b5756b29a2d903f8f6a2531
   1364             if (shouldThrowExceptionForInitializationError()) {
   1365                 throw e;
   1366             }
   1367             return false;
   1368         } finally {
   1369             if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) {
   1370                 Log.d(Constants.PERFORMANCE_TAG, "ContactsProvider2.onCreate finish");
   1371             }
   1372         }
   1373     }
   1374 
   1375     protected boolean shouldThrowExceptionForInitializationError() {
   1376         return false;
   1377     }
   1378 
   1379     private boolean initialize() {
   1380         StrictMode.setThreadPolicy(
   1381                 new StrictMode.ThreadPolicy.Builder().detectAll().penaltyLog().build());
   1382 
   1383         mFastScrollingIndexCache = new FastScrollingIndexCache(getContext());
   1384 
   1385         mContactsHelper = getDatabaseHelper(getContext());
   1386         mDbHelper.set(mContactsHelper);
   1387 
   1388         // Set up the DB helper for keeping transactions serialized.
   1389         setDbHelperToSerializeOn(mContactsHelper, CONTACTS_DB_TAG, this);
   1390 
   1391         mContactDirectoryManager = new ContactDirectoryManager(this);
   1392         mGlobalSearchSupport = new GlobalSearchSupport(this);
   1393 
   1394         // The provider is closed for business until fully initialized
   1395         mReadAccessLatch = new CountDownLatch(1);
   1396         mWriteAccessLatch = new CountDownLatch(1);
   1397 
   1398         mBackgroundThread = new HandlerThread("ContactsProviderWorker",
   1399                 Process.THREAD_PRIORITY_BACKGROUND);
   1400         mBackgroundThread.start();
   1401         mBackgroundHandler = new Handler(mBackgroundThread.getLooper()) {
   1402             @Override
   1403             public void handleMessage(Message msg) {
   1404                 performBackgroundTask(msg.what, msg.obj);
   1405             }
   1406         };
   1407 
   1408         // Set up the sub-provider for handling profiles.
   1409         mProfileProvider = newProfileProvider();
   1410         mProfileProvider.setDbHelperToSerializeOn(mContactsHelper, CONTACTS_DB_TAG, this);
   1411         ProviderInfo profileInfo = new ProviderInfo();
   1412         profileInfo.readPermission = "android.permission.READ_PROFILE";
   1413         profileInfo.writePermission = "android.permission.WRITE_PROFILE";
   1414         mProfileProvider.attachInfo(getContext(), profileInfo);
   1415         mProfileHelper = mProfileProvider.getDatabaseHelper(getContext());
   1416 
   1417         // Initialize the pre-authorized URI duration.
   1418         mPreAuthorizedUriDuration = DEFAULT_PREAUTHORIZED_URI_EXPIRATION;
   1419 
   1420         scheduleBackgroundTask(BACKGROUND_TASK_INITIALIZE);
   1421         scheduleBackgroundTask(BACKGROUND_TASK_UPDATE_ACCOUNTS);
   1422         scheduleBackgroundTask(BACKGROUND_TASK_UPDATE_LOCALE);
   1423         scheduleBackgroundTask(BACKGROUND_TASK_UPGRADE_AGGREGATION_ALGORITHM);
   1424         scheduleBackgroundTask(BACKGROUND_TASK_UPDATE_SEARCH_INDEX);
   1425         scheduleBackgroundTask(BACKGROUND_TASK_UPDATE_PROVIDER_STATUS);
   1426         scheduleBackgroundTask(BACKGROUND_TASK_OPEN_WRITE_ACCESS);
   1427         scheduleBackgroundTask(BACKGROUND_TASK_CLEANUP_PHOTOS);
   1428 
   1429         return true;
   1430     }
   1431 
   1432     /**
   1433      * (Re)allocates all locale-sensitive structures.
   1434      */
   1435     private void initForDefaultLocale() {
   1436         Context context = getContext();
   1437         mLegacyApiSupport = new LegacyApiSupport(context, mContactsHelper, this,
   1438                 mGlobalSearchSupport);
   1439         mCurrentLocale = getLocale();
   1440         mNameSplitter = mContactsHelper.createNameSplitter();
   1441         mNameLookupBuilder = new StructuredNameLookupBuilder(mNameSplitter);
   1442         mPostalSplitter = new PostalSplitter(mCurrentLocale);
   1443         mCommonNicknameCache = new CommonNicknameCache(mContactsHelper.getReadableDatabase());
   1444         ContactLocaleUtils.getIntance().setLocale(mCurrentLocale);
   1445         mContactAggregator = new ContactAggregator(this, mContactsHelper,
   1446                 createPhotoPriorityResolver(context), mNameSplitter, mCommonNicknameCache);
   1447         mContactAggregator.setEnabled(SystemProperties.getBoolean(AGGREGATE_CONTACTS, true));
   1448         mProfileAggregator = new ProfileAggregator(this, mProfileHelper,
   1449                 createPhotoPriorityResolver(context), mNameSplitter, mCommonNicknameCache);
   1450         mProfileAggregator.setEnabled(SystemProperties.getBoolean(AGGREGATE_CONTACTS, true));
   1451         mSearchIndexManager = new SearchIndexManager(this);
   1452 
   1453         mContactsPhotoStore = new PhotoStore(getContext().getFilesDir(), mContactsHelper);
   1454         mProfilePhotoStore = new PhotoStore(new File(getContext().getFilesDir(), "profile"),
   1455                 mProfileHelper);
   1456 
   1457         mDataRowHandlers = new HashMap<String, DataRowHandler>();
   1458         initDataRowHandlers(mDataRowHandlers, mContactsHelper, mContactAggregator,
   1459                 mContactsPhotoStore);
   1460         mProfileDataRowHandlers = new HashMap<String, DataRowHandler>();
   1461         initDataRowHandlers(mProfileDataRowHandlers, mProfileHelper, mProfileAggregator,
   1462                 mProfilePhotoStore);
   1463 
   1464         // Set initial thread-local state variables for the Contacts DB.
   1465         switchToContactMode();
   1466     }
   1467 
   1468     private void initDataRowHandlers(Map<String, DataRowHandler> handlerMap,
   1469             ContactsDatabaseHelper dbHelper, ContactAggregator contactAggregator,
   1470             PhotoStore photoStore) {
   1471         Context context = getContext();
   1472         handlerMap.put(Email.CONTENT_ITEM_TYPE,
   1473                 new DataRowHandlerForEmail(context, dbHelper, contactAggregator));
   1474         handlerMap.put(Im.CONTENT_ITEM_TYPE,
   1475                 new DataRowHandlerForIm(context, dbHelper, contactAggregator));
   1476         handlerMap.put(Organization.CONTENT_ITEM_TYPE,
   1477                 new DataRowHandlerForOrganization(context, dbHelper, contactAggregator));
   1478         handlerMap.put(Phone.CONTENT_ITEM_TYPE,
   1479                 new DataRowHandlerForPhoneNumber(context, dbHelper, contactAggregator));
   1480         handlerMap.put(Nickname.CONTENT_ITEM_TYPE,
   1481                 new DataRowHandlerForNickname(context, dbHelper, contactAggregator));
   1482         handlerMap.put(StructuredName.CONTENT_ITEM_TYPE,
   1483                 new DataRowHandlerForStructuredName(context, dbHelper, contactAggregator,
   1484                         mNameSplitter, mNameLookupBuilder));
   1485         handlerMap.put(StructuredPostal.CONTENT_ITEM_TYPE,
   1486                 new DataRowHandlerForStructuredPostal(context, dbHelper, contactAggregator,
   1487                         mPostalSplitter));
   1488         handlerMap.put(GroupMembership.CONTENT_ITEM_TYPE,
   1489                 new DataRowHandlerForGroupMembership(context, dbHelper, contactAggregator,
   1490                         mGroupIdCache));
   1491         handlerMap.put(Photo.CONTENT_ITEM_TYPE,
   1492                 new DataRowHandlerForPhoto(context, dbHelper, contactAggregator, photoStore,
   1493                         getMaxDisplayPhotoDim(), getMaxThumbnailDim()));
   1494         handlerMap.put(Note.CONTENT_ITEM_TYPE,
   1495                 new DataRowHandlerForNote(context, dbHelper, contactAggregator));
   1496         handlerMap.put(Identity.CONTENT_ITEM_TYPE,
   1497                 new DataRowHandlerForIdentity(context, dbHelper, contactAggregator));
   1498     }
   1499 
   1500     @VisibleForTesting
   1501     PhotoPriorityResolver createPhotoPriorityResolver(Context context) {
   1502         return new PhotoPriorityResolver(context);
   1503     }
   1504 
   1505     protected void scheduleBackgroundTask(int task) {
   1506         mBackgroundHandler.sendEmptyMessage(task);
   1507     }
   1508 
   1509     protected void scheduleBackgroundTask(int task, Object arg) {
   1510         mBackgroundHandler.sendMessage(mBackgroundHandler.obtainMessage(task, arg));
   1511     }
   1512 
   1513     protected void performBackgroundTask(int task, Object arg) {
   1514         // Make sure we operate on the contacts db by default.
   1515         switchToContactMode();
   1516         switch (task) {
   1517             case BACKGROUND_TASK_INITIALIZE: {
   1518                 initForDefaultLocale();
   1519                 mReadAccessLatch.countDown();
   1520                 mReadAccessLatch = null;
   1521                 break;
   1522             }
   1523 
   1524             case BACKGROUND_TASK_OPEN_WRITE_ACCESS: {
   1525                 if (mOkToOpenAccess) {
   1526                     mWriteAccessLatch.countDown();
   1527                     mWriteAccessLatch = null;
   1528                 }
   1529                 break;
   1530             }
   1531 
   1532             case BACKGROUND_TASK_UPDATE_ACCOUNTS: {
   1533                 Context context = getContext();
   1534                 if (!mAccountUpdateListenerRegistered) {
   1535                     AccountManager.get(context).addOnAccountsUpdatedListener(this, null, false);
   1536                     mAccountUpdateListenerRegistered = true;
   1537                 }
   1538 
   1539                 // Update the accounts for both the contacts and profile DBs.
   1540                 Account[] accounts = AccountManager.get(context).getAccounts();
   1541                 switchToContactMode();
   1542                 boolean accountsChanged = updateAccountsInBackground(accounts);
   1543                 switchToProfileMode();
   1544                 accountsChanged |= updateAccountsInBackground(accounts);
   1545 
   1546                 switchToContactMode();
   1547 
   1548                 updateContactsAccountCount(accounts);
   1549                 updateDirectoriesInBackground(accountsChanged);
   1550                 break;
   1551             }
   1552 
   1553             case BACKGROUND_TASK_UPDATE_LOCALE: {
   1554                 updateLocaleInBackground();
   1555                 break;
   1556             }
   1557 
   1558             case BACKGROUND_TASK_CHANGE_LOCALE: {
   1559                 changeLocaleInBackground();
   1560                 break;
   1561             }
   1562 
   1563             case BACKGROUND_TASK_UPGRADE_AGGREGATION_ALGORITHM: {
   1564                 if (isAggregationUpgradeNeeded()) {
   1565                     upgradeAggregationAlgorithmInBackground();
   1566                     invalidateFastScrollingIndexCache();
   1567                 }
   1568                 break;
   1569             }
   1570 
   1571             case BACKGROUND_TASK_UPDATE_SEARCH_INDEX: {
   1572                 updateSearchIndexInBackground();
   1573                 break;
   1574             }
   1575 
   1576             case BACKGROUND_TASK_UPDATE_PROVIDER_STATUS: {
   1577                 updateProviderStatus();
   1578                 break;
   1579             }
   1580 
   1581             case BACKGROUND_TASK_UPDATE_DIRECTORIES: {
   1582                 if (arg != null) {
   1583                     mContactDirectoryManager.onPackageChanged((String) arg);
   1584                 }
   1585                 break;
   1586             }
   1587 
   1588             case BACKGROUND_TASK_CLEANUP_PHOTOS: {
   1589                 // Check rate limit.
   1590                 long now = System.currentTimeMillis();
   1591                 if (now - mLastPhotoCleanup > PHOTO_CLEANUP_RATE_LIMIT) {
   1592                     mLastPhotoCleanup = now;
   1593 
   1594                     // Clean up photo stores for both contacts and profiles.
   1595                     switchToContactMode();
   1596                     cleanupPhotoStore();
   1597                     switchToProfileMode();
   1598                     cleanupPhotoStore();
   1599 
   1600                     switchToContactMode(); // Switch to the default, just in case.
   1601                     break;
   1602                 }
   1603             }
   1604         }
   1605     }
   1606 
   1607     public void onLocaleChanged() {
   1608         if (mProviderStatus != ProviderStatus.STATUS_NORMAL
   1609                 && mProviderStatus != ProviderStatus.STATUS_NO_ACCOUNTS_NO_CONTACTS) {
   1610             return;
   1611         }
   1612 
   1613         scheduleBackgroundTask(BACKGROUND_TASK_CHANGE_LOCALE);
   1614     }
   1615 
   1616     /**
   1617      * Verifies that the contacts database is properly configured for the current locale.
   1618      * If not, changes the database locale to the current locale using an asynchronous task.
   1619      * This needs to be done asynchronously because the process involves rebuilding
   1620      * large data structures (name lookup, sort keys), which can take minutes on
   1621      * a large set of contacts.
   1622      */
   1623     protected void updateLocaleInBackground() {
   1624 
   1625         // The process is already running - postpone the change
   1626         if (mProviderStatus == ProviderStatus.STATUS_CHANGING_LOCALE) {
   1627             return;
   1628         }
   1629 
   1630         final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
   1631         final String providerLocale = prefs.getString(PREF_LOCALE, null);
   1632         final Locale currentLocale = mCurrentLocale;
   1633         if (currentLocale.toString().equals(providerLocale)) {
   1634             return;
   1635         }
   1636 
   1637         int providerStatus = mProviderStatus;
   1638         setProviderStatus(ProviderStatus.STATUS_CHANGING_LOCALE);
   1639         mContactsHelper.setLocale(this, currentLocale);
   1640         mProfileHelper.setLocale(this, currentLocale);
   1641         mSearchIndexManager.updateIndex(true);
   1642         prefs.edit().putString(PREF_LOCALE, currentLocale.toString()).apply();
   1643         invalidateFastScrollingIndexCache();
   1644         setProviderStatus(providerStatus);
   1645     }
   1646 
   1647     /**
   1648      * Reinitializes the provider for a new locale.
   1649      */
   1650     private void changeLocaleInBackground() {
   1651         // Re-initializing the provider without stopping it.
   1652         // Locking the database will prevent inserts/updates/deletes from
   1653         // running at the same time, but queries may still be running
   1654         // on other threads. Those queries may return inconsistent results.
   1655         SQLiteDatabase db = mContactsHelper.getWritableDatabase();
   1656         SQLiteDatabase profileDb = mProfileHelper.getWritableDatabase();
   1657         db.beginTransaction();
   1658         profileDb.beginTransaction();
   1659         try {
   1660             initForDefaultLocale();
   1661             db.setTransactionSuccessful();
   1662             profileDb.setTransactionSuccessful();
   1663         } finally {
   1664             db.endTransaction();
   1665             profileDb.endTransaction();
   1666         }
   1667 
   1668         updateLocaleInBackground();
   1669     }
   1670 
   1671     protected void updateSearchIndexInBackground() {
   1672         mSearchIndexManager.updateIndex(false);
   1673     }
   1674 
   1675     protected void updateDirectoriesInBackground(boolean rescan) {
   1676         mContactDirectoryManager.scanAllPackages(rescan);
   1677     }
   1678 
   1679     private void updateProviderStatus() {
   1680         if (mProviderStatus != ProviderStatus.STATUS_NORMAL
   1681                 && mProviderStatus != ProviderStatus.STATUS_NO_ACCOUNTS_NO_CONTACTS) {
   1682             return;
   1683         }
   1684 
   1685         // No accounts/no contacts status is true if there are no account and
   1686         // there are no contacts or one profile contact
   1687         if (mContactsAccountCount == 0) {
   1688             long contactsNum = DatabaseUtils.queryNumEntries(mContactsHelper.getReadableDatabase(),
   1689                     Tables.CONTACTS, null);
   1690             long profileNum = DatabaseUtils.queryNumEntries(mProfileHelper.getReadableDatabase(),
   1691                     Tables.CONTACTS, null);
   1692 
   1693             // TODO: Different status if there is a profile but no contacts?
   1694             if (contactsNum == 0 && profileNum <= 1) {
   1695                 setProviderStatus(ProviderStatus.STATUS_NO_ACCOUNTS_NO_CONTACTS);
   1696             } else {
   1697                 setProviderStatus(ProviderStatus.STATUS_NORMAL);
   1698             }
   1699         } else {
   1700             setProviderStatus(ProviderStatus.STATUS_NORMAL);
   1701         }
   1702     }
   1703 
   1704     @VisibleForTesting
   1705     protected void cleanupPhotoStore() {
   1706         final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
   1707 
   1708         // Assemble the set of photo store file IDs that are in use, and send those to the photo
   1709         // store.  Any photos that aren't in that set will be deleted, and any photos that no
   1710         // longer exist in the photo store will be returned for us to clear out in the DB.
   1711         long photoMimeTypeId = mDbHelper.get().getMimeTypeId(Photo.CONTENT_ITEM_TYPE);
   1712         Cursor c = db.query(Views.DATA, new String[]{Data._ID, Photo.PHOTO_FILE_ID},
   1713                 DataColumns.MIMETYPE_ID + "=" + photoMimeTypeId + " AND "
   1714                         + Photo.PHOTO_FILE_ID + " IS NOT NULL", null, null, null, null);
   1715         Set<Long> usedPhotoFileIds = Sets.newHashSet();
   1716         Map<Long, Long> photoFileIdToDataId = Maps.newHashMap();
   1717         try {
   1718             while (c.moveToNext()) {
   1719                 long dataId = c.getLong(0);
   1720                 long photoFileId = c.getLong(1);
   1721                 usedPhotoFileIds.add(photoFileId);
   1722                 photoFileIdToDataId.put(photoFileId, dataId);
   1723             }
   1724         } finally {
   1725             c.close();
   1726         }
   1727 
   1728         // Also query for all social stream item photos.
   1729         c = db.query(Tables.STREAM_ITEM_PHOTOS + " JOIN " + Tables.STREAM_ITEMS
   1730                 + " ON " + StreamItemPhotos.STREAM_ITEM_ID + "=" + StreamItemsColumns.CONCRETE_ID,
   1731                 new String[]{
   1732                         StreamItemPhotosColumns.CONCRETE_ID,
   1733                         StreamItemPhotosColumns.CONCRETE_STREAM_ITEM_ID,
   1734                         StreamItemPhotos.PHOTO_FILE_ID
   1735                 },
   1736                 null, null, null, null, null);
   1737         Map<Long, Long> photoFileIdToStreamItemPhotoId = Maps.newHashMap();
   1738         Map<Long, Long> streamItemPhotoIdToStreamItemId = Maps.newHashMap();
   1739         try {
   1740             while (c.moveToNext()) {
   1741                 long streamItemPhotoId = c.getLong(0);
   1742                 long streamItemId = c.getLong(1);
   1743                 long photoFileId = c.getLong(2);
   1744                 usedPhotoFileIds.add(photoFileId);
   1745                 photoFileIdToStreamItemPhotoId.put(photoFileId, streamItemPhotoId);
   1746                 streamItemPhotoIdToStreamItemId.put(streamItemPhotoId, streamItemId);
   1747             }
   1748         } finally {
   1749             c.close();
   1750         }
   1751 
   1752         // Run the photo store cleanup.
   1753         Set<Long> missingPhotoIds = mPhotoStore.get().cleanup(usedPhotoFileIds);
   1754 
   1755         // If any of the keys we're using no longer exist, clean them up.  We need to do these
   1756         // using internal APIs or direct DB access to avoid permission errors.
   1757         if (!missingPhotoIds.isEmpty()) {
   1758             try {
   1759                 // Need to set the db listener because we need to run onCommit afterwards.
   1760                 // Make sure to use the proper listener depending on the current mode.
   1761                 db.beginTransactionWithListener(inProfileMode() ? mProfileProvider : this);
   1762                 for (long missingPhotoId : missingPhotoIds) {
   1763                     if (photoFileIdToDataId.containsKey(missingPhotoId)) {
   1764                         long dataId = photoFileIdToDataId.get(missingPhotoId);
   1765                         ContentValues updateValues = new ContentValues();
   1766                         updateValues.putNull(Photo.PHOTO_FILE_ID);
   1767                         updateData(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
   1768                                 updateValues, null, null, false);
   1769                     }
   1770                     if (photoFileIdToStreamItemPhotoId.containsKey(missingPhotoId)) {
   1771                         // For missing photos that were in stream item photos, just delete the
   1772                         // stream item photo.
   1773                         long streamItemPhotoId = photoFileIdToStreamItemPhotoId.get(missingPhotoId);
   1774                         db.delete(Tables.STREAM_ITEM_PHOTOS, StreamItemPhotos._ID + "=?",
   1775                                 new String[]{String.valueOf(streamItemPhotoId)});
   1776                     }
   1777                 }
   1778                 db.setTransactionSuccessful();
   1779             } catch (Exception e) {
   1780                 // Cleanup failure is not a fatal problem.  We'll try again later.
   1781                 Log.e(TAG, "Failed to clean up outdated photo references", e);
   1782             } finally {
   1783                 db.endTransaction();
   1784             }
   1785         }
   1786     }
   1787 
   1788     @Override
   1789     protected ContactsDatabaseHelper getDatabaseHelper(final Context context) {
   1790         return ContactsDatabaseHelper.getInstance(context);
   1791     }
   1792 
   1793     @Override
   1794     protected ThreadLocal<ContactsTransaction> getTransactionHolder() {
   1795         return mTransactionHolder;
   1796     }
   1797 
   1798     public ProfileProvider newProfileProvider() {
   1799         return new ProfileProvider(this);
   1800     }
   1801 
   1802     @VisibleForTesting
   1803     /* package */ PhotoStore getPhotoStore() {
   1804         return mContactsPhotoStore;
   1805     }
   1806 
   1807     @VisibleForTesting
   1808     /* package */ PhotoStore getProfilePhotoStore() {
   1809         return mProfilePhotoStore;
   1810     }
   1811 
   1812     /**
   1813      * Maximum dimension (height or width) of photo thumbnails.
   1814      */
   1815     public int getMaxThumbnailDim() {
   1816         return PhotoProcessor.getMaxThumbnailSize();
   1817     }
   1818 
   1819     /**
   1820      * Maximum dimension (height or width) of display photos.  Larger images will be scaled
   1821      * to fit.
   1822      */
   1823     public int getMaxDisplayPhotoDim() {
   1824         return PhotoProcessor.getMaxDisplayPhotoSize();
   1825     }
   1826 
   1827     @VisibleForTesting
   1828     public ContactDirectoryManager getContactDirectoryManagerForTest() {
   1829         return mContactDirectoryManager;
   1830     }
   1831 
   1832     @VisibleForTesting
   1833     protected Locale getLocale() {
   1834         return Locale.getDefault();
   1835     }
   1836 
   1837     @VisibleForTesting
   1838     final boolean inProfileMode() {
   1839         Boolean profileMode = mInProfileMode.get();
   1840         return profileMode != null && profileMode;
   1841     }
   1842 
   1843     /**
   1844      * Wipes all data from the contacts database.
   1845      */
   1846     @NeededForTesting
   1847     void wipeData() {
   1848         invalidateFastScrollingIndexCache();
   1849         mContactsHelper.wipeData();
   1850         mProfileHelper.wipeData();
   1851         mContactsPhotoStore.clear();
   1852         mProfilePhotoStore.clear();
   1853         mProviderStatus = ProviderStatus.STATUS_NO_ACCOUNTS_NO_CONTACTS;
   1854     }
   1855 
   1856     /**
   1857      * During intialization, this content provider will
   1858      * block all attempts to change contacts data. In particular, it will hold
   1859      * up all contact syncs. As soon as the import process is complete, all
   1860      * processes waiting to write to the provider are unblocked and can proceed
   1861      * to compete for the database transaction monitor.
   1862      */
   1863     private void waitForAccess(CountDownLatch latch) {
   1864         if (latch == null) {
   1865             return;
   1866         }
   1867 
   1868         while (true) {
   1869             try {
   1870                 latch.await();
   1871                 return;
   1872             } catch (InterruptedException e) {
   1873                 Thread.currentThread().interrupt();
   1874             }
   1875         }
   1876     }
   1877 
   1878     /**
   1879      * Determines whether the given URI should be directed to the profile
   1880      * database rather than the contacts database.  This is true under either
   1881      * of three conditions:
   1882      * 1. The URI itself is specifically for the profile.
   1883      * 2. The URI contains ID references that are in the profile ID-space.
   1884      * 3. The URI contains lookup key references that match the special profile lookup key.
   1885      * @param uri The URI to examine.
   1886      * @return Whether to direct the DB operation to the profile database.
   1887      */
   1888     private boolean mapsToProfileDb(Uri uri) {
   1889         return sUriMatcher.mapsToProfile(uri);
   1890     }
   1891 
   1892     /**
   1893      * Determines whether the given URI with the given values being inserted
   1894      * should be directed to the profile database rather than the contacts
   1895      * database.  This is true if the URI already maps to the profile DB from
   1896      * a call to {@link #mapsToProfileDb} or if the URI matches a URI that
   1897      * specifies parent IDs via the ContentValues, and the given ContentValues
   1898      * contains an ID in the profile ID-space.
   1899      * @param uri The URI to examine.
   1900      * @param values The values being inserted.
   1901      * @return Whether to direct the DB insert to the profile database.
   1902      */
   1903     private boolean mapsToProfileDbWithInsertedValues(Uri uri, ContentValues values) {
   1904         if (mapsToProfileDb(uri)) {
   1905             return true;
   1906         }
   1907         int match = sUriMatcher.match(uri);
   1908         if (INSERT_URI_ID_VALUE_MAP.containsKey(match)) {
   1909             String idField = INSERT_URI_ID_VALUE_MAP.get(match);
   1910             if (values.containsKey(idField)) {
   1911                 long id = values.getAsLong(idField);
   1912                 if (ContactsContract.isProfileId(id)) {
   1913                     return true;
   1914                 }
   1915             }
   1916         }
   1917         return false;
   1918     }
   1919 
   1920     /**
   1921      * Switches the provider's thread-local context variables to prepare for performing
   1922      * a profile operation.
   1923      */
   1924     private void switchToProfileMode() {
   1925         if (ENABLE_TRANSACTION_LOG) {
   1926             Log.i(TAG, "switchToProfileMode", new RuntimeException("switchToProfileMode"));
   1927         }
   1928         mDbHelper.set(mProfileHelper);
   1929         mTransactionContext.set(mProfileTransactionContext);
   1930         mAggregator.set(mProfileAggregator);
   1931         mPhotoStore.set(mProfilePhotoStore);
   1932         mInProfileMode.set(true);
   1933     }
   1934 
   1935     /**
   1936      * Switches the provider's thread-local context variables to prepare for performing
   1937      * a contacts operation.
   1938      */
   1939     private void switchToContactMode() {
   1940         if (ENABLE_TRANSACTION_LOG) {
   1941             Log.i(TAG, "switchToContactMode", new RuntimeException("switchToContactMode"));
   1942         }
   1943         mDbHelper.set(mContactsHelper);
   1944         mTransactionContext.set(mContactTransactionContext);
   1945         mAggregator.set(mContactAggregator);
   1946         mPhotoStore.set(mContactsPhotoStore);
   1947         mInProfileMode.set(false);
   1948     }
   1949 
   1950     @Override
   1951     public Uri insert(Uri uri, ContentValues values) {
   1952         waitForAccess(mWriteAccessLatch);
   1953 
   1954         // Enforce stream items access check if applicable.
   1955         enforceSocialStreamWritePermission(uri);
   1956 
   1957         if (mapsToProfileDbWithInsertedValues(uri, values)) {
   1958             switchToProfileMode();
   1959             return mProfileProvider.insert(uri, values);
   1960         } else {
   1961             switchToContactMode();
   1962             return super.insert(uri, values);
   1963         }
   1964     }
   1965 
   1966     @Override
   1967     public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
   1968         if (mWriteAccessLatch != null) {
   1969             // Update on PROVIDER_STATUS used to be used as a trigger to re-start legacy contact
   1970             // import.  Now that we no longer support it, we just ignore it.
   1971             int match = sUriMatcher.match(uri);
   1972             if (match == PROVIDER_STATUS) {
   1973                 return 0;
   1974             }
   1975         }
   1976         waitForAccess(mWriteAccessLatch);
   1977 
   1978         // Enforce stream items access check if applicable.
   1979         enforceSocialStreamWritePermission(uri);
   1980 
   1981         if (mapsToProfileDb(uri)) {
   1982             switchToProfileMode();
   1983             return mProfileProvider.update(uri, values, selection, selectionArgs);
   1984         } else {
   1985             switchToContactMode();
   1986             return super.update(uri, values, selection, selectionArgs);
   1987         }
   1988     }
   1989 
   1990     @Override
   1991     public int delete(Uri uri, String selection, String[] selectionArgs) {
   1992         waitForAccess(mWriteAccessLatch);
   1993 
   1994         // Enforce stream items access check if applicable.
   1995         enforceSocialStreamWritePermission(uri);
   1996 
   1997         if (mapsToProfileDb(uri)) {
   1998             switchToProfileMode();
   1999             return mProfileProvider.delete(uri, selection, selectionArgs);
   2000         } else {
   2001             switchToContactMode();
   2002             return super.delete(uri, selection, selectionArgs);
   2003         }
   2004     }
   2005 
   2006     @Override
   2007     public Bundle call(String method, String arg, Bundle extras) {
   2008         waitForAccess(mReadAccessLatch);
   2009         switchToContactMode();
   2010         if (method.equals(Authorization.AUTHORIZATION_METHOD)) {
   2011             Uri uri = (Uri) extras.getParcelable(Authorization.KEY_URI_TO_AUTHORIZE);
   2012 
   2013             // Check permissions on the caller.  The URI can only be pre-authorized if the caller
   2014             // already has the necessary permissions.
   2015             enforceSocialStreamReadPermission(uri);
   2016             if (mapsToProfileDb(uri)) {
   2017                 mProfileProvider.enforceReadPermission(uri);
   2018             }
   2019 
   2020             // If there hasn't been a security violation yet, we're clear to pre-authorize the URI.
   2021             Uri authUri = preAuthorizeUri(uri);
   2022             Bundle response = new Bundle();
   2023             response.putParcelable(Authorization.KEY_AUTHORIZED_URI, authUri);
   2024             return response;
   2025         }
   2026         return null;
   2027     }
   2028 
   2029     /**
   2030      * Pre-authorizes the given URI, adding an expiring permission token to it and placing that
   2031      * in our map of pre-authorized URIs.
   2032      * @param uri The URI to pre-authorize.
   2033      * @return A pre-authorized URI that will not require special permissions to use.
   2034      */
   2035     private Uri preAuthorizeUri(Uri uri) {
   2036         String token = String.valueOf(mRandom.nextLong());
   2037         Uri authUri = uri.buildUpon()
   2038                 .appendQueryParameter(PREAUTHORIZED_URI_TOKEN, token)
   2039                 .build();
   2040         long expiration = SystemClock.elapsedRealtime() + mPreAuthorizedUriDuration;
   2041         mPreAuthorizedUris.put(authUri, expiration);
   2042 
   2043         return authUri;
   2044     }
   2045 
   2046     /**
   2047      * Checks whether the given URI has an unexpired permission token that would grant access to
   2048      * query the content.  If it does, the regular permission check should be skipped.
   2049      * @param uri The URI being accessed.
   2050      * @return Whether the URI is a pre-authorized URI that is still valid.
   2051      */
   2052     public boolean isValidPreAuthorizedUri(Uri uri) {
   2053         // Only proceed if the URI has a permission token parameter.
   2054         if (uri.getQueryParameter(PREAUTHORIZED_URI_TOKEN) != null) {
   2055             // First expire any pre-authorization URIs that are no longer valid.
   2056             long now = SystemClock.elapsedRealtime();
   2057             Set<Uri> expiredUris = Sets.newHashSet();
   2058             for (Uri preAuthUri : mPreAuthorizedUris.keySet()) {
   2059                 if (mPreAuthorizedUris.get(preAuthUri) < now) {
   2060                     expiredUris.add(preAuthUri);
   2061                 }
   2062             }
   2063             for (Uri expiredUri : expiredUris) {
   2064                 mPreAuthorizedUris.remove(expiredUri);
   2065             }
   2066 
   2067             // Now check to see if the pre-authorized URI map contains the URI.
   2068             if (mPreAuthorizedUris.containsKey(uri)) {
   2069                 // Unexpired token - skip the permission check.
   2070                 return true;
   2071             }
   2072         }
   2073         return false;
   2074     }
   2075 
   2076     @Override
   2077     protected boolean yield(ContactsTransaction transaction) {
   2078         // If there's a profile transaction in progress, and we're yielding, we need to
   2079         // end it.  Unlike the Contacts DB yield (which re-starts a transaction at its
   2080         // conclusion), we can just go back into a state in which we have no active
   2081         // profile transaction, and let it be re-created as needed.  We can't hold onto
   2082         // the transaction without risking a deadlock.
   2083         SQLiteDatabase profileDb = transaction.removeDbForTag(PROFILE_DB_TAG);
   2084         if (profileDb != null) {
   2085             profileDb.setTransactionSuccessful();
   2086             profileDb.endTransaction();
   2087         }
   2088 
   2089         // Now proceed with the Contacts DB yield.
   2090         SQLiteDatabase contactsDb = transaction.getDbForTag(CONTACTS_DB_TAG);
   2091         return contactsDb != null && contactsDb.yieldIfContendedSafely(SLEEP_AFTER_YIELD_DELAY);
   2092     }
   2093 
   2094     @Override
   2095     public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
   2096             throws OperationApplicationException {
   2097         waitForAccess(mWriteAccessLatch);
   2098         return super.applyBatch(operations);
   2099     }
   2100 
   2101     @Override
   2102     public int bulkInsert(Uri uri, ContentValues[] values) {
   2103         waitForAccess(mWriteAccessLatch);
   2104         return super.bulkInsert(uri, values);
   2105     }
   2106 
   2107     @Override
   2108     public void onBegin() {
   2109         onBeginTransactionInternal(false);
   2110     }
   2111 
   2112     protected void onBeginTransactionInternal(boolean forProfile) {
   2113         if (ENABLE_TRANSACTION_LOG) {
   2114             Log.i(TAG, "onBeginTransaction: " + (forProfile ? "profile" : "contacts"),
   2115                     new RuntimeException("onBeginTransactionInternal"));
   2116         }
   2117         if (forProfile) {
   2118             switchToProfileMode();
   2119             mProfileAggregator.clearPendingAggregations();
   2120             mProfileTransactionContext.clearExceptSearchIndexUpdates();
   2121         } else {
   2122             switchToContactMode();
   2123             mContactAggregator.clearPendingAggregations();
   2124             mContactTransactionContext.clearExceptSearchIndexUpdates();
   2125         }
   2126     }
   2127 
   2128     @Override
   2129     public void onCommit() {
   2130         onCommitTransactionInternal(false);
   2131     }
   2132 
   2133     protected void onCommitTransactionInternal(boolean forProfile) {
   2134         if (ENABLE_TRANSACTION_LOG) {
   2135             Log.i(TAG, "onCommitTransactionInternal: " + (forProfile ? "profile" : "contacts"),
   2136                     new RuntimeException("onCommitTransactionInternal"));
   2137         }
   2138         if (forProfile) {
   2139             switchToProfileMode();
   2140         } else {
   2141             switchToContactMode();
   2142         }
   2143 
   2144         flushTransactionalChanges();
   2145         final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
   2146         mAggregator.get().aggregateInTransaction(mTransactionContext.get(), db);
   2147         if (mVisibleTouched) {
   2148             mVisibleTouched = false;
   2149             mDbHelper.get().updateAllVisible();
   2150 
   2151             // Need to rebuild the fast-indxer bundle.
   2152             invalidateFastScrollingIndexCache();
   2153         }
   2154 
   2155         updateSearchIndexInTransaction();
   2156 
   2157         if (mProviderStatusUpdateNeeded) {
   2158             updateProviderStatus();
   2159             mProviderStatusUpdateNeeded = false;
   2160         }
   2161     }
   2162 
   2163     @Override
   2164     public void onRollback() {
   2165         onRollbackTransactionInternal(false);
   2166     }
   2167 
   2168     protected void onRollbackTransactionInternal(boolean forProfile) {
   2169         if (ENABLE_TRANSACTION_LOG) {
   2170             Log.i(TAG, "onRollbackTransactionInternal: " + (forProfile ? "profile" : "contacts"),
   2171                     new RuntimeException("onRollbackTransactionInternal"));
   2172         }
   2173         if (forProfile) {
   2174             switchToProfileMode();
   2175         } else {
   2176             switchToContactMode();
   2177         }
   2178 
   2179         mDbHelper.get().invalidateAllCache();
   2180     }
   2181 
   2182     private void updateSearchIndexInTransaction() {
   2183         Set<Long> staleContacts = mTransactionContext.get().getStaleSearchIndexContactIds();
   2184         Set<Long> staleRawContacts = mTransactionContext.get().getStaleSearchIndexRawContactIds();
   2185         if (!staleContacts.isEmpty() || !staleRawContacts.isEmpty()) {
   2186             mSearchIndexManager.updateIndexForRawContacts(staleContacts, staleRawContacts);
   2187             mTransactionContext.get().clearSearchIndexUpdates();
   2188         }
   2189     }
   2190 
   2191     private void flushTransactionalChanges() {
   2192         if (VERBOSE_LOGGING) {
   2193             Log.v(TAG, "flushTransactionalChanges: " + (inProfileMode() ? "profile" : "contacts"));
   2194         }
   2195 
   2196         final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
   2197         for (long rawContactId : mTransactionContext.get().getInsertedRawContactIds()) {
   2198             mDbHelper.get().updateRawContactDisplayName(db, rawContactId);
   2199             mAggregator.get().onRawContactInsert(mTransactionContext.get(), db,
   2200                     rawContactId);
   2201         }
   2202 
   2203         Set<Long> dirtyRawContacts = mTransactionContext.get().getDirtyRawContactIds();
   2204         if (!dirtyRawContacts.isEmpty()) {
   2205             mSb.setLength(0);
   2206             mSb.append(UPDATE_RAW_CONTACT_SET_DIRTY_SQL);
   2207             appendIds(mSb, dirtyRawContacts);
   2208             mSb.append(")");
   2209             db.execSQL(mSb.toString());
   2210         }
   2211 
   2212         Set<Long> updatedRawContacts = mTransactionContext.get().getUpdatedRawContactIds();
   2213         if (!updatedRawContacts.isEmpty()) {
   2214             mSb.setLength(0);
   2215             mSb.append(UPDATE_RAW_CONTACT_SET_VERSION_SQL);
   2216             appendIds(mSb, updatedRawContacts);
   2217             mSb.append(")");
   2218             db.execSQL(mSb.toString());
   2219         }
   2220 
   2221         // Update sync states.
   2222         for (Map.Entry<Long, Object> entry : mTransactionContext.get().getUpdatedSyncStates()) {
   2223             long id = entry.getKey();
   2224             if (mDbHelper.get().getSyncState().update(db, id, entry.getValue()) <= 0) {
   2225                 throw new IllegalStateException(
   2226                         "unable to update sync state, does it still exist?");
   2227             }
   2228         }
   2229 
   2230         mTransactionContext.get().clearExceptSearchIndexUpdates();
   2231     }
   2232 
   2233     /**
   2234      * Appends comma separated ids.
   2235      * @param ids Should not be empty
   2236      */
   2237     private void appendIds(StringBuilder sb, Set<Long> ids) {
   2238         for (long id : ids) {
   2239             sb.append(id).append(',');
   2240         }
   2241 
   2242         sb.setLength(sb.length() - 1); // Yank the last comma
   2243     }
   2244 
   2245     @Override
   2246     protected void notifyChange() {
   2247         notifyChange(mSyncToNetwork);
   2248         mSyncToNetwork = false;
   2249     }
   2250 
   2251     protected void notifyChange(boolean syncToNetwork) {
   2252         getContext().getContentResolver().notifyChange(ContactsContract.AUTHORITY_URI, null,
   2253                 syncToNetwork);
   2254     }
   2255 
   2256     protected void setProviderStatus(int status) {
   2257         if (mProviderStatus != status) {
   2258             mProviderStatus = status;
   2259             getContext().getContentResolver().notifyChange(ProviderStatus.CONTENT_URI, null, false);
   2260         }
   2261     }
   2262 
   2263     public DataRowHandler getDataRowHandler(final String mimeType) {
   2264         if (inProfileMode()) {
   2265             return getDataRowHandlerForProfile(mimeType);
   2266         }
   2267         DataRowHandler handler = mDataRowHandlers.get(mimeType);
   2268         if (handler == null) {
   2269             handler = new DataRowHandlerForCustomMimetype(
   2270                     getContext(), mContactsHelper, mContactAggregator, mimeType);
   2271             mDataRowHandlers.put(mimeType, handler);
   2272         }
   2273         return handler;
   2274     }
   2275 
   2276     public DataRowHandler getDataRowHandlerForProfile(final String mimeType) {
   2277         DataRowHandler handler = mProfileDataRowHandlers.get(mimeType);
   2278         if (handler == null) {
   2279             handler = new DataRowHandlerForCustomMimetype(
   2280                     getContext(), mProfileHelper, mProfileAggregator, mimeType);
   2281             mProfileDataRowHandlers.put(mimeType, handler);
   2282         }
   2283         return handler;
   2284     }
   2285 
   2286     @Override
   2287     protected Uri insertInTransaction(Uri uri, ContentValues values) {
   2288         if (VERBOSE_LOGGING) {
   2289             Log.v(TAG, "insertInTransaction: uri=" + uri + "  values=[" + values + "]");
   2290         }
   2291 
   2292         final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
   2293 
   2294         final boolean callerIsSyncAdapter =
   2295                 readBooleanQueryParameter(uri, ContactsContract.CALLER_IS_SYNCADAPTER, false);
   2296 
   2297         final int match = sUriMatcher.match(uri);
   2298         long id = 0;
   2299 
   2300         switch (match) {
   2301             case SYNCSTATE:
   2302             case PROFILE_SYNCSTATE:
   2303                 id = mDbHelper.get().getSyncState().insert(db, values);
   2304                 break;
   2305 
   2306             case CONTACTS: {
   2307                 invalidateFastScrollingIndexCache();
   2308                 insertContact(values);
   2309                 break;
   2310             }
   2311 
   2312             case PROFILE: {
   2313                 throw new UnsupportedOperationException(
   2314                         "The profile contact is created automatically");
   2315             }
   2316 
   2317             case RAW_CONTACTS:
   2318             case PROFILE_RAW_CONTACTS: {
   2319                 invalidateFastScrollingIndexCache();
   2320                 id = insertRawContact(uri, values, callerIsSyncAdapter);
   2321                 mSyncToNetwork |= !callerIsSyncAdapter;
   2322                 break;
   2323             }
   2324 
   2325             case RAW_CONTACTS_ID_DATA:
   2326             case PROFILE_RAW_CONTACTS_ID_DATA: {
   2327                 invalidateFastScrollingIndexCache();
   2328                 int segment = match == RAW_CONTACTS_ID_DATA ? 1 : 2;
   2329                 values.put(Data.RAW_CONTACT_ID, uri.getPathSegments().get(segment));
   2330                 id = insertData(values, callerIsSyncAdapter);
   2331                 mSyncToNetwork |= !callerIsSyncAdapter;
   2332                 break;
   2333             }
   2334 
   2335             case RAW_CONTACTS_ID_STREAM_ITEMS: {
   2336                 values.put(StreamItems.RAW_CONTACT_ID, uri.getPathSegments().get(1));
   2337                 id = insertStreamItem(uri, values);
   2338                 mSyncToNetwork |= !callerIsSyncAdapter;
   2339                 break;
   2340             }
   2341 
   2342             case DATA:
   2343             case PROFILE_DATA: {
   2344                 invalidateFastScrollingIndexCache();
   2345                 id = insertData(values, callerIsSyncAdapter);
   2346                 mSyncToNetwork |= !callerIsSyncAdapter;
   2347                 break;
   2348             }
   2349 
   2350             case GROUPS: {
   2351                 id = insertGroup(uri, values, callerIsSyncAdapter);
   2352                 mSyncToNetwork |= !callerIsSyncAdapter;
   2353                 break;
   2354             }
   2355 
   2356             case SETTINGS: {
   2357                 id = insertSettings(uri, values);
   2358                 mSyncToNetwork |= !callerIsSyncAdapter;
   2359                 break;
   2360             }
   2361 
   2362             case STATUS_UPDATES:
   2363             case PROFILE_STATUS_UPDATES: {
   2364                 id = insertStatusUpdate(values);
   2365                 break;
   2366             }
   2367 
   2368             case STREAM_ITEMS: {
   2369                 id = insertStreamItem(uri, values);
   2370                 mSyncToNetwork |= !callerIsSyncAdapter;
   2371                 break;
   2372             }
   2373 
   2374             case STREAM_ITEMS_PHOTOS: {
   2375                 id = insertStreamItemPhoto(uri, values);
   2376                 mSyncToNetwork |= !callerIsSyncAdapter;
   2377                 break;
   2378             }
   2379 
   2380             case STREAM_ITEMS_ID_PHOTOS: {
   2381                 values.put(StreamItemPhotos.STREAM_ITEM_ID, uri.getPathSegments().get(1));
   2382                 id = insertStreamItemPhoto(uri, values);
   2383                 mSyncToNetwork |= !callerIsSyncAdapter;
   2384                 break;
   2385             }
   2386 
   2387             default:
   2388                 mSyncToNetwork = true;
   2389                 return mLegacyApiSupport.insert(uri, values);
   2390         }
   2391 
   2392         if (id < 0) {
   2393             return null;
   2394         }
   2395 
   2396         return ContentUris.withAppendedId(uri, id);
   2397     }
   2398 
   2399     /**
   2400      * If account is non-null then store it in the values. If the account is
   2401      * already specified in the values then it must be consistent with the
   2402      * account, if it is non-null.
   2403      *
   2404      * @param uri Current {@link Uri} being operated on.
   2405      * @param values {@link ContentValues} to read and possibly update.
   2406      * @throws IllegalArgumentException when only one of
   2407      *             {@link RawContacts#ACCOUNT_NAME} or
   2408      *             {@link RawContacts#ACCOUNT_TYPE} is specified, leaving the
   2409      *             other undefined.
   2410      * @throws IllegalArgumentException when {@link RawContacts#ACCOUNT_NAME}
   2411      *             and {@link RawContacts#ACCOUNT_TYPE} are inconsistent between
   2412      *             the given {@link Uri} and {@link ContentValues}.
   2413      */
   2414     private Account resolveAccount(Uri uri, ContentValues values) throws IllegalArgumentException {
   2415         String accountName = getQueryParameter(uri, RawContacts.ACCOUNT_NAME);
   2416         String accountType = getQueryParameter(uri, RawContacts.ACCOUNT_TYPE);
   2417         final boolean partialUri = TextUtils.isEmpty(accountName) ^ TextUtils.isEmpty(accountType);
   2418 
   2419         String valueAccountName = values.getAsString(RawContacts.ACCOUNT_NAME);
   2420         String valueAccountType = values.getAsString(RawContacts.ACCOUNT_TYPE);
   2421         final boolean partialValues = TextUtils.isEmpty(valueAccountName)
   2422                 ^ TextUtils.isEmpty(valueAccountType);
   2423 
   2424         if (partialUri || partialValues) {
   2425             // Throw when either account is incomplete
   2426             throw new IllegalArgumentException(mDbHelper.get().exceptionMessage(
   2427                     "Must specify both or neither of ACCOUNT_NAME and ACCOUNT_TYPE", uri));
   2428         }
   2429 
   2430         // Accounts are valid by only checking one parameter, since we've
   2431         // already ruled out partial accounts.
   2432         final boolean validUri = !TextUtils.isEmpty(accountName);
   2433         final boolean validValues = !TextUtils.isEmpty(valueAccountName);
   2434 
   2435         if (validValues && validUri) {
   2436             // Check that accounts match when both present
   2437             final boolean accountMatch = TextUtils.equals(accountName, valueAccountName)
   2438                     && TextUtils.equals(accountType, valueAccountType);
   2439             if (!accountMatch) {
   2440                 throw new IllegalArgumentException(mDbHelper.get().exceptionMessage(
   2441                         "When both specified, ACCOUNT_NAME and ACCOUNT_TYPE must match", uri));
   2442             }
   2443         } else if (validUri) {
   2444             // Fill values from Uri when not present
   2445             values.put(RawContacts.ACCOUNT_NAME, accountName);
   2446             values.put(RawContacts.ACCOUNT_TYPE, accountType);
   2447         } else if (validValues) {
   2448             accountName = valueAccountName;
   2449             accountType = valueAccountType;
   2450         } else {
   2451             return null;
   2452         }
   2453 
   2454         // Use cached Account object when matches, otherwise create
   2455         if (mAccount == null
   2456                 || !mAccount.name.equals(accountName)
   2457                 || !mAccount.type.equals(accountType)) {
   2458             mAccount = new Account(accountName, accountType);
   2459         }
   2460 
   2461         return mAccount;
   2462     }
   2463 
   2464     /**
   2465      * Resolves the account and builds an {@link AccountWithDataSet} based on the data set specified
   2466      * in the URI or values (if any).
   2467      * @param uri Current {@link Uri} being operated on.
   2468      * @param values {@link ContentValues} to read and possibly update.
   2469      */
   2470     private AccountWithDataSet resolveAccountWithDataSet(Uri uri, ContentValues values) {
   2471         final Account account = resolveAccount(uri, values);
   2472         AccountWithDataSet accountWithDataSet = null;
   2473         if (account != null) {
   2474             String dataSet = getQueryParameter(uri, RawContacts.DATA_SET);
   2475             if (dataSet == null) {
   2476                 dataSet = values.getAsString(RawContacts.DATA_SET);
   2477             } else {
   2478                 values.put(RawContacts.DATA_SET, dataSet);
   2479             }
   2480             accountWithDataSet = AccountWithDataSet.get(account.name, account.type, dataSet);
   2481         }
   2482         return accountWithDataSet;
   2483     }
   2484 
   2485     /**
   2486      * Same as {@link #resolveAccountWithDataSet}, but returns the account id for the
   2487      *     {@link AccountWithDataSet}.  Used for insert.
   2488      *
   2489      * May update the account cache; must be used only in a transaction.
   2490      */
   2491     private long resolveAccountIdInTransaction(Uri uri, ContentValues values) {
   2492         return mDbHelper.get().getOrCreateAccountIdInTransaction(
   2493                 resolveAccountWithDataSet(uri, mValues));
   2494     }
   2495 
   2496     /**
   2497      * Inserts an item in the contacts table
   2498      *
   2499      * @param values the values for the new row
   2500      * @return the row ID of the newly created row
   2501      */
   2502     private long insertContact(ContentValues values) {
   2503         throw new UnsupportedOperationException("Aggregate contacts are created automatically");
   2504     }
   2505 
   2506     /**
   2507      * Inserts an item in the raw contacts table
   2508      *
   2509      * @param uri the values for the new row
   2510      * @param values the account this contact should be associated with. may be null.
   2511      * @param callerIsSyncAdapter
   2512      * @return the row ID of the newly created row
   2513      */
   2514     private long insertRawContact(Uri uri, ContentValues values, boolean callerIsSyncAdapter) {
   2515         mValues.clear();
   2516         mValues.putAll(values);
   2517         mValues.putNull(RawContacts.CONTACT_ID);
   2518 
   2519         final long accountId = resolveAccountIdInTransaction(uri, mValues);
   2520         mValues.remove(RawContacts.ACCOUNT_NAME);
   2521         mValues.remove(RawContacts.ACCOUNT_TYPE);
   2522         mValues.remove(RawContacts.DATA_SET);
   2523         mValues.put(RawContactsColumns.ACCOUNT_ID, accountId);
   2524 
   2525         if (values.containsKey(RawContacts.DELETED)
   2526                 && values.getAsInteger(RawContacts.DELETED) != 0) {
   2527             mValues.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DISABLED);
   2528         }
   2529 
   2530         final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
   2531 
   2532         long rawContactId = db.insert(Tables.RAW_CONTACTS,
   2533                 RawContacts.CONTACT_ID, mValues);
   2534         int aggregationMode = RawContacts.AGGREGATION_MODE_DEFAULT;
   2535         if (mValues.containsKey(RawContacts.AGGREGATION_MODE)) {
   2536             aggregationMode = mValues.getAsInteger(RawContacts.AGGREGATION_MODE);
   2537         }
   2538         mAggregator.get().markNewForAggregation(rawContactId, aggregationMode);
   2539 
   2540         // Trigger creation of a Contact based on this RawContact at the end of transaction
   2541         mTransactionContext.get().rawContactInserted(rawContactId, accountId);
   2542 
   2543         if (!callerIsSyncAdapter) {
   2544             addAutoAddMembership(rawContactId);
   2545             final Long starred = values.getAsLong(RawContacts.STARRED);
   2546             if (starred != null && starred != 0) {
   2547                 updateFavoritesMembership(rawContactId, starred != 0);
   2548             }
   2549         }
   2550 
   2551         mProviderStatusUpdateNeeded = true;
   2552         return rawContactId;
   2553     }
   2554 
   2555     private void addAutoAddMembership(long rawContactId) {
   2556         final Long groupId = findGroupByRawContactId(SELECTION_AUTO_ADD_GROUPS_BY_RAW_CONTACT_ID,
   2557                 rawContactId);
   2558         if (groupId != null) {
   2559             insertDataGroupMembership(rawContactId, groupId);
   2560         }
   2561     }
   2562 
   2563     private Long findGroupByRawContactId(String selection, long rawContactId) {
   2564         final SQLiteDatabase db = mDbHelper.get().getReadableDatabase();
   2565         Cursor c = db.query(Tables.GROUPS + "," + Tables.RAW_CONTACTS,
   2566                 PROJECTION_GROUP_ID, selection,
   2567                 new String[]{Long.toString(rawContactId)},
   2568                 null /* groupBy */, null /* having */, null /* orderBy */);
   2569         try {
   2570             while (c.moveToNext()) {
   2571                 return c.getLong(0);
   2572             }
   2573             return null;
   2574         } finally {
   2575             c.close();
   2576         }
   2577     }
   2578 
   2579     private void updateFavoritesMembership(long rawContactId, boolean isStarred) {
   2580         final Long groupId = findGroupByRawContactId(SELECTION_FAVORITES_GROUPS_BY_RAW_CONTACT_ID,
   2581                 rawContactId);
   2582         if (groupId != null) {
   2583             if (isStarred) {
   2584                 insertDataGroupMembership(rawContactId, groupId);
   2585             } else {
   2586                 deleteDataGroupMembership(rawContactId, groupId);
   2587             }
   2588         }
   2589     }
   2590 
   2591     private void insertDataGroupMembership(long rawContactId, long groupId) {
   2592         ContentValues groupMembershipValues = new ContentValues();
   2593         groupMembershipValues.put(GroupMembership.GROUP_ROW_ID, groupId);
   2594         groupMembershipValues.put(GroupMembership.RAW_CONTACT_ID, rawContactId);
   2595         groupMembershipValues.put(DataColumns.MIMETYPE_ID,
   2596                 mDbHelper.get().getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE));
   2597         final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
   2598         db.insert(Tables.DATA, null, groupMembershipValues);
   2599     }
   2600 
   2601     private void deleteDataGroupMembership(long rawContactId, long groupId) {
   2602         final String[] selectionArgs = {
   2603                 Long.toString(mDbHelper.get().getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE)),
   2604                 Long.toString(groupId),
   2605                 Long.toString(rawContactId)};
   2606         final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
   2607         db.delete(Tables.DATA, SELECTION_GROUPMEMBERSHIP_DATA, selectionArgs);
   2608     }
   2609 
   2610     /**
   2611      * Inserts an item in the data table
   2612      *
   2613      * @param values the values for the new row
   2614      * @return the row ID of the newly created row
   2615      */
   2616     private long insertData(ContentValues values, boolean callerIsSyncAdapter) {
   2617         long id = 0;
   2618         mValues.clear();
   2619         mValues.putAll(values);
   2620 
   2621         long rawContactId = mValues.getAsLong(Data.RAW_CONTACT_ID);
   2622 
   2623         // Replace package with internal mapping
   2624         final String packageName = mValues.getAsString(Data.RES_PACKAGE);
   2625         if (packageName != null) {
   2626             mValues.put(DataColumns.PACKAGE_ID, mDbHelper.get().getPackageId(packageName));
   2627         }
   2628         mValues.remove(Data.RES_PACKAGE);
   2629 
   2630         // Replace mimetype with internal mapping
   2631         final String mimeType = mValues.getAsString(Data.MIMETYPE);
   2632         if (TextUtils.isEmpty(mimeType)) {
   2633             throw new IllegalArgumentException(Data.MIMETYPE + " is required");
   2634         }
   2635 
   2636         mValues.put(DataColumns.MIMETYPE_ID, mDbHelper.get().getMimeTypeId(mimeType));
   2637         mValues.remove(Data.MIMETYPE);
   2638 
   2639         DataRowHandler rowHandler = getDataRowHandler(mimeType);
   2640         final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
   2641         id = rowHandler.insert(db, mTransactionContext.get(), rawContactId, mValues);
   2642         if (!callerIsSyncAdapter) {
   2643             mTransactionContext.get().markRawContactDirty(rawContactId);
   2644         }
   2645         mTransactionContext.get().rawContactUpdated(rawContactId);
   2646         return id;
   2647     }
   2648 
   2649     /**
   2650      * Inserts an item in the stream_items table.  The account is checked against the
   2651      * account in the raw contact for which the stream item is being inserted.  If the
   2652      * new stream item results in more stream items under this raw contact than the limit,
   2653      * the oldest one will be deleted (note that if the stream item inserted was the
   2654      * oldest, it will be immediately deleted, and this will return 0).
   2655      *
   2656      * @param uri the insertion URI
   2657      * @param values the values for the new row
   2658      * @return the stream item _ID of the newly created row, or 0 if it was not created
   2659      */
   2660     private long insertStreamItem(Uri uri, ContentValues values) {
   2661         long id = 0;
   2662         mValues.clear();
   2663         mValues.putAll(values);
   2664 
   2665         long rawContactId = mValues.getAsLong(StreamItems.RAW_CONTACT_ID);
   2666 
   2667         // Don't attempt to insert accounts params - they don't exist in the stream items table.
   2668         mValues.remove(RawContacts.ACCOUNT_NAME);
   2669         mValues.remove(RawContacts.ACCOUNT_TYPE);
   2670 
   2671         // Insert the new stream item.
   2672         final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
   2673         id = db.insert(Tables.STREAM_ITEMS, null, mValues);
   2674         if (id == -1) {
   2675             // Insertion failed.
   2676             return 0;
   2677         }
   2678 
   2679         // Check to see if we're over the limit for stream items under this raw contact.
   2680         // It's possible that the inserted stream item is older than the the existing
   2681         // ones, in which case it may be deleted immediately (resetting the ID to 0).
   2682         id = cleanUpOldStreamItems(rawContactId, id);
   2683 
   2684         return id;
   2685     }
   2686 
   2687     /**
   2688      * Inserts an item in the stream_item_photos table.  The account is checked against
   2689      * the account in the raw contact that owns the stream item being modified.
   2690      *
   2691      * @param uri the insertion URI
   2692      * @param values the values for the new row
   2693      * @return the stream item photo _ID of the newly created row, or 0 if there was an issue
   2694      *     with processing the photo or creating the row
   2695      */
   2696     private long insertStreamItemPhoto(Uri uri, ContentValues values) {
   2697         long id = 0;
   2698         mValues.clear();
   2699         mValues.putAll(values);
   2700 
   2701         long streamItemId = mValues.getAsLong(StreamItemPhotos.STREAM_ITEM_ID);
   2702         if (streamItemId != 0) {
   2703             long rawContactId = lookupRawContactIdForStreamId(streamItemId);
   2704 
   2705             // Don't attempt to insert accounts params - they don't exist in the stream item
   2706             // photos table.
   2707             mValues.remove(RawContacts.ACCOUNT_NAME);
   2708             mValues.remove(RawContacts.ACCOUNT_TYPE);
   2709 
   2710             // Process the photo and store it.
   2711             if (processStreamItemPhoto(mValues, false)) {
   2712                 // Insert the stream item photo.
   2713                 final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
   2714                 id = db.insert(Tables.STREAM_ITEM_PHOTOS, null, mValues);
   2715             }
   2716         }
   2717         return id;
   2718     }
   2719 
   2720     /**
   2721      * Processes the photo contained in the {@link ContactsContract.StreamItemPhotos#PHOTO}
   2722      * field of the given values, attempting to store it in the photo store.  If successful,
   2723      * the resulting photo file ID will be added to the values for insert/update in the table.
   2724      * <p>
   2725      * If updating, it is valid for the picture to be empty or unspecified (the function will
   2726      * still return true).  If inserting, a valid picture must be specified.
   2727      * @param values The content values provided by the caller.
   2728      * @param forUpdate Whether this photo is being processed for update (vs. insert).
   2729      * @return Whether the insert or update should proceed.
   2730      */
   2731     private boolean processStreamItemPhoto(ContentValues values, boolean forUpdate) {
   2732         if (!values.containsKey(StreamItemPhotos.PHOTO)) {
   2733             return forUpdate;
   2734         }
   2735         byte[] photoBytes = values.getAsByteArray(StreamItemPhotos.PHOTO);
   2736         if (photoBytes == null) {
   2737             return forUpdate;
   2738         }
   2739 
   2740         // Process the photo and store it.
   2741         try {
   2742             long photoFileId = mPhotoStore.get().insert(new PhotoProcessor(photoBytes,
   2743                     getMaxDisplayPhotoDim(), getMaxThumbnailDim(), true), true);
   2744             if (photoFileId != 0) {
   2745                 values.put(StreamItemPhotos.PHOTO_FILE_ID, photoFileId);
   2746                 values.remove(StreamItemPhotos.PHOTO);
   2747                 return true;
   2748             } else {
   2749                 // Couldn't store the photo, return 0.
   2750                 Log.e(TAG, "Could not process stream item photo for insert");
   2751                 return false;
   2752             }
   2753         } catch (IOException ioe) {
   2754             Log.e(TAG, "Could not process stream item photo for insert", ioe);
   2755             return false;
   2756         }
   2757     }
   2758 
   2759     /**
   2760      * Looks up the raw contact ID that owns the specified stream item.
   2761      * @param streamItemId The ID of the stream item.
   2762      * @return The associated raw contact ID, or -1 if no such stream item exists.
   2763      */
   2764     private long lookupRawContactIdForStreamId(long streamItemId) {
   2765         long rawContactId = -1;
   2766         final SQLiteDatabase db = mDbHelper.get().getReadableDatabase();
   2767         Cursor c = db.query(Tables.STREAM_ITEMS,
   2768                 new String[]{StreamItems.RAW_CONTACT_ID},
   2769                 StreamItems._ID + "=?", new String[]{String.valueOf(streamItemId)},
   2770                 null, null, null);
   2771         try {
   2772             if (c.moveToFirst()) {
   2773                 rawContactId = c.getLong(0);
   2774             }
   2775         } finally {
   2776             c.close();
   2777         }
   2778         return rawContactId;
   2779     }
   2780 
   2781     /**
   2782      * If the given URI is reading stream items or stream photos, this will run a permission check
   2783      * for the android.permission.READ_SOCIAL_STREAM permission - otherwise it will do nothing.
   2784      * @param uri The URI to check.
   2785      */
   2786     private void enforceSocialStreamReadPermission(Uri uri) {
   2787         if (SOCIAL_STREAM_URIS.contains(sUriMatcher.match(uri))
   2788                 && !isValidPreAuthorizedUri(uri)) {
   2789             getContext().enforceCallingOrSelfPermission(
   2790                     "android.permission.READ_SOCIAL_STREAM", null);
   2791         }
   2792     }
   2793 
   2794     /**
   2795      * If the given URI is modifying stream items or stream photos, this will run a permission check
   2796      * for the android.permission.WRITE_SOCIAL_STREAM permission - otherwise it will do nothing.
   2797      * @param uri The URI to check.
   2798      */
   2799     private void enforceSocialStreamWritePermission(Uri uri) {
   2800         if (SOCIAL_STREAM_URIS.contains(sUriMatcher.match(uri))) {
   2801             getContext().enforceCallingOrSelfPermission(
   2802                     "android.permission.WRITE_SOCIAL_STREAM", null);
   2803         }
   2804     }
   2805 
   2806     /**
   2807      * Queries the database for stream items under the given raw contact.  If there are
   2808      * more entries than {@link ContactsProvider2#MAX_STREAM_ITEMS_PER_RAW_CONTACT},
   2809      * the oldest entries (as determined by timestamp) will be deleted.
   2810      * @param rawContactId The raw contact ID to examine for stream items.
   2811      * @param insertedStreamItemId The ID of the stream item that was just inserted,
   2812      *     prompting this cleanup.  Callers may pass 0 if no insertion prompted the
   2813      *     cleanup.
   2814      * @return The ID of the inserted stream item if it still exists after cleanup;
   2815      *     0 otherwise.
   2816      */
   2817     private long cleanUpOldStreamItems(long rawContactId, long insertedStreamItemId) {
   2818         long postCleanupInsertedStreamId = insertedStreamItemId;
   2819         final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
   2820         Cursor c = db.query(Tables.STREAM_ITEMS, new String[]{StreamItems._ID},
   2821                 StreamItems.RAW_CONTACT_ID + "=?", new String[]{String.valueOf(rawContactId)},
   2822                 null, null, StreamItems.TIMESTAMP + " DESC, " + StreamItems._ID + " DESC");
   2823         try {
   2824             int streamItemCount = c.getCount();
   2825             if (streamItemCount <= MAX_STREAM_ITEMS_PER_RAW_CONTACT) {
   2826                 // Still under the limit - nothing to clean up!
   2827                 return insertedStreamItemId;
   2828             } else {
   2829                 c.moveToLast();
   2830                 while (c.getPosition() >= MAX_STREAM_ITEMS_PER_RAW_CONTACT) {
   2831                     long streamItemId = c.getLong(0);
   2832                     if (insertedStreamItemId == streamItemId) {
   2833                         // The stream item just inserted is being deleted.
   2834                         postCleanupInsertedStreamId = 0;
   2835                     }
   2836                     deleteStreamItem(db, c.getLong(0));
   2837                     c.moveToPrevious();
   2838                 }
   2839             }
   2840         } finally {
   2841             c.close();
   2842         }
   2843         return postCleanupInsertedStreamId;
   2844     }
   2845 
   2846     /**
   2847      * Delete data row by row so that fixing of primaries etc work correctly.
   2848      */
   2849     private int deleteData(String selection, String[] selectionArgs, boolean callerIsSyncAdapter) {
   2850         int count = 0;
   2851 
   2852         final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
   2853 
   2854         // Note that the query will return data according to the access restrictions,
   2855         // so we don't need to worry about deleting data we don't have permission to read.
   2856         Uri dataUri = inProfileMode()
   2857                 ? Uri.withAppendedPath(Profile.CONTENT_URI, RawContacts.Data.CONTENT_DIRECTORY)
   2858                 : Data.CONTENT_URI;
   2859         Cursor c = query(dataUri, DataRowHandler.DataDeleteQuery.COLUMNS,
   2860                 selection, selectionArgs, null);
   2861         try {
   2862             while(c.moveToNext()) {
   2863                 long rawContactId = c.getLong(DataRowHandler.DataDeleteQuery.RAW_CONTACT_ID);
   2864                 String mimeType = c.getString(DataRowHandler.DataDeleteQuery.MIMETYPE);
   2865                 DataRowHandler rowHandler = getDataRowHandler(mimeType);
   2866                 count += rowHandler.delete(db, mTransactionContext.get(), c);
   2867                 if (!callerIsSyncAdapter) {
   2868                     mTransactionContext.get().markRawContactDirty(rawContactId);
   2869                 }
   2870             }
   2871         } finally {
   2872             c.close();
   2873         }
   2874 
   2875         return count;
   2876     }
   2877 
   2878     /**
   2879      * Delete a data row provided that it is one of the allowed mime types.
   2880      */
   2881     public int deleteData(long dataId, String[] allowedMimeTypes) {
   2882 
   2883         final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
   2884 
   2885         // Note that the query will return data according to the access restrictions,
   2886         // so we don't need to worry about deleting data we don't have permission to read.
   2887         mSelectionArgs1[0] = String.valueOf(dataId);
   2888         Cursor c = query(Data.CONTENT_URI, DataRowHandler.DataDeleteQuery.COLUMNS, Data._ID + "=?",
   2889                 mSelectionArgs1, null);
   2890 
   2891         try {
   2892             if (!c.moveToFirst()) {
   2893                 return 0;
   2894             }
   2895 
   2896             String mimeType = c.getString(DataRowHandler.DataDeleteQuery.MIMETYPE);
   2897             boolean valid = false;
   2898             for (int i = 0; i < allowedMimeTypes.length; i++) {
   2899                 if (TextUtils.equals(mimeType, allowedMimeTypes[i])) {
   2900                     valid = true;
   2901                     break;
   2902                 }
   2903             }
   2904 
   2905             if (!valid) {
   2906                 throw new IllegalArgumentException("Data type mismatch: expected "
   2907                         + Lists.newArrayList(allowedMimeTypes));
   2908             }
   2909             DataRowHandler rowHandler = getDataRowHandler(mimeType);
   2910             return rowHandler.delete(db, mTransactionContext.get(), c);
   2911         } finally {
   2912             c.close();
   2913         }
   2914     }
   2915 
   2916     /**
   2917      * Inserts an item in the groups table
   2918      */
   2919     private long insertGroup(Uri uri, ContentValues values, boolean callerIsSyncAdapter) {
   2920         mValues.clear();
   2921         mValues.putAll(values);
   2922 
   2923         final long accountId = mDbHelper.get().getOrCreateAccountIdInTransaction(
   2924                 resolveAccountWithDataSet(uri, mValues));
   2925         mValues.remove(Groups.ACCOUNT_NAME);
   2926         mValues.remove(Groups.ACCOUNT_TYPE);
   2927         mValues.remove(Groups.DATA_SET);
   2928         mValues.put(GroupsColumns.ACCOUNT_ID, accountId);
   2929 
   2930         // Replace package with internal mapping
   2931         final String packageName = mValues.getAsString(Groups.RES_PACKAGE);
   2932         if (packageName != null) {
   2933             mValues.put(GroupsColumns.PACKAGE_ID, mDbHelper.get().getPackageId(packageName));
   2934         }
   2935         mValues.remove(Groups.RES_PACKAGE);
   2936 
   2937         final boolean isFavoritesGroup = mValues.getAsLong(Groups.FAVORITES) != null
   2938                 ? mValues.getAsLong(Groups.FAVORITES) != 0
   2939                 : false;
   2940 
   2941         if (!callerIsSyncAdapter) {
   2942             mValues.put(Groups.DIRTY, 1);
   2943         }
   2944 
   2945         final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
   2946 
   2947         long result = db.insert(Tables.GROUPS, Groups.TITLE, mValues);
   2948 
   2949         if (!callerIsSyncAdapter && isFavoritesGroup) {
   2950             // If the inserted group is a favorite group, add all starred raw contacts to it.
   2951             mSelectionArgs1[0] = Long.toString(accountId);
   2952             Cursor c = db.query(Tables.RAW_CONTACTS,
   2953                     new String[]{RawContacts._ID, RawContacts.STARRED},
   2954                     RawContactsColumns.CONCRETE_ACCOUNT_ID + "=?", mSelectionArgs1,
   2955                     null, null, null);
   2956             try {
   2957                 while (c.moveToNext()) {
   2958                     if (c.getLong(1) != 0) {
   2959                         final long rawContactId = c.getLong(0);
   2960                         insertDataGroupMembership(rawContactId, result);
   2961                         mTransactionContext.get().markRawContactDirty(rawContactId);
   2962                     }
   2963                 }
   2964             } finally {
   2965                 c.close();
   2966             }
   2967         }
   2968 
   2969         if (mValues.containsKey(Groups.GROUP_VISIBLE)) {
   2970             mVisibleTouched = true;
   2971         }
   2972 
   2973         return result;
   2974     }
   2975 
   2976     private long insertSettings(Uri uri, ContentValues values) {
   2977         // Before inserting, ensure that no settings record already exists for the
   2978         // values being inserted (this used to be enforced by a primary key, but that no
   2979         // longer works with the nullable data_set field added).
   2980         String accountName = values.getAsString(Settings.ACCOUNT_NAME);
   2981         String accountType = values.getAsString(Settings.ACCOUNT_TYPE);
   2982         String dataSet = values.getAsString(Settings.DATA_SET);
   2983         Uri.Builder settingsUri = Settings.CONTENT_URI.buildUpon();
   2984         if (accountName != null) {
   2985             settingsUri.appendQueryParameter(Settings.ACCOUNT_NAME, accountName);
   2986         }
   2987         if (accountType != null) {
   2988             settingsUri.appendQueryParameter(Settings.ACCOUNT_TYPE, accountType);
   2989         }
   2990         if (dataSet != null) {
   2991             settingsUri.appendQueryParameter(Settings.DATA_SET, dataSet);
   2992         }
   2993         Cursor c = queryLocal(settingsUri.build(), null, null, null, null, 0, null);
   2994         try {
   2995             if (c.getCount() > 0) {
   2996                 // If a record was found, replace it with the new values.
   2997                 String selection = null;
   2998                 String[] selectionArgs = null;
   2999                 if (accountName != null && accountType != null) {
   3000                     selection = Settings.ACCOUNT_NAME + "=? AND " + Settings.ACCOUNT_TYPE + "=?";
   3001                     if (dataSet == null) {
   3002                         selection += " AND " + Settings.DATA_SET + " IS NULL";
   3003                         selectionArgs = new String[] {accountName, accountType};
   3004                     } else {
   3005                         selection += " AND " + Settings.DATA_SET + "=?";
   3006                         selectionArgs = new String[] {accountName, accountType, dataSet};
   3007                     }
   3008                 }
   3009                 return updateSettings(uri, values, selection, selectionArgs);
   3010             }
   3011         } finally {
   3012             c.close();
   3013         }
   3014 
   3015         final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
   3016 
   3017         // If we didn't find a duplicate, we're fine to insert.
   3018         final long id = db.insert(Tables.SETTINGS, null, values);
   3019 
   3020         if (values.containsKey(Settings.UNGROUPED_VISIBLE)) {
   3021             mVisibleTouched = true;
   3022         }
   3023 
   3024         return id;
   3025     }
   3026 
   3027     /**
   3028      * Inserts a status update.
   3029      */
   3030     public long insertStatusUpdate(ContentValues values) {
   3031         final String handle = values.getAsString(StatusUpdates.IM_HANDLE);
   3032         final Integer protocol = values.getAsInteger(StatusUpdates.PROTOCOL);
   3033         String customProtocol = null;
   3034 
   3035         final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
   3036 
   3037         if (protocol != null && protocol == Im.PROTOCOL_CUSTOM) {
   3038             customProtocol = values.getAsString(StatusUpdates.CUSTOM_PROTOCOL);
   3039             if (TextUtils.isEmpty(customProtocol)) {
   3040                 throw new IllegalArgumentException(
   3041                         "CUSTOM_PROTOCOL is required when PROTOCOL=PROTOCOL_CUSTOM");
   3042             }
   3043         }
   3044 
   3045         long rawContactId = -1;
   3046         long contactId = -1;
   3047         Long dataId = values.getAsLong(StatusUpdates.DATA_ID);
   3048         String accountType = null;
   3049         String accountName = null;
   3050         mSb.setLength(0);
   3051         mSelectionArgs.clear();
   3052         if (dataId != null) {
   3053             // Lookup the contact info for the given data row.
   3054 
   3055             mSb.append(Tables.DATA + "." + Data._ID + "=?");
   3056             mSelectionArgs.add(String.valueOf(dataId));
   3057         } else {
   3058             // Lookup the data row to attach this presence update to
   3059 
   3060             if (TextUtils.isEmpty(handle) || protocol == null) {
   3061                 throw new IllegalArgumentException("PROTOCOL and IM_HANDLE are required");
   3062             }
   3063 
   3064             // TODO: generalize to allow other providers to match against email
   3065             boolean matchEmail = Im.PROTOCOL_GOOGLE_TALK == protocol;
   3066 
   3067             String mimeTypeIdIm = String.valueOf(mDbHelper.get().getMimeTypeIdForIm());
   3068             if (matchEmail) {
   3069                 String mimeTypeIdEmail = String.valueOf(mDbHelper.get().getMimeTypeIdForEmail());
   3070 
   3071                 // The following hack forces SQLite to use the (mimetype_id,data1) index, otherwise
   3072                 // the "OR" conjunction confuses it and it switches to a full scan of
   3073                 // the raw_contacts table.
   3074 
   3075                 // This code relies on the fact that Im.DATA and Email.DATA are in fact the same
   3076                 // column - Data.DATA1
   3077                 mSb.append(DataColumns.MIMETYPE_ID + " IN (?,?)" +
   3078                         " AND " + Data.DATA1 + "=?" +
   3079                         " AND ((" + DataColumns.MIMETYPE_ID + "=? AND " + Im.PROTOCOL + "=?");
   3080                 mSelectionArgs.add(mimeTypeIdEmail);
   3081                 mSelectionArgs.add(mimeTypeIdIm);
   3082                 mSelectionArgs.add(handle);
   3083                 mSelectionArgs.add(mimeTypeIdIm);
   3084                 mSelectionArgs.add(String.valueOf(protocol));
   3085                 if (customProtocol != null) {
   3086                     mSb.append(" AND " + Im.CUSTOM_PROTOCOL + "=?");
   3087                     mSelectionArgs.add(customProtocol);
   3088                 }
   3089                 mSb.append(") OR (" + DataColumns.MIMETYPE_ID + "=?))");
   3090                 mSelectionArgs.add(mimeTypeIdEmail);
   3091             } else {
   3092                 mSb.append(DataColumns.MIMETYPE_ID + "=?" +
   3093                         " AND " + Im.PROTOCOL + "=?" +
   3094                         " AND " + Im.DATA + "=?");
   3095                 mSelectionArgs.add(mimeTypeIdIm);
   3096                 mSelectionArgs.add(String.valueOf(protocol));
   3097                 mSelectionArgs.add(handle);
   3098                 if (customProtocol != null) {
   3099                     mSb.append(" AND " + Im.CUSTOM_PROTOCOL + "=?");
   3100                     mSelectionArgs.add(customProtocol);
   3101                 }
   3102             }
   3103 
   3104             if (values.containsKey(StatusUpdates.DATA_ID)) {
   3105                 mSb.append(" AND " + DataColumns.CONCRETE_ID + "=?");
   3106                 mSelectionArgs.add(values.getAsString(StatusUpdates.DATA_ID));
   3107             }
   3108         }
   3109 
   3110         Cursor cursor = null;
   3111         try {
   3112             cursor = db.query(DataContactsQuery.TABLE, DataContactsQuery.PROJECTION,
   3113                     mSb.toString(), mSelectionArgs.toArray(EMPTY_STRING_ARRAY), null, null,
   3114                     Clauses.CONTACT_VISIBLE + " DESC, " + Data.RAW_CONTACT_ID);
   3115             if (cursor.moveToFirst()) {
   3116                 dataId = cursor.getLong(DataContactsQuery.DATA_ID);
   3117                 rawContactId = cursor.getLong(DataContactsQuery.RAW_CONTACT_ID);
   3118                 accountType = cursor.getString(DataContactsQuery.ACCOUNT_TYPE);
   3119                 accountName = cursor.getString(DataContactsQuery.ACCOUNT_NAME);
   3120                 contactId = cursor.getLong(DataContactsQuery.CONTACT_ID);
   3121             } else {
   3122                 // No contact found, return a null URI
   3123                 return -1;
   3124             }
   3125         } finally {
   3126             if (cursor != null) {
   3127                 cursor.close();
   3128             }
   3129         }
   3130 
   3131         if (values.containsKey(StatusUpdates.PRESENCE)) {
   3132             if (customProtocol == null) {
   3133                 // We cannot allow a null in the custom protocol field, because SQLite3 does not
   3134                 // properly enforce uniqueness of null values
   3135                 customProtocol = "";
   3136             }
   3137 
   3138             mValues.clear();
   3139             mValues.put(StatusUpdates.DATA_ID, dataId);
   3140             mValues.put(PresenceColumns.RAW_CONTACT_ID, rawContactId);
   3141             mValues.put(PresenceColumns.CONTACT_ID, contactId);
   3142             mValues.put(StatusUpdates.PROTOCOL, protocol);
   3143             mValues.put(StatusUpdates.CUSTOM_PROTOCOL, customProtocol);
   3144             mValues.put(StatusUpdates.IM_HANDLE, handle);
   3145             if (values.containsKey(StatusUpdates.IM_ACCOUNT)) {
   3146                 mValues.put(StatusUpdates.IM_ACCOUNT, values.getAsString(StatusUpdates.IM_ACCOUNT));
   3147             }
   3148             mValues.put(StatusUpdates.PRESENCE,
   3149                     values.getAsString(StatusUpdates.PRESENCE));
   3150             mValues.put(StatusUpdates.CHAT_CAPABILITY,
   3151                     values.getAsString(StatusUpdates.CHAT_CAPABILITY));
   3152 
   3153             // Insert the presence update
   3154             db.replace(Tables.PRESENCE, null, mValues);
   3155         }
   3156 
   3157 
   3158         if (values.containsKey(StatusUpdates.STATUS)) {
   3159             String status = values.getAsString(StatusUpdates.STATUS);
   3160             String resPackage = values.getAsString(StatusUpdates.STATUS_RES_PACKAGE);
   3161             Resources resources = getContext().getResources();
   3162             if (!TextUtils.isEmpty(resPackage)) {
   3163                 PackageManager pm = getContext().getPackageManager();
   3164                 try {
   3165                     resources = pm.getResourcesForApplication(resPackage);
   3166                 } catch (NameNotFoundException e) {
   3167                     Log.w(TAG, "Contact status update resource package not found: "
   3168                             + resPackage);
   3169                 }
   3170             }
   3171             Integer labelResourceId = values.getAsInteger(StatusUpdates.STATUS_LABEL);
   3172 
   3173             if ((labelResourceId == null || labelResourceId == 0) && protocol != null) {
   3174                 labelResourceId = Im.getProtocolLabelResource(protocol);
   3175             }
   3176             String labelResource = getResourceName(resources, "string", labelResourceId);
   3177 
   3178             Integer iconResourceId = values.getAsInteger(StatusUpdates.STATUS_ICON);
   3179             // TODO compute the default icon based on the protocol
   3180 
   3181             String iconResource = getResourceName(resources, "drawable", iconResourceId);
   3182 
   3183             if (TextUtils.isEmpty(status)) {
   3184                 mDbHelper.get().deleteStatusUpdate(dataId);
   3185             } else {
   3186                 Long timestamp = values.getAsLong(StatusUpdates.STATUS_TIMESTAMP);
   3187                 if (timestamp != null) {
   3188                     mDbHelper.get().replaceStatusUpdate(dataId, timestamp, status, resPackage,
   3189                             iconResourceId, labelResourceId);
   3190                 } else {
   3191                     mDbHelper.get().insertStatusUpdate(dataId, status, resPackage, iconResourceId,
   3192                             labelResourceId);
   3193                 }
   3194 
   3195                 // For forward compatibility with the new stream item API, insert this status update
   3196                 // there as well.  If we already have a stream item from this source, update that
   3197                 // one instead of inserting a new one (since the semantics of the old status update
   3198                 // API is to only have a single record).
   3199                 if (rawContactId != -1 && !TextUtils.isEmpty(status)) {
   3200                     ContentValues streamItemValues = new ContentValues();
   3201                     streamItemValues.put(StreamItems.RAW_CONTACT_ID, rawContactId);
   3202                     // Status updates are text only but stream items are HTML.
   3203                     streamItemValues.put(StreamItems.TEXT, statusUpdateToHtml(status));
   3204                     streamItemValues.put(StreamItems.COMMENTS, "");
   3205                     streamItemValues.put(StreamItems.RES_PACKAGE, resPackage);
   3206                     streamItemValues.put(StreamItems.RES_ICON, iconResource);
   3207                     streamItemValues.put(StreamItems.RES_LABEL, labelResource);
   3208                     streamItemValues.put(StreamItems.TIMESTAMP,
   3209                             timestamp == null ? System.currentTimeMillis() : timestamp);
   3210 
   3211                     // Note: The following is basically a workaround for the fact that status
   3212                     // updates didn't do any sort of account enforcement, while social stream item
   3213                     // updates do.  We can't expect callers of the old API to start passing account
   3214                     // information along, so we just populate the account params appropriately for
   3215                     // the raw contact.  Data set is not relevant here, as we only check account
   3216                     // name and type.
   3217                     if (accountName != null && accountType != null) {
   3218                         streamItemValues.put(RawContacts.ACCOUNT_NAME, accountName);
   3219                         streamItemValues.put(RawContacts.ACCOUNT_TYPE, accountType);
   3220                     }
   3221 
   3222                     // Check for an existing stream item from this source, and insert or update.
   3223                     Uri streamUri = StreamItems.CONTENT_URI;
   3224                     Cursor c = queryLocal(streamUri, new String[]{StreamItems._ID},
   3225                             StreamItems.RAW_CONTACT_ID + "=?",
   3226                             new String[]{String.valueOf(rawContactId)},
   3227                             null, -1 /* directory ID */, null);
   3228                     try {
   3229                         if (c.getCount() > 0) {
   3230                             c.moveToFirst();
   3231                             updateInTransaction(ContentUris.withAppendedId(streamUri, c.getLong(0)),
   3232                                     streamItemValues, null, null);
   3233                         } else {
   3234                             insertInTransaction(streamUri, streamItemValues);
   3235                         }
   3236                     } finally {
   3237                         c.close();
   3238                     }
   3239                 }
   3240             }
   3241         }
   3242 
   3243         if (contactId != -1) {
   3244             mAggregator.get().updateLastStatusUpdateId(contactId);
   3245         }
   3246 
   3247         return dataId;
   3248     }
   3249 
   3250     /** Converts a status update to HTML. */
   3251     private String statusUpdateToHtml(String status) {
   3252         return TextUtils.htmlEncode(status);
   3253     }
   3254 
   3255     private String getResourceName(Resources resources, String expectedType, Integer resourceId) {
   3256         try {
   3257             if (resourceId == null || resourceId == 0) return null;
   3258 
   3259             // Resource has an invalid type (e.g. a string as icon)? ignore
   3260             final String resourceEntryName = resources.getResourceEntryName(resourceId);
   3261             final String resourceTypeName = resources.getResourceTypeName(resourceId);
   3262             if (!expectedType.equals(resourceTypeName)) {
   3263                 Log.w(TAG, "Resource " + resourceId + " (" + resourceEntryName + ") is of type " +
   3264                         resourceTypeName + " but " + expectedType + " is required.");
   3265                 return null;
   3266             }
   3267 
   3268             return resourceEntryName;
   3269         } catch (NotFoundException e) {
   3270             return null;
   3271         }
   3272     }
   3273 
   3274     @Override
   3275     protected int deleteInTransaction(Uri uri, String selection, String[] selectionArgs) {
   3276         if (VERBOSE_LOGGING) {
   3277             Log.v(TAG, "deleteInTransaction: uri=" + uri +
   3278                     "  selection=[" + selection + "]  args=" + Arrays.toString(selectionArgs));
   3279         }
   3280 
   3281         final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
   3282 
   3283         flushTransactionalChanges();
   3284         final boolean callerIsSyncAdapter =
   3285                 readBooleanQueryParameter(uri, ContactsContract.CALLER_IS_SYNCADAPTER, false);
   3286         final int match = sUriMatcher.match(uri);
   3287         switch (match) {
   3288             case SYNCSTATE:
   3289             case PROFILE_SYNCSTATE:
   3290                 return mDbHelper.get().getSyncState().delete(db, selection,
   3291                         selectionArgs);
   3292 
   3293             case SYNCSTATE_ID: {
   3294                 String selectionWithId =
   3295                         (SyncStateContract.Columns._ID + "=" + ContentUris.parseId(uri) + " ")
   3296                         + (selection == null ? "" : " AND (" + selection + ")");
   3297                 return mDbHelper.get().getSyncState().delete(db, selectionWithId,
   3298                         selectionArgs);
   3299             }
   3300 
   3301             case PROFILE_SYNCSTATE_ID: {
   3302                 String selectionWithId =
   3303                         (SyncStateContract.Columns._ID + "=" + ContentUris.parseId(uri) + " ")
   3304                         + (selection == null ? "" : " AND (" + selection + ")");
   3305                 return mProfileHelper.getSyncState().delete(db, selectionWithId,
   3306                         selectionArgs);
   3307             }
   3308 
   3309             case CONTACTS: {
   3310                 invalidateFastScrollingIndexCache();
   3311                 // TODO
   3312                 return 0;
   3313             }
   3314 
   3315             case CONTACTS_ID: {
   3316                 invalidateFastScrollingIndexCache();
   3317                 long contactId = ContentUris.parseId(uri);
   3318                 return deleteContact(contactId, callerIsSyncAdapter);
   3319             }
   3320 
   3321             case CONTACTS_LOOKUP: {
   3322                 invalidateFastScrollingIndexCache();
   3323                 final List<String> pathSegments = uri.getPathSegments();
   3324                 final int segmentCount = pathSegments.size();
   3325                 if (segmentCount < 3) {
   3326                     throw new IllegalArgumentException(mDbHelper.get().exceptionMessage(
   3327                             "Missing a lookup key", uri));
   3328                 }
   3329                 final String lookupKey = pathSegments.get(2);
   3330                 final long contactId = lookupContactIdByLookupKey(db, lookupKey);
   3331                 return deleteContact(contactId, callerIsSyncAdapter);
   3332             }
   3333 
   3334             case CONTACTS_LOOKUP_ID: {
   3335                 invalidateFastScrollingIndexCache();
   3336                 // lookup contact by id and lookup key to see if they still match the actual record
   3337                 final List<String> pathSegments = uri.getPathSegments();
   3338                 final String lookupKey = pathSegments.get(2);
   3339                 SQLiteQueryBuilder lookupQb = new SQLiteQueryBuilder();
   3340                 setTablesAndProjectionMapForContacts(lookupQb, uri, null);
   3341                 long contactId = ContentUris.parseId(uri);
   3342                 String[] args;
   3343                 if (selectionArgs == null) {
   3344                     args = new String[2];
   3345                 } else {
   3346                     args = new String[selectionArgs.length + 2];
   3347                     System.arraycopy(selectionArgs, 0, args, 2, selectionArgs.length);
   3348                 }
   3349                 args[0] = String.valueOf(contactId);
   3350                 args[1] = Uri.encode(lookupKey);
   3351                 lookupQb.appendWhere(Contacts._ID + "=? AND " + Contacts.LOOKUP_KEY + "=?");
   3352                 Cursor c = query(db, lookupQb, null, selection, args, null, null,
   3353                         null, null, null);
   3354                 try {
   3355                     if (c.getCount() == 1) {
   3356                         // contact was unmodified so go ahead and delete it
   3357                         return deleteContact(contactId, callerIsSyncAdapter);
   3358                     } else {
   3359                         // row was changed (e.g. the merging might have changed), we got multiple
   3360                         // rows or the supplied selection filtered the record out
   3361                         return 0;
   3362                     }
   3363                 } finally {
   3364                     c.close();
   3365                 }
   3366             }
   3367 
   3368             case CONTACTS_DELETE_USAGE: {
   3369                 return deleteDataUsage();
   3370             }
   3371 
   3372             case RAW_CONTACTS:
   3373             case PROFILE_RAW_CONTACTS: {
   3374                 invalidateFastScrollingIndexCache();
   3375                 int numDeletes = 0;
   3376                 Cursor c = db.query(Views.RAW_CONTACTS,
   3377                         new String[]{RawContacts._ID, RawContacts.CONTACT_ID},
   3378                         appendAccountIdToSelection(uri, selection), selectionArgs,
   3379                         null, null, null);
   3380                 try {
   3381                     while (c.moveToNext()) {
   3382                         final long rawContactId = c.getLong(0);
   3383                         long contactId = c.getLong(1);
   3384                         numDeletes += deleteRawContact(rawContactId, contactId,
   3385                                 callerIsSyncAdapter);
   3386                     }
   3387                 } finally {
   3388                     c.close();
   3389                 }
   3390                 return numDeletes;
   3391             }
   3392 
   3393             case RAW_CONTACTS_ID:
   3394             case PROFILE_RAW_CONTACTS_ID: {
   3395                 invalidateFastScrollingIndexCache();
   3396                 final long rawContactId = ContentUris.parseId(uri);
   3397                 return deleteRawContact(rawContactId, mDbHelper.get().getContactId(rawContactId),
   3398                         callerIsSyncAdapter);
   3399             }
   3400 
   3401             case DATA:
   3402             case PROFILE_DATA: {
   3403                 invalidateFastScrollingIndexCache();
   3404                 mSyncToNetwork |= !callerIsSyncAdapter;
   3405                 return deleteData(appendAccountToSelection(uri, selection), selectionArgs,
   3406                         callerIsSyncAdapter);
   3407             }
   3408 
   3409             case DATA_ID:
   3410             case PHONES_ID:
   3411             case EMAILS_ID:
   3412             case CALLABLES_ID:
   3413             case POSTALS_ID:
   3414             case PROFILE_DATA_ID: {
   3415                 invalidateFastScrollingIndexCache();
   3416                 long dataId = ContentUris.parseId(uri);
   3417                 mSyncToNetwork |= !callerIsSyncAdapter;
   3418                 mSelectionArgs1[0] = String.valueOf(dataId);
   3419                 return deleteData(Data._ID + "=?", mSelectionArgs1, callerIsSyncAdapter);
   3420             }
   3421 
   3422             case GROUPS_ID: {
   3423                 mSyncToNetwork |= !callerIsSyncAdapter;
   3424                 return deleteGroup(uri, ContentUris.parseId(uri), callerIsSyncAdapter);
   3425             }
   3426 
   3427             case GROUPS: {
   3428                 int numDeletes = 0;
   3429                 Cursor c = db.query(Views.GROUPS, Projections.ID,
   3430                         appendAccountIdToSelection(uri, selection), selectionArgs,
   3431                         null, null, null);
   3432                 try {
   3433                     while (c.moveToNext()) {
   3434                         numDeletes += deleteGroup(uri, c.getLong(0), callerIsSyncAdapter);
   3435                     }
   3436                 } finally {
   3437                     c.close();
   3438                 }
   3439                 if (numDeletes > 0) {
   3440                     mSyncToNetwork |= !callerIsSyncAdapter;
   3441                 }
   3442                 return numDeletes;
   3443             }
   3444 
   3445             case SETTINGS: {
   3446                 mSyncToNetwork |= !callerIsSyncAdapter;
   3447                 return deleteSettings(uri, appendAccountToSelection(uri, selection), selectionArgs);
   3448             }
   3449 
   3450             case STATUS_UPDATES:
   3451             case PROFILE_STATUS_UPDATES: {
   3452                 return deleteStatusUpdates(selection, selectionArgs);
   3453             }
   3454 
   3455             case STREAM_ITEMS: {
   3456                 mSyncToNetwork |= !callerIsSyncAdapter;
   3457                 return deleteStreamItems(uri, new ContentValues(), selection, selectionArgs);
   3458             }
   3459 
   3460             case STREAM_ITEMS_ID: {
   3461                 mSyncToNetwork |= !callerIsSyncAdapter;
   3462                 return deleteStreamItems(uri, new ContentValues(),
   3463                         StreamItems._ID + "=?",
   3464                         new String[]{uri.getLastPathSegment()});
   3465             }
   3466 
   3467             case RAW_CONTACTS_ID_STREAM_ITEMS_ID: {
   3468                 mSyncToNetwork |= !callerIsSyncAdapter;
   3469                 String rawContactId = uri.getPathSegments().get(1);
   3470                 String streamItemId = uri.getLastPathSegment();
   3471                 return deleteStreamItems(uri, new ContentValues(),
   3472                         StreamItems.RAW_CONTACT_ID + "=? AND " + StreamItems._ID + "=?",
   3473                         new String[]{rawContactId, streamItemId});
   3474 
   3475             }
   3476 
   3477             case STREAM_ITEMS_ID_PHOTOS: {
   3478                 mSyncToNetwork |= !callerIsSyncAdapter;
   3479                 String streamItemId = uri.getPathSegments().get(1);
   3480                 String selectionWithId =
   3481                         (StreamItemPhotos.STREAM_ITEM_ID + "=" + streamItemId + " ")
   3482                                 + (selection == null ? "" : " AND (" + selection + ")");
   3483                 return deleteStreamItemPhotos(uri, new ContentValues(),
   3484                         selectionWithId, selectionArgs);
   3485             }
   3486 
   3487             case STREAM_ITEMS_ID_PHOTOS_ID: {
   3488                 mSyncToNetwork |= !callerIsSyncAdapter;
   3489                 String streamItemId = uri.getPathSegments().get(1);
   3490                 String streamItemPhotoId = uri.getPathSegments().get(3);
   3491                 return deleteStreamItemPhotos(uri, new ContentValues(),
   3492                         StreamItemPhotosColumns.CONCRETE_ID + "=? AND "
   3493                                 + StreamItemPhotos.STREAM_ITEM_ID + "=?",
   3494                         new String[]{streamItemPhotoId, streamItemId});
   3495             }
   3496 
   3497             default: {
   3498                 mSyncToNetwork = true;
   3499                 return mLegacyApiSupport.delete(uri, selection, selectionArgs);
   3500             }
   3501         }
   3502     }
   3503 
   3504     public int deleteGroup(Uri uri, long groupId, boolean callerIsSyncAdapter) {
   3505         final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
   3506         mGroupIdCache.clear();
   3507         final long groupMembershipMimetypeId = mDbHelper.get()
   3508                 .getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE);
   3509         db.delete(Tables.DATA, DataColumns.MIMETYPE_ID + "="
   3510                 + groupMembershipMimetypeId + " AND " + GroupMembership.GROUP_ROW_ID + "="
   3511                 + groupId, null);
   3512 
   3513         try {
   3514             if (callerIsSyncAdapter) {
   3515                 return db.delete(Tables.GROUPS, Groups._ID + "=" + groupId, null);
   3516             } else {
   3517                 mValues.clear();
   3518                 mValues.put(Groups.DELETED, 1);
   3519                 mValues.put(Groups.DIRTY, 1);
   3520                 return db.update(Tables.GROUPS, mValues, Groups._ID + "=" + groupId,
   3521                         null);
   3522             }
   3523         } finally {
   3524             mVisibleTouched = true;
   3525         }
   3526     }
   3527 
   3528     private int deleteSettings(Uri uri, String selection, String[] selectionArgs) {
   3529         final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
   3530         final int count = db.delete(Tables.SETTINGS, selection, selectionArgs);
   3531         mVisibleTouched = true;
   3532         return count;
   3533     }
   3534 
   3535     private int deleteContact(long contactId, boolean callerIsSyncAdapter) {
   3536         final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
   3537         mSelectionArgs1[0] = Long.toString(contactId);
   3538         Cursor c = db.query(Tables.RAW_CONTACTS, new String[]{RawContacts._ID},
   3539                 RawContacts.CONTACT_ID + "=?", mSelectionArgs1,
   3540                 null, null, null);
   3541         try {
   3542             while (c.moveToNext()) {
   3543                 long rawContactId = c.getLong(0);
   3544                 markRawContactAsDeleted(db, rawContactId, callerIsSyncAdapter);
   3545             }
   3546         } finally {
   3547             c.close();
   3548         }
   3549 
   3550         mProviderStatusUpdateNeeded = true;
   3551 
   3552         return db.delete(Tables.CONTACTS, Contacts._ID + "=" + contactId, null);
   3553     }
   3554 
   3555     public int deleteRawContact(long rawContactId, long contactId, boolean callerIsSyncAdapter) {
   3556         mAggregator.get().invalidateAggregationExceptionCache();
   3557         mProviderStatusUpdateNeeded = true;
   3558 
   3559         final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
   3560 
   3561         // Find and delete stream items associated with the raw contact.
   3562         Cursor c = db.query(Tables.STREAM_ITEMS,
   3563                 new String[]{StreamItems._ID},
   3564                 StreamItems.RAW_CONTACT_ID + "=?", new String[]{String.valueOf(rawContactId)},
   3565                 null, null, null);
   3566         try {
   3567             while (c.moveToNext()) {
   3568                 deleteStreamItem(db, c.getLong(0));
   3569             }
   3570         } finally {
   3571             c.close();
   3572         }
   3573 
   3574         if (callerIsSyncAdapter || rawContactIsLocal(rawContactId)) {
   3575             db.delete(Tables.PRESENCE,
   3576                     PresenceColumns.RAW_CONTACT_ID + "=" + rawContactId, null);
   3577             int count = db.delete(Tables.RAW_CONTACTS,
   3578                     RawContacts._ID + "=" + rawContactId, null);
   3579             mAggregator.get().updateAggregateData(mTransactionContext.get(), contactId);
   3580             return count;
   3581         } else {
   3582             mDbHelper.get().removeContactIfSingleton(rawContactId);
   3583             return markRawContactAsDeleted(db, rawContactId, callerIsSyncAdapter);
   3584         }
   3585     }
   3586 
   3587     /**
   3588      * Returns whether the given raw contact ID is local (i.e. has no account associated with it).
   3589      */
   3590     private boolean rawContactIsLocal(long rawContactId) {
   3591         final SQLiteDatabase db = mDbHelper.get().getReadableDatabase();
   3592         Cursor c = db.query(Tables.RAW_CONTACTS, Projections.LITERAL_ONE,
   3593                 RawContactsColumns.CONCRETE_ID + "=? AND " +
   3594                 RawContactsColumns.ACCOUNT_ID + "=" + Clauses.LOCAL_ACCOUNT_ID,
   3595                 new String[] {String.valueOf(rawContactId)}, null, null, null);
   3596         try {
   3597             return c.getCount() > 0;
   3598         } finally {
   3599             c.close();
   3600         }
   3601     }
   3602 
   3603     private int deleteStatusUpdates(String selection, String[] selectionArgs) {
   3604       // delete from both tables: presence and status_updates
   3605       // TODO should account type/name be appended to the where clause?
   3606       if (VERBOSE_LOGGING) {
   3607           Log.v(TAG, "deleting data from status_updates for " + selection);
   3608       }
   3609       final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
   3610       db.delete(Tables.STATUS_UPDATES, getWhereClauseForStatusUpdatesTable(selection),
   3611           selectionArgs);
   3612       return db.delete(Tables.PRESENCE, selection, selectionArgs);
   3613     }
   3614 
   3615     private int deleteStreamItems(Uri uri, ContentValues values, String selection,
   3616             String[] selectionArgs) {
   3617         final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
   3618         int count = 0;
   3619         final Cursor c = db.query(Views.STREAM_ITEMS, Projections.ID,
   3620                 selection, selectionArgs, null, null, null);
   3621         try {
   3622             c.moveToPosition(-1);
   3623             while (c.moveToNext()) {
   3624                 count += deleteStreamItem(db, c.getLong(0));
   3625             }
   3626         } finally {
   3627             c.close();
   3628         }
   3629         return count;
   3630     }
   3631 
   3632     private int deleteStreamItem(SQLiteDatabase db, long streamItemId) {
   3633         deleteStreamItemPhotos(streamItemId);
   3634         return db.delete(Tables.STREAM_ITEMS, StreamItems._ID + "=?",
   3635                 new String[]{String.valueOf(streamItemId)});
   3636     }
   3637 
   3638     private int deleteStreamItemPhotos(Uri uri, ContentValues values, String selection,
   3639             String[] selectionArgs) {
   3640         final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
   3641         return db.delete(Tables.STREAM_ITEM_PHOTOS, selection, selectionArgs);
   3642     }
   3643 
   3644     private int deleteStreamItemPhotos(long streamItemId) {
   3645         final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
   3646         // Note that this does not enforce the modifying account.
   3647         return db.delete(Tables.STREAM_ITEM_PHOTOS,
   3648                 StreamItemPhotos.STREAM_ITEM_ID + "=?",
   3649                 new String[]{String.valueOf(streamItemId)});
   3650     }
   3651 
   3652     private int markRawContactAsDeleted(SQLiteDatabase db, long rawContactId,
   3653             boolean callerIsSyncAdapter) {
   3654         mSyncToNetwork = true;
   3655 
   3656         mValues.clear();
   3657         mValues.put(RawContacts.DELETED, 1);
   3658         mValues.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DISABLED);
   3659         mValues.put(RawContactsColumns.AGGREGATION_NEEDED, 1);
   3660         mValues.putNull(RawContacts.CONTACT_ID);
   3661         mValues.put(RawContacts.DIRTY, 1);
   3662         return updateRawContact(db, rawContactId, mValues, callerIsSyncAdapter);
   3663     }
   3664 
   3665     private int deleteDataUsage() {
   3666         final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
   3667         db.execSQL("UPDATE " + Tables.RAW_CONTACTS + " SET " +
   3668                 Contacts.TIMES_CONTACTED + "=0," +
   3669                 Contacts.LAST_TIME_CONTACTED + "=NULL"
   3670                 );
   3671         db.execSQL("UPDATE " + Tables.CONTACTS + " SET " +
   3672                 Contacts.TIMES_CONTACTED + "=0," +
   3673                 Contacts.LAST_TIME_CONTACTED + "=NULL"
   3674                 );
   3675         db.delete(Tables.DATA_USAGE_STAT, null, null);
   3676 
   3677         return 1;
   3678     }
   3679 
   3680     @Override
   3681     protected int updateInTransaction(Uri uri, ContentValues values, String selection,
   3682             String[] selectionArgs) {
   3683         if (VERBOSE_LOGGING) {
   3684             Log.v(TAG, "updateInTransaction: uri=" + uri +
   3685                     "  selection=[" + selection + "]  args=" + Arrays.toString(selectionArgs) +
   3686                     "  values=[" + values + "]");
   3687         }
   3688 
   3689         final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
   3690 
   3691         int count = 0;
   3692 
   3693         final int match = sUriMatcher.match(uri);
   3694         if (match == SYNCSTATE_ID && selection == null) {
   3695             long rowId = ContentUris.parseId(uri);
   3696             Object data = values.get(ContactsContract.SyncState.DATA);
   3697             mTransactionContext.get().syncStateUpdated(rowId, data);
   3698             return 1;
   3699         }
   3700         flushTransactionalChanges();
   3701         final boolean callerIsSyncAdapter =
   3702                 readBooleanQueryParameter(uri, ContactsContract.CALLER_IS_SYNCADAPTER, false);
   3703         switch(match) {
   3704             case SYNCSTATE:
   3705             case PROFILE_SYNCSTATE:
   3706                 return mDbHelper.get().getSyncState().update(db, values,
   3707                         appendAccountToSelection(uri, selection), selectionArgs);
   3708 
   3709             case SYNCSTATE_ID: {
   3710                 selection = appendAccountToSelection(uri, selection);
   3711                 String selectionWithId =
   3712                         (SyncStateContract.Columns._ID + "=" + ContentUris.parseId(uri) + " ")
   3713                         + (selection == null ? "" : " AND (" + selection + ")");
   3714                 return mDbHelper.get().getSyncState().update(db, values,
   3715                         selectionWithId, selectionArgs);
   3716             }
   3717 
   3718             case PROFILE_SYNCSTATE_ID: {
   3719                 selection = appendAccountToSelection(uri, selection);
   3720                 String selectionWithId =
   3721                         (SyncStateContract.Columns._ID + "=" + ContentUris.parseId(uri) + " ")
   3722                         + (selection == null ? "" : " AND (" + selection + ")");
   3723                 return mProfileHelper.getSyncState().update(db, values,
   3724                         selectionWithId, selectionArgs);
   3725             }
   3726 
   3727             case CONTACTS:
   3728             case PROFILE: {
   3729                 invalidateFastScrollingIndexCache();
   3730                 count = updateContactOptions(values, selection, selectionArgs, callerIsSyncAdapter);
   3731                 break;
   3732             }
   3733 
   3734             case CONTACTS_ID: {
   3735                 invalidateFastScrollingIndexCache();
   3736                 count = updateContactOptions(db, ContentUris.parseId(uri), values,
   3737                         callerIsSyncAdapter);
   3738                 break;
   3739             }
   3740 
   3741             case CONTACTS_LOOKUP:
   3742             case CONTACTS_LOOKUP_ID: {
   3743                 invalidateFastScrollingIndexCache();
   3744                 final List<String> pathSegments = uri.getPathSegments();
   3745                 final int segmentCount = pathSegments.size();
   3746                 if (segmentCount < 3) {
   3747                     throw new IllegalArgumentException(mDbHelper.get().exceptionMessage(
   3748                             "Missing a lookup key", uri));
   3749                 }
   3750                 final String lookupKey = pathSegments.get(2);
   3751                 final long contactId = lookupContactIdByLookupKey(db, lookupKey);
   3752                 count = updateContactOptions(db, contactId, values, callerIsSyncAdapter);
   3753                 break;
   3754             }
   3755 
   3756             case RAW_CONTACTS_ID_DATA:
   3757             case PROFILE_RAW_CONTACTS_ID_DATA: {
   3758                 invalidateFastScrollingIndexCache();
   3759                 int segment = match == RAW_CONTACTS_ID_DATA ? 1 : 2;
   3760                 final String rawContactId = uri.getPathSegments().get(segment);
   3761                 String selectionWithId = (Data.RAW_CONTACT_ID + "=" + rawContactId + " ")
   3762                     + (selection == null ? "" : " AND " + selection);
   3763 
   3764                 count = updateData(uri, values, selectionWithId, selectionArgs, callerIsSyncAdapter);
   3765 
   3766                 break;
   3767             }
   3768 
   3769             case DATA:
   3770             case PROFILE_DATA: {
   3771                 invalidateFastScrollingIndexCache();
   3772                 count = updateData(uri, values, appendAccountToSelection(uri, selection),
   3773                         selectionArgs, callerIsSyncAdapter);
   3774                 if (count > 0) {
   3775                     mSyncToNetwork |= !callerIsSyncAdapter;
   3776                 }
   3777                 break;
   3778             }
   3779 
   3780             case DATA_ID:
   3781             case PHONES_ID:
   3782             case EMAILS_ID:
   3783             case CALLABLES_ID:
   3784             case POSTALS_ID: {
   3785                 invalidateFastScrollingIndexCache();
   3786                 count = updateData(uri, values, selection, selectionArgs, callerIsSyncAdapter);
   3787                 if (count > 0) {
   3788                     mSyncToNetwork |= !callerIsSyncAdapter;
   3789                 }
   3790                 break;
   3791             }
   3792 
   3793             case RAW_CONTACTS:
   3794             case PROFILE_RAW_CONTACTS: {
   3795                 invalidateFastScrollingIndexCache();
   3796                 selection = appendAccountIdToSelection(uri, selection);
   3797                 count = updateRawContacts(values, selection, selectionArgs, callerIsSyncAdapter);
   3798                 break;
   3799             }
   3800 
   3801             case RAW_CONTACTS_ID: {
   3802                 invalidateFastScrollingIndexCache();
   3803                 long rawContactId = ContentUris.parseId(uri);
   3804                 if (selection != null) {
   3805                     selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId));
   3806                     count = updateRawContacts(values, RawContacts._ID + "=?"
   3807                                     + " AND(" + selection + ")", selectionArgs,
   3808                             callerIsSyncAdapter);
   3809                 } else {
   3810                     mSelectionArgs1[0] = String.valueOf(rawContactId);
   3811                     count = updateRawContacts(values, RawContacts._ID + "=?", mSelectionArgs1,
   3812                             callerIsSyncAdapter);
   3813                 }
   3814                 break;
   3815             }
   3816 
   3817             case GROUPS: {
   3818                count = updateGroups(uri, values, appendAccountIdToSelection(uri, selection),
   3819                         selectionArgs, callerIsSyncAdapter);
   3820                 if (count > 0) {
   3821                     mSyncToNetwork |= !callerIsSyncAdapter;
   3822                 }
   3823                 break;
   3824             }
   3825 
   3826             case GROUPS_ID: {
   3827                 long groupId = ContentUris.parseId(uri);
   3828                 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(groupId));
   3829                 String selectionWithId = Groups._ID + "=? "
   3830                         + (selection == null ? "" : " AND " + selection);
   3831                 count = updateGroups(uri, values, selectionWithId, selectionArgs,
   3832                         callerIsSyncAdapter);
   3833                 if (count > 0) {
   3834                     mSyncToNetwork |= !callerIsSyncAdapter;
   3835                 }
   3836                 break;
   3837             }
   3838 
   3839             case AGGREGATION_EXCEPTIONS: {
   3840                 count = updateAggregationException(db, values);
   3841                 invalidateFastScrollingIndexCache();
   3842                 break;
   3843             }
   3844 
   3845             case SETTINGS: {
   3846                 count = updateSettings(uri, values, appendAccountToSelection(uri, selection),
   3847                         selectionArgs);
   3848                 mSyncToNetwork |= !callerIsSyncAdapter;
   3849                 break;
   3850             }
   3851 
   3852             case STATUS_UPDATES:
   3853             case PROFILE_STATUS_UPDATES: {
   3854                 count = updateStatusUpdate(uri, values, selection, selectionArgs);
   3855                 break;
   3856             }
   3857 
   3858             case STREAM_ITEMS: {
   3859                 count = updateStreamItems(uri, values, selection, selectionArgs);
   3860                 break;
   3861             }
   3862 
   3863             case STREAM_ITEMS_ID: {
   3864                 count = updateStreamItems(uri, values, StreamItems._ID + "=?",
   3865                         new String[]{uri.getLastPathSegment()});
   3866                 break;
   3867             }
   3868 
   3869             case RAW_CONTACTS_ID_STREAM_ITEMS_ID: {
   3870                 String rawContactId = uri.getPathSegments().get(1);
   3871                 String streamItemId = uri.getLastPathSegment();
   3872                 count = updateStreamItems(uri, values,
   3873                         StreamItems.RAW_CONTACT_ID + "=? AND " + StreamItems._ID + "=?",
   3874                         new String[]{rawContactId, streamItemId});
   3875                 break;
   3876             }
   3877 
   3878             case STREAM_ITEMS_PHOTOS: {
   3879                 count = updateStreamItemPhotos(uri, values, selection, selectionArgs);
   3880                 break;
   3881             }
   3882 
   3883             case STREAM_ITEMS_ID_PHOTOS: {
   3884                 String streamItemId = uri.getPathSegments().get(1);
   3885                 count = updateStreamItemPhotos(uri, values,
   3886                         StreamItemPhotos.STREAM_ITEM_ID + "=?", new String[]{streamItemId});
   3887                 break;
   3888             }
   3889 
   3890             case STREAM_ITEMS_ID_PHOTOS_ID: {
   3891                 String streamItemId = uri.getPathSegments().get(1);
   3892                 String streamItemPhotoId = uri.getPathSegments().get(3);
   3893                 count = updateStreamItemPhotos(uri, values,
   3894                         StreamItemPhotosColumns.CONCRETE_ID + "=? AND " +
   3895                                 StreamItemPhotosColumns.CONCRETE_STREAM_ITEM_ID + "=?",
   3896                         new String[]{streamItemPhotoId, streamItemId});
   3897                 break;
   3898             }
   3899 
   3900             case DIRECTORIES: {
   3901                 mContactDirectoryManager.scanPackagesByUid(Binder.getCallingUid());
   3902                 count = 1;
   3903                 break;
   3904             }
   3905 
   3906             case DATA_USAGE_FEEDBACK_ID: {
   3907                 if (handleDataUsageFeedback(uri)) {
   3908                     count = 1;
   3909                 } else {
   3910                     count = 0;
   3911                 }
   3912                 break;
   3913             }
   3914 
   3915             default: {
   3916                 mSyncToNetwork = true;
   3917                 return mLegacyApiSupport.update(uri, values, selection, selectionArgs);
   3918             }
   3919         }
   3920 
   3921         return count;
   3922     }
   3923 
   3924     private int updateStatusUpdate(Uri uri, ContentValues values, String selection,
   3925         String[] selectionArgs) {
   3926         final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
   3927         // update status_updates table, if status is provided
   3928         // TODO should account type/name be appended to the where clause?
   3929         int updateCount = 0;
   3930         ContentValues settableValues = getSettableColumnsForStatusUpdatesTable(values);
   3931         if (settableValues.size() > 0) {
   3932           updateCount = db.update(Tables.STATUS_UPDATES,
   3933                     settableValues,
   3934                     getWhereClauseForStatusUpdatesTable(selection),
   3935                     selectionArgs);
   3936         }
   3937 
   3938         // now update the Presence table
   3939         settableValues = getSettableColumnsForPresenceTable(values);
   3940         if (settableValues.size() > 0) {
   3941           updateCount = db.update(Tables.PRESENCE, settableValues,
   3942                     selection, selectionArgs);
   3943         }
   3944         // TODO updateCount is not entirely a valid count of updated rows because 2 tables could
   3945         // potentially get updated in this method.
   3946         return updateCount;
   3947     }
   3948 
   3949     private int updateStreamItems(Uri uri, ContentValues values, String selection,
   3950             String[] selectionArgs) {
   3951         // Stream items can't be moved to a new raw contact.
   3952         values.remove(StreamItems.RAW_CONTACT_ID);
   3953 
   3954         // Don't attempt to update accounts params - they don't exist in the stream items table.
   3955         values.remove(RawContacts.ACCOUNT_NAME);
   3956         values.remove(RawContacts.ACCOUNT_TYPE);
   3957 
   3958         final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
   3959 
   3960         // If there's been no exception, the update should be fine.
   3961         return db.update(Tables.STREAM_ITEMS, values, selection, selectionArgs);
   3962     }
   3963 
   3964     private int updateStreamItemPhotos(Uri uri, ContentValues values, String selection,
   3965             String[] selectionArgs) {
   3966         // Stream item photos can't be moved to a new stream item.
   3967         values.remove(StreamItemPhotos.STREAM_ITEM_ID);
   3968 
   3969         // Don't attempt to update accounts params - they don't exist in the stream item
   3970         // photos table.
   3971         values.remove(RawContacts.ACCOUNT_NAME);
   3972         values.remove(RawContacts.ACCOUNT_TYPE);
   3973 
   3974         final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
   3975 
   3976         // Process the photo (since we're updating, it's valid for the photo to not be present).
   3977         if (processStreamItemPhoto(values, true)) {
   3978             // If there's been no exception, the update should be fine.
   3979             return db.update(Tables.STREAM_ITEM_PHOTOS, values, selection,
   3980                     selectionArgs);
   3981         }
   3982         return 0;
   3983     }
   3984 
   3985     /**
   3986      * Build a where clause to select the rows to be updated in status_updates table.
   3987      */
   3988     private String getWhereClauseForStatusUpdatesTable(String selection) {
   3989         mSb.setLength(0);
   3990         mSb.append(WHERE_CLAUSE_FOR_STATUS_UPDATES_TABLE);
   3991         mSb.append(selection);
   3992         mSb.append(")");
   3993         return mSb.toString();
   3994     }
   3995 
   3996     private ContentValues getSettableColumnsForStatusUpdatesTable(ContentValues values) {
   3997         mValues.clear();
   3998         ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS, values,
   3999             StatusUpdates.STATUS);
   4000         ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS_TIMESTAMP, values,
   4001             StatusUpdates.STATUS_TIMESTAMP);
   4002         ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS_RES_PACKAGE, values,
   4003             StatusUpdates.STATUS_RES_PACKAGE);
   4004         ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS_LABEL, values,
   4005             StatusUpdates.STATUS_LABEL);
   4006         ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.STATUS_ICON, values,
   4007             StatusUpdates.STATUS_ICON);
   4008         return mValues;
   4009     }
   4010 
   4011     private ContentValues getSettableColumnsForPresenceTable(ContentValues values) {
   4012         mValues.clear();
   4013         ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.PRESENCE, values,
   4014             StatusUpdates.PRESENCE);
   4015         ContactsDatabaseHelper.copyStringValue(mValues, StatusUpdates.CHAT_CAPABILITY, values,
   4016                 StatusUpdates.CHAT_CAPABILITY);
   4017         return mValues;
   4018     }
   4019 
   4020     private interface GroupAccountQuery {
   4021         String TABLE = Views.GROUPS;
   4022 
   4023         String[] COLUMNS = new String[] {
   4024                 Groups._ID,
   4025                 Groups.ACCOUNT_TYPE,
   4026                 Groups.ACCOUNT_NAME,
   4027                 Groups.DATA_SET,
   4028         };
   4029         int ID = 0;
   4030         int ACCOUNT_TYPE = 1;
   4031         int ACCOUNT_NAME = 2;
   4032         int DATA_SET = 3;
   4033     }
   4034 
   4035     private int updateGroups(Uri uri, ContentValues originalValues, String selectionWithId,
   4036             String[] selectionArgs, boolean callerIsSyncAdapter) {
   4037         mGroupIdCache.clear();
   4038 
   4039         final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
   4040         final ContactsDatabaseHelper dbHelper = mDbHelper.get();
   4041 
   4042         final ContentValues updatedValues = new ContentValues();
   4043         updatedValues.putAll(originalValues);
   4044 
   4045         if (!callerIsSyncAdapter && !updatedValues.containsKey(Groups.DIRTY)) {
   4046             updatedValues.put(Groups.DIRTY, 1);
   4047         }
   4048         if (updatedValues.containsKey(Groups.GROUP_VISIBLE)) {
   4049             mVisibleTouched = true;
   4050         }
   4051 
   4052         // Prepare for account change
   4053         final boolean isAccountNameChanging = updatedValues.containsKey(Groups.ACCOUNT_NAME);
   4054         final boolean isAccountTypeChanging = updatedValues.containsKey(Groups.ACCOUNT_TYPE);
   4055         final boolean isDataSetChanging = updatedValues.containsKey(Groups.DATA_SET);
   4056         final boolean isAccountChanging = isAccountNameChanging || isAccountTypeChanging
   4057                 || isDataSetChanging;
   4058         final String updatedAccountName = updatedValues.getAsString(Groups.ACCOUNT_NAME);
   4059         final String updatedAccountType = updatedValues.getAsString(Groups.ACCOUNT_TYPE);
   4060         final String updatedDataSet = updatedValues.getAsString(Groups.DATA_SET);
   4061 
   4062         updatedValues.remove(Groups.ACCOUNT_NAME);
   4063         updatedValues.remove(Groups.ACCOUNT_TYPE);
   4064         updatedValues.remove(Groups.DATA_SET);
   4065 
   4066         // We later call requestSync() on all affected accounts.
   4067         final Set<Account> affectedAccounts = Sets.newHashSet();
   4068 
   4069         // Look for all affected rows, and change them row by row.
   4070         final Cursor c = db.query(GroupAccountQuery.TABLE, GroupAccountQuery.COLUMNS,
   4071                 selectionWithId, selectionArgs, null, null, null);
   4072         int returnCount = 0;
   4073         try {
   4074             c.moveToPosition(-1);
   4075             while (c.moveToNext()) {
   4076                 final long groupId = c.getLong(GroupAccountQuery.ID);
   4077 
   4078                 mSelectionArgs1[0] = Long.toString(groupId);
   4079 
   4080                 final String accountName = isAccountNameChanging
   4081                         ? updatedAccountName : c.getString(GroupAccountQuery.ACCOUNT_NAME);
   4082                 final String accountType = isAccountTypeChanging
   4083                         ? updatedAccountType : c.getString(GroupAccountQuery.ACCOUNT_TYPE);
   4084                 final String dataSet = isDataSetChanging
   4085                         ? updatedDataSet : c.getString(GroupAccountQuery.DATA_SET);
   4086 
   4087                 if (isAccountChanging) {
   4088                     final long accountId = dbHelper.getOrCreateAccountIdInTransaction(
   4089                             AccountWithDataSet.get(accountName, accountType, dataSet));
   4090                     updatedValues.put(GroupsColumns.ACCOUNT_ID, accountId);
   4091                 }
   4092 
   4093                 // Finally do the actual update.
   4094                 final int count = db.update(Tables.GROUPS, updatedValues,
   4095                         GroupsColumns.CONCRETE_ID + "=?", mSelectionArgs1);
   4096 
   4097                 if ((count > 0)
   4098                         && !TextUtils.isEmpty(accountName)
   4099                         && !TextUtils.isEmpty(accountType)) {
   4100                     affectedAccounts.add(new Account(accountName, accountType));
   4101                 }
   4102 
   4103                 returnCount += count;
   4104             }
   4105         } finally {
   4106             c.close();
   4107         }
   4108 
   4109         // TODO: This will not work for groups that have a data set specified, since the content
   4110         // resolver will not be able to request a sync for the right source (unless it is updated
   4111         // to key off account with data set).
   4112         // i.e. requestSync only takes Account, not AccountWithDataSet.
   4113         if (updatedValues.containsKey(Groups.SHOULD_SYNC)
   4114                 && updatedValues.getAsInteger(Groups.SHOULD_SYNC) != 0) {
   4115             for (Account account : affectedAccounts) {
   4116                 ContentResolver.requestSync(account, ContactsContract.AUTHORITY, new Bundle());
   4117             }
   4118         }
   4119         return returnCount;
   4120     }
   4121 
   4122     private int updateSettings(Uri uri, ContentValues values, String selection,
   4123             String[] selectionArgs) {
   4124         final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
   4125         final int count = db.update(Tables.SETTINGS, values, selection, selectionArgs);
   4126         if (values.containsKey(Settings.UNGROUPED_VISIBLE)) {
   4127             mVisibleTouched = true;
   4128         }
   4129         return count;
   4130     }
   4131 
   4132     private int updateRawContacts(ContentValues values, String selection, String[] selectionArgs,
   4133             boolean callerIsSyncAdapter) {
   4134         if (values.containsKey(RawContacts.CONTACT_ID)) {
   4135             throw new IllegalArgumentException(RawContacts.CONTACT_ID + " should not be included " +
   4136                     "in content values. Contact IDs are assigned automatically");
   4137         }
   4138 
   4139         if (!callerIsSyncAdapter) {
   4140             selection = DatabaseUtils.concatenateWhere(selection,
   4141                     RawContacts.RAW_CONTACT_IS_READ_ONLY + "=0");
   4142         }
   4143 
   4144         int count = 0;
   4145         final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
   4146         Cursor cursor = db.query(Views.RAW_CONTACTS,
   4147                 Projections.ID, selection,
   4148                 selectionArgs, null, null, null);
   4149         try {
   4150             while (cursor.moveToNext()) {
   4151                 long rawContactId = cursor.getLong(0);
   4152                 updateRawContact(db, rawContactId, values, callerIsSyncAdapter);
   4153                 count++;
   4154             }
   4155         } finally {
   4156             cursor.close();
   4157         }
   4158 
   4159         return count;
   4160     }
   4161 
   4162     private int updateRawContact(SQLiteDatabase db, long rawContactId, ContentValues values,
   4163             boolean callerIsSyncAdapter) {
   4164         final String selection = RawContactsColumns.CONCRETE_ID + " = ?";
   4165         mSelectionArgs1[0] = Long.toString(rawContactId);
   4166 
   4167         final ContactsDatabaseHelper dbHelper = mDbHelper.get();
   4168 
   4169         final boolean requestUndoDelete = (values.containsKey(RawContacts.DELETED)
   4170                 && values.getAsInteger(RawContacts.DELETED) == 0);
   4171 
   4172         final boolean isAccountNameChanging = values.containsKey(RawContacts.ACCOUNT_NAME);
   4173         final boolean isAccountTypeChanging = values.containsKey(RawContacts.ACCOUNT_TYPE);
   4174         final boolean isDataSetChanging = values.containsKey(RawContacts.DATA_SET);
   4175         final boolean isAccountChanging = isAccountNameChanging || isAccountTypeChanging
   4176                 || isDataSetChanging;
   4177 
   4178         int previousDeleted = 0;
   4179         long accountId = 0;
   4180         String oldAccountType = null;
   4181         String oldAccountName = null;
   4182         String oldDataSet = null;
   4183 
   4184         if (requestUndoDelete || isAccountChanging) {
   4185             Cursor cursor = db.query(RawContactsQuery.TABLE, RawContactsQuery.COLUMNS,
   4186                     selection, mSelectionArgs1, null, null, null);
   4187             try {
   4188                 if (cursor.moveToFirst()) {
   4189                     previousDeleted = cursor.getInt(RawContactsQuery.DELETED);
   4190                     accountId = cursor.getLong(RawContactsQuery.ACCOUNT_ID);
   4191                     oldAccountType = cursor.getString(RawContactsQuery.ACCOUNT_TYPE);
   4192                     oldAccountName = cursor.getString(RawContactsQuery.ACCOUNT_NAME);
   4193                     oldDataSet = cursor.getString(RawContactsQuery.DATA_SET);
   4194                 }
   4195             } finally {
   4196                 cursor.close();
   4197             }
   4198             if (isAccountChanging) {
   4199                 // We can't change the original ContentValues, as it'll be re-used over all
   4200                 // updateRawContact invocations in a transaction, so we need to create a new one.
   4201                 // (However we don't want to use mValues here, because mValues may be used in some
   4202                 // other methods that are called by this method.)
   4203                 final ContentValues originalValues = values;
   4204                 values = new ContentValues();
   4205                 values.clear();
   4206                 values.putAll(originalValues);
   4207 
   4208                 final AccountWithDataSet newAccountWithDataSet = AccountWithDataSet.get(
   4209                         isAccountNameChanging
   4210                             ? values.getAsString(RawContacts.ACCOUNT_NAME) : oldAccountName,
   4211                         isAccountTypeChanging
   4212                             ? values.getAsString(RawContacts.ACCOUNT_TYPE) : oldAccountType,
   4213                         isDataSetChanging
   4214                             ? values.getAsString(RawContacts.DATA_SET) : oldDataSet
   4215                         );
   4216                 accountId = dbHelper.getOrCreateAccountIdInTransaction(newAccountWithDataSet);
   4217 
   4218                 values.put(RawContactsColumns.ACCOUNT_ID, accountId);
   4219 
   4220                 values.remove(RawContacts.ACCOUNT_NAME);
   4221                 values.remove(RawContacts.ACCOUNT_TYPE);
   4222                 values.remove(RawContacts.DATA_SET);
   4223             }
   4224         }
   4225         if (requestUndoDelete) {
   4226             values.put(ContactsContract.RawContacts.AGGREGATION_MODE,
   4227                     ContactsContract.RawContacts.AGGREGATION_MODE_DEFAULT);
   4228         }
   4229 
   4230         int count = db.update(Tables.RAW_CONTACTS, values, selection, mSelectionArgs1);
   4231         if (count != 0) {
   4232             if (values.containsKey(RawContacts.AGGREGATION_MODE)) {
   4233                 int aggregationMode = values.getAsInteger(RawContacts.AGGREGATION_MODE);
   4234 
   4235                 // As per ContactsContract documentation, changing aggregation mode
   4236                 // to DEFAULT should not trigger aggregation
   4237                 if (aggregationMode != RawContacts.AGGREGATION_MODE_DEFAULT) {
   4238                     mAggregator.get().markForAggregation(rawContactId, aggregationMode, false);
   4239                 }
   4240             }
   4241             if (values.containsKey(RawContacts.STARRED)) {
   4242                 if (!callerIsSyncAdapter) {
   4243                     updateFavoritesMembership(rawContactId,
   4244                             values.getAsLong(RawContacts.STARRED) != 0);
   4245                 }
   4246                 mAggregator.get().updateStarred(rawContactId);
   4247             } else {
   4248                 // if this raw contact is being associated with an account, then update the
   4249                 // favorites group membership based on whether or not this contact is starred.
   4250                 // If it is starred, add a group membership, if one doesn't already exist
   4251                 // otherwise delete any matching group memberships.
   4252                 if (!callerIsSyncAdapter && isAccountChanging) {
   4253                     boolean starred = 0 != DatabaseUtils.longForQuery(db,
   4254                             SELECTION_STARRED_FROM_RAW_CONTACTS,
   4255                             new String[]{Long.toString(rawContactId)});
   4256                     updateFavoritesMembership(rawContactId, starred);
   4257                 }
   4258             }
   4259 
   4260             // if this raw contact is being associated with an account, then add a
   4261             // group membership to the group marked as AutoAdd, if any.
   4262             if (!callerIsSyncAdapter && isAccountChanging) {
   4263                 addAutoAddMembership(rawContactId);
   4264             }
   4265 
   4266             if (values.containsKey(RawContacts.SOURCE_ID)) {
   4267                 mAggregator.get().updateLookupKeyForRawContact(db, rawContactId);
   4268             }
   4269             if (values.containsKey(RawContacts.NAME_VERIFIED)) {
   4270 
   4271                 // If setting NAME_VERIFIED for this raw contact, reset it for all
   4272                 // other raw contacts in the same aggregate
   4273                 if (values.getAsInteger(RawContacts.NAME_VERIFIED) != 0) {
   4274                     mDbHelper.get().resetNameVerifiedForOtherRawContacts(rawContactId);
   4275                 }
   4276                 mAggregator.get().updateDisplayNameForRawContact(db, rawContactId);
   4277             }
   4278             if (requestUndoDelete && previousDeleted == 1) {
   4279                 // Note before the accounts refactoring, we used to use the *old* account here,
   4280                 // which doesn't make sense, so now we pass the *new* account.
   4281                 // (In practice it doesn't matter because there's probably no apps that undo-delete
   4282                 // and change accounts at the same time.)
   4283                 mTransactionContext.get().rawContactInserted(rawContactId, accountId);
   4284             }
   4285         }
   4286         return count;
   4287     }
   4288 
   4289     private int updateData(Uri uri, ContentValues values, String selection,
   4290             String[] selectionArgs, boolean callerIsSyncAdapter) {
   4291         mValues.clear();
   4292         mValues.putAll(values);
   4293         mValues.remove(Data._ID);
   4294         mValues.remove(Data.RAW_CONTACT_ID);
   4295         mValues.remove(Data.MIMETYPE);
   4296 
   4297         String packageName = values.getAsString(Data.RES_PACKAGE);
   4298         if (packageName != null) {
   4299             mValues.remove(Data.RES_PACKAGE);
   4300             mValues.put(DataColumns.PACKAGE_ID, mDbHelper.get().getPackageId(packageName));
   4301         }
   4302 
   4303         if (!callerIsSyncAdapter) {
   4304             selection = DatabaseUtils.concatenateWhere(selection,
   4305                     Data.IS_READ_ONLY + "=0");
   4306         }
   4307 
   4308         int count = 0;
   4309 
   4310         // Note that the query will return data according to the access restrictions,
   4311         // so we don't need to worry about updating data we don't have permission to read.
   4312         Cursor c = queryLocal(uri,
   4313                 DataRowHandler.DataUpdateQuery.COLUMNS,
   4314                 selection, selectionArgs, null, -1 /* directory ID */, null);
   4315         try {
   4316             while(c.moveToNext()) {
   4317                 count += updateData(mValues, c, callerIsSyncAdapter);
   4318             }
   4319         } finally {
   4320             c.close();
   4321         }
   4322 
   4323         return count;
   4324     }
   4325 
   4326     private int updateData(ContentValues values, Cursor c, boolean callerIsSyncAdapter) {
   4327         if (values.size() == 0) {
   4328             return 0;
   4329         }
   4330 
   4331         final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
   4332 
   4333         final String mimeType = c.getString(DataRowHandler.DataUpdateQuery.MIMETYPE);
   4334         DataRowHandler rowHandler = getDataRowHandler(mimeType);
   4335         boolean updated =
   4336                 rowHandler.update(db, mTransactionContext.get(), values, c,
   4337                         callerIsSyncAdapter);
   4338         if (Photo.CONTENT_ITEM_TYPE.equals(mimeType)) {
   4339             scheduleBackgroundTask(BACKGROUND_TASK_CLEANUP_PHOTOS);
   4340         }
   4341         return updated ? 1 : 0;
   4342     }
   4343 
   4344     private int updateContactOptions(ContentValues values, String selection,
   4345             String[] selectionArgs, boolean callerIsSyncAdapter) {
   4346         int count = 0;
   4347         final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
   4348 
   4349         Cursor cursor = db.query(Views.CONTACTS,
   4350                 new String[] { Contacts._ID }, selection, selectionArgs, null, null, null);
   4351         try {
   4352             while (cursor.moveToNext()) {
   4353                 long contactId = cursor.getLong(0);
   4354 
   4355                 updateContactOptions(db, contactId, values, callerIsSyncAdapter);
   4356                 count++;
   4357             }
   4358         } finally {
   4359             cursor.close();
   4360         }
   4361 
   4362         return count;
   4363     }
   4364 
   4365     private int updateContactOptions(SQLiteDatabase db, long contactId, ContentValues values,
   4366             boolean callerIsSyncAdapter) {
   4367 
   4368         mValues.clear();
   4369         ContactsDatabaseHelper.copyStringValue(mValues, RawContacts.CUSTOM_RINGTONE,
   4370                 values, Contacts.CUSTOM_RINGTONE);
   4371         ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.SEND_TO_VOICEMAIL,
   4372                 values, Contacts.SEND_TO_VOICEMAIL);
   4373         ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.LAST_TIME_CONTACTED,
   4374                 values, Contacts.LAST_TIME_CONTACTED);
   4375         ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.TIMES_CONTACTED,
   4376                 values, Contacts.TIMES_CONTACTED);
   4377         ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.STARRED,
   4378                 values, Contacts.STARRED);
   4379 
   4380         // Nothing to update - just return
   4381         if (mValues.size() == 0) {
   4382             return 0;
   4383         }
   4384 
   4385         if (mValues.containsKey(RawContacts.STARRED)) {
   4386             // Mark dirty when changing starred to trigger sync
   4387             mValues.put(RawContacts.DIRTY, 1);
   4388         }
   4389 
   4390         mSelectionArgs1[0] = String.valueOf(contactId);
   4391         db.update(Tables.RAW_CONTACTS, mValues, RawContacts.CONTACT_ID + "=?"
   4392                 + " AND " + RawContacts.RAW_CONTACT_IS_READ_ONLY + "=0", mSelectionArgs1);
   4393 
   4394         if (mValues.containsKey(RawContacts.STARRED) && !callerIsSyncAdapter) {
   4395             Cursor cursor = db.query(Views.RAW_CONTACTS,
   4396                     new String[] { RawContacts._ID }, RawContacts.CONTACT_ID + "=?",
   4397                     mSelectionArgs1, null, null, null);
   4398             try {
   4399                 while (cursor.moveToNext()) {
   4400                     long rawContactId = cursor.getLong(0);
   4401                     updateFavoritesMembership(rawContactId,
   4402                             mValues.getAsLong(RawContacts.STARRED) != 0);
   4403                 }
   4404             } finally {
   4405                 cursor.close();
   4406             }
   4407         }
   4408 
   4409         // Copy changeable values to prevent automatically managed fields from
   4410         // being explicitly updated by clients.
   4411         mValues.clear();
   4412         ContactsDatabaseHelper.copyStringValue(mValues, RawContacts.CUSTOM_RINGTONE,
   4413                 values, Contacts.CUSTOM_RINGTONE);
   4414         ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.SEND_TO_VOICEMAIL,
   4415                 values, Contacts.SEND_TO_VOICEMAIL);
   4416         ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.LAST_TIME_CONTACTED,
   4417                 values, Contacts.LAST_TIME_CONTACTED);
   4418         ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.TIMES_CONTACTED,
   4419                 values, Contacts.TIMES_CONTACTED);
   4420         ContactsDatabaseHelper.copyLongValue(mValues, RawContacts.STARRED,
   4421                 values, Contacts.STARRED);
   4422 
   4423         int rslt = db.update(Tables.CONTACTS, mValues, Contacts._ID + "=?",
   4424                 mSelectionArgs1);
   4425 
   4426         if (values.containsKey(Contacts.LAST_TIME_CONTACTED) &&
   4427                 !values.containsKey(Contacts.TIMES_CONTACTED)) {
   4428             db.execSQL(UPDATE_TIMES_CONTACTED_CONTACTS_TABLE, mSelectionArgs1);
   4429             db.execSQL(UPDATE_TIMES_CONTACTED_RAWCONTACTS_TABLE, mSelectionArgs1);
   4430         }
   4431         return rslt;
   4432     }
   4433 
   4434     private int updateAggregationException(SQLiteDatabase db, ContentValues values) {
   4435         int exceptionType = values.getAsInteger(AggregationExceptions.TYPE);
   4436         long rcId1 = values.getAsInteger(AggregationExceptions.RAW_CONTACT_ID1);
   4437         long rcId2 = values.getAsInteger(AggregationExceptions.RAW_CONTACT_ID2);
   4438 
   4439         long rawContactId1;
   4440         long rawContactId2;
   4441         if (rcId1 < rcId2) {
   4442             rawContactId1 = rcId1;
   4443             rawContactId2 = rcId2;
   4444         } else {
   4445             rawContactId2 = rcId1;
   4446             rawContactId1 = rcId2;
   4447         }
   4448 
   4449         if (exceptionType == AggregationExceptions.TYPE_AUTOMATIC) {
   4450             mSelectionArgs2[0] = String.valueOf(rawContactId1);
   4451             mSelectionArgs2[1] = String.valueOf(rawContactId2);
   4452             db.delete(Tables.AGGREGATION_EXCEPTIONS,
   4453                     AggregationExceptions.RAW_CONTACT_ID1 + "=? AND "
   4454                     + AggregationExceptions.RAW_CONTACT_ID2 + "=?", mSelectionArgs2);
   4455         } else {
   4456             ContentValues exceptionValues = new ContentValues(3);
   4457             exceptionValues.put(AggregationExceptions.TYPE, exceptionType);
   4458             exceptionValues.put(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
   4459             exceptionValues.put(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
   4460             db.replace(Tables.AGGREGATION_EXCEPTIONS, AggregationExceptions._ID,
   4461                     exceptionValues);
   4462         }
   4463 
   4464         mAggregator.get().invalidateAggregationExceptionCache();
   4465         mAggregator.get().markForAggregation(rawContactId1,
   4466                 RawContacts.AGGREGATION_MODE_DEFAULT, true);
   4467         mAggregator.get().markForAggregation(rawContactId2,
   4468                 RawContacts.AGGREGATION_MODE_DEFAULT, true);
   4469 
   4470         mAggregator.get().aggregateContact(mTransactionContext.get(), db, rawContactId1);
   4471         mAggregator.get().aggregateContact(mTransactionContext.get(), db, rawContactId2);
   4472 
   4473         // The return value is fake - we just confirm that we made a change, not count actual
   4474         // rows changed.
   4475         return 1;
   4476     }
   4477 
   4478     @Override
   4479     public void onAccountsUpdated(Account[] accounts) {
   4480         scheduleBackgroundTask(BACKGROUND_TASK_UPDATE_ACCOUNTS);
   4481     }
   4482 
   4483     private static final String ACCOUNT_STRING_SEPARATOR_OUTER = "\u0001";
   4484     private static final String ACCOUNT_STRING_SEPARATOR_INNER = "\u0002";
   4485 
   4486     /** return serialized version of {@code accounts} */
   4487     @VisibleForTesting
   4488     static String accountsToString(Set<Account> accounts) {
   4489         final StringBuilder sb = new StringBuilder();
   4490         for (Account account : accounts) {
   4491             if (sb.length() > 0) {
   4492                 sb.append(ACCOUNT_STRING_SEPARATOR_OUTER);
   4493             }
   4494             sb.append(account.name);
   4495             sb.append(ACCOUNT_STRING_SEPARATOR_INNER);
   4496             sb.append(account.type);
   4497         }
   4498         return sb.toString();
   4499     }
   4500 
   4501     /**
   4502      * de-serialize string returned by {@link #accountsToString} and return it.
   4503      * If {@code accountsString} is malformed it'll throw {@link IllegalArgumentException}.
   4504      */
   4505     @VisibleForTesting
   4506     static Set<Account> stringToAccounts(String accountsString) {
   4507         final Set<Account> ret = Sets.newHashSet();
   4508         if (accountsString.length() == 0) return ret; // no accounts
   4509         try {
   4510             for (String accountString : accountsString.split(ACCOUNT_STRING_SEPARATOR_OUTER)) {
   4511                 String[] nameAndType = accountString.split(ACCOUNT_STRING_SEPARATOR_INNER);
   4512                 ret.add(new Account(nameAndType[0], nameAndType[1]));
   4513             }
   4514             return ret;
   4515         } catch (RuntimeException ex) {
   4516             throw new IllegalArgumentException("Malformed string", ex);
   4517         }
   4518     }
   4519 
   4520     /**
   4521      * @return {@code true} if the given {@code currentSystemAccounts} are different from the
   4522      *    accounts we know, which are stored in the {@link DbProperties#KNOWN_ACCOUNTS} property.
   4523      */
   4524     @VisibleForTesting
   4525     boolean haveAccountsChanged(Account[] currentSystemAccounts) {
   4526         final ContactsDatabaseHelper dbHelper = mDbHelper.get();
   4527         final Set<Account> knownAccountSet;
   4528         try {
   4529             knownAccountSet = stringToAccounts(
   4530                     dbHelper.getProperty(DbProperties.KNOWN_ACCOUNTS, ""));
   4531         } catch (IllegalArgumentException e) {
   4532             // Failed to get the last known accounts for an unknown reason.  Let's just
   4533             // treat as if accounts have changed.
   4534             return true;
   4535         }
   4536         final Set<Account> currentAccounts = Sets.newHashSet(currentSystemAccounts);
   4537         return !knownAccountSet.equals(currentAccounts);
   4538     }
   4539 
   4540     @VisibleForTesting
   4541     void saveAccounts(Account[] systemAccounts) {
   4542         final ContactsDatabaseHelper dbHelper = mDbHelper.get();
   4543         dbHelper.setProperty(DbProperties.KNOWN_ACCOUNTS,
   4544                 accountsToString(Sets.newHashSet(systemAccounts)));
   4545     }
   4546 
   4547     private boolean updateAccountsInBackground(Account[] systemAccounts) {
   4548         if (!haveAccountsChanged(systemAccounts)) {
   4549             return false;
   4550         }
   4551         if ("1".equals(SystemProperties.get(DEBUG_PROPERTY_KEEP_STALE_ACCOUNT_DATA))) {
   4552             Log.w(TAG, "Accounts changed, but not removing stale data for " +
   4553                     DEBUG_PROPERTY_KEEP_STALE_ACCOUNT_DATA);
   4554             return true;
   4555         }
   4556         Log.i(TAG, "Accounts changed");
   4557 
   4558         invalidateFastScrollingIndexCache();
   4559 
   4560         final ContactsDatabaseHelper dbHelper = mDbHelper.get();
   4561         final SQLiteDatabase db = dbHelper.getWritableDatabase();
   4562         db.beginTransaction();
   4563 
   4564         // WARNING: This method can be run in either contacts mode or profile mode.  It is
   4565         // absolutely imperative that no calls be made inside the following try block that can
   4566         // interact with the contacts DB.  Otherwise it is quite possible for a deadlock to occur.
   4567         try {
   4568             // First, remove stale rows from raw_contacts, groups, and related tables.
   4569 
   4570             // All accounts that are used in raw_contacts and/or groups.
   4571             final Set<AccountWithDataSet> knownAccountsWithDataSets
   4572                     = dbHelper.getAllAccountsWithDataSets();
   4573 
   4574             // Find the accounts that have been removed.
   4575             final List<AccountWithDataSet> accountsWithDataSetsToDelete = Lists.newArrayList();
   4576             for (AccountWithDataSet knownAccountWithDataSet : knownAccountsWithDataSets) {
   4577                 if (knownAccountWithDataSet.isLocalAccount()
   4578                         || knownAccountWithDataSet.inSystemAccounts(systemAccounts)) {
   4579                     continue;
   4580                 }
   4581                 accountsWithDataSetsToDelete.add(knownAccountWithDataSet);
   4582             }
   4583 
   4584             if (!accountsWithDataSetsToDelete.isEmpty()) {
   4585                 for (AccountWithDataSet accountWithDataSet : accountsWithDataSetsToDelete) {
   4586                     Log.d(TAG, "removing data for removed account " + accountWithDataSet);
   4587                     final Long accountIdOrNull = dbHelper.getAccountIdOrNull(accountWithDataSet);
   4588 
   4589                     // getAccountIdOrNull() really shouldn't return null here, but just in case...
   4590                     if (accountIdOrNull != null) {
   4591                         final String[] accountIdParams =
   4592                                 new String[] {Long.toString(accountIdOrNull)};
   4593                         db.execSQL(
   4594                                 "DELETE FROM " + Tables.GROUPS +
   4595                                 " WHERE " + GroupsColumns.ACCOUNT_ID + " = ?",
   4596                                 accountIdParams);
   4597                         db.execSQL(
   4598                                 "DELETE FROM " + Tables.PRESENCE +
   4599                                 " WHERE " + PresenceColumns.RAW_CONTACT_ID + " IN (" +
   4600                                         "SELECT " + RawContacts._ID +
   4601                                         " FROM " + Tables.RAW_CONTACTS +
   4602                                         " WHERE " + RawContactsColumns.ACCOUNT_ID + " = ?)",
   4603                                         accountIdParams);
   4604                         db.execSQL(
   4605                                 "DELETE FROM " + Tables.STREAM_ITEM_PHOTOS +
   4606                                 " WHERE " + StreamItemPhotos.STREAM_ITEM_ID + " IN (" +
   4607                                         "SELECT " + StreamItems._ID +
   4608                                         " FROM " + Tables.STREAM_ITEMS +
   4609                                         " WHERE " + StreamItems.RAW_CONTACT_ID + " IN (" +
   4610                                                 "SELECT " + RawContacts._ID +
   4611                                                 " FROM " + Tables.RAW_CONTACTS +
   4612                                                 " WHERE " + RawContactsColumns.ACCOUNT_ID + "=?))",
   4613                                                 accountIdParams);
   4614                         db.execSQL(
   4615                                 "DELETE FROM " + Tables.STREAM_ITEMS +
   4616                                 " WHERE " + StreamItems.RAW_CONTACT_ID + " IN (" +
   4617                                         "SELECT " + RawContacts._ID +
   4618                                         " FROM " + Tables.RAW_CONTACTS +
   4619                                         " WHERE " + RawContactsColumns.ACCOUNT_ID + " = ?)",
   4620                                         accountIdParams);
   4621                         db.execSQL(
   4622                                 "DELETE FROM " + Tables.RAW_CONTACTS +
   4623                                 " WHERE " + RawContactsColumns.ACCOUNT_ID + " = ?",
   4624                                 accountIdParams);
   4625                         db.execSQL(
   4626                                 "DELETE FROM " + Tables.ACCOUNTS +
   4627                                 " WHERE " + AccountsColumns._ID + "=?",
   4628                                 accountIdParams);
   4629                     }
   4630                 }
   4631 
   4632                 // Find all aggregated contacts that used to contain the raw contacts
   4633                 // we have just deleted and see if they are still referencing the deleted
   4634                 // names or photos.  If so, fix up those contacts.
   4635                 HashSet<Long> orphanContactIds = Sets.newHashSet();
   4636                 Cursor cursor = db.rawQuery("SELECT " + Contacts._ID +
   4637                         " FROM " + Tables.CONTACTS +
   4638                         " WHERE (" + Contacts.NAME_RAW_CONTACT_ID + " NOT NULL AND " +
   4639                                 Contacts.NAME_RAW_CONTACT_ID + " NOT IN " +
   4640                                         "(SELECT " + RawContacts._ID +
   4641                                         " FROM " + Tables.RAW_CONTACTS + "))" +
   4642                         " OR (" + Contacts.PHOTO_ID + " NOT NULL AND " +
   4643                                 Contacts.PHOTO_ID + " NOT IN " +
   4644                                         "(SELECT " + Data._ID +
   4645                                         " FROM " + Tables.DATA + "))", null);
   4646                 try {
   4647                     while (cursor.moveToNext()) {
   4648                         orphanContactIds.add(cursor.getLong(0));
   4649                     }
   4650                 } finally {
   4651                     cursor.close();
   4652                 }
   4653 
   4654                 for (Long contactId : orphanContactIds) {
   4655                     mAggregator.get().updateAggregateData(mTransactionContext.get(), contactId);
   4656                 }
   4657                 dbHelper.updateAllVisible();
   4658 
   4659                 // Don't bother updating the search index if we're in profile mode - there is no
   4660                 // search index for the profile DB, and updating it for the contacts DB in this case
   4661                 // makes no sense and risks a deadlock.
   4662                 if (!inProfileMode()) {
   4663                     // TODO Fix it.  It only updates index for contacts/raw_contacts that the
   4664                     // current transaction context knows updated, but here in this method we don't
   4665                     // update that information, so effectively it's no-op.
   4666                     // We can probably just schedule BACKGROUND_TASK_UPDATE_SEARCH_INDEX.
   4667                     // (But make sure it's not scheduled yet. We schedule this task in initialize()
   4668                     // too.)
   4669                     updateSearchIndexInTransaction();
   4670                 }
   4671             }
   4672 
   4673             // Second, remove stale rows from Tables.SETTINGS and Tables.DIRECTORIES
   4674             removeStaleAccountRows(Tables.SETTINGS, Settings.ACCOUNT_NAME, Settings.ACCOUNT_TYPE,
   4675                     systemAccounts);
   4676             removeStaleAccountRows(Tables.DIRECTORIES, Directory.ACCOUNT_NAME,
   4677                     Directory.ACCOUNT_TYPE, systemAccounts);
   4678 
   4679             // Third, remaining tasks that must be done in a transaction.
   4680             // TODO: Should sync state take data set into consideration?
   4681             dbHelper.getSyncState().onAccountsChanged(db, systemAccounts);
   4682 
   4683             saveAccounts(systemAccounts);
   4684 
   4685             db.setTransactionSuccessful();
   4686         } finally {
   4687             db.endTransaction();
   4688         }
   4689         mAccountWritability.clear();
   4690 
   4691         updateContactsAccountCount(systemAccounts);
   4692         updateProviderStatus();
   4693         return true;
   4694     }
   4695 
   4696     private void updateContactsAccountCount(Account[] accounts) {
   4697         int count = 0;
   4698         for (Account account : accounts) {
   4699             if (isContactsAccount(account)) {
   4700                 count++;
   4701             }
   4702         }
   4703         mContactsAccountCount = count;
   4704     }
   4705 
   4706     protected boolean isContactsAccount(Account account) {
   4707         final IContentService cs = ContentResolver.getContentService();
   4708         try {
   4709             return cs.getIsSyncable(account, ContactsContract.AUTHORITY) > 0;
   4710         } catch (RemoteException e) {
   4711             Log.e(TAG, "Cannot obtain sync flag for account: " + account, e);
   4712             return false;
   4713         }
   4714     }
   4715 
   4716     public void onPackageChanged(String packageName) {
   4717         scheduleBackgroundTask(BACKGROUND_TASK_UPDATE_DIRECTORIES, packageName);
   4718     }
   4719 
   4720     public void removeStaleAccountRows(String table, String accountNameColumn,
   4721             String accountTypeColumn, Account[] systemAccounts) {
   4722         final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
   4723         final Cursor c = db.rawQuery(
   4724                 "SELECT DISTINCT " + accountNameColumn +
   4725                 "," + accountTypeColumn +
   4726                 " FROM " + table, null);
   4727         try {
   4728             c.moveToPosition(-1);
   4729             while (c.moveToNext()) {
   4730                 final AccountWithDataSet accountWithDataSet = AccountWithDataSet.get(
   4731                         c.getString(0), c.getString(1), null);
   4732                 if (accountWithDataSet.isLocalAccount()
   4733                         || accountWithDataSet.inSystemAccounts(systemAccounts)) {
   4734                     // Account still exists.
   4735                     continue;
   4736                 }
   4737 
   4738                 db.execSQL("DELETE FROM " + table +
   4739                         " WHERE " + accountNameColumn + "=? AND " +
   4740                         accountTypeColumn + "=?",
   4741                         new String[] {accountWithDataSet.getAccountName(),
   4742                                 accountWithDataSet.getAccountType()});
   4743             }
   4744         } finally {
   4745             c.close();
   4746         }
   4747     }
   4748 
   4749     @Override
   4750     public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
   4751             String sortOrder) {
   4752         return query(uri, projection, selection, selectionArgs, sortOrder, null);
   4753     }
   4754 
   4755     @Override
   4756     public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
   4757             String sortOrder, CancellationSignal cancellationSignal) {
   4758         if (VERBOSE_LOGGING) {
   4759             Log.v(TAG, "query: uri=" + uri + "  projection=" + Arrays.toString(projection) +
   4760                     "  selection=[" + selection + "]  args=" + Arrays.toString(selectionArgs) +
   4761                     "  order=[" + sortOrder + "]");
   4762         }
   4763 
   4764         waitForAccess(mReadAccessLatch);
   4765 
   4766         // Enforce stream items access check if applicable.
   4767         enforceSocialStreamReadPermission(uri);
   4768 
   4769         // Query the profile DB if appropriate.
   4770         if (mapsToProfileDb(uri)) {
   4771             switchToProfileMode();
   4772             return mProfileProvider.query(uri, projection, selection, selectionArgs, sortOrder,
   4773                     cancellationSignal);
   4774         }
   4775 
   4776         // Otherwise proceed with a normal query against the contacts DB.
   4777         switchToContactMode();
   4778         String directory = getQueryParameter(uri, ContactsContract.DIRECTORY_PARAM_KEY);
   4779         if (directory == null) {
   4780             return addSnippetExtrasToCursor(uri,
   4781                     queryLocal(uri, projection, selection, selectionArgs, sortOrder, -1,
   4782                     cancellationSignal));
   4783         } else if (directory.equals("0")) {
   4784             return addSnippetExtrasToCursor(uri,
   4785                     queryLocal(uri, projection, selection, selectionArgs, sortOrder,
   4786                     Directory.DEFAULT, cancellationSignal));
   4787         } else if (directory.equals("1")) {
   4788             return addSnippetExtrasToCursor(uri,
   4789                     queryLocal(uri, projection, selection, selectionArgs, sortOrder,
   4790                     Directory.LOCAL_INVISIBLE, cancellationSignal));
   4791         }
   4792 
   4793         DirectoryInfo directoryInfo = getDirectoryAuthority(directory);
   4794         if (directoryInfo == null) {
   4795             Log.e(TAG, "Invalid directory ID: " + uri);
   4796             return null;
   4797         }
   4798 
   4799         Builder builder = new Uri.Builder();
   4800         builder.scheme(ContentResolver.SCHEME_CONTENT);
   4801         builder.authority(directoryInfo.authority);
   4802         builder.encodedPath(uri.getEncodedPath());
   4803         if (directoryInfo.accountName != null) {
   4804             builder.appendQueryParameter(RawContacts.ACCOUNT_NAME, directoryInfo.accountName);
   4805         }
   4806         if (directoryInfo.accountType != null) {
   4807             builder.appendQueryParameter(RawContacts.ACCOUNT_TYPE, directoryInfo.accountType);
   4808         }
   4809 
   4810         String limit = getLimit(uri);
   4811         if (limit != null) {
   4812             builder.appendQueryParameter(ContactsContract.LIMIT_PARAM_KEY, limit);
   4813         }
   4814 
   4815         Uri directoryUri = builder.build();
   4816 
   4817         if (projection == null) {
   4818             projection = getDefaultProjection(uri);
   4819         }
   4820 
   4821         Cursor cursor = getContext().getContentResolver().query(directoryUri, projection, selection,
   4822                 selectionArgs, sortOrder);
   4823 
   4824         if (cursor == null) {
   4825             return null;
   4826         }
   4827 
   4828         // Load the cursor contents into a memory cursor (backed by a cursor window) and close the
   4829         // underlying cursor.
   4830         try {
   4831             MemoryCursor memCursor = new MemoryCursor(null, cursor.getColumnNames());
   4832             memCursor.fillFromCursor(cursor);
   4833             return memCursor;
   4834         } finally {
   4835             cursor.close();
   4836         }
   4837     }
   4838 
   4839     private Cursor addSnippetExtrasToCursor(Uri uri, Cursor cursor) {
   4840 
   4841         // If the cursor doesn't contain a snippet column, don't bother wrapping it.
   4842         if (cursor.getColumnIndex(SearchSnippetColumns.SNIPPET) < 0) {
   4843             return cursor;
   4844         }
   4845 
   4846         String query = uri.getLastPathSegment();
   4847 
   4848         // Snippet data is needed for the snippeting on the client side, so store it in the cursor
   4849         if (cursor instanceof AbstractCursor && deferredSnippetingRequested(uri)){
   4850             Bundle oldExtras = cursor.getExtras();
   4851             Bundle extras = new Bundle();
   4852             if (oldExtras != null) {
   4853                 extras.putAll(oldExtras);
   4854             }
   4855             extras.putString(ContactsContract.DEFERRED_SNIPPETING_QUERY, query);
   4856 
   4857             ((AbstractCursor) cursor).setExtras(extras);
   4858         }
   4859         return cursor;
   4860     }
   4861 
   4862     private Cursor addDeferredSnippetingExtra(Cursor cursor) {
   4863         if (cursor instanceof AbstractCursor){
   4864             Bundle oldExtras = cursor.getExtras();
   4865             Bundle extras = new Bundle();
   4866             if (oldExtras != null) {
   4867                 extras.putAll(oldExtras);
   4868             }
   4869             extras.putBoolean(ContactsContract.DEFERRED_SNIPPETING, true);
   4870             ((AbstractCursor) cursor).setExtras(extras);
   4871         }
   4872         return cursor;
   4873     }
   4874 
   4875     private static final class DirectoryQuery {
   4876         public static final String[] COLUMNS = new String[] {
   4877                 Directory._ID,
   4878                 Directory.DIRECTORY_AUTHORITY,
   4879                 Directory.ACCOUNT_NAME,
   4880                 Directory.ACCOUNT_TYPE
   4881         };
   4882 
   4883         public static final int DIRECTORY_ID = 0;
   4884         public static final int AUTHORITY = 1;
   4885         public static final int ACCOUNT_NAME = 2;
   4886         public static final int ACCOUNT_TYPE = 3;
   4887     }
   4888 
   4889     /**
   4890      * Reads and caches directory information for the database.
   4891      */
   4892     private DirectoryInfo getDirectoryAuthority(String directoryId) {
   4893         synchronized (mDirectoryCache) {
   4894             if (!mDirectoryCacheValid) {
   4895                 mDirectoryCache.clear();
   4896                 SQLiteDatabase db = mDbHelper.get().getReadableDatabase();
   4897                 Cursor cursor = db.query(Tables.DIRECTORIES,
   4898                         DirectoryQuery.COLUMNS,
   4899                         null, null, null, null, null);
   4900                 try {
   4901                     while (cursor.moveToNext()) {
   4902                         DirectoryInfo info = new DirectoryInfo();
   4903                         String id = cursor.getString(DirectoryQuery.DIRECTORY_ID);
   4904                         info.authority = cursor.getString(DirectoryQuery.AUTHORITY);
   4905                         info.accountName = cursor.getString(DirectoryQuery.ACCOUNT_NAME);
   4906                         info.accountType = cursor.getString(DirectoryQuery.ACCOUNT_TYPE);
   4907                         mDirectoryCache.put(id, info);
   4908                     }
   4909                 } finally {
   4910                     cursor.close();
   4911                 }
   4912                 mDirectoryCacheValid = true;
   4913             }
   4914 
   4915             return mDirectoryCache.get(directoryId);
   4916         }
   4917     }
   4918 
   4919     public void resetDirectoryCache() {
   4920         synchronized(mDirectoryCache) {
   4921             mDirectoryCacheValid = false;
   4922         }
   4923     }
   4924 
   4925     protected Cursor queryLocal(final Uri uri, final String[] projection, String selection,
   4926             String[] selectionArgs, String sortOrder, final long directoryId,
   4927             final CancellationSignal cancellationSignal) {
   4928 
   4929         final SQLiteDatabase db = mDbHelper.get().getReadableDatabase();
   4930 
   4931         SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
   4932         String groupBy = null;
   4933         String having = null;
   4934         String limit = getLimit(uri);
   4935         boolean snippetDeferred = false;
   4936 
   4937         // The expression used in bundleLetterCountExtras() to get count.
   4938         String addressBookIndexerCountExpression = null;
   4939 
   4940         final int match = sUriMatcher.match(uri);
   4941         switch (match) {
   4942             case SYNCSTATE:
   4943             case PROFILE_SYNCSTATE:
   4944                 return mDbHelper.get().getSyncState().query(db, projection, selection,
   4945                         selectionArgs, sortOrder);
   4946 
   4947             case CONTACTS: {
   4948                 setTablesAndProjectionMapForContacts(qb, uri, projection);
   4949                 appendLocalDirectoryAndAccountSelectionIfNeeded(qb, directoryId, uri);
   4950                 break;
   4951             }
   4952 
   4953             case CONTACTS_ID: {
   4954                 long contactId = ContentUris.parseId(uri);
   4955                 setTablesAndProjectionMapForContacts(qb, uri, projection);
   4956                 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(contactId));
   4957                 qb.appendWhere(Contacts._ID + "=?");
   4958                 break;
   4959             }
   4960 
   4961             case CONTACTS_LOOKUP:
   4962             case CONTACTS_LOOKUP_ID: {
   4963                 List<String> pathSegments = uri.getPathSegments();
   4964                 int segmentCount = pathSegments.size();
   4965                 if (segmentCount < 3) {
   4966                     throw new IllegalArgumentException(mDbHelper.get().exceptionMessage(
   4967                             "Missing a lookup key", uri));
   4968                 }
   4969 
   4970                 String lookupKey = pathSegments.get(2);
   4971                 if (segmentCount == 4) {
   4972                     long contactId = Long.parseLong(pathSegments.get(3));
   4973                     SQLiteQueryBuilder lookupQb = new SQLiteQueryBuilder();
   4974                     setTablesAndProjectionMapForContacts(lookupQb, uri, projection);
   4975 
   4976                     Cursor c = queryWithContactIdAndLookupKey(lookupQb, db, uri,
   4977                             projection, selection, selectionArgs, sortOrder, groupBy, limit,
   4978                             Contacts._ID, contactId, Contacts.LOOKUP_KEY, lookupKey,
   4979                             cancellationSignal);
   4980                     if (c != null) {
   4981                         return c;
   4982                     }
   4983                 }
   4984 
   4985                 setTablesAndProjectionMapForContacts(qb, uri, projection);
   4986                 selectionArgs = insertSelectionArg(selectionArgs,
   4987                         String.valueOf(lookupContactIdByLookupKey(db, lookupKey)));
   4988                 qb.appendWhere(Contacts._ID + "=?");
   4989                 break;
   4990             }
   4991 
   4992             case CONTACTS_LOOKUP_DATA:
   4993             case CONTACTS_LOOKUP_ID_DATA:
   4994             case CONTACTS_LOOKUP_PHOTO:
   4995             case CONTACTS_LOOKUP_ID_PHOTO: {
   4996                 List<String> pathSegments = uri.getPathSegments();
   4997                 int segmentCount = pathSegments.size();
   4998                 if (segmentCount < 4) {
   4999                     throw new IllegalArgumentException(mDbHelper.get().exceptionMessage(
   5000                             "Missing a lookup key", uri));
   5001                 }
   5002                 String lookupKey = pathSegments.get(2);
   5003                 if (segmentCount == 5) {
   5004                     long contactId = Long.parseLong(pathSegments.get(3));
   5005                     SQLiteQueryBuilder lookupQb = new SQLiteQueryBuilder();
   5006                     setTablesAndProjectionMapForData(lookupQb, uri, projection, false);
   5007                     if (match == CONTACTS_LOOKUP_PHOTO || match == CONTACTS_LOOKUP_ID_PHOTO) {
   5008                         lookupQb.appendWhere(" AND " + Data._ID + "=" + Contacts.PHOTO_ID);
   5009                     }
   5010                     lookupQb.appendWhere(" AND ");
   5011                     Cursor c = queryWithContactIdAndLookupKey(lookupQb, db, uri,
   5012                             projection, selection, selectionArgs, sortOrder, groupBy, limit,
   5013                             Data.CONTACT_ID, contactId, Data.LOOKUP_KEY, lookupKey,
   5014                             cancellationSignal);
   5015                     if (c != null) {
   5016                         return c;
   5017                     }
   5018 
   5019                     // TODO see if the contact exists but has no data rows (rare)
   5020                 }
   5021 
   5022                 setTablesAndProjectionMapForData(qb, uri, projection, false);
   5023                 long contactId = lookupContactIdByLookupKey(db, lookupKey);
   5024                 selectionArgs = insertSelectionArg(selectionArgs,
   5025                         String.valueOf(contactId));
   5026                 if (match == CONTACTS_LOOKUP_PHOTO || match == CONTACTS_LOOKUP_ID_PHOTO) {
   5027                     qb.appendWhere(" AND " + Data._ID + "=" + Contacts.PHOTO_ID);
   5028                 }
   5029                 qb.appendWhere(" AND " + Data.CONTACT_ID + "=?");
   5030                 break;
   5031             }
   5032 
   5033             case CONTACTS_ID_STREAM_ITEMS: {
   5034                 long contactId = Long.parseLong(uri.getPathSegments().get(1));
   5035                 setTablesAndProjectionMapForStreamItems(qb);
   5036                 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(contactId));
   5037                 qb.appendWhere(StreamItems.CONTACT_ID + "=?");
   5038                 break;
   5039             }
   5040 
   5041             case CONTACTS_LOOKUP_STREAM_ITEMS:
   5042             case CONTACTS_LOOKUP_ID_STREAM_ITEMS: {
   5043                 List<String> pathSegments = uri.getPathSegments();
   5044                 int segmentCount = pathSegments.size();
   5045                 if (segmentCount < 4) {
   5046                     throw new IllegalArgumentException(mDbHelper.get().exceptionMessage(
   5047                             "Missing a lookup key", uri));
   5048                 }
   5049                 String lookupKey = pathSegments.get(2);
   5050                 if (segmentCount == 5) {
   5051                     long contactId = Long.parseLong(pathSegments.get(3));
   5052                     SQLiteQueryBuilder lookupQb = new SQLiteQueryBuilder();
   5053                     setTablesAndProjectionMapForStreamItems(lookupQb);
   5054                     Cursor c = queryWithContactIdAndLookupKey(lookupQb, db, uri,
   5055                             projection, selection, selectionArgs, sortOrder, groupBy, limit,
   5056                             StreamItems.CONTACT_ID, contactId,
   5057                             StreamItems.CONTACT_LOOKUP_KEY, lookupKey,
   5058                             cancellationSignal);
   5059                     if (c != null) {
   5060                         return c;
   5061                     }
   5062                 }
   5063 
   5064                 setTablesAndProjectionMapForStreamItems(qb);
   5065                 long contactId = lookupContactIdByLookupKey(db, lookupKey);
   5066                 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(contactId));
   5067                 qb.appendWhere(RawContacts.CONTACT_ID + "=?");
   5068                 break;
   5069             }
   5070 
   5071             case CONTACTS_AS_VCARD: {
   5072                 final String lookupKey = Uri.encode(uri.getPathSegments().get(2));
   5073                 long contactId = lookupContactIdByLookupKey(db, lookupKey);
   5074                 qb.setTables(Views.CONTACTS);
   5075                 qb.setProjectionMap(sContactsVCardProjectionMap);
   5076                 selectionArgs = insertSelectionArg(selectionArgs,
   5077                         String.valueOf(contactId));
   5078                 qb.appendWhere(Contacts._ID + "=?");
   5079                 break;
   5080             }
   5081 
   5082             case CONTACTS_AS_MULTI_VCARD: {
   5083                 SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd_HHmmss");
   5084                 String currentDateString = dateFormat.format(new Date()).toString();
   5085                 return db.rawQuery(
   5086                     "SELECT" +
   5087                     " 'vcards_' || ? || '.vcf' AS " + OpenableColumns.DISPLAY_NAME + "," +
   5088                     " NULL AS " + OpenableColumns.SIZE,
   5089                     new String[] { currentDateString });
   5090             }
   5091 
   5092             case CONTACTS_FILTER: {
   5093                 String filterParam = "";
   5094                 boolean deferredSnipRequested = deferredSnippetingRequested(uri);
   5095                 if (uri.getPathSegments().size() > 2) {
   5096                     filterParam = uri.getLastPathSegment();
   5097                 }
   5098 
   5099                 // If the query consists of a single word, we can do snippetizing after-the-fact for
   5100                 // a performance boost.  Otherwise, we can't defer.
   5101                 snippetDeferred = isSingleWordQuery(filterParam)
   5102                     && deferredSnipRequested && snippetNeeded(projection);
   5103                 setTablesAndProjectionMapForContactsWithSnippet(
   5104                         qb, uri, projection, filterParam, directoryId,
   5105                         snippetDeferred);
   5106                 break;
   5107             }
   5108 
   5109             case CONTACTS_STREQUENT_FILTER:
   5110             case CONTACTS_STREQUENT: {
   5111                 // Basically the resultant SQL should look like this:
   5112                 // (SQL for listing starred items)
   5113                 // UNION ALL
   5114                 // (SQL for listing frequently contacted items)
   5115                 // ORDER BY ...
   5116 
   5117                 final boolean phoneOnly = readBooleanQueryParameter(
   5118                         uri, ContactsContract.STREQUENT_PHONE_ONLY, false);
   5119                 if (match == CONTACTS_STREQUENT_FILTER && uri.getPathSegments().size() > 3) {
   5120                     String filterParam = uri.getLastPathSegment();
   5121                     StringBuilder sb = new StringBuilder();
   5122                     sb.append(Contacts._ID + " IN ");
   5123                     appendContactFilterAsNestedQuery(sb, filterParam);
   5124                     selection = DbQueryUtils.concatenateClauses(selection, sb.toString());
   5125                 }
   5126 
   5127                 String[] subProjection = null;
   5128                 if (projection != null) {
   5129                     subProjection = new String[projection.length + 2];
   5130                     System.arraycopy(projection, 0, subProjection, 0, projection.length);
   5131                     subProjection[projection.length + 0] = DataUsageStatColumns.TIMES_USED;
   5132                     subProjection[projection.length + 1] = DataUsageStatColumns.LAST_TIME_USED;
   5133                 }
   5134 
   5135                 // Build the first query for starred
   5136                 setTablesAndProjectionMapForContacts(qb, uri, projection, false);
   5137                 qb.setProjectionMap(phoneOnly ?
   5138                         sStrequentPhoneOnlyStarredProjectionMap
   5139                         : sStrequentStarredProjectionMap);
   5140                 if (phoneOnly) {
   5141                     qb.appendWhere(DbQueryUtils.concatenateClauses(
   5142                             selection, Contacts.HAS_PHONE_NUMBER + "=1"));
   5143                 }
   5144                 qb.setStrict(true);
   5145                 final String starredInnerQuery = qb.buildQuery(subProjection,
   5146                         Contacts.STARRED + "=1", Contacts._ID, null,
   5147                         Contacts.DISPLAY_NAME + " COLLATE LOCALIZED ASC", null);
   5148 
   5149                 // Reset the builder.
   5150                 qb = new SQLiteQueryBuilder();
   5151                 qb.setStrict(true);
   5152 
   5153                 // Build the second query for frequent part. These JOINS can be very slow
   5154                 // if assembled in the wrong order. Be sure to test changes against huge databases.
   5155                 final String frequentInnerQuery;
   5156                 if (phoneOnly) {
   5157                     final StringBuilder tableBuilder = new StringBuilder();
   5158                     // In phone only mode, we need to look at view_data instead of
   5159                     // contacts/raw_contacts to obtain actual phone numbers. One problem is that
   5160                     // view_data is much larger than view_contacts, so our query might become much
   5161                     // slower.
   5162                     //
   5163                     // To avoid the possible slow down, we start from data usage table and join
   5164                     // view_data to the table, assuming data usage table is quite smaller than
   5165                     // data rows (almost always it should be), and we don't want any phone
   5166                     // numbers not used by the user. This way sqlite is able to drop a number of
   5167                     // rows in view_data in the early stage of data lookup.
   5168                     tableBuilder.append(Tables.DATA_USAGE_STAT
   5169                             + " INNER JOIN " + Views.DATA + " " + Tables.DATA
   5170                             + " ON (" + DataUsageStatColumns.CONCRETE_DATA_ID + "="
   5171                                 + DataColumns.CONCRETE_ID + " AND "
   5172                             + DataUsageStatColumns.CONCRETE_USAGE_TYPE + "="
   5173                                 + DataUsageStatColumns.USAGE_TYPE_INT_CALL + ")");
   5174                     appendContactPresenceJoin(tableBuilder, projection, RawContacts.CONTACT_ID);
   5175                     appendContactStatusUpdateJoin(tableBuilder, projection,
   5176                             ContactsColumns.LAST_STATUS_UPDATE_ID);
   5177 
   5178                     qb.setTables(tableBuilder.toString());
   5179                     qb.setProjectionMap(sStrequentPhoneOnlyFrequentProjectionMap);
   5180                     final long phoneMimeTypeId =
   5181                             mDbHelper.get().getMimeTypeId(Phone.CONTENT_ITEM_TYPE);
   5182                     final long sipMimeTypeId =
   5183                             mDbHelper.get().getMimeTypeId(SipAddress.CONTENT_ITEM_TYPE);
   5184                     qb.appendWhere(DbQueryUtils.concatenateClauses(
   5185                             selection,
   5186                             "(" + Contacts.STARRED + "=0 OR " + Contacts.STARRED + " IS NULL",
   5187                             DataColumns.MIMETYPE_ID + " IN (" +
   5188                             phoneMimeTypeId + ", " + sipMimeTypeId + ")) AND (" +
   5189                             RawContacts.CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY + ")"));
   5190                     frequentInnerQuery = qb.buildQuery(subProjection, null, null, null,
   5191                             SORT_BY_DATA_USAGE, "25");
   5192                 } else {
   5193                     setTablesAndProjectionMapForContacts(qb, uri, projection, true);
   5194                     qb.setProjectionMap(sStrequentFrequentProjectionMap);
   5195                     qb.appendWhere(DbQueryUtils.concatenateClauses(
   5196                             selection,
   5197                             "(" + Contacts.STARRED + " =0 OR " + Contacts.STARRED + " IS NULL)"));
   5198                     // Note frequentInnerQuery is a grouping query, so the "IN default_directory"
   5199                     // selection needs to be in HAVING, not in WHERE.
   5200                     final String HAVING =
   5201                             RawContacts.CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY;
   5202                     frequentInnerQuery = qb.buildQuery(subProjection,
   5203                             null, Contacts._ID, HAVING, SORT_BY_DATA_USAGE, "25");
   5204                 }
   5205 
   5206                 // We need to wrap the inner queries in an extra select, because they contain
   5207                 // their own SORT and LIMIT
   5208                 final String frequentQuery = "SELECT * FROM (" + frequentInnerQuery + ")";
   5209                 final String starredQuery = "SELECT * FROM (" + starredInnerQuery + ")";
   5210 
   5211                 // Put them together
   5212                 final String unionQuery =
   5213                         qb.buildUnionQuery(new String[] {starredQuery, frequentQuery}, null, null);
   5214 
   5215                 // Here, we need to use selection / selectionArgs (supplied from users) "twice",
   5216                 // as we want them both for starred items and for frequently contacted items.
   5217                 //
   5218                 // e.g. if the user specify selection = "starred =?" and selectionArgs = "0",
   5219                 // the resultant SQL should be like:
   5220                 // SELECT ... WHERE starred =? AND ...
   5221                 // UNION ALL
   5222                 // SELECT ... WHERE starred =? AND ...
   5223                 String[] doubledSelectionArgs = null;
   5224                 if (selectionArgs != null) {
   5225                     final int length = selectionArgs.length;
   5226                     doubledSelectionArgs = new String[length * 2];
   5227                     System.arraycopy(selectionArgs, 0, doubledSelectionArgs, 0, length);
   5228                     System.arraycopy(selectionArgs, 0, doubledSelectionArgs, length, length);
   5229                 }
   5230 
   5231                 Cursor cursor = db.rawQuery(unionQuery, doubledSelectionArgs);
   5232                 if (cursor != null) {
   5233                     cursor.setNotificationUri(getContext().getContentResolver(),
   5234                             ContactsContract.AUTHORITY_URI);
   5235                 }
   5236                 return cursor;
   5237             }
   5238 
   5239             case CONTACTS_FREQUENT: {
   5240                 setTablesAndProjectionMapForContacts(qb, uri, projection, true);
   5241                 qb.setProjectionMap(sStrequentFrequentProjectionMap);
   5242                 groupBy = Contacts._ID;
   5243                 having = Contacts._ID + " IN " + Tables.DEFAULT_DIRECTORY;
   5244                 if (!TextUtils.isEmpty(sortOrder)) {
   5245                     sortOrder = FREQUENT_ORDER_BY + ", " + sortOrder;
   5246                 } else {
   5247                     sortOrder = FREQUENT_ORDER_BY;
   5248                 }
   5249                 break;
   5250             }
   5251 
   5252             case CONTACTS_GROUP: {
   5253                 setTablesAndProjectionMapForContacts(qb, uri, projection);
   5254                 if (uri.getPathSegments().size() > 2) {
   5255                     qb.appendWhere(CONTACTS_IN_GROUP_SELECT);
   5256                     String groupMimeTypeId = String.valueOf(
   5257                             mDbHelper.get().getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE));
   5258                     selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
   5259                     selectionArgs = insertSelectionArg(selectionArgs, groupMimeTypeId);
   5260                 }
   5261                 break;
   5262             }
   5263 
   5264             case PROFILE: {
   5265                 setTablesAndProjectionMapForContacts(qb, uri, projection);
   5266                 break;
   5267             }
   5268 
   5269             case PROFILE_ENTITIES: {
   5270                 setTablesAndProjectionMapForEntities(qb, uri, projection);
   5271                 break;
   5272             }
   5273 
   5274             case PROFILE_AS_VCARD: {
   5275                 qb.setTables(Views.CONTACTS);
   5276                 qb.setProjectionMap(sContactsVCardProjectionMap);
   5277                 break;
   5278             }
   5279 
   5280             case CONTACTS_ID_DATA: {
   5281                 long contactId = Long.parseLong(uri.getPathSegments().get(1));
   5282                 setTablesAndProjectionMapForData(qb, uri, projection, false);
   5283                 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(contactId));
   5284                 qb.appendWhere(" AND " + RawContacts.CONTACT_ID + "=?");
   5285                 break;
   5286             }
   5287 
   5288             case CONTACTS_ID_PHOTO: {
   5289                 long contactId = Long.parseLong(uri.getPathSegments().get(1));
   5290                 setTablesAndProjectionMapForData(qb, uri, projection, false);
   5291                 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(contactId));
   5292                 qb.appendWhere(" AND " + RawContacts.CONTACT_ID + "=?");
   5293                 qb.appendWhere(" AND " + Data._ID + "=" + Contacts.PHOTO_ID);
   5294                 break;
   5295             }
   5296 
   5297             case CONTACTS_ID_ENTITIES: {
   5298                 long contactId = Long.parseLong(uri.getPathSegments().get(1));
   5299                 setTablesAndProjectionMapForEntities(qb, uri, projection);
   5300                 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(contactId));
   5301                 qb.appendWhere(" AND " + RawContacts.CONTACT_ID + "=?");
   5302                 break;
   5303             }
   5304 
   5305             case CONTACTS_LOOKUP_ENTITIES:
   5306             case CONTACTS_LOOKUP_ID_ENTITIES: {
   5307                 List<String> pathSegments = uri.getPathSegments();
   5308                 int segmentCount = pathSegments.size();
   5309                 if (segmentCount < 4) {
   5310                     throw new IllegalArgumentException(mDbHelper.get().exceptionMessage(
   5311                             "Missing a lookup key", uri));
   5312                 }
   5313                 String lookupKey = pathSegments.get(2);
   5314                 if (segmentCount == 5) {
   5315                     long contactId = Long.parseLong(pathSegments.get(3));
   5316                     SQLiteQueryBuilder lookupQb = new SQLiteQueryBuilder();
   5317                     setTablesAndProjectionMapForEntities(lookupQb, uri, projection);
   5318                     lookupQb.appendWhere(" AND ");
   5319 
   5320                     Cursor c = queryWithContactIdAndLookupKey(lookupQb, db, uri,
   5321                             projection, selection, selectionArgs, sortOrder, groupBy, limit,
   5322                             Contacts.Entity.CONTACT_ID, contactId,
   5323                             Contacts.Entity.LOOKUP_KEY, lookupKey,
   5324                             cancellationSignal);
   5325                     if (c != null) {
   5326                         return c;
   5327                     }
   5328                 }
   5329 
   5330                 setTablesAndProjectionMapForEntities(qb, uri, projection);
   5331                 selectionArgs = insertSelectionArg(selectionArgs,
   5332                         String.valueOf(lookupContactIdByLookupKey(db, lookupKey)));
   5333                 qb.appendWhere(" AND " + Contacts.Entity.CONTACT_ID + "=?");
   5334                 break;
   5335             }
   5336 
   5337             case STREAM_ITEMS: {
   5338                 setTablesAndProjectionMapForStreamItems(qb);
   5339                 break;
   5340             }
   5341 
   5342             case STREAM_ITEMS_ID: {
   5343                 setTablesAndProjectionMapForStreamItems(qb);
   5344                 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
   5345                 qb.appendWhere(StreamItems._ID + "=?");
   5346                 break;
   5347             }
   5348 
   5349             case STREAM_ITEMS_LIMIT: {
   5350                 return buildSingleRowResult(projection, new String[] {StreamItems.MAX_ITEMS},
   5351                         new Object[] {MAX_STREAM_ITEMS_PER_RAW_CONTACT});
   5352             }
   5353 
   5354             case STREAM_ITEMS_PHOTOS: {
   5355                 setTablesAndProjectionMapForStreamItemPhotos(qb);
   5356                 break;
   5357             }
   5358 
   5359             case STREAM_ITEMS_ID_PHOTOS: {
   5360                 setTablesAndProjectionMapForStreamItemPhotos(qb);
   5361                 String streamItemId = uri.getPathSegments().get(1);
   5362                 selectionArgs = insertSelectionArg(selectionArgs, streamItemId);
   5363                 qb.appendWhere(StreamItemPhotosColumns.CONCRETE_STREAM_ITEM_ID + "=?");
   5364                 break;
   5365             }
   5366 
   5367             case STREAM_ITEMS_ID_PHOTOS_ID: {
   5368                 setTablesAndProjectionMapForStreamItemPhotos(qb);
   5369                 String streamItemId = uri.getPathSegments().get(1);
   5370                 String streamItemPhotoId = uri.getPathSegments().get(3);
   5371                 selectionArgs = insertSelectionArg(selectionArgs, streamItemPhotoId);
   5372                 selectionArgs = insertSelectionArg(selectionArgs, streamItemId);
   5373                 qb.appendWhere(StreamItemPhotosColumns.CONCRETE_STREAM_ITEM_ID + "=? AND " +
   5374                         StreamItemPhotosColumns.CONCRETE_ID + "=?");
   5375                 break;
   5376             }
   5377 
   5378             case PHOTO_DIMENSIONS: {
   5379                 return buildSingleRowResult(projection,
   5380                         new String[] {DisplayPhoto.DISPLAY_MAX_DIM, DisplayPhoto.THUMBNAIL_MAX_DIM},
   5381                         new Object[] {getMaxDisplayPhotoDim(), getMaxThumbnailDim()});
   5382             }
   5383 
   5384             case PHONES:
   5385             case CALLABLES: {
   5386                 final String mimeTypeIsPhoneExpression =
   5387                         DataColumns.MIMETYPE_ID + "=" + mDbHelper.get().getMimeTypeIdForPhone();
   5388                 final String mimeTypeIsSipExpression =
   5389                         DataColumns.MIMETYPE_ID + "=" + mDbHelper.get().getMimeTypeIdForSip();
   5390                 setTablesAndProjectionMapForData(qb, uri, projection, false);
   5391                 if (match == CALLABLES) {
   5392                     qb.appendWhere(" AND ((" + mimeTypeIsPhoneExpression +
   5393                             ") OR (" + mimeTypeIsSipExpression + "))");
   5394                 } else {
   5395                     qb.appendWhere(" AND " + mimeTypeIsPhoneExpression);
   5396                 }
   5397 
   5398                 final boolean removeDuplicates = readBooleanQueryParameter(
   5399                         uri, ContactsContract.REMOVE_DUPLICATE_ENTRIES, false);
   5400                 if (removeDuplicates) {
   5401                     groupBy = RawContacts.CONTACT_ID + ", " + Data.DATA1;
   5402 
   5403                     // In this case, because we dedupe phone numbers, the address book indexer needs
   5404                     // to take it into account too.  (Otherwise headers will appear in wrong
   5405                     // positions.)
   5406                     // So use count(distinct pair(CONTACT_ID, PHONE NUMBER)) instead of count(*).
   5407                     // But because there's no such thing as pair() on sqlite, we use
   5408                     // CONTACT_ID || ',' || PHONE NUMBER instead.
   5409                     // This only slows down the query by 14% with 10,000 contacts.
   5410                     addressBookIndexerCountExpression = "DISTINCT "
   5411                             + RawContacts.CONTACT_ID + "||','||" + Data.DATA1;
   5412                 }
   5413                 break;
   5414             }
   5415 
   5416             case PHONES_ID:
   5417             case CALLABLES_ID: {
   5418                 final String mimeTypeIsPhoneExpression =
   5419                         DataColumns.MIMETYPE_ID + "=" + mDbHelper.get().getMimeTypeIdForPhone();
   5420                 final String mimeTypeIsSipExpression =
   5421                         DataColumns.MIMETYPE_ID + "=" + mDbHelper.get().getMimeTypeIdForSip();
   5422                 setTablesAndProjectionMapForData(qb, uri, projection, false);
   5423                 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
   5424                 if (match == CALLABLES_ID) {
   5425                     qb.appendWhere(" AND ((" + mimeTypeIsPhoneExpression +
   5426                             ") OR (" + mimeTypeIsSipExpression + "))");
   5427                 } else {
   5428                     qb.appendWhere(" AND " + mimeTypeIsPhoneExpression);
   5429                 }
   5430                 qb.appendWhere(" AND " + Data._ID + "=?");
   5431                 break;
   5432             }
   5433 
   5434             case PHONES_FILTER:
   5435             case CALLABLES_FILTER: {
   5436                 final String mimeTypeIsPhoneExpression =
   5437                         DataColumns.MIMETYPE_ID + "=" + mDbHelper.get().getMimeTypeIdForPhone();
   5438                 final String mimeTypeIsSipExpression =
   5439                         DataColumns.MIMETYPE_ID + "=" + mDbHelper.get().getMimeTypeIdForSip();
   5440 
   5441                 String typeParam = uri.getQueryParameter(DataUsageFeedback.USAGE_TYPE);
   5442                 final int typeInt = getDataUsageFeedbackType(typeParam,
   5443                         DataUsageStatColumns.USAGE_TYPE_INT_CALL);
   5444                 setTablesAndProjectionMapForData(qb, uri, projection, true, typeInt);
   5445                 if (match == CALLABLES_FILTER) {
   5446                     qb.appendWhere(" AND ((" + mimeTypeIsPhoneExpression +
   5447                             ") OR (" + mimeTypeIsSipExpression + "))");
   5448                 } else {
   5449                     qb.appendWhere(" AND " + mimeTypeIsPhoneExpression);
   5450                 }
   5451 
   5452                 if (uri.getPathSegments().size() > 2) {
   5453                     final String filterParam = uri.getLastPathSegment();
   5454                     final boolean searchDisplayName = uri.getBooleanQueryParameter(
   5455                             Phone.SEARCH_DISPLAY_NAME_KEY, true);
   5456                     final boolean searchPhoneNumber = uri.getBooleanQueryParameter(
   5457                             Phone.SEARCH_PHONE_NUMBER_KEY, true);
   5458 
   5459                     final StringBuilder sb = new StringBuilder();
   5460                     sb.append(" AND (");
   5461 
   5462                     boolean hasCondition = false;
   5463                     // TODO This only searches the name field.  Search other fields, such as
   5464                     // note, nickname, as well.  (Which should be disabled by default.)
   5465                     // Fix EMAILS_FILTER too.
   5466                     final String ftsMatchQuery =
   5467                             searchDisplayName
   5468                             ? SearchIndexManager.getFtsMatchQuery(filterParam,
   5469                                     FtsQueryBuilder.UNSCOPED_NORMALIZING)
   5470                             : null;
   5471                     if (!TextUtils.isEmpty(ftsMatchQuery)) {
   5472                         sb.append(Data.RAW_CONTACT_ID + " IN " +
   5473                                 "(SELECT " + RawContactsColumns.CONCRETE_ID +
   5474                                 " FROM " + Tables.SEARCH_INDEX +
   5475                                 " JOIN " + Tables.RAW_CONTACTS +
   5476                                 " ON (" + Tables.SEARCH_INDEX + "." + SearchIndexColumns.CONTACT_ID
   5477                                         + "=" + RawContactsColumns.CONCRETE_CONTACT_ID + ")" +
   5478                                 " WHERE " + SearchIndexColumns.NAME + " MATCH '");
   5479                         sb.append(ftsMatchQuery);
   5480                         sb.append("')");
   5481                         hasCondition = true;
   5482                     }
   5483 
   5484                     if (searchPhoneNumber) {
   5485                         final String number = PhoneNumberUtils.normalizeNumber(filterParam);
   5486                         if (!TextUtils.isEmpty(number)) {
   5487                             if (hasCondition) {
   5488                                 sb.append(" OR ");
   5489                             }
   5490                             sb.append(Data._ID +
   5491                                     " IN (SELECT DISTINCT " + PhoneLookupColumns.DATA_ID
   5492                                     + " FROM " + Tables.PHONE_LOOKUP
   5493                                     + " WHERE " + PhoneLookupColumns.NORMALIZED_NUMBER + " LIKE '");
   5494                             sb.append(number);
   5495                             sb.append("%')");
   5496                             hasCondition = true;
   5497                         }
   5498 
   5499                         if (!TextUtils.isEmpty(filterParam) && match == CALLABLES_FILTER) {
   5500                             // If the request is via Callable uri, Sip addresses matching the filter
   5501                             // parameter should be returned.
   5502                             if (hasCondition) {
   5503                                 sb.append(" OR ");
   5504                             }
   5505                             sb.append("(");
   5506                             sb.append(mimeTypeIsSipExpression);
   5507                             sb.append(" AND ((" + Data.DATA1 + " LIKE ");
   5508                             DatabaseUtils.appendEscapedSQLString(sb, filterParam + '%');
   5509                             sb.append(") OR (" + Data.DATA1 + " LIKE ");
   5510                             // Users may want SIP URIs starting from "sip:"
   5511                             DatabaseUtils.appendEscapedSQLString(sb, "sip:"+ filterParam + '%');
   5512                             sb.append(")))");
   5513                             hasCondition = true;
   5514                         }
   5515                     }
   5516 
   5517                     if (!hasCondition) {
   5518                         // If it is neither a phone number nor a name, the query should return
   5519                         // an empty cursor.  Let's ensure that.
   5520                         sb.append("0");
   5521                     }
   5522                     sb.append(")");
   5523                     qb.appendWhere(sb);
   5524                 }
   5525                 if (match == CALLABLES_FILTER) {
   5526                     // If the row is for a phone number that has a normalized form, we should use
   5527                     // the normalized one as PHONES_FILTER does, while we shouldn't do that
   5528                     // if the row is for a sip address.
   5529                     String isPhoneAndHasNormalized = "("
   5530                         + mimeTypeIsPhoneExpression + " AND "
   5531                         + Phone.NORMALIZED_NUMBER + " IS NOT NULL)";
   5532                     groupBy = "(CASE WHEN " + isPhoneAndHasNormalized
   5533                         + " THEN " + Phone.NORMALIZED_NUMBER
   5534                         + " ELSE " + Phone.NUMBER + " END), " + RawContacts.CONTACT_ID;
   5535                 } else {
   5536                     groupBy = "(CASE WHEN " + Phone.NORMALIZED_NUMBER
   5537                         + " IS NOT NULL THEN " + Phone.NORMALIZED_NUMBER
   5538                         + " ELSE " + Phone.NUMBER + " END), " + RawContacts.CONTACT_ID;
   5539                 }
   5540                 if (sortOrder == null) {
   5541                     final String accountPromotionSortOrder = getAccountPromotionSortOrder(uri);
   5542                     if (!TextUtils.isEmpty(accountPromotionSortOrder)) {
   5543                         sortOrder = accountPromotionSortOrder + ", " + PHONE_FILTER_SORT_ORDER;
   5544                     } else {
   5545                         sortOrder = PHONE_FILTER_SORT_ORDER;
   5546                     }
   5547                 }
   5548                 break;
   5549             }
   5550 
   5551             case EMAILS: {
   5552                 setTablesAndProjectionMapForData(qb, uri, projection, false);
   5553                 qb.appendWhere(" AND " + DataColumns.MIMETYPE_ID + " = "
   5554                         + mDbHelper.get().getMimeTypeIdForEmail());
   5555 
   5556                 final boolean removeDuplicates = readBooleanQueryParameter(
   5557                         uri, ContactsContract.REMOVE_DUPLICATE_ENTRIES, false);
   5558                 if (removeDuplicates) {
   5559                     groupBy = RawContacts.CONTACT_ID + ", " + Data.DATA1;
   5560 
   5561                     // See PHONES for more detail.
   5562                     addressBookIndexerCountExpression = "DISTINCT "
   5563                             + RawContacts.CONTACT_ID + "||','||" + Data.DATA1;
   5564                 }
   5565                 break;
   5566             }
   5567 
   5568             case EMAILS_ID: {
   5569                 setTablesAndProjectionMapForData(qb, uri, projection, false);
   5570                 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
   5571                 qb.appendWhere(" AND " + DataColumns.MIMETYPE_ID + " = "
   5572                         + mDbHelper.get().getMimeTypeIdForEmail()
   5573                         + " AND " + Data._ID + "=?");
   5574                 break;
   5575             }
   5576 
   5577             case EMAILS_LOOKUP: {
   5578                 setTablesAndProjectionMapForData(qb, uri, projection, false);
   5579                 qb.appendWhere(" AND " + DataColumns.MIMETYPE_ID + " = "
   5580                         + mDbHelper.get().getMimeTypeIdForEmail());
   5581                 if (uri.getPathSegments().size() > 2) {
   5582                     String email = uri.getLastPathSegment();
   5583                     String address = mDbHelper.get().extractAddressFromEmailAddress(email);
   5584                     selectionArgs = insertSelectionArg(selectionArgs, address);
   5585                     qb.appendWhere(" AND UPPER(" + Email.DATA + ")=UPPER(?)");
   5586                 }
   5587                 // unless told otherwise, we'll return visible before invisible contacts
   5588                 if (sortOrder == null) {
   5589                     sortOrder = "(" + RawContacts.CONTACT_ID + " IN " +
   5590                             Tables.DEFAULT_DIRECTORY + ") DESC";
   5591                 }
   5592                 break;
   5593             }
   5594 
   5595             case EMAILS_FILTER: {
   5596                 String typeParam = uri.getQueryParameter(DataUsageFeedback.USAGE_TYPE);
   5597                 final int typeInt = getDataUsageFeedbackType(typeParam,
   5598                         DataUsageStatColumns.USAGE_TYPE_INT_LONG_TEXT);
   5599                 setTablesAndProjectionMapForData(qb, uri, projection, true, typeInt);
   5600                 String filterParam = null;
   5601 
   5602                 if (uri.getPathSegments().size() > 3) {
   5603                     filterParam = uri.getLastPathSegment();
   5604                     if (TextUtils.isEmpty(filterParam)) {
   5605                         filterParam = null;
   5606                     }
   5607                 }
   5608 
   5609                 if (filterParam == null) {
   5610                     // If the filter is unspecified, return nothing
   5611                     qb.appendWhere(" AND 0");
   5612                 } else {
   5613                     StringBuilder sb = new StringBuilder();
   5614                     sb.append(" AND " + Data._ID + " IN (");
   5615                     sb.append(
   5616                             "SELECT " + Data._ID +
   5617                             " FROM " + Tables.DATA +
   5618                             " WHERE " + DataColumns.MIMETYPE_ID + "=");
   5619                     sb.append(mDbHelper.get().getMimeTypeIdForEmail());
   5620                     sb.append(" AND " + Data.DATA1 + " LIKE ");
   5621                     DatabaseUtils.appendEscapedSQLString(sb, filterParam + '%');
   5622                     if (!filterParam.contains("@")) {
   5623                         sb.append(
   5624                                 " UNION SELECT " + Data._ID +
   5625                                 " FROM " + Tables.DATA +
   5626                                 " WHERE +" + DataColumns.MIMETYPE_ID + "=");
   5627                         sb.append(mDbHelper.get().getMimeTypeIdForEmail());
   5628                         sb.append(" AND " + Data.RAW_CONTACT_ID + " IN " +
   5629                                 "(SELECT " + RawContactsColumns.CONCRETE_ID +
   5630                                 " FROM " + Tables.SEARCH_INDEX +
   5631                                 " JOIN " + Tables.RAW_CONTACTS +
   5632                                 " ON (" + Tables.SEARCH_INDEX + "." + SearchIndexColumns.CONTACT_ID
   5633                                         + "=" + RawContactsColumns.CONCRETE_CONTACT_ID + ")" +
   5634                                 " WHERE " + SearchIndexColumns.NAME + " MATCH '");
   5635                         final String ftsMatchQuery = SearchIndexManager.getFtsMatchQuery(
   5636                                 filterParam, FtsQueryBuilder.UNSCOPED_NORMALIZING);
   5637                         sb.append(ftsMatchQuery);
   5638                         sb.append("')");
   5639                     }
   5640                     sb.append(")");
   5641                     qb.appendWhere(sb);
   5642                 }
   5643                 groupBy = Email.DATA + "," + RawContacts.CONTACT_ID;
   5644                 if (sortOrder == null) {
   5645                     final String accountPromotionSortOrder = getAccountPromotionSortOrder(uri);
   5646                     if (!TextUtils.isEmpty(accountPromotionSortOrder)) {
   5647                         sortOrder = accountPromotionSortOrder + ", " + EMAIL_FILTER_SORT_ORDER;
   5648                     } else {
   5649                         sortOrder = EMAIL_FILTER_SORT_ORDER;
   5650                     }
   5651                 }
   5652                 break;
   5653             }
   5654 
   5655             case POSTALS: {
   5656                 setTablesAndProjectionMapForData(qb, uri, projection, false);
   5657                 qb.appendWhere(" AND " + DataColumns.MIMETYPE_ID + " = "
   5658                         + mDbHelper.get().getMimeTypeIdForStructuredPostal());
   5659 
   5660                 final boolean removeDuplicates = readBooleanQueryParameter(
   5661                         uri, ContactsContract.REMOVE_DUPLICATE_ENTRIES, false);
   5662                 if (removeDuplicates) {
   5663                     groupBy = RawContacts.CONTACT_ID + ", " + Data.DATA1;
   5664 
   5665                     // See PHONES for more detail.
   5666                     addressBookIndexerCountExpression = "DISTINCT "
   5667                             + RawContacts.CONTACT_ID + "||','||" + Data.DATA1;
   5668                 }
   5669                 break;
   5670             }
   5671 
   5672             case POSTALS_ID: {
   5673                 setTablesAndProjectionMapForData(qb, uri, projection, false);
   5674                 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
   5675                 qb.appendWhere(" AND " + DataColumns.MIMETYPE_ID + " = "
   5676                         + mDbHelper.get().getMimeTypeIdForStructuredPostal());
   5677                 qb.appendWhere(" AND " + Data._ID + "=?");
   5678                 break;
   5679             }
   5680 
   5681             case RAW_CONTACTS:
   5682             case PROFILE_RAW_CONTACTS: {
   5683                 setTablesAndProjectionMapForRawContacts(qb, uri);
   5684                 break;
   5685             }
   5686 
   5687             case RAW_CONTACTS_ID:
   5688             case PROFILE_RAW_CONTACTS_ID: {
   5689                 long rawContactId = ContentUris.parseId(uri);
   5690                 setTablesAndProjectionMapForRawContacts(qb, uri);
   5691                 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId));
   5692                 qb.appendWhere(" AND " + RawContacts._ID + "=?");
   5693                 break;
   5694             }
   5695 
   5696             case RAW_CONTACTS_ID_DATA:
   5697             case PROFILE_RAW_CONTACTS_ID_DATA: {
   5698                 int segment = match == RAW_CONTACTS_ID_DATA ? 1 : 2;
   5699                 long rawContactId = Long.parseLong(uri.getPathSegments().get(segment));
   5700                 setTablesAndProjectionMapForData(qb, uri, projection, false);
   5701                 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId));
   5702                 qb.appendWhere(" AND " + Data.RAW_CONTACT_ID + "=?");
   5703                 break;
   5704             }
   5705 
   5706             case RAW_CONTACTS_ID_STREAM_ITEMS: {
   5707                 long rawContactId = Long.parseLong(uri.getPathSegments().get(1));
   5708                 setTablesAndProjectionMapForStreamItems(qb);
   5709                 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId));
   5710                 qb.appendWhere(StreamItems.RAW_CONTACT_ID + "=?");
   5711                 break;
   5712             }
   5713 
   5714             case RAW_CONTACTS_ID_STREAM_ITEMS_ID: {
   5715                 long rawContactId = Long.parseLong(uri.getPathSegments().get(1));
   5716                 long streamItemId = Long.parseLong(uri.getPathSegments().get(3));
   5717                 setTablesAndProjectionMapForStreamItems(qb);
   5718                 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(streamItemId));
   5719                 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId));
   5720                 qb.appendWhere(StreamItems.RAW_CONTACT_ID + "=? AND " +
   5721                         StreamItems._ID + "=?");
   5722                 break;
   5723             }
   5724 
   5725             case PROFILE_RAW_CONTACTS_ID_ENTITIES: {
   5726                 long rawContactId = Long.parseLong(uri.getPathSegments().get(2));
   5727                 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId));
   5728                 setTablesAndProjectionMapForRawEntities(qb, uri);
   5729                 qb.appendWhere(" AND " + RawContacts._ID + "=?");
   5730                 break;
   5731             }
   5732 
   5733             case DATA:
   5734             case PROFILE_DATA: {
   5735                 setTablesAndProjectionMapForData(qb, uri, projection, false);
   5736                 break;
   5737             }
   5738 
   5739             case DATA_ID:
   5740             case PROFILE_DATA_ID: {
   5741                 setTablesAndProjectionMapForData(qb, uri, projection, false);
   5742                 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
   5743                 qb.appendWhere(" AND " + Data._ID + "=?");
   5744                 break;
   5745             }
   5746 
   5747             case PROFILE_PHOTO: {
   5748                 setTablesAndProjectionMapForData(qb, uri, projection, false);
   5749                 qb.appendWhere(" AND " + Data._ID + "=" + Contacts.PHOTO_ID);
   5750                 break;
   5751             }
   5752 
   5753             case PHONE_LOOKUP: {
   5754                 // Phone lookup cannot be combined with a selection
   5755                 selection = null;
   5756                 selectionArgs = null;
   5757                 if (uri.getBooleanQueryParameter(PhoneLookup.QUERY_PARAMETER_SIP_ADDRESS, false)) {
   5758                     if (TextUtils.isEmpty(sortOrder)) {
   5759                         // Default the sort order to something reasonable so we get consistent
   5760                         // results when callers don't request an ordering
   5761                         sortOrder = Contacts.DISPLAY_NAME + " ASC";
   5762                     }
   5763 
   5764                     String sipAddress = uri.getPathSegments().size() > 1
   5765                             ? Uri.decode(uri.getLastPathSegment()) : "";
   5766                     setTablesAndProjectionMapForData(qb, uri, null, false, true);
   5767                     StringBuilder sb = new StringBuilder();
   5768                     selectionArgs = mDbHelper.get().buildSipContactQuery(sb, sipAddress);
   5769                     selection = sb.toString();
   5770                 } else {
   5771                     // Use this flag to track whether sortOrder was originally empty
   5772                     boolean sortOrderIsEmpty = false;
   5773                     if (TextUtils.isEmpty(sortOrder)) {
   5774                         // Default the sort order to something reasonable so we get consistent
   5775                         // results when callers don't request an ordering
   5776                         sortOrder = " length(lookup.normalized_number) DESC";
   5777                         sortOrderIsEmpty = true;
   5778                     }
   5779 
   5780                     String number = uri.getPathSegments().size() > 1
   5781                             ? uri.getLastPathSegment() : "";
   5782                     String numberE164 = PhoneNumberUtils.formatNumberToE164(number,
   5783                             mDbHelper.get().getCurrentCountryIso());
   5784                     String normalizedNumber =
   5785                             PhoneNumberUtils.normalizeNumber(number);
   5786                     mDbHelper.get().buildPhoneLookupAndContactQuery(
   5787                             qb, normalizedNumber, numberE164);
   5788                     qb.setProjectionMap(sPhoneLookupProjectionMap);
   5789 
   5790                     // Peek at the results of the first query (which attempts to use fully
   5791                     // normalized and internationalized numbers for comparison).  If no results
   5792                     // were returned, fall back to using the SQLite function
   5793                     // phone_number_compare_loose.
   5794                     qb.setStrict(true);
   5795                     boolean foundResult = false;
   5796                     Cursor cursor = query(db, qb, projection, selection, selectionArgs,
   5797                             sortOrder, groupBy, null, limit, cancellationSignal);
   5798                     try {
   5799                         if (cursor.getCount() > 0) {
   5800                             foundResult = true;
   5801                             return cursor;
   5802                         } else {
   5803                             // Use fallback lookup method
   5804 
   5805                             qb = new SQLiteQueryBuilder();
   5806 
   5807                             // use the raw number instead of the normalized number because
   5808                             // phone_number_compare_loose in SQLite works only with non-normalized
   5809                             // numbers
   5810                             mDbHelper.get().buildFallbackPhoneLookupAndContactQuery(qb, number);
   5811 
   5812                             qb.setProjectionMap(sPhoneLookupProjectionMap);
   5813                         }
   5814                     } finally {
   5815                         if (!foundResult) {
   5816                             // We'll be returning a different cursor, so close this one.
   5817                             cursor.close();
   5818                         }
   5819                     }
   5820                 }
   5821                 break;
   5822             }
   5823 
   5824             case GROUPS: {
   5825                 qb.setTables(Views.GROUPS);
   5826                 qb.setProjectionMap(sGroupsProjectionMap);
   5827                 appendAccountIdFromParameter(qb, uri);
   5828                 break;
   5829             }
   5830 
   5831             case GROUPS_ID: {
   5832                 qb.setTables(Views.GROUPS);
   5833                 qb.setProjectionMap(sGroupsProjectionMap);
   5834                 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
   5835                 qb.appendWhere(Groups._ID + "=?");
   5836                 break;
   5837             }
   5838 
   5839             case GROUPS_SUMMARY: {
   5840                 String tables = Views.GROUPS + " AS " + Tables.GROUPS;
   5841                 if (ContactsDatabaseHelper.isInProjection(projection, Groups.SUMMARY_COUNT)) {
   5842                     tables = tables + Joins.GROUP_MEMBER_COUNT;
   5843                 }
   5844                 if (ContactsDatabaseHelper.isInProjection(projection,
   5845                         Groups.SUMMARY_GROUP_COUNT_PER_ACCOUNT)) {
   5846                     // TODO Add join for this column too (and update the projection map)
   5847                     // TODO Also remove Groups.PARAM_RETURN_GROUP_COUNT_PER_ACCOUNT when it works.
   5848                     Log.w(TAG, Groups.SUMMARY_GROUP_COUNT_PER_ACCOUNT + " is not supported yet");
   5849                 }
   5850                 qb.setTables(tables);
   5851                 qb.setProjectionMap(sGroupsSummaryProjectionMap);
   5852                 appendAccountIdFromParameter(qb, uri);
   5853                 groupBy = GroupsColumns.CONCRETE_ID;
   5854                 break;
   5855             }
   5856 
   5857             case AGGREGATION_EXCEPTIONS: {
   5858                 qb.setTables(Tables.AGGREGATION_EXCEPTIONS);
   5859                 qb.setProjectionMap(sAggregationExceptionsProjectionMap);
   5860                 break;
   5861             }
   5862 
   5863             case AGGREGATION_SUGGESTIONS: {
   5864                 long contactId = Long.parseLong(uri.getPathSegments().get(1));
   5865                 String filter = null;
   5866                 if (uri.getPathSegments().size() > 3) {
   5867                     filter = uri.getPathSegments().get(3);
   5868                 }
   5869                 final int maxSuggestions;
   5870                 if (limit != null) {
   5871                     maxSuggestions = Integer.parseInt(limit);
   5872                 } else {
   5873                     maxSuggestions = DEFAULT_MAX_SUGGESTIONS;
   5874                 }
   5875 
   5876                 ArrayList<AggregationSuggestionParameter> parameters = null;
   5877                 List<String> query = uri.getQueryParameters("query");
   5878                 if (query != null && !query.isEmpty()) {
   5879                     parameters = new ArrayList<AggregationSuggestionParameter>(query.size());
   5880                     for (String parameter : query) {
   5881                         int offset = parameter.indexOf(':');
   5882                         parameters.add(offset == -1
   5883                                 ? new AggregationSuggestionParameter(
   5884                                         AggregationSuggestions.PARAMETER_MATCH_NAME,
   5885                                         parameter)
   5886                                 : new AggregationSuggestionParameter(
   5887                                         parameter.substring(0, offset),
   5888                                         parameter.substring(offset + 1)));
   5889                     }
   5890                 }
   5891 
   5892                 setTablesAndProjectionMapForContacts(qb, uri, projection);
   5893 
   5894                 return mAggregator.get().queryAggregationSuggestions(qb, projection, contactId,
   5895                         maxSuggestions, filter, parameters);
   5896             }
   5897 
   5898             case SETTINGS: {
   5899                 qb.setTables(Tables.SETTINGS);
   5900                 qb.setProjectionMap(sSettingsProjectionMap);
   5901                 appendAccountFromParameter(qb, uri);
   5902 
   5903                 // When requesting specific columns, this query requires
   5904                 // late-binding of the GroupMembership MIME-type.
   5905                 final String groupMembershipMimetypeId = Long.toString(mDbHelper.get()
   5906                         .getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE));
   5907                 if (projection != null && projection.length != 0 &&
   5908                         mDbHelper.get().isInProjection(projection, Settings.UNGROUPED_COUNT)) {
   5909                     selectionArgs = insertSelectionArg(selectionArgs, groupMembershipMimetypeId);
   5910                 }
   5911                 if (projection != null && projection.length != 0 &&
   5912                         mDbHelper.get().isInProjection(
   5913                                 projection, Settings.UNGROUPED_WITH_PHONES)) {
   5914                     selectionArgs = insertSelectionArg(selectionArgs, groupMembershipMimetypeId);
   5915                 }
   5916 
   5917                 break;
   5918             }
   5919 
   5920             case STATUS_UPDATES:
   5921             case PROFILE_STATUS_UPDATES: {
   5922                 setTableAndProjectionMapForStatusUpdates(qb, projection);
   5923                 break;
   5924             }
   5925 
   5926             case STATUS_UPDATES_ID: {
   5927                 setTableAndProjectionMapForStatusUpdates(qb, projection);
   5928                 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
   5929                 qb.appendWhere(DataColumns.CONCRETE_ID + "=?");
   5930                 break;
   5931             }
   5932 
   5933             case SEARCH_SUGGESTIONS: {
   5934                 return mGlobalSearchSupport.handleSearchSuggestionsQuery(
   5935                         db, uri, projection, limit);
   5936             }
   5937 
   5938             case SEARCH_SHORTCUT: {
   5939                 String lookupKey = uri.getLastPathSegment();
   5940                 String filter = getQueryParameter(
   5941                         uri, SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA);
   5942                 return mGlobalSearchSupport.handleSearchShortcutRefresh(
   5943                         db, projection, lookupKey, filter);
   5944             }
   5945 
   5946             case RAW_CONTACT_ENTITIES:
   5947             case PROFILE_RAW_CONTACT_ENTITIES: {
   5948                 setTablesAndProjectionMapForRawEntities(qb, uri);
   5949                 break;
   5950             }
   5951 
   5952             case RAW_CONTACT_ID_ENTITY: {
   5953                 long rawContactId = Long.parseLong(uri.getPathSegments().get(1));
   5954                 setTablesAndProjectionMapForRawEntities(qb, uri);
   5955                 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId));
   5956                 qb.appendWhere(" AND " + RawContacts._ID + "=?");
   5957                 break;
   5958             }
   5959 
   5960             case PROVIDER_STATUS: {
   5961                 return buildSingleRowResult(projection,
   5962                         new String[] {ProviderStatus.STATUS, ProviderStatus.DATA1},
   5963                         new Object[] {mProviderStatus, mEstimatedStorageRequirement});
   5964             }
   5965 
   5966             case DIRECTORIES : {
   5967                 qb.setTables(Tables.DIRECTORIES);
   5968                 qb.setProjectionMap(sDirectoryProjectionMap);
   5969                 break;
   5970             }
   5971 
   5972             case DIRECTORIES_ID : {
   5973                 long id = ContentUris.parseId(uri);
   5974                 qb.setTables(Tables.DIRECTORIES);
   5975                 qb.setProjectionMap(sDirectoryProjectionMap);
   5976                 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(id));
   5977                 qb.appendWhere(Directory._ID + "=?");
   5978                 break;
   5979             }
   5980 
   5981             case COMPLETE_NAME: {
   5982                 return completeName(uri, projection);
   5983             }
   5984 
   5985             default:
   5986                 return mLegacyApiSupport.query(uri, projection, selection, selectionArgs,
   5987                         sortOrder, limit);
   5988         }
   5989 
   5990         qb.setStrict(true);
   5991 
   5992         Cursor cursor =
   5993                 query(db, qb, projection, selection, selectionArgs, sortOrder, groupBy,
   5994                 having, limit, cancellationSignal);
   5995 
   5996         if (readBooleanQueryParameter(uri, ContactCounts.ADDRESS_BOOK_INDEX_EXTRAS, false)) {
   5997             bundleFastScrollingIndexExtras(cursor, uri, db, qb, selection,
   5998                     selectionArgs, sortOrder, addressBookIndexerCountExpression,
   5999                     cancellationSignal);
   6000         }
   6001         if (snippetDeferred) {
   6002             cursor = addDeferredSnippetingExtra(cursor);
   6003         }
   6004 
   6005         return cursor;
   6006     }
   6007 
   6008 
   6009     private Cursor query(final SQLiteDatabase db, SQLiteQueryBuilder qb, String[] projection,
   6010             String selection, String[] selectionArgs, String sortOrder, String groupBy,
   6011             String having, String limit, CancellationSignal cancellationSignal) {
   6012         if (projection != null && projection.length == 1
   6013                 && BaseColumns._COUNT.equals(projection[0])) {
   6014             qb.setProjectionMap(sCountProjectionMap);
   6015         }
   6016         final Cursor c = qb.query(db, projection, selection, selectionArgs, groupBy, having,
   6017                 sortOrder, limit, cancellationSignal);
   6018         if (c != null) {
   6019             c.setNotificationUri(getContext().getContentResolver(), ContactsContract.AUTHORITY_URI);
   6020         }
   6021         return c;
   6022     }
   6023 
   6024 
   6025     /**
   6026      * Runs the query with the supplied contact ID and lookup ID.  If the query succeeds,
   6027      * it returns the resulting cursor, otherwise it returns null and the calling
   6028      * method needs to resolve the lookup key and rerun the query.
   6029      * @param cancellationSignal
   6030      */
   6031     private Cursor queryWithContactIdAndLookupKey(SQLiteQueryBuilder lookupQb,
   6032             SQLiteDatabase db, Uri uri,
   6033             String[] projection, String selection, String[] selectionArgs,
   6034             String sortOrder, String groupBy, String limit,
   6035             String contactIdColumn, long contactId, String lookupKeyColumn, String lookupKey,
   6036             CancellationSignal cancellationSignal) {
   6037         String[] args;
   6038         if (selectionArgs == null) {
   6039             args = new String[2];
   6040         } else {
   6041             args = new String[selectionArgs.length + 2];
   6042             System.arraycopy(selectionArgs, 0, args, 2, selectionArgs.length);
   6043         }
   6044         args[0] = String.valueOf(contactId);
   6045         args[1] = Uri.encode(lookupKey);
   6046         lookupQb.appendWhere(contactIdColumn + "=? AND " + lookupKeyColumn + "=?");
   6047         Cursor c = query(db, lookupQb, projection, selection, args, sortOrder,
   6048                 groupBy, null, limit, cancellationSignal);
   6049         if (c.getCount() != 0) {
   6050             return c;
   6051         }
   6052 
   6053         c.close();
   6054         return null;
   6055     }
   6056 
   6057     private void invalidateFastScrollingIndexCache() {
   6058         // FastScrollingIndexCache is thread-safe, no need to synchronize here.
   6059         mFastScrollingIndexCache.invalidate();
   6060     }
   6061 
   6062     /**
   6063      * Add the "fast scrolling index" bundle, generated by {@link #getFastScrollingIndexExtras},
   6064      * to a cursor as extras.  It first checks {@link FastScrollingIndexCache} to see if we
   6065      * already have a cached result.
   6066      */
   6067     private void bundleFastScrollingIndexExtras(Cursor cursor, Uri queryUri,
   6068             final SQLiteDatabase db, SQLiteQueryBuilder qb, String selection,
   6069             String[] selectionArgs, String sortOrder, String countExpression,
   6070             CancellationSignal cancellationSignal) {
   6071         if (!(cursor instanceof AbstractCursor)) {
   6072             Log.w(TAG, "Unable to bundle extras.  Cursor is not AbstractCursor.");
   6073             return;
   6074         }
   6075         Bundle b;
   6076         // Note even though FastScrollingIndexCache is thread-safe, we really need to put the
   6077         // put-get pair in a single synchronized block, so that even if multiple-threads request the
   6078         // same index at the same time (which actually happens on the phone app) we only execute
   6079         // the query once.
   6080         //
   6081         // This doesn't cause deadlock, because only reader threads get here but not writer
   6082         // threads.  (Writer threads may call invalidateFastScrollingIndexCache(), but it doesn't
   6083         // synchronize on mFastScrollingIndexCache)
   6084         //
   6085         // All reader and writer threads share the single lock object internally in
   6086         // FastScrollingIndexCache, but the lock scope is limited within each put(), get() and
   6087         // invalidate() call, so it won't deadlock.
   6088 
   6089         // Synchronizing on a non-static field is generally not a good idea, but nobody should
   6090         // modify mFastScrollingIndexCache once initialized, and it shouldn't be null at this point.
   6091         synchronized (mFastScrollingIndexCache) {
   6092             // First, try the cache.
   6093             mFastScrollingIndexCacheRequestCount++;
   6094             b = mFastScrollingIndexCache.get(queryUri, selection, selectionArgs, sortOrder,
   6095                     countExpression);
   6096 
   6097             if (b == null) {
   6098                 mFastScrollingIndexCacheMissCount++;
   6099                 // Not in the cache.  Generate and put.
   6100                 final long start = System.currentTimeMillis();
   6101 
   6102                 b = getFastScrollingIndexExtras(queryUri, db, qb, selection, selectionArgs,
   6103                         sortOrder, countExpression, cancellationSignal, getLocale());
   6104 
   6105                 final long end = System.currentTimeMillis();
   6106                 final int time = (int) (end - start);
   6107                 mTotalTimeFastScrollingIndexGenerate += time;
   6108                 if (VERBOSE_LOGGING) {
   6109                     Log.v(TAG, "getLetterCountExtraBundle took " + time + "ms");
   6110                 }
   6111                 mFastScrollingIndexCache.put(queryUri, selection, selectionArgs, sortOrder,
   6112                         countExpression, b);
   6113             }
   6114         }
   6115         ((AbstractCursor) cursor).setExtras(b);
   6116     }
   6117 
   6118     private static final class AddressBookIndexQuery {
   6119         public static final String LETTER = "letter";
   6120         public static final String TITLE = "title";
   6121         public static final String COUNT = "count";
   6122 
   6123         public static final String[] COLUMNS = new String[] {
   6124                 LETTER, TITLE, COUNT
   6125         };
   6126 
   6127         public static final int COLUMN_LETTER = 0;
   6128         public static final int COLUMN_TITLE = 1;
   6129         public static final int COLUMN_COUNT = 2;
   6130 
   6131         // The first letter of the sort key column is what is used for the index headings.
   6132         public static final String SECTION_HEADING = "SUBSTR(%1$s,1,1)";
   6133 
   6134         public static final String ORDER_BY = LETTER + " COLLATE " + PHONEBOOK_COLLATOR_NAME;
   6135     }
   6136 
   6137     /**
   6138      * Computes counts by the address book index titles and returns it as {@link Bundle} which
   6139      * will be appended to a {@link Cursor} as extras.
   6140      */
   6141     private static Bundle getFastScrollingIndexExtras(final Uri queryUri, final SQLiteDatabase db,
   6142             final SQLiteQueryBuilder qb, final String selection, final String[] selectionArgs,
   6143             final String sortOrder, String countExpression,
   6144             final CancellationSignal cancellationSignal, final Locale currentLocale) {
   6145         String sortKey;
   6146 
   6147         // The sort order suffix could be something like "DESC".
   6148         // We want to preserve it in the query even though we will change
   6149         // the sort column itself.
   6150         String sortOrderSuffix = "";
   6151         if (sortOrder != null) {
   6152             int spaceIndex = sortOrder.indexOf(' ');
   6153             if (spaceIndex != -1) {
   6154                 sortKey = sortOrder.substring(0, spaceIndex);
   6155                 sortOrderSuffix = sortOrder.substring(spaceIndex);
   6156             } else {
   6157                 sortKey = sortOrder;
   6158             }
   6159         } else {
   6160             sortKey = Contacts.SORT_KEY_PRIMARY;
   6161         }
   6162 
   6163         HashMap<String, String> projectionMap = Maps.newHashMap();
   6164         String sectionHeading = String.format(Locale.US, AddressBookIndexQuery.SECTION_HEADING,
   6165                 sortKey);
   6166         projectionMap.put(AddressBookIndexQuery.LETTER,
   6167                 sectionHeading + " AS " + AddressBookIndexQuery.LETTER);
   6168 
   6169         // If "what to count" is not specified, we just count all records.
   6170         if (TextUtils.isEmpty(countExpression)) {
   6171             countExpression = "*";
   6172         }
   6173 
   6174         /**
   6175          * Use the GET_PHONEBOOK_INDEX function, which is an android extension for SQLite3,
   6176          * to map the first letter of the sort key to a character that is traditionally
   6177          * used in phonebooks to represent that letter.  For example, in Korean it will
   6178          * be the first consonant in the letter; for Japanese it will be Hiragana rather
   6179          * than Katakana.
   6180          */
   6181         projectionMap.put(AddressBookIndexQuery.TITLE,
   6182                 "GET_PHONEBOOK_INDEX(" + sectionHeading + ",'" + currentLocale.toString() + "')"
   6183                         + " AS " + AddressBookIndexQuery.TITLE);
   6184         projectionMap.put(AddressBookIndexQuery.COUNT,
   6185                 "COUNT(" + countExpression + ") AS " + AddressBookIndexQuery.COUNT);
   6186         qb.setProjectionMap(projectionMap);
   6187 
   6188         Cursor indexCursor = qb.query(db, AddressBookIndexQuery.COLUMNS, selection, selectionArgs,
   6189                 AddressBookIndexQuery.ORDER_BY, null /* having */,
   6190                 AddressBookIndexQuery.ORDER_BY + sortOrderSuffix,
   6191                 null, cancellationSignal);
   6192 
   6193         try {
   6194             int groupCount = indexCursor.getCount();
   6195             String titles[] = new String[groupCount];
   6196             int counts[] = new int[groupCount];
   6197             int indexCount = 0;
   6198             String currentTitle = null;
   6199 
   6200             // Since GET_PHONEBOOK_INDEX is a many-to-1 function, we may end up
   6201             // with multiple entries for the same title.  The following code
   6202             // collapses those duplicates.
   6203             for (int i = 0; i < groupCount; i++) {
   6204                 indexCursor.moveToNext();
   6205                 String title = indexCursor.getString(AddressBookIndexQuery.COLUMN_TITLE);
   6206                 if (title == null) {
   6207                     title = "";
   6208                 }
   6209                 int count = indexCursor.getInt(AddressBookIndexQuery.COLUMN_COUNT);
   6210                 if (indexCount == 0 || !TextUtils.equals(title, currentTitle)) {
   6211                     titles[indexCount] = currentTitle = title;
   6212                     counts[indexCount] = count;
   6213                     indexCount++;
   6214                 } else {
   6215                     counts[indexCount - 1] += count;
   6216                 }
   6217             }
   6218 
   6219             if (indexCount < groupCount) {
   6220                 String[] newTitles = new String[indexCount];
   6221                 System.arraycopy(titles, 0, newTitles, 0, indexCount);
   6222                 titles = newTitles;
   6223 
   6224                 int[] newCounts = new int[indexCount];
   6225                 System.arraycopy(counts, 0, newCounts, 0, indexCount);
   6226                 counts = newCounts;
   6227             }
   6228             return FastScrollingIndexCache.buildExtraBundle(titles, counts);
   6229         } finally {
   6230             indexCursor.close();
   6231         }
   6232     }
   6233 
   6234     /**
   6235      * Returns the contact Id for the contact identified by the lookupKey.
   6236      * Robust against changes in the lookup key: if the key has changed, will
   6237      * look up the contact by the raw contact IDs or name encoded in the lookup
   6238      * key.
   6239      */
   6240     public long lookupContactIdByLookupKey(SQLiteDatabase db, String lookupKey) {
   6241         ContactLookupKey key = new ContactLookupKey();
   6242         ArrayList<LookupKeySegment> segments = key.parse(lookupKey);
   6243 
   6244         long contactId = -1;
   6245         if (lookupKeyContainsType(segments, ContactLookupKey.LOOKUP_TYPE_PROFILE)) {
   6246             // We should already be in a profile database context, so just look up a single contact.
   6247            contactId = lookupSingleContactId(db);
   6248         }
   6249 
   6250         if (lookupKeyContainsType(segments, ContactLookupKey.LOOKUP_TYPE_SOURCE_ID)) {
   6251             contactId = lookupContactIdBySourceIds(db, segments);
   6252             if (contactId != -1) {
   6253                 return contactId;
   6254             }
   6255         }
   6256 
   6257         boolean hasRawContactIds =
   6258                 lookupKeyContainsType(segments, ContactLookupKey.LOOKUP_TYPE_RAW_CONTACT_ID);
   6259         if (hasRawContactIds) {
   6260             contactId = lookupContactIdByRawContactIds(db, segments);
   6261             if (contactId != -1) {
   6262                 return contactId;
   6263             }
   6264         }
   6265 
   6266         if (hasRawContactIds
   6267                 || lookupKeyContainsType(segments, ContactLookupKey.LOOKUP_TYPE_DISPLAY_NAME)) {
   6268             contactId = lookupContactIdByDisplayNames(db, segments);
   6269         }
   6270 
   6271         return contactId;
   6272     }
   6273 
   6274     private long lookupSingleContactId(SQLiteDatabase db) {
   6275         Cursor c = db.query(Tables.CONTACTS, new String[] {Contacts._ID},
   6276                 null, null, null, null, null, "1");
   6277         try {
   6278             if (c.moveToFirst()) {
   6279                 return c.getLong(0);
   6280             } else {
   6281                 return -1;
   6282             }
   6283         } finally {
   6284             c.close();
   6285         }
   6286     }
   6287 
   6288     private interface LookupBySourceIdQuery {
   6289         String TABLE = Views.RAW_CONTACTS;
   6290 
   6291         String COLUMNS[] = {
   6292                 RawContacts.CONTACT_ID,
   6293                 RawContacts.ACCOUNT_TYPE_AND_DATA_SET,
   6294                 RawContacts.ACCOUNT_NAME,
   6295                 RawContacts.SOURCE_ID
   6296         };
   6297 
   6298         int CONTACT_ID = 0;
   6299         int ACCOUNT_TYPE_AND_DATA_SET = 1;
   6300         int ACCOUNT_NAME = 2;
   6301         int SOURCE_ID = 3;
   6302     }
   6303 
   6304     private long lookupContactIdBySourceIds(SQLiteDatabase db,
   6305                 ArrayList<LookupKeySegment> segments) {
   6306         StringBuilder sb = new StringBuilder();
   6307         sb.append(RawContacts.SOURCE_ID + " IN (");
   6308         for (int i = 0; i < segments.size(); i++) {
   6309             LookupKeySegment segment = segments.get(i);
   6310             if (segment.lookupType == ContactLookupKey.LOOKUP_TYPE_SOURCE_ID) {
   6311                 DatabaseUtils.appendEscapedSQLString(sb, segment.key);
   6312                 sb.append(",");
   6313             }
   6314         }
   6315         sb.setLength(sb.length() - 1);      // Last comma
   6316         sb.append(") AND " + RawContacts.CONTACT_ID + " NOT NULL");
   6317 
   6318         Cursor c = db.query(LookupBySourceIdQuery.TABLE, LookupBySourceIdQuery.COLUMNS,
   6319                  sb.toString(), null, null, null, null);
   6320         try {
   6321             while (c.moveToNext()) {
   6322                 String accountTypeAndDataSet =
   6323                         c.getString(LookupBySourceIdQuery.ACCOUNT_TYPE_AND_DATA_SET);
   6324                 String accountName = c.getString(LookupBySourceIdQuery.ACCOUNT_NAME);
   6325                 int accountHashCode =
   6326                         ContactLookupKey.getAccountHashCode(accountTypeAndDataSet, accountName);
   6327                 String sourceId = c.getString(LookupBySourceIdQuery.SOURCE_ID);
   6328                 for (int i = 0; i < segments.size(); i++) {
   6329                     LookupKeySegment segment = segments.get(i);
   6330                     if (segment.lookupType == ContactLookupKey.LOOKUP_TYPE_SOURCE_ID
   6331                             && accountHashCode == segment.accountHashCode
   6332                             && segment.key.equals(sourceId)) {
   6333                         segment.contactId = c.getLong(LookupBySourceIdQuery.CONTACT_ID);
   6334                         break;
   6335                     }
   6336                 }
   6337             }
   6338         } finally {
   6339             c.close();
   6340         }
   6341 
   6342         return getMostReferencedContactId(segments);
   6343     }
   6344 
   6345     private interface LookupByRawContactIdQuery {
   6346         String TABLE = Views.RAW_CONTACTS;
   6347 
   6348         String COLUMNS[] = {
   6349                 RawContacts.CONTACT_ID,
   6350                 RawContacts.ACCOUNT_TYPE_AND_DATA_SET,
   6351                 RawContacts.ACCOUNT_NAME,
   6352                 RawContacts._ID,
   6353         };
   6354 
   6355         int CONTACT_ID = 0;
   6356         int ACCOUNT_TYPE_AND_DATA_SET = 1;
   6357         int ACCOUNT_NAME = 2;
   6358         int ID = 3;
   6359     }
   6360 
   6361     private long lookupContactIdByRawContactIds(SQLiteDatabase db,
   6362             ArrayList<LookupKeySegment> segments) {
   6363         StringBuilder sb = new StringBuilder();
   6364         sb.append(RawContacts._ID + " IN (");
   6365         for (int i = 0; i < segments.size(); i++) {
   6366             LookupKeySegment segment = segments.get(i);
   6367             if (segment.lookupType == ContactLookupKey.LOOKUP_TYPE_RAW_CONTACT_ID) {
   6368                 sb.append(segment.rawContactId);
   6369                 sb.append(",");
   6370             }
   6371         }
   6372         sb.setLength(sb.length() - 1);      // Last comma
   6373         sb.append(") AND " + RawContacts.CONTACT_ID + " NOT NULL");
   6374 
   6375         Cursor c = db.query(LookupByRawContactIdQuery.TABLE, LookupByRawContactIdQuery.COLUMNS,
   6376                  sb.toString(), null, null, null, null);
   6377         try {
   6378             while (c.moveToNext()) {
   6379                 String accountTypeAndDataSet = c.getString(
   6380                         LookupByRawContactIdQuery.ACCOUNT_TYPE_AND_DATA_SET);
   6381                 String accountName = c.getString(LookupByRawContactIdQuery.ACCOUNT_NAME);
   6382                 int accountHashCode =
   6383                         ContactLookupKey.getAccountHashCode(accountTypeAndDataSet, accountName);
   6384                 String rawContactId = c.getString(LookupByRawContactIdQuery.ID);
   6385                 for (int i = 0; i < segments.size(); i++) {
   6386                     LookupKeySegment segment = segments.get(i);
   6387                     if (segment.lookupType == ContactLookupKey.LOOKUP_TYPE_RAW_CONTACT_ID
   6388                             && accountHashCode == segment.accountHashCode
   6389                             && segment.rawContactId.equals(rawContactId)) {
   6390                         segment.contactId = c.getLong(LookupByRawContactIdQuery.CONTACT_ID);
   6391                         break;
   6392                     }
   6393                 }
   6394             }
   6395         } finally {
   6396             c.close();
   6397         }
   6398 
   6399         return getMostReferencedContactId(segments);
   6400     }
   6401 
   6402     private interface LookupByDisplayNameQuery {
   6403         String TABLE = Tables.NAME_LOOKUP_JOIN_RAW_CONTACTS;
   6404 
   6405         String COLUMNS[] = {
   6406                 RawContacts.CONTACT_ID,
   6407                 RawContacts.ACCOUNT_TYPE_AND_DATA_SET,
   6408                 RawContacts.ACCOUNT_NAME,
   6409                 NameLookupColumns.NORMALIZED_NAME
   6410         };
   6411 
   6412         int CONTACT_ID = 0;
   6413         int ACCOUNT_TYPE_AND_DATA_SET = 1;
   6414         int ACCOUNT_NAME = 2;
   6415         int NORMALIZED_NAME = 3;
   6416     }
   6417 
   6418     private long lookupContactIdByDisplayNames(SQLiteDatabase db,
   6419                 ArrayList<LookupKeySegment> segments) {
   6420         StringBuilder sb = new StringBuilder();
   6421         sb.append(NameLookupColumns.NORMALIZED_NAME + " IN (");
   6422         for (int i = 0; i < segments.size(); i++) {
   6423             LookupKeySegment segment = segments.get(i);
   6424             if (segment.lookupType == ContactLookupKey.LOOKUP_TYPE_DISPLAY_NAME
   6425                     || segment.lookupType == ContactLookupKey.LOOKUP_TYPE_RAW_CONTACT_ID) {
   6426                 DatabaseUtils.appendEscapedSQLString(sb, segment.key);
   6427                 sb.append(",");
   6428             }
   6429         }
   6430         sb.setLength(sb.length() - 1);      // Last comma
   6431         sb.append(") AND " + NameLookupColumns.NAME_TYPE + "=" + NameLookupType.NAME_COLLATION_KEY
   6432                 + " AND " + RawContacts.CONTACT_ID + " NOT NULL");
   6433 
   6434         Cursor c = db.query(LookupByDisplayNameQuery.TABLE, LookupByDisplayNameQuery.COLUMNS,
   6435                  sb.toString(), null, null, null, null);
   6436         try {
   6437             while (c.moveToNext()) {
   6438                 String accountTypeAndDataSet =
   6439                         c.getString(LookupByDisplayNameQuery.ACCOUNT_TYPE_AND_DATA_SET);
   6440                 String accountName = c.getString(LookupByDisplayNameQuery.ACCOUNT_NAME);
   6441                 int accountHashCode =
   6442                         ContactLookupKey.getAccountHashCode(accountTypeAndDataSet, accountName);
   6443                 String name = c.getString(LookupByDisplayNameQuery.NORMALIZED_NAME);
   6444                 for (int i = 0; i < segments.size(); i++) {
   6445                     LookupKeySegment segment = segments.get(i);
   6446                     if ((segment.lookupType == ContactLookupKey.LOOKUP_TYPE_DISPLAY_NAME
   6447                             || segment.lookupType == ContactLookupKey.LOOKUP_TYPE_RAW_CONTACT_ID)
   6448                             && accountHashCode == segment.accountHashCode
   6449                             && segment.key.equals(name)) {
   6450                         segment.contactId = c.getLong(LookupByDisplayNameQuery.CONTACT_ID);
   6451                         break;
   6452                     }
   6453                 }
   6454             }
   6455         } finally {
   6456             c.close();
   6457         }
   6458 
   6459         return getMostReferencedContactId(segments);
   6460     }
   6461 
   6462     private boolean lookupKeyContainsType(ArrayList<LookupKeySegment> segments, int lookupType) {
   6463         for (int i = 0; i < segments.size(); i++) {
   6464             LookupKeySegment segment = segments.get(i);
   6465             if (segment.lookupType == lookupType) {
   6466                 return true;
   6467             }
   6468         }
   6469 
   6470         return false;
   6471     }
   6472 
   6473     /**
   6474      * Returns the contact ID that is mentioned the highest number of times.
   6475      */
   6476     private long getMostReferencedContactId(ArrayList<LookupKeySegment> segments) {
   6477         Collections.sort(segments);
   6478 
   6479         long bestContactId = -1;
   6480         int bestRefCount = 0;
   6481 
   6482         long contactId = -1;
   6483         int count = 0;
   6484 
   6485         int segmentCount = segments.size();
   6486         for (int i = 0; i < segmentCount; i++) {
   6487             LookupKeySegment segment = segments.get(i);
   6488             if (segment.contactId != -1) {
   6489                 if (segment.contactId == contactId) {
   6490                     count++;
   6491                 } else {
   6492                     if (count > bestRefCount) {
   6493                         bestContactId = contactId;
   6494                         bestRefCount = count;
   6495                     }
   6496                     contactId = segment.contactId;
   6497                     count = 1;
   6498                 }
   6499             }
   6500         }
   6501         if (count > bestRefCount) {
   6502             return contactId;
   6503         } else {
   6504             return bestContactId;
   6505         }
   6506     }
   6507 
   6508     private void setTablesAndProjectionMapForContacts(SQLiteQueryBuilder qb, Uri uri,
   6509             String[] projection) {
   6510         setTablesAndProjectionMapForContacts(qb, uri, projection, false);
   6511     }
   6512 
   6513     /**
   6514      * @param includeDataUsageStat true when the table should include DataUsageStat table.
   6515      * Note that this uses INNER JOIN instead of LEFT OUTER JOIN, so some of data in Contacts
   6516      * may be dropped.
   6517      */
   6518     private void setTablesAndProjectionMapForContacts(SQLiteQueryBuilder qb, Uri uri,
   6519             String[] projection, boolean includeDataUsageStat) {
   6520         StringBuilder sb = new StringBuilder();
   6521         if (includeDataUsageStat) {
   6522             sb.append(Views.DATA_USAGE_STAT + " AS " + Tables.DATA_USAGE_STAT);
   6523             sb.append(" INNER JOIN ");
   6524         }
   6525 
   6526         sb.append(Views.CONTACTS);
   6527 
   6528         // Just for frequently contacted contacts in Strequent Uri handling.
   6529         if (includeDataUsageStat) {
   6530             sb.append(" ON (" +
   6531                     DbQueryUtils.concatenateClauses(
   6532                             DataUsageStatColumns.CONCRETE_TIMES_USED + " > 0",
   6533                             RawContacts.CONTACT_ID + "=" + Views.CONTACTS + "." + Contacts._ID) +
   6534                     ")");
   6535         }
   6536 
   6537         appendContactPresenceJoin(sb, projection, Contacts._ID);
   6538         appendContactStatusUpdateJoin(sb, projection, ContactsColumns.LAST_STATUS_UPDATE_ID);
   6539         qb.setTables(sb.toString());
   6540         qb.setProjectionMap(sContactsProjectionMap);
   6541     }
   6542 
   6543     /**
   6544      * Finds name lookup records matching the supplied filter, picks one arbitrary match per
   6545      * contact and joins that with other contacts tables.
   6546      */
   6547     private void setTablesAndProjectionMapForContactsWithSnippet(SQLiteQueryBuilder qb, Uri uri,
   6548             String[] projection, String filter, long directoryId, boolean deferSnippeting) {
   6549 
   6550         StringBuilder sb = new StringBuilder();
   6551         sb.append(Views.CONTACTS);
   6552 
   6553         if (filter != null) {
   6554             filter = filter.trim();
   6555         }
   6556 
   6557         if (TextUtils.isEmpty(filter) || (directoryId != -1 && directoryId != Directory.DEFAULT)) {
   6558             sb.append(" JOIN (SELECT NULL AS " + SearchSnippetColumns.SNIPPET + " WHERE 0)");
   6559         } else {
   6560             appendSearchIndexJoin(sb, uri, projection, filter, deferSnippeting);
   6561         }
   6562         appendContactPresenceJoin(sb, projection, Contacts._ID);
   6563         appendContactStatusUpdateJoin(sb, projection, ContactsColumns.LAST_STATUS_UPDATE_ID);
   6564         qb.setTables(sb.toString());
   6565         qb.setProjectionMap(sContactsProjectionWithSnippetMap);
   6566     }
   6567 
   6568     private void appendSearchIndexJoin(
   6569             StringBuilder sb, Uri uri, String[] projection, String filter,
   6570             boolean  deferSnippeting) {
   6571 
   6572         if (snippetNeeded(projection)) {
   6573             String[] args = null;
   6574             String snippetArgs =
   6575                     getQueryParameter(uri, SearchSnippetColumns.SNIPPET_ARGS_PARAM_KEY);
   6576             if (snippetArgs != null) {
   6577                 args = snippetArgs.split(",");
   6578             }
   6579 
   6580             String startMatch = args != null && args.length > 0 ? args[0]
   6581                     : DEFAULT_SNIPPET_ARG_START_MATCH;
   6582             String endMatch = args != null && args.length > 1 ? args[1]
   6583                     : DEFAULT_SNIPPET_ARG_END_MATCH;
   6584             String ellipsis = args != null && args.length > 2 ? args[2]
   6585                     : DEFAULT_SNIPPET_ARG_ELLIPSIS;
   6586             int maxTokens = args != null && args.length > 3 ? Integer.parseInt(args[3])
   6587                     : DEFAULT_SNIPPET_ARG_MAX_TOKENS;
   6588 
   6589             appendSearchIndexJoin(
   6590                     sb, filter, true, startMatch, endMatch, ellipsis, maxTokens,
   6591                     deferSnippeting);
   6592         } else {
   6593             appendSearchIndexJoin(sb, filter, false, null, null, null, 0, false);
   6594         }
   6595     }
   6596 
   6597     public void appendSearchIndexJoin(StringBuilder sb, String filter,
   6598             boolean snippetNeeded, String startMatch, String endMatch, String ellipsis,
   6599             int maxTokens, boolean deferSnippeting) {
   6600         boolean isEmailAddress = false;
   6601         String emailAddress = null;
   6602         boolean isPhoneNumber = false;
   6603         String phoneNumber = null;
   6604         String numberE164 = null;
   6605 
   6606 
   6607         if (filter.indexOf('@') != -1) {
   6608             emailAddress = mDbHelper.get().extractAddressFromEmailAddress(filter);
   6609             isEmailAddress = !TextUtils.isEmpty(emailAddress);
   6610         } else {
   6611             isPhoneNumber = isPhoneNumber(filter);
   6612             if (isPhoneNumber) {
   6613                 phoneNumber = PhoneNumberUtils.normalizeNumber(filter);
   6614                 numberE164 = PhoneNumberUtils.formatNumberToE164(phoneNumber,
   6615                         mDbHelper.get().getCountryIso());
   6616             }
   6617         }
   6618 
   6619         final String SNIPPET_CONTACT_ID = "snippet_contact_id";
   6620         sb.append(" JOIN (SELECT " + SearchIndexColumns.CONTACT_ID + " AS " + SNIPPET_CONTACT_ID);
   6621         if (snippetNeeded) {
   6622             sb.append(", ");
   6623             if (isEmailAddress) {
   6624                 sb.append("ifnull(");
   6625                 if (!deferSnippeting) {
   6626                     // Add the snippet marker only when we're really creating snippet.
   6627                     DatabaseUtils.appendEscapedSQLString(sb, startMatch);
   6628                     sb.append("||");
   6629                 }
   6630                 sb.append("(SELECT MIN(" + Email.ADDRESS + ")");
   6631                 sb.append(" FROM " + Tables.DATA_JOIN_RAW_CONTACTS);
   6632                 sb.append(" WHERE  " + Tables.SEARCH_INDEX + "." + SearchIndexColumns.CONTACT_ID);
   6633                 sb.append("=" + RawContacts.CONTACT_ID + " AND " + Email.ADDRESS + " LIKE ");
   6634                 DatabaseUtils.appendEscapedSQLString(sb, filter + "%");
   6635                 sb.append(")");
   6636                 if (!deferSnippeting) {
   6637                     sb.append("||");
   6638                     DatabaseUtils.appendEscapedSQLString(sb, endMatch);
   6639                 }
   6640                 sb.append(",");
   6641 
   6642                 if (deferSnippeting) {
   6643                     sb.append(SearchIndexColumns.CONTENT);
   6644                 } else {
   6645                     appendSnippetFunction(sb, startMatch, endMatch, ellipsis, maxTokens);
   6646                 }
   6647                 sb.append(")");
   6648             } else if (isPhoneNumber) {
   6649                 sb.append("ifnull(");
   6650                 if (!deferSnippeting) {
   6651                     // Add the snippet marker only when we're really creating snippet.
   6652                     DatabaseUtils.appendEscapedSQLString(sb, startMatch);
   6653                     sb.append("||");
   6654                 }
   6655                 sb.append("(SELECT MIN(" + Phone.NUMBER + ")");
   6656                 sb.append(" FROM " +
   6657                         Tables.DATA_JOIN_RAW_CONTACTS + " JOIN " + Tables.PHONE_LOOKUP);
   6658                 sb.append(" ON " + DataColumns.CONCRETE_ID);
   6659                 sb.append("=" + Tables.PHONE_LOOKUP + "." + PhoneLookupColumns.DATA_ID);
   6660                 sb.append(" WHERE  " + Tables.SEARCH_INDEX + "." + SearchIndexColumns.CONTACT_ID);
   6661                 sb.append("=" + RawContacts.CONTACT_ID);
   6662                 sb.append(" AND " + PhoneLookupColumns.NORMALIZED_NUMBER + " LIKE '");
   6663                 sb.append(phoneNumber);
   6664                 sb.append("%'");
   6665                 if (!TextUtils.isEmpty(numberE164)) {
   6666                     sb.append(" OR " + PhoneLookupColumns.NORMALIZED_NUMBER + " LIKE '");
   6667                     sb.append(numberE164);
   6668                     sb.append("%'");
   6669                 }
   6670                 sb.append(")");
   6671                 if (! deferSnippeting) {
   6672                     sb.append("||");
   6673                     DatabaseUtils.appendEscapedSQLString(sb, endMatch);
   6674                 }
   6675                 sb.append(",");
   6676 
   6677                 if (deferSnippeting) {
   6678                     sb.append(SearchIndexColumns.CONTENT);
   6679                 } else {
   6680                     appendSnippetFunction(sb, startMatch, endMatch, ellipsis, maxTokens);
   6681                 }
   6682                 sb.append(")");
   6683             } else {
   6684                 final String normalizedFilter = NameNormalizer.normalize(filter);
   6685                 if (!TextUtils.isEmpty(normalizedFilter)) {
   6686                     if (deferSnippeting) {
   6687                         sb.append(SearchIndexColumns.CONTENT);
   6688                     } else {
   6689                         sb.append("(CASE WHEN EXISTS (SELECT 1 FROM ");
   6690                         sb.append(Tables.RAW_CONTACTS + " AS rc INNER JOIN ");
   6691                         sb.append(Tables.NAME_LOOKUP + " AS nl ON (rc." + RawContacts._ID);
   6692                         sb.append("=nl." + NameLookupColumns.RAW_CONTACT_ID);
   6693                         sb.append(") WHERE nl." + NameLookupColumns.NORMALIZED_NAME);
   6694                         sb.append(" GLOB '" + normalizedFilter + "*' AND ");
   6695                         sb.append("nl." + NameLookupColumns.NAME_TYPE + "=");
   6696                         sb.append(NameLookupType.NAME_COLLATION_KEY + " AND ");
   6697                         sb.append(Tables.SEARCH_INDEX + "." + SearchIndexColumns.CONTACT_ID);
   6698                         sb.append("=rc." + RawContacts.CONTACT_ID);
   6699                         sb.append(") THEN NULL ELSE ");
   6700                         appendSnippetFunction(sb, startMatch, endMatch, ellipsis, maxTokens);
   6701                         sb.append(" END)");
   6702                     }
   6703                 } else {
   6704                     sb.append("NULL");
   6705                 }
   6706             }
   6707             sb.append(" AS " + SearchSnippetColumns.SNIPPET);
   6708         }
   6709 
   6710         sb.append(" FROM " + Tables.SEARCH_INDEX);
   6711         sb.append(" WHERE ");
   6712         sb.append(Tables.SEARCH_INDEX + " MATCH '");
   6713         if (isEmailAddress) {
   6714             // we know that the emailAddress contains a @. This phrase search should be
   6715             // scoped against "content:" only, but unfortunately SQLite doesn't support
   6716             // phrases and scoped columns at once. This is fine in this case however, because:
   6717             //  - We can't erronously match against name, as name is all-hex (so the @ can't match)
   6718             //  - We can't match against tokens, because phone-numbers can't contain @
   6719             final String sanitizedEmailAddress =
   6720                     emailAddress == null ? "" : sanitizeMatch(emailAddress);
   6721             sb.append("\"");
   6722             sb.append(sanitizedEmailAddress);
   6723             sb.append("*\"");
   6724         } else if (isPhoneNumber) {
   6725             // normalized version of the phone number (phoneNumber can only have + and digits)
   6726             final String phoneNumberCriteria = " OR tokens:" + phoneNumber + "*";
   6727 
   6728             // international version of this number (numberE164 can only have + and digits)
   6729             final String numberE164Criteria =
   6730                     (numberE164 != null && !TextUtils.equals(numberE164, phoneNumber))
   6731                     ? " OR tokens:" + numberE164 + "*"
   6732                     : "";
   6733 
   6734             // combine all criteria
   6735             final String commonCriteria =
   6736                     phoneNumberCriteria + numberE164Criteria;
   6737 
   6738             // search in content
   6739             sb.append(SearchIndexManager.getFtsMatchQuery(filter,
   6740                     FtsQueryBuilder.getDigitsQueryBuilder(commonCriteria)));
   6741         } else {
   6742             // general case: not a phone number, not an email-address
   6743             sb.append(SearchIndexManager.getFtsMatchQuery(filter,
   6744                     FtsQueryBuilder.SCOPED_NAME_NORMALIZING));
   6745         }
   6746         // Omit results in "Other Contacts".
   6747         sb.append("' AND " + SNIPPET_CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY + ")");
   6748         sb.append(" ON (" + Contacts._ID + "=" + SNIPPET_CONTACT_ID + ")");
   6749     }
   6750 
   6751     private static String sanitizeMatch(String filter) {
   6752         return filter.replace("'", "").replace("*", "").replace("-", "").replace("\"", "");
   6753     }
   6754 
   6755     private void appendSnippetFunction(
   6756             StringBuilder sb, String startMatch, String endMatch, String ellipsis, int maxTokens) {
   6757         sb.append("snippet(" + Tables.SEARCH_INDEX + ",");
   6758         DatabaseUtils.appendEscapedSQLString(sb, startMatch);
   6759         sb.append(",");
   6760         DatabaseUtils.appendEscapedSQLString(sb, endMatch);
   6761         sb.append(",");
   6762         DatabaseUtils.appendEscapedSQLString(sb, ellipsis);
   6763 
   6764         // The index of the column used for the snippet, "content"
   6765         sb.append(",1,");
   6766         sb.append(maxTokens);
   6767         sb.append(")");
   6768     }
   6769 
   6770     private void setTablesAndProjectionMapForRawContacts(SQLiteQueryBuilder qb, Uri uri) {
   6771         StringBuilder sb = new StringBuilder();
   6772         sb.append(Views.RAW_CONTACTS);
   6773         qb.setTables(sb.toString());
   6774         qb.setProjectionMap(sRawContactsProjectionMap);
   6775         appendAccountIdFromParameter(qb, uri);
   6776     }
   6777 
   6778     private void setTablesAndProjectionMapForRawEntities(SQLiteQueryBuilder qb, Uri uri) {
   6779         qb.setTables(Views.RAW_ENTITIES);
   6780         qb.setProjectionMap(sRawEntityProjectionMap);
   6781         appendAccountIdFromParameter(qb, uri);
   6782     }
   6783 
   6784     private void setTablesAndProjectionMapForData(SQLiteQueryBuilder qb, Uri uri,
   6785             String[] projection, boolean distinct) {
   6786         setTablesAndProjectionMapForData(qb, uri, projection, distinct, false, null);
   6787     }
   6788 
   6789     private void setTablesAndProjectionMapForData(SQLiteQueryBuilder qb, Uri uri,
   6790             String[] projection, boolean distinct, boolean addSipLookupColumns) {
   6791         setTablesAndProjectionMapForData(qb, uri, projection, distinct, addSipLookupColumns, null);
   6792     }
   6793 
   6794     /**
   6795      * @param usageType when non-null {@link Tables#DATA_USAGE_STAT} is joined with the specified
   6796      * type.
   6797      */
   6798     private void setTablesAndProjectionMapForData(SQLiteQueryBuilder qb, Uri uri,
   6799             String[] projection, boolean distinct, Integer usageType) {
   6800         setTablesAndProjectionMapForData(qb, uri, projection, distinct, false, usageType);
   6801     }
   6802 
   6803     private void setTablesAndProjectionMapForData(SQLiteQueryBuilder qb, Uri uri,
   6804             String[] projection, boolean distinct, boolean addSipLookupColumns, Integer usageType) {
   6805         StringBuilder sb = new StringBuilder();
   6806         sb.append(Views.DATA);
   6807         sb.append(" data");
   6808 
   6809         appendContactPresenceJoin(sb, projection, RawContacts.CONTACT_ID);
   6810         appendContactStatusUpdateJoin(sb, projection, ContactsColumns.LAST_STATUS_UPDATE_ID);
   6811         appendDataPresenceJoin(sb, projection, DataColumns.CONCRETE_ID);
   6812         appendDataStatusUpdateJoin(sb, projection, DataColumns.CONCRETE_ID);
   6813 
   6814         if (usageType != null) {
   6815             appendDataUsageStatJoin(sb, usageType, DataColumns.CONCRETE_ID);
   6816         }
   6817 
   6818         qb.setTables(sb.toString());
   6819 
   6820         boolean useDistinct = distinct || !ContactsDatabaseHelper.isInProjection(
   6821                 projection, DISTINCT_DATA_PROHIBITING_COLUMNS);
   6822         qb.setDistinct(useDistinct);
   6823 
   6824         final ProjectionMap projectionMap;
   6825         if (addSipLookupColumns) {
   6826             projectionMap = useDistinct
   6827                     ? sDistinctDataSipLookupProjectionMap : sDataSipLookupProjectionMap;
   6828         } else {
   6829             projectionMap = useDistinct ? sDistinctDataProjectionMap : sDataProjectionMap;
   6830         }
   6831 
   6832         qb.setProjectionMap(projectionMap);
   6833         appendAccountIdFromParameter(qb, uri);
   6834     }
   6835 
   6836     private void setTableAndProjectionMapForStatusUpdates(SQLiteQueryBuilder qb,
   6837             String[] projection) {
   6838         StringBuilder sb = new StringBuilder();
   6839         sb.append(Views.DATA);
   6840         sb.append(" data");
   6841         appendDataPresenceJoin(sb, projection, DataColumns.CONCRETE_ID);
   6842         appendDataStatusUpdateJoin(sb, projection, DataColumns.CONCRETE_ID);
   6843 
   6844         qb.setTables(sb.toString());
   6845         qb.setProjectionMap(sStatusUpdatesProjectionMap);
   6846     }
   6847 
   6848     private void setTablesAndProjectionMapForStreamItems(SQLiteQueryBuilder qb) {
   6849         qb.setTables(Views.STREAM_ITEMS);
   6850         qb.setProjectionMap(sStreamItemsProjectionMap);
   6851     }
   6852 
   6853     private void setTablesAndProjectionMapForStreamItemPhotos(SQLiteQueryBuilder qb) {
   6854         qb.setTables(Tables.PHOTO_FILES
   6855                 + " JOIN " + Tables.STREAM_ITEM_PHOTOS + " ON ("
   6856                 + StreamItemPhotosColumns.CONCRETE_PHOTO_FILE_ID + "="
   6857                 + PhotoFilesColumns.CONCRETE_ID
   6858                 + ") JOIN " + Tables.STREAM_ITEMS + " ON ("
   6859                 + StreamItemPhotosColumns.CONCRETE_STREAM_ITEM_ID + "="
   6860                 + StreamItemsColumns.CONCRETE_ID + ")"
   6861                 + " JOIN " + Tables.RAW_CONTACTS + " ON ("
   6862                 + StreamItemsColumns.CONCRETE_RAW_CONTACT_ID + "=" + RawContactsColumns.CONCRETE_ID
   6863                 + ")");
   6864         qb.setProjectionMap(sStreamItemPhotosProjectionMap);
   6865     }
   6866 
   6867     private void setTablesAndProjectionMapForEntities(SQLiteQueryBuilder qb, Uri uri,
   6868             String[] projection) {
   6869         StringBuilder sb = new StringBuilder();
   6870         sb.append(Views.ENTITIES);
   6871         sb.append(" data");
   6872 
   6873         appendContactPresenceJoin(sb, projection, Contacts.Entity.CONTACT_ID);
   6874         appendContactStatusUpdateJoin(sb, projection, ContactsColumns.LAST_STATUS_UPDATE_ID);
   6875         appendDataPresenceJoin(sb, projection, Contacts.Entity.DATA_ID);
   6876         appendDataStatusUpdateJoin(sb, projection, Contacts.Entity.DATA_ID);
   6877 
   6878         qb.setTables(sb.toString());
   6879         qb.setProjectionMap(sEntityProjectionMap);
   6880         appendAccountIdFromParameter(qb, uri);
   6881     }
   6882 
   6883     private void appendContactStatusUpdateJoin(StringBuilder sb, String[] projection,
   6884             String lastStatusUpdateIdColumn) {
   6885         if (ContactsDatabaseHelper.isInProjection(projection,
   6886                 Contacts.CONTACT_STATUS,
   6887                 Contacts.CONTACT_STATUS_RES_PACKAGE,
   6888                 Contacts.CONTACT_STATUS_ICON,
   6889                 Contacts.CONTACT_STATUS_LABEL,
   6890                 Contacts.CONTACT_STATUS_TIMESTAMP)) {
   6891             sb.append(" LEFT OUTER JOIN " + Tables.STATUS_UPDATES + " "
   6892                     + ContactsStatusUpdatesColumns.ALIAS +
   6893                     " ON (" + lastStatusUpdateIdColumn + "="
   6894                             + ContactsStatusUpdatesColumns.CONCRETE_DATA_ID + ")");
   6895         }
   6896     }
   6897 
   6898     private void appendDataStatusUpdateJoin(StringBuilder sb, String[] projection,
   6899             String dataIdColumn) {
   6900         if (ContactsDatabaseHelper.isInProjection(projection,
   6901                 StatusUpdates.STATUS,
   6902                 StatusUpdates.STATUS_RES_PACKAGE,
   6903                 StatusUpdates.STATUS_ICON,
   6904                 StatusUpdates.STATUS_LABEL,
   6905                 StatusUpdates.STATUS_TIMESTAMP)) {
   6906             sb.append(" LEFT OUTER JOIN " + Tables.STATUS_UPDATES +
   6907                     " ON (" + StatusUpdatesColumns.CONCRETE_DATA_ID + "="
   6908                             + dataIdColumn + ")");
   6909         }
   6910     }
   6911 
   6912     private void appendDataUsageStatJoin(StringBuilder sb, int usageType, String dataIdColumn) {
   6913         sb.append(" LEFT OUTER JOIN " + Tables.DATA_USAGE_STAT +
   6914                 " ON (" + DataUsageStatColumns.CONCRETE_DATA_ID + "=" + dataIdColumn +
   6915                 " AND " + DataUsageStatColumns.CONCRETE_USAGE_TYPE + "=" + usageType + ")");
   6916     }
   6917 
   6918     private void appendContactPresenceJoin(StringBuilder sb, String[] projection,
   6919             String contactIdColumn) {
   6920         if (ContactsDatabaseHelper.isInProjection(projection,
   6921                 Contacts.CONTACT_PRESENCE, Contacts.CONTACT_CHAT_CAPABILITY)) {
   6922             sb.append(" LEFT OUTER JOIN " + Tables.AGGREGATED_PRESENCE +
   6923                     " ON (" + contactIdColumn + " = "
   6924                             + AggregatedPresenceColumns.CONCRETE_CONTACT_ID + ")");
   6925         }
   6926     }
   6927 
   6928     private void appendDataPresenceJoin(StringBuilder sb, String[] projection,
   6929             String dataIdColumn) {
   6930         if (ContactsDatabaseHelper.isInProjection(
   6931                 projection, Data.PRESENCE, Data.CHAT_CAPABILITY)) {
   6932             sb.append(" LEFT OUTER JOIN " + Tables.PRESENCE +
   6933                     " ON (" + StatusUpdates.DATA_ID + "=" + dataIdColumn + ")");
   6934         }
   6935     }
   6936 
   6937     private void appendLocalDirectoryAndAccountSelectionIfNeeded(SQLiteQueryBuilder qb,
   6938             long directoryId, Uri uri) {
   6939         final StringBuilder sb = new StringBuilder();
   6940         if (directoryId == Directory.DEFAULT) {
   6941             sb.append("(" + Contacts._ID + " IN " + Tables.DEFAULT_DIRECTORY + ")");
   6942         } else if (directoryId == Directory.LOCAL_INVISIBLE){
   6943             sb.append("(" + Contacts._ID + " NOT IN " + Tables.DEFAULT_DIRECTORY + ")");
   6944         } else {
   6945             sb.append("(1)");
   6946         }
   6947 
   6948         final AccountWithDataSet accountWithDataSet = getAccountWithDataSetFromUri(uri);
   6949         // Accounts are valid by only checking one parameter, since we've
   6950         // already ruled out partial accounts.
   6951         final boolean validAccount = !TextUtils.isEmpty(accountWithDataSet.getAccountName());
   6952         if (validAccount) {
   6953             final Long accountId = mDbHelper.get().getAccountIdOrNull(accountWithDataSet);
   6954             if (accountId == null) {
   6955                 // No such account.
   6956                 sb.setLength(0);
   6957                 sb.append("(1=2)");
   6958             } else {
   6959                 sb.append(
   6960                         " AND (" + Contacts._ID + " IN (" +
   6961                         "SELECT " + RawContacts.CONTACT_ID + " FROM " + Tables.RAW_CONTACTS +
   6962                         " WHERE " + RawContactsColumns.ACCOUNT_ID + "=" + accountId.toString() +
   6963                         "))");
   6964             }
   6965         }
   6966         qb.appendWhere(sb.toString());
   6967     }
   6968 
   6969     private void appendAccountFromParameter(SQLiteQueryBuilder qb, Uri uri) {
   6970         final AccountWithDataSet accountWithDataSet = getAccountWithDataSetFromUri(uri);
   6971 
   6972         // Accounts are valid by only checking one parameter, since we've
   6973         // already ruled out partial accounts.
   6974         final boolean validAccount = !TextUtils.isEmpty(accountWithDataSet.getAccountName());
   6975         if (validAccount) {
   6976             String toAppend = "(" + RawContacts.ACCOUNT_NAME + "="
   6977                     + DatabaseUtils.sqlEscapeString(accountWithDataSet.getAccountName()) + " AND "
   6978                     + RawContacts.ACCOUNT_TYPE + "="
   6979                     + DatabaseUtils.sqlEscapeString(accountWithDataSet.getAccountType());
   6980             if (accountWithDataSet.getDataSet() == null) {
   6981                 toAppend += " AND " + RawContacts.DATA_SET + " IS NULL";
   6982             } else {
   6983                 toAppend += " AND " + RawContacts.DATA_SET + "=" +
   6984                         DatabaseUtils.sqlEscapeString(accountWithDataSet.getDataSet());
   6985             }
   6986             toAppend += ")";
   6987             qb.appendWhere(toAppend);
   6988         } else {
   6989             qb.appendWhere("1");
   6990         }
   6991     }
   6992 
   6993     private void appendAccountIdFromParameter(SQLiteQueryBuilder qb, Uri uri) {
   6994         final AccountWithDataSet accountWithDataSet = getAccountWithDataSetFromUri(uri);
   6995 
   6996         // Accounts are valid by only checking one parameter, since we've
   6997         // already ruled out partial accounts.
   6998         final boolean validAccount = !TextUtils.isEmpty(accountWithDataSet.getAccountName());
   6999         if (validAccount) {
   7000             final Long accountId = mDbHelper.get().getAccountIdOrNull(accountWithDataSet);
   7001             if (accountId == null) {
   7002                 // No such account.
   7003                 qb.appendWhere("(1=2)");
   7004             } else {
   7005                 qb.appendWhere(
   7006                         "(" + RawContactsColumns.ACCOUNT_ID + "=" + accountId.toString() + ")");
   7007             }
   7008         } else {
   7009             qb.appendWhere("1");
   7010         }
   7011     }
   7012 
   7013     private AccountWithDataSet getAccountWithDataSetFromUri(Uri uri) {
   7014         final String accountName = getQueryParameter(uri, RawContacts.ACCOUNT_NAME);
   7015         final String accountType = getQueryParameter(uri, RawContacts.ACCOUNT_TYPE);
   7016         final String dataSet = getQueryParameter(uri, RawContacts.DATA_SET);
   7017 
   7018         final boolean partialUri = TextUtils.isEmpty(accountName) ^ TextUtils.isEmpty(accountType);
   7019         if (partialUri) {
   7020             // Throw when either account is incomplete
   7021             throw new IllegalArgumentException(mDbHelper.get().exceptionMessage(
   7022                     "Must specify both or neither of ACCOUNT_NAME and ACCOUNT_TYPE", uri));
   7023         }
   7024         return AccountWithDataSet.get(accountName, accountType, dataSet);
   7025     }
   7026 
   7027     private String appendAccountToSelection(Uri uri, String selection) {
   7028         final AccountWithDataSet accountWithDataSet = getAccountWithDataSetFromUri(uri);
   7029 
   7030         // Accounts are valid by only checking one parameter, since we've
   7031         // already ruled out partial accounts.
   7032         final boolean validAccount = !TextUtils.isEmpty(accountWithDataSet.getAccountName());
   7033         if (validAccount) {
   7034             StringBuilder selectionSb = new StringBuilder(RawContacts.ACCOUNT_NAME + "=");
   7035             selectionSb.append(DatabaseUtils.sqlEscapeString(accountWithDataSet.getAccountName()));
   7036             selectionSb.append(" AND " + RawContacts.ACCOUNT_TYPE + "=");
   7037             selectionSb.append(DatabaseUtils.sqlEscapeString(accountWithDataSet.getAccountType()));
   7038             if (accountWithDataSet.getDataSet() == null) {
   7039                 selectionSb.append(" AND " + RawContacts.DATA_SET + " IS NULL");
   7040             } else {
   7041                 selectionSb.append(" AND " + RawContacts.DATA_SET + "=")
   7042                         .append(DatabaseUtils.sqlEscapeString(accountWithDataSet.getDataSet()));
   7043             }
   7044             if (!TextUtils.isEmpty(selection)) {
   7045                 selectionSb.append(" AND (");
   7046                 selectionSb.append(selection);
   7047                 selectionSb.append(')');
   7048             }
   7049             return selectionSb.toString();
   7050         } else {
   7051             return selection;
   7052         }
   7053     }
   7054 
   7055     private String appendAccountIdToSelection(Uri uri, String selection) {
   7056         final AccountWithDataSet accountWithDataSet = getAccountWithDataSetFromUri(uri);
   7057 
   7058         // Accounts are valid by only checking one parameter, since we've
   7059         // already ruled out partial accounts.
   7060         final boolean validAccount = !TextUtils.isEmpty(accountWithDataSet.getAccountName());
   7061         if (validAccount) {
   7062             final StringBuilder selectionSb = new StringBuilder();
   7063 
   7064             final Long accountId = mDbHelper.get().getAccountIdOrNull(accountWithDataSet);
   7065             if (accountId == null) {
   7066                 // No such account in the accounts table.  This means, there's no rows to be
   7067                 // selected.
   7068                 // Note even in this case, we still need to append the original selection, because
   7069                 // it may have query parameters.  If we remove these we'll get the # of parameters
   7070                 // mismatch exception.
   7071                 selectionSb.append("(1=2)");
   7072             } else {
   7073                 selectionSb.append(RawContactsColumns.ACCOUNT_ID + "=");
   7074                 selectionSb.append(Long.toString(accountId));
   7075             }
   7076 
   7077             if (!TextUtils.isEmpty(selection)) {
   7078                 selectionSb.append(" AND (");
   7079                 selectionSb.append(selection);
   7080                 selectionSb.append(')');
   7081             }
   7082             return selectionSb.toString();
   7083         } else {
   7084             return selection;
   7085         }
   7086     }
   7087 
   7088     /**
   7089      * Gets the value of the "limit" URI query parameter.
   7090      *
   7091      * @return A string containing a non-negative integer, or <code>null</code> if
   7092      *         the parameter is not set, or is set to an invalid value.
   7093      */
   7094     private String getLimit(Uri uri) {
   7095         String limitParam = getQueryParameter(uri, ContactsContract.LIMIT_PARAM_KEY);
   7096         if (limitParam == null) {
   7097             return null;
   7098         }
   7099         // make sure that the limit is a non-negative integer
   7100         try {
   7101             int l = Integer.parseInt(limitParam);
   7102             if (l < 0) {
   7103                 Log.w(TAG, "Invalid limit parameter: " + limitParam);
   7104                 return null;
   7105             }
   7106             return String.valueOf(l);
   7107         } catch (NumberFormatException ex) {
   7108             Log.w(TAG, "Invalid limit parameter: " + limitParam);
   7109             return null;
   7110         }
   7111     }
   7112 
   7113     @Override
   7114     public AssetFileDescriptor openAssetFile(Uri uri, String mode) throws FileNotFoundException {
   7115         boolean success = false;
   7116         try {
   7117             if (mode.equals("r")) {
   7118                 waitForAccess(mReadAccessLatch);
   7119             } else {
   7120                 waitForAccess(mWriteAccessLatch);
   7121             }
   7122             final AssetFileDescriptor ret;
   7123             if (mapsToProfileDb(uri)) {
   7124                 switchToProfileMode();
   7125                 ret = mProfileProvider.openAssetFile(uri, mode);
   7126             } else {
   7127                 switchToContactMode();
   7128                 ret = openAssetFileLocal(uri, mode);
   7129             }
   7130             success = true;
   7131             return ret;
   7132         } finally {
   7133             if (VERBOSE_LOGGING) {
   7134                 Log.v(TAG, "openAssetFile uri=" + uri + " mode=" + mode + " success=" + success);
   7135             }
   7136         }
   7137     }
   7138 
   7139     public AssetFileDescriptor openAssetFileLocal(Uri uri, String mode)
   7140             throws FileNotFoundException {
   7141 
   7142         final boolean writing = mode.contains("w");
   7143 
   7144         final SQLiteDatabase db = mDbHelper.get().getDatabase(writing);
   7145 
   7146         int match = sUriMatcher.match(uri);
   7147         switch (match) {
   7148             case CONTACTS_ID_PHOTO: {
   7149                 long contactId = Long.parseLong(uri.getPathSegments().get(1));
   7150                 return openPhotoAssetFile(db, uri, mode,
   7151                         Data._ID + "=" + Contacts.PHOTO_ID + " AND " +
   7152                                 RawContacts.CONTACT_ID + "=?",
   7153                         new String[]{String.valueOf(contactId)});
   7154             }
   7155 
   7156             case CONTACTS_ID_DISPLAY_PHOTO: {
   7157                 if (!mode.equals("r")) {
   7158                     throw new IllegalArgumentException(
   7159                             "Display photos retrieved by contact ID can only be read.");
   7160                 }
   7161                 long contactId = Long.parseLong(uri.getPathSegments().get(1));
   7162                 Cursor c = db.query(Tables.CONTACTS,
   7163                         new String[]{Contacts.PHOTO_FILE_ID},
   7164                         Contacts._ID + "=?", new String[]{String.valueOf(contactId)},
   7165                         null, null, null);
   7166                 try {
   7167                     if (c.moveToFirst()) {
   7168                         long photoFileId = c.getLong(0);
   7169                         return openDisplayPhotoForRead(photoFileId);
   7170                     } else {
   7171                         // No contact for this ID.
   7172                         throw new FileNotFoundException(uri.toString());
   7173                     }
   7174                 } finally {
   7175                     c.close();
   7176                 }
   7177             }
   7178 
   7179             case PROFILE_DISPLAY_PHOTO: {
   7180                 if (!mode.equals("r")) {
   7181                     throw new IllegalArgumentException(
   7182                             "Display photos retrieved by contact ID can only be read.");
   7183                 }
   7184                 Cursor c = db.query(Tables.CONTACTS,
   7185                         new String[]{Contacts.PHOTO_FILE_ID}, null, null, null, null, null);
   7186                 try {
   7187                     if (c.moveToFirst()) {
   7188                         long photoFileId = c.getLong(0);
   7189                         return openDisplayPhotoForRead(photoFileId);
   7190                     } else {
   7191                         // No profile record.
   7192                         throw new FileNotFoundException(uri.toString());
   7193                     }
   7194                 } finally {
   7195                     c.close();
   7196                 }
   7197             }
   7198 
   7199             case CONTACTS_LOOKUP_PHOTO:
   7200             case CONTACTS_LOOKUP_ID_PHOTO:
   7201             case CONTACTS_LOOKUP_DISPLAY_PHOTO:
   7202             case CONTACTS_LOOKUP_ID_DISPLAY_PHOTO: {
   7203                 if (!mode.equals("r")) {
   7204                     throw new IllegalArgumentException(
   7205                             "Photos retrieved by contact lookup key can only be read.");
   7206                 }
   7207                 List<String> pathSegments = uri.getPathSegments();
   7208                 int segmentCount = pathSegments.size();
   7209                 if (segmentCount < 4) {
   7210                     throw new IllegalArgumentException(mDbHelper.get().exceptionMessage(
   7211                             "Missing a lookup key", uri));
   7212                 }
   7213 
   7214                 boolean forDisplayPhoto = (match == CONTACTS_LOOKUP_ID_DISPLAY_PHOTO
   7215                         || match == CONTACTS_LOOKUP_DISPLAY_PHOTO);
   7216                 String lookupKey = pathSegments.get(2);
   7217                 String[] projection = new String[]{Contacts.PHOTO_ID, Contacts.PHOTO_FILE_ID};
   7218                 if (segmentCount == 5) {
   7219                     long contactId = Long.parseLong(pathSegments.get(3));
   7220                     SQLiteQueryBuilder lookupQb = new SQLiteQueryBuilder();
   7221                     setTablesAndProjectionMapForContacts(lookupQb, uri, projection);
   7222                     Cursor c = queryWithContactIdAndLookupKey(lookupQb, db, uri,
   7223                             projection, null, null, null, null, null,
   7224                             Contacts._ID, contactId, Contacts.LOOKUP_KEY, lookupKey, null);
   7225                     if (c != null) {
   7226                         try {
   7227                             c.moveToFirst();
   7228                             if (forDisplayPhoto) {
   7229                                 long photoFileId =
   7230                                         c.getLong(c.getColumnIndex(Contacts.PHOTO_FILE_ID));
   7231                                 return openDisplayPhotoForRead(photoFileId);
   7232                             } else {
   7233                                 long photoId = c.getLong(c.getColumnIndex(Contacts.PHOTO_ID));
   7234                                 return openPhotoAssetFile(db, uri, mode,
   7235                                         Data._ID + "=?", new String[]{String.valueOf(photoId)});
   7236                             }
   7237                         } finally {
   7238                             c.close();
   7239                         }
   7240                     }
   7241                 }
   7242 
   7243                 SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
   7244                 setTablesAndProjectionMapForContacts(qb, uri, projection);
   7245                 long contactId = lookupContactIdByLookupKey(db, lookupKey);
   7246                 Cursor c = qb.query(db, projection, Contacts._ID + "=?",
   7247                         new String[]{String.valueOf(contactId)}, null, null, null);
   7248                 try {
   7249                     c.moveToFirst();
   7250                     if (forDisplayPhoto) {
   7251                         long photoFileId = c.getLong(c.getColumnIndex(Contacts.PHOTO_FILE_ID));
   7252                         return openDisplayPhotoForRead(photoFileId);
   7253                     } else {
   7254                         long photoId = c.getLong(c.getColumnIndex(Contacts.PHOTO_ID));
   7255                         return openPhotoAssetFile(db, uri, mode,
   7256                                 Data._ID + "=?", new String[]{String.valueOf(photoId)});
   7257                     }
   7258                 } finally {
   7259                     c.close();
   7260                 }
   7261             }
   7262 
   7263             case RAW_CONTACTS_ID_DISPLAY_PHOTO: {
   7264                 long rawContactId = Long.parseLong(uri.getPathSegments().get(1));
   7265                 boolean writeable = !mode.equals("r");
   7266 
   7267                 // Find the primary photo data record for this raw contact.
   7268                 SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
   7269                 String[] projection = new String[]{Data._ID, Photo.PHOTO_FILE_ID};
   7270                 setTablesAndProjectionMapForData(qb, uri, projection, false);
   7271                 long photoMimetypeId = mDbHelper.get().getMimeTypeId(Photo.CONTENT_ITEM_TYPE);
   7272                 Cursor c = qb.query(db, projection,
   7273                         Data.RAW_CONTACT_ID + "=? AND " + DataColumns.MIMETYPE_ID + "=?",
   7274                         new String[]{String.valueOf(rawContactId), String.valueOf(photoMimetypeId)},
   7275                         null, null, Data.IS_PRIMARY + " DESC");
   7276                 long dataId = 0;
   7277                 long photoFileId = 0;
   7278                 try {
   7279                     if (c.getCount() >= 1) {
   7280                         c.moveToFirst();
   7281                         dataId = c.getLong(0);
   7282                         photoFileId = c.getLong(1);
   7283                     }
   7284                 } finally {
   7285                     c.close();
   7286                 }
   7287 
   7288                 // If writeable, open a writeable file descriptor that we can monitor.
   7289                 // When the caller finishes writing content, we'll process the photo and
   7290                 // update the data record.
   7291                 if (writeable) {
   7292                     return openDisplayPhotoForWrite(rawContactId, dataId, uri, mode);
   7293                 } else {
   7294                     return openDisplayPhotoForRead(photoFileId);
   7295                 }
   7296             }
   7297 
   7298             case DISPLAY_PHOTO_ID: {
   7299                 long photoFileId = ContentUris.parseId(uri);
   7300                 if (!mode.equals("r")) {
   7301                     throw new IllegalArgumentException(
   7302                             "Display photos retrieved by key can only be read.");
   7303                 }
   7304                 return openDisplayPhotoForRead(photoFileId);
   7305             }
   7306 
   7307             case DATA_ID: {
   7308                 long dataId = Long.parseLong(uri.getPathSegments().get(1));
   7309                 long photoMimetypeId = mDbHelper.get().getMimeTypeId(Photo.CONTENT_ITEM_TYPE);
   7310                 return openPhotoAssetFile(db, uri, mode,
   7311                         Data._ID + "=? AND " + DataColumns.MIMETYPE_ID + "=" + photoMimetypeId,
   7312                         new String[]{String.valueOf(dataId)});
   7313             }
   7314 
   7315             case PROFILE_AS_VCARD: {
   7316                 // When opening a contact as file, we pass back contents as a
   7317                 // vCard-encoded stream. We build into a local buffer first,
   7318                 // then pipe into MemoryFile once the exact size is known.
   7319                 final ByteArrayOutputStream localStream = new ByteArrayOutputStream();
   7320                 outputRawContactsAsVCard(uri, localStream, null, null);
   7321                 return buildAssetFileDescriptor(localStream);
   7322             }
   7323 
   7324             case CONTACTS_AS_VCARD: {
   7325                 // When opening a contact as file, we pass back contents as a
   7326                 // vCard-encoded stream. We build into a local buffer first,
   7327                 // then pipe into MemoryFile once the exact size is known.
   7328                 final ByteArrayOutputStream localStream = new ByteArrayOutputStream();
   7329                 outputRawContactsAsVCard(uri, localStream, null, null);
   7330                 return buildAssetFileDescriptor(localStream);
   7331             }
   7332 
   7333             case CONTACTS_AS_MULTI_VCARD: {
   7334                 final String lookupKeys = uri.getPathSegments().get(2);
   7335                 final String[] loopupKeyList = lookupKeys.split(":");
   7336                 final StringBuilder inBuilder = new StringBuilder();
   7337                 Uri queryUri = Contacts.CONTENT_URI;
   7338                 int index = 0;
   7339 
   7340                 // SQLite has limits on how many parameters can be used
   7341                 // so the IDs are concatenated to a query string here instead
   7342                 for (String lookupKey : loopupKeyList) {
   7343                     if (index == 0) {
   7344                         inBuilder.append("(");
   7345                     } else {
   7346                         inBuilder.append(",");
   7347                     }
   7348                     // TODO: Figure out what to do if the profile contact is in the list.
   7349                     long contactId = lookupContactIdByLookupKey(db, lookupKey);
   7350                     inBuilder.append(contactId);
   7351                     index++;
   7352                 }
   7353                 inBuilder.append(')');
   7354                 final String selection = Contacts._ID + " IN " + inBuilder.toString();
   7355 
   7356                 // When opening a contact as file, we pass back contents as a
   7357                 // vCard-encoded stream. We build into a local buffer first,
   7358                 // then pipe into MemoryFile once the exact size is known.
   7359                 final ByteArrayOutputStream localStream = new ByteArrayOutputStream();
   7360                 outputRawContactsAsVCard(queryUri, localStream, selection, null);
   7361                 return buildAssetFileDescriptor(localStream);
   7362             }
   7363 
   7364             default:
   7365                 throw new FileNotFoundException(mDbHelper.get().exceptionMessage(
   7366                         "File does not exist", uri));
   7367         }
   7368     }
   7369 
   7370     private AssetFileDescriptor openPhotoAssetFile(SQLiteDatabase db, Uri uri, String mode,
   7371             String selection, String[] selectionArgs)
   7372             throws FileNotFoundException {
   7373         if (!"r".equals(mode)) {
   7374             throw new FileNotFoundException(mDbHelper.get().exceptionMessage("Mode " + mode
   7375                     + " not supported.", uri));
   7376         }
   7377 
   7378         String sql =
   7379                 "SELECT " + Photo.PHOTO + " FROM " + Views.DATA +
   7380                 " WHERE " + selection;
   7381         try {
   7382             return makeAssetFileDescriptor(
   7383                     DatabaseUtils.blobFileDescriptorForQuery(db, sql, selectionArgs));
   7384         } catch (SQLiteDoneException e) {
   7385             // this will happen if the DB query returns no rows (i.e. contact does not exist)
   7386             throw new FileNotFoundException(uri.toString());
   7387         }
   7388     }
   7389 
   7390     /**
   7391      * Opens a display photo from the photo store for reading.
   7392      * @param photoFileId The display photo file ID
   7393      * @return An asset file descriptor that allows the file to be read.
   7394      * @throws FileNotFoundException If no photo file for the given ID exists.
   7395      */
   7396     private AssetFileDescriptor openDisplayPhotoForRead(long photoFileId)
   7397             throws FileNotFoundException {
   7398         PhotoStore.Entry entry = mPhotoStore.get().get(photoFileId);
   7399         if (entry != null) {
   7400             try {
   7401                 return makeAssetFileDescriptor(
   7402                         ParcelFileDescriptor.open(new File(entry.path),
   7403                                 ParcelFileDescriptor.MODE_READ_ONLY),
   7404                         entry.size);
   7405             } catch (FileNotFoundException fnfe) {
   7406                 scheduleBackgroundTask(BACKGROUND_TASK_CLEANUP_PHOTOS);
   7407                 throw fnfe;
   7408             }
   7409         } else {
   7410             scheduleBackgroundTask(BACKGROUND_TASK_CLEANUP_PHOTOS);
   7411             throw new FileNotFoundException("No photo file found for ID " + photoFileId);
   7412         }
   7413     }
   7414 
   7415     /**
   7416      * Opens a file descriptor for a photo to be written.  When the caller completes writing
   7417      * to the file (closing the output stream), the image will be parsed out and processed.
   7418      * If processing succeeds, the given raw contact ID's primary photo record will be
   7419      * populated with the inserted image (if no primary photo record exists, the data ID can
   7420      * be left as 0, and a new data record will be inserted).
   7421      * @param rawContactId Raw contact ID this photo entry should be associated with.
   7422      * @param dataId Data ID for a photo mimetype that will be updated with the inserted
   7423      *     image.  May be set to 0, in which case the inserted image will trigger creation
   7424      *     of a new primary photo image data row for the raw contact.
   7425      * @param uri The URI being used to access this file.
   7426      * @param mode Read/write mode string.
   7427      * @return An asset file descriptor the caller can use to write an image file for the
   7428      *     raw contact.
   7429      */
   7430     private AssetFileDescriptor openDisplayPhotoForWrite(long rawContactId, long dataId, Uri uri,
   7431             String mode) {
   7432         try {
   7433             ParcelFileDescriptor[] pipeFds = ParcelFileDescriptor.createPipe();
   7434             PipeMonitor pipeMonitor = new PipeMonitor(rawContactId, dataId, pipeFds[0]);
   7435             pipeMonitor.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Object[]) null);
   7436             return new AssetFileDescriptor(pipeFds[1], 0, AssetFileDescriptor.UNKNOWN_LENGTH);
   7437         } catch (IOException ioe) {
   7438             Log.e(TAG, "Could not create temp image file in mode " + mode);
   7439             return null;
   7440         }
   7441     }
   7442 
   7443     /**
   7444      * Async task that monitors the given file descriptor (the read end of a pipe) for
   7445      * the writer finishing.  If the data from the pipe contains a valid image, the image
   7446      * is either inserted into the given raw contact or updated in the given data row.
   7447      */
   7448     private class PipeMonitor extends AsyncTask<Object, Object, Object> {
   7449         private final ParcelFileDescriptor mDescriptor;
   7450         private final long mRawContactId;
   7451         private final long mDataId;
   7452         private PipeMonitor(long rawContactId, long dataId, ParcelFileDescriptor descriptor) {
   7453             mRawContactId = rawContactId;
   7454             mDataId = dataId;
   7455             mDescriptor = descriptor;
   7456         }
   7457 
   7458         @Override
   7459         protected Object doInBackground(Object... params) {
   7460             AutoCloseInputStream is = new AutoCloseInputStream(mDescriptor);
   7461             try {
   7462                 Bitmap b = BitmapFactory.decodeStream(is);
   7463                 if (b != null) {
   7464                     waitForAccess(mWriteAccessLatch);
   7465                     PhotoProcessor processor = new PhotoProcessor(b, getMaxDisplayPhotoDim(),
   7466                             getMaxThumbnailDim());
   7467 
   7468                     // Store the compressed photo in the photo store.
   7469                     PhotoStore photoStore = ContactsContract.isProfileId(mRawContactId)
   7470                             ? mProfilePhotoStore
   7471                             : mContactsPhotoStore;
   7472                     long photoFileId = photoStore.insert(processor);
   7473 
   7474                     // Depending on whether we already had a data row to attach the photo
   7475                     // to, do an update or insert.
   7476                     if (mDataId != 0) {
   7477                         // Update the data record with the new photo.
   7478                         ContentValues updateValues = new ContentValues();
   7479 
   7480                         // Signal that photo processing has already been handled.
   7481                         updateValues.put(DataRowHandlerForPhoto.SKIP_PROCESSING_KEY, true);
   7482 
   7483                         if (photoFileId != 0) {
   7484                             updateValues.put(Photo.PHOTO_FILE_ID, photoFileId);
   7485                         }
   7486                         updateValues.put(Photo.PHOTO, processor.getThumbnailPhotoBytes());
   7487                         update(ContentUris.withAppendedId(Data.CONTENT_URI, mDataId),
   7488                                 updateValues, null, null);
   7489                     } else {
   7490                         // Insert a new primary data record with the photo.
   7491                         ContentValues insertValues = new ContentValues();
   7492 
   7493                         // Signal that photo processing has already been handled.
   7494                         insertValues.put(DataRowHandlerForPhoto.SKIP_PROCESSING_KEY, true);
   7495 
   7496                         insertValues.put(Data.MIMETYPE, Photo.CONTENT_ITEM_TYPE);
   7497                         insertValues.put(Data.IS_PRIMARY, 1);
   7498                         if (photoFileId != 0) {
   7499                             insertValues.put(Photo.PHOTO_FILE_ID, photoFileId);
   7500                         }
   7501                         insertValues.put(Photo.PHOTO, processor.getThumbnailPhotoBytes());
   7502                         insert(RawContacts.CONTENT_URI.buildUpon()
   7503                                 .appendPath(String.valueOf(mRawContactId))
   7504                                 .appendPath(RawContacts.Data.CONTENT_DIRECTORY).build(),
   7505                                 insertValues);
   7506                     }
   7507 
   7508                 }
   7509             } catch (IOException e) {
   7510                 throw new RuntimeException(e);
   7511             }
   7512             return null;
   7513         }
   7514     }
   7515 
   7516     private static final String CONTACT_MEMORY_FILE_NAME = "contactAssetFile";
   7517 
   7518     /**
   7519      * Returns an {@link AssetFileDescriptor} backed by the
   7520      * contents of the given {@link ByteArrayOutputStream}.
   7521      */
   7522     private AssetFileDescriptor buildAssetFileDescriptor(ByteArrayOutputStream stream) {
   7523         try {
   7524             stream.flush();
   7525 
   7526             final byte[] byteData = stream.toByteArray();
   7527 
   7528             return makeAssetFileDescriptor(
   7529                     ParcelFileDescriptor.fromData(byteData, CONTACT_MEMORY_FILE_NAME),
   7530                     byteData.length);
   7531         } catch (IOException e) {
   7532             Log.w(TAG, "Problem writing stream into an ParcelFileDescriptor: " + e.toString());
   7533             return null;
   7534         }
   7535     }
   7536 
   7537     private AssetFileDescriptor makeAssetFileDescriptor(ParcelFileDescriptor fd) {
   7538         return makeAssetFileDescriptor(fd, AssetFileDescriptor.UNKNOWN_LENGTH);
   7539     }
   7540 
   7541     private AssetFileDescriptor makeAssetFileDescriptor(ParcelFileDescriptor fd, long length) {
   7542         return fd != null ? new AssetFileDescriptor(fd, 0, length) : null;
   7543     }
   7544 
   7545     /**
   7546      * Output {@link RawContacts} matching the requested selection in the vCard
   7547      * format to the given {@link OutputStream}. This method returns silently if
   7548      * any errors encountered.
   7549      */
   7550     private void outputRawContactsAsVCard(Uri uri, OutputStream stream,
   7551             String selection, String[] selectionArgs) {
   7552         final Context context = this.getContext();
   7553         int vcardconfig = VCardConfig.VCARD_TYPE_DEFAULT;
   7554         if(uri.getBooleanQueryParameter(
   7555                 Contacts.QUERY_PARAMETER_VCARD_NO_PHOTO, false)) {
   7556             vcardconfig |= VCardConfig.FLAG_REFRAIN_IMAGE_EXPORT;
   7557         }
   7558         final VCardComposer composer =
   7559                 new VCardComposer(context, vcardconfig, false);
   7560         Writer writer = null;
   7561         final Uri rawContactsUri;
   7562         if (mapsToProfileDb(uri)) {
   7563             // Pre-authorize the URI, since the caller would have already gone through the
   7564             // permission check to get here, but the pre-authorization at the top level wouldn't
   7565             // carry over to the raw contact.
   7566             rawContactsUri = preAuthorizeUri(RawContactsEntity.PROFILE_CONTENT_URI);
   7567         } else {
   7568             rawContactsUri = RawContactsEntity.CONTENT_URI;
   7569         }
   7570         try {
   7571             writer = new BufferedWriter(new OutputStreamWriter(stream));
   7572             if (!composer.init(uri, selection, selectionArgs, null, rawContactsUri)) {
   7573                 Log.w(TAG, "Failed to init VCardComposer");
   7574                 return;
   7575             }
   7576 
   7577             while (!composer.isAfterLast()) {
   7578                 writer.write(composer.createOneEntry());
   7579             }
   7580         } catch (IOException e) {
   7581             Log.e(TAG, "IOException: " + e);
   7582         } finally {
   7583             composer.terminate();
   7584             if (writer != null) {
   7585                 try {
   7586                     writer.close();
   7587                 } catch (IOException e) {
   7588                     Log.w(TAG, "IOException during closing output stream: " + e);
   7589                 }
   7590             }
   7591         }
   7592     }
   7593 
   7594     @Override
   7595     public String getType(Uri uri) {
   7596         final int match = sUriMatcher.match(uri);
   7597         switch (match) {
   7598             case CONTACTS:
   7599                 return Contacts.CONTENT_TYPE;
   7600             case CONTACTS_LOOKUP:
   7601             case CONTACTS_ID:
   7602             case CONTACTS_LOOKUP_ID:
   7603             case PROFILE:
   7604                 return Contacts.CONTENT_ITEM_TYPE;
   7605             case CONTACTS_AS_VCARD:
   7606             case CONTACTS_AS_MULTI_VCARD:
   7607             case PROFILE_AS_VCARD:
   7608                 return Contacts.CONTENT_VCARD_TYPE;
   7609             case CONTACTS_ID_PHOTO:
   7610             case CONTACTS_LOOKUP_PHOTO:
   7611             case CONTACTS_LOOKUP_ID_PHOTO:
   7612             case CONTACTS_ID_DISPLAY_PHOTO:
   7613             case CONTACTS_LOOKUP_DISPLAY_PHOTO:
   7614             case CONTACTS_LOOKUP_ID_DISPLAY_PHOTO:
   7615             case RAW_CONTACTS_ID_DISPLAY_PHOTO:
   7616             case DISPLAY_PHOTO_ID:
   7617                 return "image/jpeg";
   7618             case RAW_CONTACTS:
   7619             case PROFILE_RAW_CONTACTS:
   7620                 return RawContacts.CONTENT_TYPE;
   7621             case RAW_CONTACTS_ID:
   7622             case PROFILE_RAW_CONTACTS_ID:
   7623                 return RawContacts.CONTENT_ITEM_TYPE;
   7624             case DATA:
   7625             case PROFILE_DATA:
   7626                 return Data.CONTENT_TYPE;
   7627             case DATA_ID:
   7628                 // We need db access for this.
   7629                 waitForAccess(mReadAccessLatch);
   7630 
   7631                 long id = ContentUris.parseId(uri);
   7632                 if (ContactsContract.isProfileId(id)) {
   7633                     return mProfileHelper.getDataMimeType(id);
   7634                 } else {
   7635                     return mContactsHelper.getDataMimeType(id);
   7636                 }
   7637             case PHONES:
   7638                 return Phone.CONTENT_TYPE;
   7639             case PHONES_ID:
   7640                 return Phone.CONTENT_ITEM_TYPE;
   7641             case PHONE_LOOKUP:
   7642                 return PhoneLookup.CONTENT_TYPE;
   7643             case EMAILS:
   7644                 return Email.CONTENT_TYPE;
   7645             case EMAILS_ID:
   7646                 return Email.CONTENT_ITEM_TYPE;
   7647             case POSTALS:
   7648                 return StructuredPostal.CONTENT_TYPE;
   7649             case POSTALS_ID:
   7650                 return StructuredPostal.CONTENT_ITEM_TYPE;
   7651             case AGGREGATION_EXCEPTIONS:
   7652                 return AggregationExceptions.CONTENT_TYPE;
   7653             case AGGREGATION_EXCEPTION_ID:
   7654                 return AggregationExceptions.CONTENT_ITEM_TYPE;
   7655             case SETTINGS:
   7656                 return Settings.CONTENT_TYPE;
   7657             case AGGREGATION_SUGGESTIONS:
   7658                 return Contacts.CONTENT_TYPE;
   7659             case SEARCH_SUGGESTIONS:
   7660                 return SearchManager.SUGGEST_MIME_TYPE;
   7661             case SEARCH_SHORTCUT:
   7662                 return SearchManager.SHORTCUT_MIME_TYPE;
   7663             case DIRECTORIES:
   7664                 return Directory.CONTENT_TYPE;
   7665             case DIRECTORIES_ID:
   7666                 return Directory.CONTENT_ITEM_TYPE;
   7667             case STREAM_ITEMS:
   7668                 return StreamItems.CONTENT_TYPE;
   7669             case STREAM_ITEMS_ID:
   7670                 return StreamItems.CONTENT_ITEM_TYPE;
   7671             case STREAM_ITEMS_ID_PHOTOS:
   7672                 return StreamItems.StreamItemPhotos.CONTENT_TYPE;
   7673             case STREAM_ITEMS_ID_PHOTOS_ID:
   7674                 return StreamItems.StreamItemPhotos.CONTENT_ITEM_TYPE;
   7675             case STREAM_ITEMS_PHOTOS:
   7676                 throw new UnsupportedOperationException("Not supported for write-only URI " + uri);
   7677             default:
   7678                 waitForAccess(mReadAccessLatch);
   7679                 return mLegacyApiSupport.getType(uri);
   7680         }
   7681     }
   7682 
   7683     public String[] getDefaultProjection(Uri uri) {
   7684         final int match = sUriMatcher.match(uri);
   7685         switch (match) {
   7686             case CONTACTS:
   7687             case CONTACTS_LOOKUP:
   7688             case CONTACTS_ID:
   7689             case CONTACTS_LOOKUP_ID:
   7690             case AGGREGATION_SUGGESTIONS:
   7691             case PROFILE:
   7692                 return sContactsProjectionMap.getColumnNames();
   7693 
   7694             case CONTACTS_ID_ENTITIES:
   7695             case PROFILE_ENTITIES:
   7696                 return sEntityProjectionMap.getColumnNames();
   7697 
   7698             case CONTACTS_AS_VCARD:
   7699             case CONTACTS_AS_MULTI_VCARD:
   7700             case PROFILE_AS_VCARD:
   7701                 return sContactsVCardProjectionMap.getColumnNames();
   7702 
   7703             case RAW_CONTACTS:
   7704             case RAW_CONTACTS_ID:
   7705             case PROFILE_RAW_CONTACTS:
   7706             case PROFILE_RAW_CONTACTS_ID:
   7707                 return sRawContactsProjectionMap.getColumnNames();
   7708 
   7709             case DATA_ID:
   7710             case PHONES:
   7711             case PHONES_ID:
   7712             case EMAILS:
   7713             case EMAILS_ID:
   7714             case POSTALS:
   7715             case POSTALS_ID:
   7716             case PROFILE_DATA:
   7717                 return sDataProjectionMap.getColumnNames();
   7718 
   7719             case PHONE_LOOKUP:
   7720                 return sPhoneLookupProjectionMap.getColumnNames();
   7721 
   7722             case AGGREGATION_EXCEPTIONS:
   7723             case AGGREGATION_EXCEPTION_ID:
   7724                 return sAggregationExceptionsProjectionMap.getColumnNames();
   7725 
   7726             case SETTINGS:
   7727                 return sSettingsProjectionMap.getColumnNames();
   7728 
   7729             case DIRECTORIES:
   7730             case DIRECTORIES_ID:
   7731                 return sDirectoryProjectionMap.getColumnNames();
   7732 
   7733             default:
   7734                 return null;
   7735         }
   7736     }
   7737 
   7738     private class StructuredNameLookupBuilder extends NameLookupBuilder {
   7739 
   7740         public StructuredNameLookupBuilder(NameSplitter splitter) {
   7741             super(splitter);
   7742         }
   7743 
   7744         @Override
   7745         protected void insertNameLookup(long rawContactId, long dataId, int lookupType,
   7746                 String name) {
   7747             mDbHelper.get().insertNameLookup(rawContactId, dataId, lookupType, name);
   7748         }
   7749 
   7750         @Override
   7751         protected String[] getCommonNicknameClusters(String normalizedName) {
   7752             return mCommonNicknameCache.getCommonNicknameClusters(normalizedName);
   7753         }
   7754     }
   7755 
   7756     public void appendContactFilterAsNestedQuery(StringBuilder sb, String filterParam) {
   7757         sb.append("(" +
   7758                 "SELECT DISTINCT " + RawContacts.CONTACT_ID +
   7759                 " FROM " + Tables.RAW_CONTACTS +
   7760                 " JOIN " + Tables.NAME_LOOKUP +
   7761                 " ON(" + RawContactsColumns.CONCRETE_ID + "="
   7762                         + NameLookupColumns.RAW_CONTACT_ID + ")" +
   7763                 " WHERE normalized_name GLOB '");
   7764         sb.append(NameNormalizer.normalize(filterParam));
   7765         sb.append("*' AND " + NameLookupColumns.NAME_TYPE +
   7766                     " IN(" + CONTACT_LOOKUP_NAME_TYPES + "))");
   7767     }
   7768 
   7769     public boolean isPhoneNumber(String query) {
   7770         if (TextUtils.isEmpty(query)) {
   7771             return false;
   7772         }
   7773         // assume a phone number if it has at least 1 digit
   7774         return countPhoneNumberDigits(query) > 0;
   7775     }
   7776 
   7777     /**
   7778      * Returns the number of digitis in a phone number ignoring special characters such as '-'.
   7779      * If the string is not a valid phone number, 0 is returned.
   7780      */
   7781     public static int countPhoneNumberDigits(String query) {
   7782         int numDigits = 0;
   7783         int len = query.length();
   7784         for (int i = 0; i < len; i++) {
   7785             char c = query.charAt(i);
   7786             if (Character.isDigit(c)) {
   7787                 numDigits ++;
   7788             } else if (c == '*' || c == '#' || c == 'N' || c == '.' || c == ';'
   7789                     || c == '-' || c == '(' || c == ')' || c == ' ') {
   7790                 // carry on
   7791             } else if (c == '+' && numDigits == 0) {
   7792                 // plus before any digits is ok
   7793             } else {
   7794                 return 0; // not a phone number
   7795             }
   7796         }
   7797         return numDigits;
   7798     }
   7799 
   7800     /**
   7801      * Takes components of a name from the query parameters and returns a cursor with those
   7802      * components as well as all missing components.  There is no database activity involved
   7803      * in this so the call can be made on the UI thread.
   7804      */
   7805     private Cursor completeName(Uri uri, String[] projection) {
   7806         if (projection == null) {
   7807             projection = sDataProjectionMap.getColumnNames();
   7808         }
   7809 
   7810         ContentValues values = new ContentValues();
   7811         DataRowHandlerForStructuredName handler = (DataRowHandlerForStructuredName)
   7812                 getDataRowHandler(StructuredName.CONTENT_ITEM_TYPE);
   7813 
   7814         copyQueryParamsToContentValues(values, uri,
   7815                 StructuredName.DISPLAY_NAME,
   7816                 StructuredName.PREFIX,
   7817                 StructuredName.GIVEN_NAME,
   7818                 StructuredName.MIDDLE_NAME,
   7819                 StructuredName.FAMILY_NAME,
   7820                 StructuredName.SUFFIX,
   7821                 StructuredName.PHONETIC_NAME,
   7822                 StructuredName.PHONETIC_FAMILY_NAME,
   7823                 StructuredName.PHONETIC_MIDDLE_NAME,
   7824                 StructuredName.PHONETIC_GIVEN_NAME
   7825         );
   7826 
   7827         handler.fixStructuredNameComponents(values, values);
   7828 
   7829         MatrixCursor cursor = new MatrixCursor(projection);
   7830         Object[] row = new Object[projection.length];
   7831         for (int i = 0; i < projection.length; i++) {
   7832             row[i] = values.get(projection[i]);
   7833         }
   7834         cursor.addRow(row);
   7835         return cursor;
   7836     }
   7837 
   7838     private void copyQueryParamsToContentValues(ContentValues values, Uri uri, String... columns) {
   7839         for (String column : columns) {
   7840             String param = uri.getQueryParameter(column);
   7841             if (param != null) {
   7842                 values.put(column, param);
   7843             }
   7844         }
   7845     }
   7846 
   7847 
   7848     /**
   7849      * Inserts an argument at the beginning of the selection arg list.
   7850      */
   7851     private String[] insertSelectionArg(String[] selectionArgs, String arg) {
   7852         if (selectionArgs == null) {
   7853             return new String[] {arg};
   7854         } else {
   7855             int newLength = selectionArgs.length + 1;
   7856             String[] newSelectionArgs = new String[newLength];
   7857             newSelectionArgs[0] = arg;
   7858             System.arraycopy(selectionArgs, 0, newSelectionArgs, 1, selectionArgs.length);
   7859             return newSelectionArgs;
   7860         }
   7861     }
   7862 
   7863     protected Account getDefaultAccount() {
   7864         AccountManager accountManager = AccountManager.get(getContext());
   7865         try {
   7866             Account[] accounts = accountManager.getAccountsByType(DEFAULT_ACCOUNT_TYPE);
   7867             if (accounts != null && accounts.length > 0) {
   7868                 return accounts[0];
   7869             }
   7870         } catch (Throwable e) {
   7871             Log.e(TAG, "Cannot determine the default account for contacts compatibility", e);
   7872         }
   7873         return null;
   7874     }
   7875 
   7876     /**
   7877      * Returns true if the specified account type and data set is writable.
   7878      */
   7879     public boolean isWritableAccountWithDataSet(String accountTypeAndDataSet) {
   7880         if (accountTypeAndDataSet == null) {
   7881             return true;
   7882         }
   7883 
   7884         Boolean writable = mAccountWritability.get(accountTypeAndDataSet);
   7885         if (writable != null) {
   7886             return writable;
   7887         }
   7888 
   7889         IContentService contentService = ContentResolver.getContentService();
   7890         try {
   7891             // TODO(dsantoro): Need to update this logic to allow for sub-accounts.
   7892             for (SyncAdapterType sync : contentService.getSyncAdapterTypes()) {
   7893                 if (ContactsContract.AUTHORITY.equals(sync.authority) &&
   7894                         accountTypeAndDataSet.equals(sync.accountType)) {
   7895                     writable = sync.supportsUploading();
   7896                     break;
   7897                 }
   7898             }
   7899         } catch (RemoteException e) {
   7900             Log.e(TAG, "Could not acquire sync adapter types");
   7901         }
   7902 
   7903         if (writable == null) {
   7904             writable = false;
   7905         }
   7906 
   7907         mAccountWritability.put(accountTypeAndDataSet, writable);
   7908         return writable;
   7909     }
   7910 
   7911 
   7912     /* package */ static boolean readBooleanQueryParameter(Uri uri, String parameter,
   7913             boolean defaultValue) {
   7914 
   7915         // Manually parse the query, which is much faster than calling uri.getQueryParameter
   7916         String query = uri.getEncodedQuery();
   7917         if (query == null) {
   7918             return defaultValue;
   7919         }
   7920 
   7921         int index = query.indexOf(parameter);
   7922         if (index == -1) {
   7923             return defaultValue;
   7924         }
   7925 
   7926         index += parameter.length();
   7927 
   7928         return !matchQueryParameter(query, index, "=0", false)
   7929                 && !matchQueryParameter(query, index, "=false", true);
   7930     }
   7931 
   7932     private static boolean matchQueryParameter(String query, int index, String value,
   7933             boolean ignoreCase) {
   7934         int length = value.length();
   7935         return query.regionMatches(ignoreCase, index, value, 0, length)
   7936                 && (query.length() == index + length || query.charAt(index + length) == '&');
   7937     }
   7938 
   7939     /**
   7940      * A fast re-implementation of {@link Uri#getQueryParameter}
   7941      */
   7942     /* package */ static String getQueryParameter(Uri uri, String parameter) {
   7943         String query = uri.getEncodedQuery();
   7944         if (query == null) {
   7945             return null;
   7946         }
   7947 
   7948         int queryLength = query.length();
   7949         int parameterLength = parameter.length();
   7950 
   7951         String value;
   7952         int index = 0;
   7953         while (true) {
   7954             index = query.indexOf(parameter, index);
   7955             if (index == -1) {
   7956                 return null;
   7957             }
   7958 
   7959             // Should match against the whole parameter instead of its suffix.
   7960             // e.g. The parameter "param" must not be found in "some_param=val".
   7961             if (index > 0) {
   7962                 char prevChar = query.charAt(index - 1);
   7963                 if (prevChar != '?' && prevChar != '&') {
   7964                     // With "some_param=val1&param=val2", we should find second "param" occurrence.
   7965                     index += parameterLength;
   7966                     continue;
   7967                 }
   7968             }
   7969 
   7970             index += parameterLength;
   7971 
   7972             if (queryLength == index) {
   7973                 return null;
   7974             }
   7975 
   7976             if (query.charAt(index) == '=') {
   7977                 index++;
   7978                 break;
   7979             }
   7980         }
   7981 
   7982         int ampIndex = query.indexOf('&', index);
   7983         if (ampIndex == -1) {
   7984             value = query.substring(index);
   7985         } else {
   7986             value = query.substring(index, ampIndex);
   7987         }
   7988 
   7989         return Uri.decode(value);
   7990     }
   7991 
   7992     protected boolean isAggregationUpgradeNeeded() {
   7993         if (!mContactAggregator.isEnabled()) {
   7994             return false;
   7995         }
   7996 
   7997         int version = Integer.parseInt(mContactsHelper.getProperty(
   7998                 DbProperties.AGGREGATION_ALGORITHM, "1"));
   7999         return version < PROPERTY_AGGREGATION_ALGORITHM_VERSION;
   8000     }
   8001 
   8002     protected void upgradeAggregationAlgorithmInBackground() {
   8003         Log.i(TAG, "Upgrading aggregation algorithm");
   8004 
   8005         final long start = SystemClock.elapsedRealtime();
   8006         setProviderStatus(ProviderStatus.STATUS_UPGRADING);
   8007 
   8008         // Re-aggregate all visible raw contacts.
   8009         try {
   8010             int count = 0;
   8011             SQLiteDatabase db = null;
   8012             boolean success = false;
   8013             boolean transactionStarted = false;
   8014             try {
   8015                 // Re-aggregation is only for the contacts DB.
   8016                 switchToContactMode();
   8017                 db = mContactsHelper.getWritableDatabase();
   8018 
   8019                 // Start the actual process.
   8020                 db.beginTransaction();
   8021                 transactionStarted = true;
   8022 
   8023                 count = mContactAggregator.markAllVisibleForAggregation(db);
   8024                 mContactAggregator.aggregateInTransaction(mTransactionContext.get(), db);
   8025 
   8026                 updateSearchIndexInTransaction();
   8027 
   8028                 updateAggregationAlgorithmVersion();
   8029 
   8030                 db.setTransactionSuccessful();
   8031 
   8032                 success = true;
   8033             } finally {
   8034                 mTransactionContext.get().clearAll();
   8035                 if (transactionStarted) {
   8036                     db.endTransaction();
   8037                 }
   8038                 final long end = SystemClock.elapsedRealtime();
   8039                 Log.i(TAG, "Aggregation algorithm upgraded for " + count + " raw contacts"
   8040                         + (success ? (" in " + (end - start) + "ms") : " failed"));
   8041             }
   8042         } catch (RuntimeException e) {
   8043             Log.e(TAG, "Failed to upgrade aggregation algorithm; continuing anyway.", e);
   8044 
   8045             // Got some exception during re-aggregation.  Re-aggregation isn't that important, so
   8046             // just bump the aggregation algorithm version and let the provider start normally.
   8047             try {
   8048                 final SQLiteDatabase db =  mContactsHelper.getWritableDatabase();
   8049                 db.beginTransaction();
   8050                 try {
   8051                     updateAggregationAlgorithmVersion();
   8052                     db.setTransactionSuccessful();
   8053                 } finally {
   8054                     db.endTransaction();
   8055                 }
   8056             } catch (RuntimeException e2) {
   8057                 // Couldn't even update the algorithm version...  There's really nothing we can do
   8058                 // here, so just go ahead and start the provider.  Next time the provider starts
   8059                 // it'll try re-aggregation again, which may or may not succeed.
   8060                 Log.e(TAG, "Failed to bump aggregation algorithm version; continuing anyway.", e2);
   8061             }
   8062         } finally { // Need one more finally because endTransaction() may fail.
   8063             setProviderStatus(ProviderStatus.STATUS_NORMAL);
   8064         }
   8065     }
   8066 
   8067     private void updateAggregationAlgorithmVersion() {
   8068         mContactsHelper.setProperty(DbProperties.AGGREGATION_ALGORITHM,
   8069                 String.valueOf(PROPERTY_AGGREGATION_ALGORITHM_VERSION));
   8070     }
   8071 
   8072     @VisibleForTesting
   8073     boolean isPhone() {
   8074         if (!mIsPhoneInitialized) {
   8075             mIsPhone = new TelephonyManager(getContext()).isVoiceCapable();
   8076             mIsPhoneInitialized = true;
   8077         }
   8078         return mIsPhone;
   8079     }
   8080 
   8081     boolean isVoiceCapable() {
   8082         // this copied from com.android.phone.PhoneApp.onCreate():
   8083 
   8084         // "voice capable" flag.
   8085         // This flag currently comes from a resource (which is
   8086         // overrideable on a per-product basis):
   8087         return getContext().getResources()
   8088                 .getBoolean(com.android.internal.R.bool.config_voice_capable);
   8089         // ...but this might eventually become a PackageManager "system
   8090         // feature" instead, in which case we'd do something like:
   8091         // return
   8092         //   getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELEPHONY_VOICE_CALLS);
   8093     }
   8094 
   8095     private boolean handleDataUsageFeedback(Uri uri) {
   8096         final long currentTimeMillis = Clock.getInstance().currentTimeMillis();
   8097         final String usageType = uri.getQueryParameter(DataUsageFeedback.USAGE_TYPE);
   8098         final String[] ids = uri.getLastPathSegment().trim().split(",");
   8099         final ArrayList<Long> dataIds = new ArrayList<Long>(ids.length);
   8100 
   8101         for (String id : ids) {
   8102             dataIds.add(Long.valueOf(id));
   8103         }
   8104         final boolean successful;
   8105         if (TextUtils.isEmpty(usageType)) {
   8106             Log.w(TAG, "Method for data usage feedback isn't specified. Ignoring.");
   8107             successful = false;
   8108         } else {
   8109             successful = updateDataUsageStat(dataIds, usageType, currentTimeMillis) > 0;
   8110         }
   8111 
   8112         // Handle old API. This doesn't affect the result of this entire method.
   8113         final StringBuilder rawContactIdSelect = new StringBuilder();
   8114         rawContactIdSelect.append("SELECT " + Data.RAW_CONTACT_ID + " FROM " + Tables.DATA +
   8115                 " WHERE " + Data._ID + " IN (");
   8116         for (int i = 0; i < ids.length; i++) {
   8117             if (i > 0) rawContactIdSelect.append(",");
   8118             rawContactIdSelect.append(ids[i]);
   8119         }
   8120         rawContactIdSelect.append(")");
   8121 
   8122         final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
   8123 
   8124         mSelectionArgs1[0] = String.valueOf(currentTimeMillis);
   8125 
   8126         db.execSQL("UPDATE " + Tables.RAW_CONTACTS +
   8127                 " SET " + RawContacts.LAST_TIME_CONTACTED + "=?" +
   8128                 "," + RawContacts.TIMES_CONTACTED + "=" +
   8129                     "ifnull(" + RawContacts.TIMES_CONTACTED + ",0) + 1" +
   8130                 " WHERE " + RawContacts._ID + " IN (" + rawContactIdSelect.toString() + ")"
   8131                 , mSelectionArgs1);
   8132         db.execSQL("UPDATE " + Tables.CONTACTS +
   8133                 " SET " + Contacts.LAST_TIME_CONTACTED + "=?" +
   8134                 "," + Contacts.TIMES_CONTACTED + "=" +
   8135                     "ifnull(" + Contacts.TIMES_CONTACTED + ",0) + 1" +
   8136                 " WHERE " + Contacts._ID + " IN (SELECT " + RawContacts.CONTACT_ID +
   8137                     " FROM " + Tables.RAW_CONTACTS +
   8138                     " WHERE " + RawContacts._ID + " IN (" + rawContactIdSelect.toString() + "))"
   8139                 , mSelectionArgs1);
   8140         return successful;
   8141     }
   8142 
   8143     private interface DataUsageStatQuery {
   8144         String TABLE = Tables.DATA_USAGE_STAT;
   8145 
   8146         String[] COLUMNS = new String[] {
   8147                 DataUsageStatColumns._ID,
   8148         };
   8149         int ID = 0;
   8150 
   8151         String SELECTION = DataUsageStatColumns.DATA_ID + " =? AND "
   8152                 + DataUsageStatColumns.USAGE_TYPE_INT + " =?";
   8153     }
   8154 
   8155     /**
   8156      * Update {@link Tables#DATA_USAGE_STAT}.
   8157      *
   8158      * @return the number of rows affected.
   8159      */
   8160     @VisibleForTesting
   8161     /* package */ int updateDataUsageStat(
   8162             List<Long> dataIds, String type, long currentTimeMillis) {
   8163 
   8164         final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
   8165 
   8166         final String typeString = String.valueOf(getDataUsageFeedbackType(type, null));
   8167         final String currentTimeMillisString = String.valueOf(currentTimeMillis);
   8168 
   8169         for (long dataId : dataIds) {
   8170             final String dataIdString = String.valueOf(dataId);
   8171             mSelectionArgs2[0] = dataIdString;
   8172             mSelectionArgs2[1] = typeString;
   8173             final Cursor cursor = db.query(DataUsageStatQuery.TABLE,
   8174                     DataUsageStatQuery.COLUMNS, DataUsageStatQuery.SELECTION,
   8175                     mSelectionArgs2, null, null, null);
   8176             try {
   8177                 if (cursor.moveToFirst()) {
   8178                     final long id = cursor.getLong(DataUsageStatQuery.ID);
   8179 
   8180                     mSelectionArgs2[0] = currentTimeMillisString;
   8181                     mSelectionArgs2[1] = String.valueOf(id);
   8182 
   8183                     db.execSQL("UPDATE " + Tables.DATA_USAGE_STAT +
   8184                             " SET " + DataUsageStatColumns.TIMES_USED + "=" +
   8185                                 "ifnull(" + DataUsageStatColumns.TIMES_USED +",0)+1" +
   8186                             "," + DataUsageStatColumns.LAST_TIME_USED + "=?" +
   8187                             " WHERE " + DataUsageStatColumns._ID + "=?",
   8188                             mSelectionArgs2);
   8189                 } else {
   8190                     mSelectionArgs4[0] = dataIdString;
   8191                     mSelectionArgs4[1] = typeString;
   8192                     mSelectionArgs4[2] = "1"; // times used
   8193                     mSelectionArgs4[3] = currentTimeMillisString;
   8194                     db.execSQL("INSERT INTO " + Tables.DATA_USAGE_STAT +
   8195                             "(" + DataUsageStatColumns.DATA_ID +
   8196                             "," + DataUsageStatColumns.USAGE_TYPE_INT +
   8197                             "," + DataUsageStatColumns.TIMES_USED +
   8198                             "," + DataUsageStatColumns.LAST_TIME_USED +
   8199                             ") VALUES (?,?,?,?)",
   8200                             mSelectionArgs4);
   8201                 }
   8202             } finally {
   8203                 cursor.close();
   8204             }
   8205         }
   8206 
   8207         return dataIds.size();
   8208     }
   8209 
   8210     /**
   8211      * Returns a sort order String for promoting data rows (email addresses, phone numbers, etc.)
   8212      * associated with a primary account. The primary account should be supplied from applications
   8213      * with {@link ContactsContract#PRIMARY_ACCOUNT_NAME} and
   8214      * {@link ContactsContract#PRIMARY_ACCOUNT_TYPE}. Null will be returned when the primary
   8215      * account isn't available.
   8216      */
   8217     private String getAccountPromotionSortOrder(Uri uri) {
   8218         final String primaryAccountName =
   8219                 uri.getQueryParameter(ContactsContract.PRIMARY_ACCOUNT_NAME);
   8220         final String primaryAccountType =
   8221                 uri.getQueryParameter(ContactsContract.PRIMARY_ACCOUNT_TYPE);
   8222 
   8223         // Data rows associated with primary account should be promoted.
   8224         if (!TextUtils.isEmpty(primaryAccountName)) {
   8225             StringBuilder sb = new StringBuilder();
   8226             sb.append("(CASE WHEN " + RawContacts.ACCOUNT_NAME + "=");
   8227             DatabaseUtils.appendEscapedSQLString(sb, primaryAccountName);
   8228             if (!TextUtils.isEmpty(primaryAccountType)) {
   8229                 sb.append(" AND " + RawContacts.ACCOUNT_TYPE + "=");
   8230                 DatabaseUtils.appendEscapedSQLString(sb, primaryAccountType);
   8231             }
   8232             sb.append(" THEN 0 ELSE 1 END)");
   8233             return sb.toString();
   8234         } else {
   8235             return null;
   8236         }
   8237     }
   8238 
   8239     /**
   8240      * Checks the URI for a deferred snippeting request
   8241      * @return a boolean indicating if a deferred snippeting request is in the RI
   8242      */
   8243     private boolean deferredSnippetingRequested(Uri uri) {
   8244         String deferredSnippeting =
   8245             getQueryParameter(uri, SearchSnippetColumns.DEFERRED_SNIPPETING_KEY);
   8246         return !TextUtils.isEmpty(deferredSnippeting) &&  deferredSnippeting.equals("1");
   8247     }
   8248 
   8249     /**
   8250      * Checks if query is a single word or not.
   8251      * @return a boolean indicating if the query is one word or not
   8252      */
   8253     private boolean isSingleWordQuery(String query) {
   8254         return query.split(QUERY_TOKENIZER_REGEX).length == 1;
   8255     }
   8256 
   8257     /**
   8258      * Checks the projection for a SNIPPET column indicating that a snippet is needed
   8259      * @return a boolean indicating if a snippet is needed or not.
   8260      */
   8261     private boolean snippetNeeded(String [] projection) {
   8262         return ContactsDatabaseHelper.isInProjection(projection, SearchSnippetColumns.SNIPPET);
   8263     }
   8264 
   8265     /**
   8266      * Create a single row cursor for a simple, informational queries, such as
   8267      * {@link ProviderStatus#CONTENT_URI}.
   8268      */
   8269     @VisibleForTesting
   8270     static Cursor buildSingleRowResult(String[] projection, String[] availableColumns,
   8271             Object[] data) {
   8272         Preconditions.checkArgument(availableColumns.length == data.length);
   8273         if (projection == null) {
   8274             projection = availableColumns;
   8275         }
   8276         final MatrixCursor c = new MatrixCursor(projection, 1);
   8277         final RowBuilder row = c.newRow();
   8278 
   8279         // It's O(n^2), but it's okay because we only have a few columns.
   8280         for (int i = 0; i < c.getColumnCount(); i++) {
   8281             final String column = c.getColumnName(i);
   8282 
   8283             boolean found = false;
   8284             for (int j = 0; j < availableColumns.length; j++) {
   8285                 if (availableColumns[j].equals(column)) {
   8286                     row.add(data[j]);
   8287                     found = true;
   8288                     break;
   8289                 }
   8290             }
   8291             if (!found) {
   8292                 throw new IllegalArgumentException("Invalid column " + projection[i]);
   8293             }
   8294         }
   8295         return c;
   8296     }
   8297 
   8298     /**
   8299      * @return the currently active {@link ContactsDatabaseHelper} for the current thread.
   8300      */
   8301     @NeededForTesting
   8302     public ContactsDatabaseHelper getThreadActiveDatabaseHelperForTest() {
   8303         return mDbHelper.get();
   8304     }
   8305 
   8306     @Override
   8307     public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
   8308         pw.print("FastScrollingIndex stats:\n");
   8309         pw.printf("request=%d  miss=%d (%d%%)  avg time=%dms\n",
   8310                 mFastScrollingIndexCacheRequestCount,
   8311                 mFastScrollingIndexCacheMissCount,
   8312                 safeDiv(mFastScrollingIndexCacheMissCount * 100,
   8313                         mFastScrollingIndexCacheRequestCount),
   8314                 safeDiv(mTotalTimeFastScrollingIndexGenerate, mFastScrollingIndexCacheMissCount)
   8315                 );
   8316     }
   8317 
   8318     private static final long safeDiv(long dividend, long divisor) {
   8319         return (divisor == 0) ? 0 : dividend / divisor;
   8320     }
   8321 
   8322     private static final int getDataUsageFeedbackType(String type, Integer defaultType) {
   8323         if (DataUsageFeedback.USAGE_TYPE_CALL.equals(type)) {
   8324             return DataUsageStatColumns.USAGE_TYPE_INT_CALL; // 0
   8325         }
   8326         if (DataUsageFeedback.USAGE_TYPE_LONG_TEXT.equals(type)) {
   8327             return DataUsageStatColumns.USAGE_TYPE_INT_LONG_TEXT; // 1
   8328         }
   8329         if (DataUsageFeedback.USAGE_TYPE_SHORT_TEXT.equals(type)) {
   8330             return DataUsageStatColumns.USAGE_TYPE_INT_SHORT_TEXT; // 2
   8331         }
   8332         if (defaultType != null) {
   8333             return defaultType;
   8334         }
   8335         throw new IllegalArgumentException("Invalid usage type " + type);
   8336     }
   8337 
   8338     /** Use only for debug logging */
   8339     @Override
   8340     public String toString() {
   8341         return "ContactsProvider2";
   8342     }
   8343 
   8344     @NeededForTesting
   8345     public void switchToProfileModeForTest() {
   8346         switchToProfileMode();
   8347     }
   8348 }
   8349