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