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