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