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