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