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