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.DatabaseUtils;
     44 import android.database.MatrixCursor;
     45 import android.database.MatrixCursor.RowBuilder;
     46 import android.database.sqlite.SQLiteDatabase;
     47 import android.database.sqlite.SQLiteDoneException;
     48 import android.database.sqlite.SQLiteQueryBuilder;
     49 import android.graphics.Bitmap;
     50 import android.graphics.BitmapFactory;
     51 import android.net.Uri;
     52 import android.net.Uri.Builder;
     53 import android.os.AsyncTask;
     54 import android.os.Binder;
     55 import android.os.Bundle;
     56 import android.os.CancellationSignal;
     57 import android.os.Handler;
     58 import android.os.HandlerThread;
     59 import android.os.Message;
     60 import android.os.ParcelFileDescriptor;
     61 import android.os.ParcelFileDescriptor.AutoCloseInputStream;
     62 import android.os.Process;
     63 import android.os.RemoteException;
     64 import android.os.StrictMode;
     65 import android.os.SystemClock;
     66 import android.os.SystemProperties;
     67 import android.preference.PreferenceManager;
     68 import android.provider.BaseColumns;
     69 import android.provider.ContactsContract;
     70 import android.provider.ContactsContract.AggregationExceptions;
     71 import android.provider.ContactsContract.Authorization;
     72 import android.provider.ContactsContract.CommonDataKinds.Contactables;
     73 import android.provider.ContactsContract.CommonDataKinds.Email;
     74 import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
     75 import android.provider.ContactsContract.CommonDataKinds.Identity;
     76 import android.provider.ContactsContract.CommonDataKinds.Im;
     77 import android.provider.ContactsContract.CommonDataKinds.Nickname;
     78 import android.provider.ContactsContract.CommonDataKinds.Note;
     79 import android.provider.ContactsContract.CommonDataKinds.Organization;
     80 import android.provider.ContactsContract.CommonDataKinds.Phone;
     81 import android.provider.ContactsContract.CommonDataKinds.Photo;
     82 import android.provider.ContactsContract.CommonDataKinds.SipAddress;
     83 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
     84 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
     85 import android.provider.ContactsContract.Contacts;
     86 import android.provider.ContactsContract.Contacts.AggregationSuggestions;
     87 import android.provider.ContactsContract.Data;
     88 import android.provider.ContactsContract.DataUsageFeedback;
     89 import android.provider.ContactsContract.DeletedContacts;
     90 import android.provider.ContactsContract.Directory;
     91 import android.provider.ContactsContract.DisplayPhoto;
     92 import android.provider.ContactsContract.Groups;
     93 import android.provider.ContactsContract.PhoneLookup;
     94 import android.provider.ContactsContract.PhotoFiles;
     95 import android.provider.ContactsContract.PinnedPositions;
     96 import android.provider.ContactsContract.Profile;
     97 import android.provider.ContactsContract.ProviderStatus;
     98 import android.provider.ContactsContract.RawContacts;
     99 import android.provider.ContactsContract.RawContactsEntity;
    100 import android.provider.ContactsContract.SearchSnippets;
    101 import android.provider.ContactsContract.Settings;
    102 import android.provider.ContactsContract.StatusUpdates;
    103 import android.provider.ContactsContract.StreamItemPhotos;
    104 import android.provider.ContactsContract.StreamItems;
    105 import android.provider.OpenableColumns;
    106 import android.provider.SyncStateContract;
    107 import android.telephony.PhoneNumberUtils;
    108 import android.telephony.TelephonyManager;
    109 import android.text.TextUtils;
    110 import android.util.Log;
    111 
    112 import com.android.common.content.ProjectionMap;
    113 import com.android.common.content.SyncStateContentProviderHelper;
    114 import com.android.common.io.MoreCloseables;
    115 import com.android.internal.util.ArrayUtils;
    116 import com.android.providers.contacts.ContactLookupKey.LookupKeySegment;
    117 import com.android.providers.contacts.ContactsDatabaseHelper.AccountsColumns;
    118 import com.android.providers.contacts.ContactsDatabaseHelper.AggregatedPresenceColumns;
    119 import com.android.providers.contacts.ContactsDatabaseHelper.AggregationExceptionColumns;
    120 import com.android.providers.contacts.ContactsDatabaseHelper.Clauses;
    121 import com.android.providers.contacts.ContactsDatabaseHelper.ContactsColumns;
    122 import com.android.providers.contacts.ContactsDatabaseHelper.ContactsStatusUpdatesColumns;
    123 import com.android.providers.contacts.ContactsDatabaseHelper.DataColumns;
    124 import com.android.providers.contacts.ContactsDatabaseHelper.DataUsageStatColumns;
    125 import com.android.providers.contacts.ContactsDatabaseHelper.DbProperties;
    126 import com.android.providers.contacts.ContactsDatabaseHelper.GroupsColumns;
    127 import com.android.providers.contacts.ContactsDatabaseHelper.Joins;
    128 import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupColumns;
    129 import com.android.providers.contacts.ContactsDatabaseHelper.NameLookupType;
    130 import com.android.providers.contacts.ContactsDatabaseHelper.PhoneLookupColumns;
    131 import com.android.providers.contacts.ContactsDatabaseHelper.PhotoFilesColumns;
    132 import com.android.providers.contacts.ContactsDatabaseHelper.PresenceColumns;
    133 import com.android.providers.contacts.ContactsDatabaseHelper.Projections;
    134 import com.android.providers.contacts.ContactsDatabaseHelper.RawContactsColumns;
    135 import com.android.providers.contacts.ContactsDatabaseHelper.SearchIndexColumns;
    136 import com.android.providers.contacts.ContactsDatabaseHelper.SettingsColumns;
    137 import com.android.providers.contacts.ContactsDatabaseHelper.StatusUpdatesColumns;
    138 import com.android.providers.contacts.ContactsDatabaseHelper.StreamItemPhotosColumns;
    139 import com.android.providers.contacts.ContactsDatabaseHelper.StreamItemsColumns;
    140 import com.android.providers.contacts.ContactsDatabaseHelper.Tables;
    141 import com.android.providers.contacts.ContactsDatabaseHelper.ViewGroupsColumns;
    142 import com.android.providers.contacts.ContactsDatabaseHelper.Views;
    143 import com.android.providers.contacts.SearchIndexManager.FtsQueryBuilder;
    144 import com.android.providers.contacts.aggregation.ContactAggregator;
    145 import com.android.providers.contacts.aggregation.ContactAggregator.AggregationSuggestionParameter;
    146 import com.android.providers.contacts.aggregation.ProfileAggregator;
    147 import com.android.providers.contacts.aggregation.util.CommonNicknameCache;
    148 import com.android.providers.contacts.database.ContactsTableUtil;
    149 import com.android.providers.contacts.database.DeletedContactsTableUtil;
    150 import com.android.providers.contacts.database.MoreDatabaseUtils;
    151 import com.android.providers.contacts.util.Clock;
    152 import com.android.providers.contacts.util.DbQueryUtils;
    153 import com.android.providers.contacts.util.NeededForTesting;
    154 import com.android.providers.contacts.util.UserUtils;
    155 import com.android.vcard.VCardComposer;
    156 import com.android.vcard.VCardConfig;
    157 
    158 import com.google.android.collect.Lists;
    159 import com.google.android.collect.Maps;
    160 import com.google.android.collect.Sets;
    161 import com.google.common.annotations.VisibleForTesting;
    162 import com.google.common.base.Preconditions;
    163 
    164 import libcore.io.IoUtils;
    165 
    166 import java.io.BufferedWriter;
    167 import java.io.ByteArrayOutputStream;
    168 import java.io.File;
    169 import java.io.FileDescriptor;
    170 import java.io.FileNotFoundException;
    171 import java.io.IOException;
    172 import java.io.OutputStream;
    173 import java.io.OutputStreamWriter;
    174 import java.io.PrintWriter;
    175 import java.io.Writer;
    176 import java.security.SecureRandom;
    177 import java.text.SimpleDateFormat;
    178 import java.util.ArrayList;
    179 import java.util.Arrays;
    180 import java.util.Collections;
    181 import java.util.Date;
    182 import java.util.HashMap;
    183 import java.util.HashSet;
    184 import java.util.List;
    185 import java.util.Locale;
    186 import java.util.Map;
    187 import java.util.Set;
    188 import java.util.concurrent.CountDownLatch;
    189 
    190 /**
    191  * Contacts content provider. The contract between this provider and applications
    192  * is defined in {@link ContactsContract}.
    193  */
    194 public class ContactsProvider2 extends AbstractContactsProvider
    195         implements OnAccountsUpdateListener {
    196 
    197     private static final String WRITE_PERMISSION = "android.permission.WRITE_CONTACTS";
    198 
    199     /* package */ static final String UPDATE_TIMES_CONTACTED_CONTACTS_TABLE =
    200           "UPDATE " + Tables.CONTACTS + " SET " + Contacts.TIMES_CONTACTED + "=" +
    201           " ifnull(" + Contacts.TIMES_CONTACTED + ",0)+1" +
    202           " WHERE " + Contacts._ID + "=?";
    203 
    204     /* package */ static final String UPDATE_TIMES_CONTACTED_RAWCONTACTS_TABLE =
    205           "UPDATE " + Tables.RAW_CONTACTS + " SET " + RawContacts.TIMES_CONTACTED + "=" +
    206           " ifnull(" + RawContacts.TIMES_CONTACTED + ",0)+1 " +
    207           " WHERE " + RawContacts.CONTACT_ID + "=?";
    208 
    209     /* package */ static final String PHONEBOOK_COLLATOR_NAME = "PHONEBOOK";
    210 
    211     // Regex for splitting query strings - we split on any group of non-alphanumeric characters,
    212     // excluding the @ symbol.
    213     /* package */ static final String QUERY_TOKENIZER_REGEX = "[^\\w@]+";
    214 
    215     // The database tag to use for representing the contacts DB in contacts transactions.
    216     /* package */ static final String CONTACTS_DB_TAG = "contacts";
    217 
    218     // The database tag to use for representing the profile DB in contacts transactions.
    219     /* package */ static final String PROFILE_DB_TAG = "profile";
    220 
    221     private static final String ACCOUNT_STRING_SEPARATOR_OUTER = "\u0001";
    222     private static final String ACCOUNT_STRING_SEPARATOR_INNER = "\u0002";
    223 
    224     private static final int BACKGROUND_TASK_INITIALIZE = 0;
    225     private static final int BACKGROUND_TASK_OPEN_WRITE_ACCESS = 1;
    226     private static final int BACKGROUND_TASK_UPDATE_ACCOUNTS = 3;
    227     private static final int BACKGROUND_TASK_UPDATE_LOCALE = 4;
    228     private static final int BACKGROUND_TASK_UPGRADE_AGGREGATION_ALGORITHM = 5;
    229     private static final int BACKGROUND_TASK_UPDATE_SEARCH_INDEX = 6;
    230     private static final int BACKGROUND_TASK_UPDATE_PROVIDER_STATUS = 7;
    231     private static final int BACKGROUND_TASK_UPDATE_DIRECTORIES = 8;
    232     private static final int BACKGROUND_TASK_CHANGE_LOCALE = 9;
    233     private static final int BACKGROUND_TASK_CLEANUP_PHOTOS = 10;
    234     private static final int BACKGROUND_TASK_CLEAN_DELETE_LOG = 11;
    235 
    236     /** Default for the maximum number of returned aggregation suggestions. */
    237     private static final int DEFAULT_MAX_SUGGESTIONS = 5;
    238 
    239     /** Limit for the maximum number of social stream items to store under a raw contact. */
    240     private static final int MAX_STREAM_ITEMS_PER_RAW_CONTACT = 5;
    241 
    242     /** Rate limit (in milliseconds) for photo cleanup.  Do it at most once per day. */
    243     private static final int PHOTO_CLEANUP_RATE_LIMIT = 24 * 60 * 60 * 1000;
    244 
    245     /** Maximum length of a phone number that can be inserted into the database */
    246     private static final int PHONE_NUMBER_LENGTH_LIMIT = 1000;
    247 
    248     /**
    249      * Default expiration duration for pre-authorized URIs.  May be overridden from a secure
    250      * setting.
    251      */
    252     private static final int DEFAULT_PREAUTHORIZED_URI_EXPIRATION = 5 * 60 * 1000;
    253 
    254     private static final int USAGE_TYPE_ALL = -1;
    255 
    256     /**
    257      * Random URI parameter that will be appended to preauthorized URIs for uniqueness.
    258      */
    259     private static final String PREAUTHORIZED_URI_TOKEN = "perm_token";
    260 
    261     private static final String PREF_LOCALE = "locale";
    262 
    263     private static final int PROPERTY_AGGREGATION_ALGORITHM_VERSION = 4;
    264 
    265     private static final String AGGREGATE_CONTACTS = "sync.contacts.aggregate";
    266 
    267     private static final String CONTACT_MEMORY_FILE_NAME = "contactAssetFile";
    268 
    269     /**
    270      * If set to "1", we don't remove account data when accounts have been removed.
    271      *
    272      * This should be used sparingly; even though there are data still available, the UI
    273      * don't know anything about them, so they won't show up in the contact filter screen, and
    274      * the contact card/editor may get confused to see unknown custom mimetypes.
    275      *
    276      * We can't spell it out because a property name must be less than 32 chars.
    277      */
    278     private static final String DEBUG_PROPERTY_KEEP_STALE_ACCOUNT_DATA =
    279             "debug.contacts.ksad";
    280 
    281     private static final ProfileAwareUriMatcher sUriMatcher =
    282             new ProfileAwareUriMatcher(UriMatcher.NO_MATCH);
    283 
    284     private static final String FREQUENT_ORDER_BY = DataUsageStatColumns.TIMES_USED + " DESC,"
    285             + Contacts.DISPLAY_NAME + " COLLATE LOCALIZED ASC";
    286 
    287     private static final int CONTACTS = 1000;
    288     private static final int CONTACTS_ID = 1001;
    289     private static final int CONTACTS_LOOKUP = 1002;
    290     private static final int CONTACTS_LOOKUP_ID = 1003;
    291     private static final int CONTACTS_ID_DATA = 1004;
    292     private static final int CONTACTS_FILTER = 1005;
    293     private static final int CONTACTS_STREQUENT = 1006;
    294     private static final int CONTACTS_STREQUENT_FILTER = 1007;
    295     private static final int CONTACTS_GROUP = 1008;
    296     private static final int CONTACTS_ID_PHOTO = 1009;
    297     private static final int CONTACTS_LOOKUP_PHOTO = 1010;
    298     private static final int CONTACTS_LOOKUP_ID_PHOTO = 1011;
    299     private static final int CONTACTS_ID_DISPLAY_PHOTO = 1012;
    300     private static final int CONTACTS_LOOKUP_DISPLAY_PHOTO = 1013;
    301     private static final int CONTACTS_LOOKUP_ID_DISPLAY_PHOTO = 1014;
    302     private static final int CONTACTS_AS_VCARD = 1015;
    303     private static final int CONTACTS_AS_MULTI_VCARD = 1016;
    304     private static final int CONTACTS_LOOKUP_DATA = 1017;
    305     private static final int CONTACTS_LOOKUP_ID_DATA = 1018;
    306     private static final int CONTACTS_ID_ENTITIES = 1019;
    307     private static final int CONTACTS_LOOKUP_ENTITIES = 1020;
    308     private static final int CONTACTS_LOOKUP_ID_ENTITIES = 1021;
    309     private static final int CONTACTS_ID_STREAM_ITEMS = 1022;
    310     private static final int CONTACTS_LOOKUP_STREAM_ITEMS = 1023;
    311     private static final int CONTACTS_LOOKUP_ID_STREAM_ITEMS = 1024;
    312     private static final int CONTACTS_FREQUENT = 1025;
    313     private static final int CONTACTS_DELETE_USAGE = 1026;
    314     private static final int CONTACTS_ID_PHOTO_CORP = 1027;
    315     private static final int CONTACTS_ID_DISPLAY_PHOTO_CORP = 1028;
    316 
    317     private static final int RAW_CONTACTS = 2002;
    318     private static final int RAW_CONTACTS_ID = 2003;
    319     private static final int RAW_CONTACTS_ID_DATA = 2004;
    320     private static final int RAW_CONTACT_ID_ENTITY = 2005;
    321     private static final int RAW_CONTACTS_ID_DISPLAY_PHOTO = 2006;
    322     private static final int RAW_CONTACTS_ID_STREAM_ITEMS = 2007;
    323     private static final int RAW_CONTACTS_ID_STREAM_ITEMS_ID = 2008;
    324 
    325     private static final int DATA = 3000;
    326     private static final int DATA_ID = 3001;
    327     private static final int PHONES = 3002;
    328     private static final int PHONES_ID = 3003;
    329     private static final int PHONES_FILTER = 3004;
    330     private static final int EMAILS = 3005;
    331     private static final int EMAILS_ID = 3006;
    332     private static final int EMAILS_LOOKUP = 3007;
    333     private static final int EMAILS_FILTER = 3008;
    334     private static final int POSTALS = 3009;
    335     private static final int POSTALS_ID = 3010;
    336     private static final int CALLABLES = 3011;
    337     private static final int CALLABLES_ID = 3012;
    338     private static final int CALLABLES_FILTER = 3013;
    339     private static final int CONTACTABLES = 3014;
    340     private static final int CONTACTABLES_FILTER = 3015;
    341 
    342     private static final int PHONE_LOOKUP = 4000;
    343     private static final int PHONE_LOOKUP_ENTERPRISE = 4001;
    344 
    345     private static final int AGGREGATION_EXCEPTIONS = 6000;
    346     private static final int AGGREGATION_EXCEPTION_ID = 6001;
    347 
    348     private static final int STATUS_UPDATES = 7000;
    349     private static final int STATUS_UPDATES_ID = 7001;
    350 
    351     private static final int AGGREGATION_SUGGESTIONS = 8000;
    352 
    353     private static final int SETTINGS = 9000;
    354 
    355     private static final int GROUPS = 10000;
    356     private static final int GROUPS_ID = 10001;
    357     private static final int GROUPS_SUMMARY = 10003;
    358 
    359     private static final int SYNCSTATE = 11000;
    360     private static final int SYNCSTATE_ID = 11001;
    361     private static final int PROFILE_SYNCSTATE = 11002;
    362     private static final int PROFILE_SYNCSTATE_ID = 11003;
    363 
    364     private static final int SEARCH_SUGGESTIONS = 12001;
    365     private static final int SEARCH_SHORTCUT = 12002;
    366 
    367     private static final int RAW_CONTACT_ENTITIES = 15001;
    368 
    369     private static final int PROVIDER_STATUS = 16001;
    370 
    371     private static final int DIRECTORIES = 17001;
    372     private static final int DIRECTORIES_ID = 17002;
    373 
    374     private static final int COMPLETE_NAME = 18000;
    375 
    376     private static final int PROFILE = 19000;
    377     private static final int PROFILE_ENTITIES = 19001;
    378     private static final int PROFILE_DATA = 19002;
    379     private static final int PROFILE_DATA_ID = 19003;
    380     private static final int PROFILE_AS_VCARD = 19004;
    381     private static final int PROFILE_RAW_CONTACTS = 19005;
    382     private static final int PROFILE_RAW_CONTACTS_ID = 19006;
    383     private static final int PROFILE_RAW_CONTACTS_ID_DATA = 19007;
    384     private static final int PROFILE_RAW_CONTACTS_ID_ENTITIES = 19008;
    385     private static final int PROFILE_STATUS_UPDATES = 19009;
    386     private static final int PROFILE_RAW_CONTACT_ENTITIES = 19010;
    387     private static final int PROFILE_PHOTO = 19011;
    388     private static final int PROFILE_DISPLAY_PHOTO = 19012;
    389 
    390     private static final int DATA_USAGE_FEEDBACK_ID = 20001;
    391 
    392     private static final int STREAM_ITEMS = 21000;
    393     private static final int STREAM_ITEMS_PHOTOS = 21001;
    394     private static final int STREAM_ITEMS_ID = 21002;
    395     private static final int STREAM_ITEMS_ID_PHOTOS = 21003;
    396     private static final int STREAM_ITEMS_ID_PHOTOS_ID = 21004;
    397     private static final int STREAM_ITEMS_LIMIT = 21005;
    398 
    399     private static final int DISPLAY_PHOTO_ID = 22000;
    400     private static final int PHOTO_DIMENSIONS = 22001;
    401 
    402     private static final int DELETED_CONTACTS = 23000;
    403     private static final int DELETED_CONTACTS_ID = 23001;
    404 
    405     // Inserts into URIs in this map will direct to the profile database if the parent record's
    406     // value (looked up from the ContentValues object with the key specified by the value in this
    407     // map) is in the profile ID-space (see {@link ProfileDatabaseHelper#PROFILE_ID_SPACE}).
    408     private static final Map<Integer, String> INSERT_URI_ID_VALUE_MAP = Maps.newHashMap();
    409     static {
    410         INSERT_URI_ID_VALUE_MAP.put(DATA, Data.RAW_CONTACT_ID);
    411         INSERT_URI_ID_VALUE_MAP.put(RAW_CONTACTS_ID_DATA, Data.RAW_CONTACT_ID);
    412         INSERT_URI_ID_VALUE_MAP.put(STATUS_UPDATES, StatusUpdates.DATA_ID);
    413         INSERT_URI_ID_VALUE_MAP.put(STREAM_ITEMS, StreamItems.RAW_CONTACT_ID);
    414         INSERT_URI_ID_VALUE_MAP.put(RAW_CONTACTS_ID_STREAM_ITEMS, StreamItems.RAW_CONTACT_ID);
    415         INSERT_URI_ID_VALUE_MAP.put(STREAM_ITEMS_PHOTOS, StreamItemPhotos.STREAM_ITEM_ID);
    416         INSERT_URI_ID_VALUE_MAP.put(STREAM_ITEMS_ID_PHOTOS, StreamItemPhotos.STREAM_ITEM_ID);
    417     }
    418 
    419     // Any interactions that involve these URIs will also require the calling package to have either
    420     // android.permission.READ_SOCIAL_STREAM permission or android.permission.WRITE_SOCIAL_STREAM
    421     // permission, depending on the type of operation being performed.
    422     private static final List<Integer> SOCIAL_STREAM_URIS = Lists.newArrayList(
    423             CONTACTS_ID_STREAM_ITEMS,
    424             CONTACTS_LOOKUP_STREAM_ITEMS,
    425             CONTACTS_LOOKUP_ID_STREAM_ITEMS,
    426             RAW_CONTACTS_ID_STREAM_ITEMS,
    427             RAW_CONTACTS_ID_STREAM_ITEMS_ID,
    428             STREAM_ITEMS,
    429             STREAM_ITEMS_PHOTOS,
    430             STREAM_ITEMS_ID,
    431             STREAM_ITEMS_ID_PHOTOS,
    432             STREAM_ITEMS_ID_PHOTOS_ID
    433     );
    434 
    435     private static final String SELECTION_FAVORITES_GROUPS_BY_RAW_CONTACT_ID =
    436             RawContactsColumns.CONCRETE_ID + "=? AND "
    437                 + GroupsColumns.CONCRETE_ACCOUNT_ID + "=" + RawContactsColumns.CONCRETE_ACCOUNT_ID
    438                 + " AND " + Groups.FAVORITES + " != 0";
    439 
    440     private static final String SELECTION_AUTO_ADD_GROUPS_BY_RAW_CONTACT_ID =
    441             RawContactsColumns.CONCRETE_ID + "=? AND "
    442                 + GroupsColumns.CONCRETE_ACCOUNT_ID + "=" + RawContactsColumns.CONCRETE_ACCOUNT_ID
    443                 + " AND " + Groups.AUTO_ADD + " != 0";
    444 
    445     private static final String[] PROJECTION_GROUP_ID
    446             = new String[] {Tables.GROUPS + "." + Groups._ID};
    447 
    448     private static final String SELECTION_GROUPMEMBERSHIP_DATA = DataColumns.MIMETYPE_ID + "=? "
    449             + "AND " + GroupMembership.GROUP_ROW_ID + "=? "
    450             + "AND " + GroupMembership.RAW_CONTACT_ID + "=?";
    451 
    452     private static final String SELECTION_STARRED_FROM_RAW_CONTACTS =
    453             "SELECT " + RawContacts.STARRED
    454                     + " FROM " + Tables.RAW_CONTACTS + " WHERE " + RawContacts._ID + "=?";
    455 
    456     private interface DataContactsQuery {
    457         public static final String TABLE = "data "
    458                 + "JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id) "
    459                 + "JOIN " + Tables.ACCOUNTS + " ON ("
    460                     + AccountsColumns.CONCRETE_ID + "=" + RawContactsColumns.CONCRETE_ACCOUNT_ID
    461                     + ")"
    462                 + "JOIN contacts ON (raw_contacts.contact_id = contacts._id)";
    463 
    464         public static final String[] PROJECTION = new String[] {
    465             RawContactsColumns.CONCRETE_ID,
    466             AccountsColumns.CONCRETE_ACCOUNT_TYPE,
    467             AccountsColumns.CONCRETE_ACCOUNT_NAME,
    468             AccountsColumns.CONCRETE_DATA_SET,
    469             DataColumns.CONCRETE_ID,
    470             ContactsColumns.CONCRETE_ID
    471         };
    472 
    473         public static final int RAW_CONTACT_ID = 0;
    474         public static final int ACCOUNT_TYPE = 1;
    475         public static final int ACCOUNT_NAME = 2;
    476         public static final int DATA_SET = 3;
    477         public static final int DATA_ID = 4;
    478         public static final int CONTACT_ID = 5;
    479     }
    480 
    481     interface RawContactsQuery {
    482         String TABLE = Tables.RAW_CONTACTS_JOIN_ACCOUNTS;
    483 
    484         String[] COLUMNS = new String[] {
    485                 RawContacts.DELETED,
    486                 RawContactsColumns.ACCOUNT_ID,
    487                 AccountsColumns.CONCRETE_ACCOUNT_TYPE,
    488                 AccountsColumns.CONCRETE_ACCOUNT_NAME,
    489                 AccountsColumns.CONCRETE_DATA_SET,
    490         };
    491 
    492         int DELETED = 0;
    493         int ACCOUNT_ID = 1;
    494         int ACCOUNT_TYPE = 2;
    495         int ACCOUNT_NAME = 3;
    496         int DATA_SET = 4;
    497     }
    498 
    499     private static final String DEFAULT_ACCOUNT_TYPE = "com.google";
    500 
    501     /** Sql where statement for filtering on groups. */
    502     private static final String CONTACTS_IN_GROUP_SELECT =
    503             Contacts._ID + " IN "
    504                     + "(SELECT " + RawContacts.CONTACT_ID
    505                     + " FROM " + Tables.RAW_CONTACTS
    506                     + " WHERE " + RawContactsColumns.CONCRETE_ID + " IN "
    507                             + "(SELECT " + DataColumns.CONCRETE_RAW_CONTACT_ID
    508                             + " FROM " + Tables.DATA_JOIN_MIMETYPES
    509                             + " WHERE " + DataColumns.MIMETYPE_ID + "=?"
    510                                     + " AND " + GroupMembership.GROUP_ROW_ID + "="
    511                                     + "(SELECT " + Tables.GROUPS + "." + Groups._ID
    512                                     + " FROM " + Tables.GROUPS
    513                                     + " WHERE " + Groups.TITLE + "=?)))";
    514 
    515     /** Sql for updating DIRTY flag on multiple raw contacts */
    516     private static final String UPDATE_RAW_CONTACT_SET_DIRTY_SQL =
    517             "UPDATE " + Tables.RAW_CONTACTS +
    518             " SET " + RawContacts.DIRTY + "=1" +
    519             " WHERE " + RawContacts._ID + " IN (";
    520 
    521     /** Sql for updating VERSION on multiple raw contacts */
    522     private static final String UPDATE_RAW_CONTACT_SET_VERSION_SQL =
    523             "UPDATE " + Tables.RAW_CONTACTS +
    524             " SET " + RawContacts.VERSION + " = " + RawContacts.VERSION + " + 1" +
    525             " WHERE " + RawContacts._ID + " IN (";
    526 
    527     /** Sql for undemoting a demoted contact **/
    528     private static final String UNDEMOTE_CONTACT =
    529             "UPDATE " + Tables.CONTACTS +
    530             " SET " + Contacts.PINNED + " = " + PinnedPositions.UNPINNED +
    531             " WHERE " + Contacts._ID + " = ?1 AND " + Contacts.PINNED + " <= " +
    532             PinnedPositions.DEMOTED;
    533 
    534     /** Sql for undemoting a demoted raw contact **/
    535     private static final String UNDEMOTE_RAW_CONTACT =
    536             "UPDATE " + Tables.RAW_CONTACTS +
    537             " SET " + RawContacts.PINNED + " = " + PinnedPositions.UNPINNED +
    538             " WHERE " + RawContacts.CONTACT_ID + " = ?1 AND " + Contacts.PINNED + " <= " +
    539             PinnedPositions.DEMOTED;
    540 
    541     // Contacts contacted within the last 3 days (in seconds)
    542     private static final long LAST_TIME_USED_3_DAYS_SEC = 3L * 24 * 60 * 60;
    543 
    544     // Contacts contacted within the last 7 days (in seconds)
    545     private static final long LAST_TIME_USED_7_DAYS_SEC = 7L * 24 * 60 * 60;
    546 
    547     // Contacts contacted within the last 14 days (in seconds)
    548     private static final long LAST_TIME_USED_14_DAYS_SEC = 14L * 24 * 60 * 60;
    549 
    550     // Contacts contacted within the last 30 days (in seconds)
    551     private static final long LAST_TIME_USED_30_DAYS_SEC = 30L * 24 * 60 * 60;
    552 
    553     private static final String TIME_SINCE_LAST_USED_SEC =
    554             "(strftime('%s', 'now') - " + DataUsageStatColumns.LAST_TIME_USED + "/1000)";
    555 
    556     private static final String SORT_BY_DATA_USAGE =
    557             "(CASE WHEN " + TIME_SINCE_LAST_USED_SEC + " < " + LAST_TIME_USED_3_DAYS_SEC +
    558             " THEN 0 " +
    559                     " WHEN " + TIME_SINCE_LAST_USED_SEC + " < " + LAST_TIME_USED_7_DAYS_SEC +
    560             " THEN 1 " +
    561                     " WHEN " + TIME_SINCE_LAST_USED_SEC + " < " + LAST_TIME_USED_14_DAYS_SEC +
    562             " THEN 2 " +
    563                     " WHEN " + TIME_SINCE_LAST_USED_SEC + " < " + LAST_TIME_USED_30_DAYS_SEC +
    564             " THEN 3 " +
    565             " ELSE 4 END), " +
    566             DataUsageStatColumns.TIMES_USED + " DESC";
    567 
    568     /*
    569      * Sorting order for email address suggestions: first starred, then the rest.
    570      * Within the two groups:
    571      * - three buckets: very recently contacted, then fairly recently contacted, then the rest.
    572      * Within each of the bucket - descending count of times contacted (both for data row and for
    573      * contact row).
    574      * If all else fails, in_visible_group, alphabetical.
    575      * (Super)primary email address is returned before other addresses for the same contact.
    576      */
    577     private static final String EMAIL_FILTER_SORT_ORDER =
    578         Contacts.STARRED + " DESC, "
    579         + Data.IS_SUPER_PRIMARY + " DESC, "
    580         + SORT_BY_DATA_USAGE + ", "
    581         + Contacts.IN_VISIBLE_GROUP + " DESC, "
    582         + Contacts.DISPLAY_NAME + " COLLATE LOCALIZED ASC, "
    583         + Data.CONTACT_ID + ", "
    584         + Data.IS_PRIMARY + " DESC";
    585 
    586     /** Currently same as {@link #EMAIL_FILTER_SORT_ORDER} */
    587     private static final String PHONE_FILTER_SORT_ORDER = EMAIL_FILTER_SORT_ORDER;
    588 
    589     /** Name lookup types used for contact filtering */
    590     private static final String CONTACT_LOOKUP_NAME_TYPES =
    591             NameLookupType.NAME_COLLATION_KEY + "," +
    592             NameLookupType.EMAIL_BASED_NICKNAME + "," +
    593             NameLookupType.NICKNAME;
    594 
    595     /**
    596      * If any of these columns are used in a Data projection, there is no point in
    597      * using the DISTINCT keyword, which can negatively affect performance.
    598      */
    599     private static final String[] DISTINCT_DATA_PROHIBITING_COLUMNS = {
    600             Data._ID,
    601             Data.RAW_CONTACT_ID,
    602             Data.NAME_RAW_CONTACT_ID,
    603             RawContacts.ACCOUNT_NAME,
    604             RawContacts.ACCOUNT_TYPE,
    605             RawContacts.DATA_SET,
    606             RawContacts.ACCOUNT_TYPE_AND_DATA_SET,
    607             RawContacts.DIRTY,
    608             RawContacts.NAME_VERIFIED,
    609             RawContacts.SOURCE_ID,
    610             RawContacts.VERSION,
    611     };
    612 
    613     private static final ProjectionMap sContactsColumns = ProjectionMap.builder()
    614             .add(Contacts.CUSTOM_RINGTONE)
    615             .add(Contacts.DISPLAY_NAME)
    616             .add(Contacts.DISPLAY_NAME_ALTERNATIVE)
    617             .add(Contacts.DISPLAY_NAME_SOURCE)
    618             .add(Contacts.IN_DEFAULT_DIRECTORY)
    619             .add(Contacts.IN_VISIBLE_GROUP)
    620             .add(Contacts.LAST_TIME_CONTACTED)
    621             .add(Contacts.LOOKUP_KEY)
    622             .add(Contacts.PHONETIC_NAME)
    623             .add(Contacts.PHONETIC_NAME_STYLE)
    624             .add(Contacts.PHOTO_ID)
    625             .add(Contacts.PHOTO_FILE_ID)
    626             .add(Contacts.PHOTO_URI)
    627             .add(Contacts.PHOTO_THUMBNAIL_URI)
    628             .add(Contacts.SEND_TO_VOICEMAIL)
    629             .add(Contacts.SORT_KEY_ALTERNATIVE)
    630             .add(Contacts.SORT_KEY_PRIMARY)
    631             .add(ContactsColumns.PHONEBOOK_LABEL_PRIMARY)
    632             .add(ContactsColumns.PHONEBOOK_BUCKET_PRIMARY)
    633             .add(ContactsColumns.PHONEBOOK_LABEL_ALTERNATIVE)
    634             .add(ContactsColumns.PHONEBOOK_BUCKET_ALTERNATIVE)
    635             .add(Contacts.STARRED)
    636             .add(Contacts.PINNED)
    637             .add(Contacts.TIMES_CONTACTED)
    638             .add(Contacts.HAS_PHONE_NUMBER)
    639             .add(Contacts.CONTACT_LAST_UPDATED_TIMESTAMP)
    640             .build();
    641 
    642     private static final ProjectionMap sContactsPresenceColumns = ProjectionMap.builder()
    643             .add(Contacts.CONTACT_PRESENCE,
    644                     Tables.AGGREGATED_PRESENCE + "." + StatusUpdates.PRESENCE)
    645             .add(Contacts.CONTACT_CHAT_CAPABILITY,
    646                     Tables.AGGREGATED_PRESENCE + "." + StatusUpdates.CHAT_CAPABILITY)
    647             .add(Contacts.CONTACT_STATUS,
    648                     ContactsStatusUpdatesColumns.CONCRETE_STATUS)
    649             .add(Contacts.CONTACT_STATUS_TIMESTAMP,
    650                     ContactsStatusUpdatesColumns.CONCRETE_STATUS_TIMESTAMP)
    651             .add(Contacts.CONTACT_STATUS_RES_PACKAGE,
    652                     ContactsStatusUpdatesColumns.CONCRETE_STATUS_RES_PACKAGE)
    653             .add(Contacts.CONTACT_STATUS_LABEL,
    654                     ContactsStatusUpdatesColumns.CONCRETE_STATUS_LABEL)
    655             .add(Contacts.CONTACT_STATUS_ICON,
    656                     ContactsStatusUpdatesColumns.CONCRETE_STATUS_ICON)
    657             .build();
    658 
    659     private static final ProjectionMap sSnippetColumns = ProjectionMap.builder()
    660             .add(SearchSnippets.SNIPPET)
    661             .build();
    662 
    663     private static final ProjectionMap sRawContactColumns = ProjectionMap.builder()
    664             .add(RawContacts.ACCOUNT_NAME)
    665             .add(RawContacts.ACCOUNT_TYPE)
    666             .add(RawContacts.DATA_SET)
    667             .add(RawContacts.ACCOUNT_TYPE_AND_DATA_SET)
    668             .add(RawContacts.DIRTY)
    669             .add(RawContacts.NAME_VERIFIED)
    670             .add(RawContacts.SOURCE_ID)
    671             .add(RawContacts.VERSION)
    672             .build();
    673 
    674     private static final ProjectionMap sRawContactSyncColumns = ProjectionMap.builder()
    675             .add(RawContacts.SYNC1)
    676             .add(RawContacts.SYNC2)
    677             .add(RawContacts.SYNC3)
    678             .add(RawContacts.SYNC4)
    679             .build();
    680 
    681     private static final ProjectionMap sDataColumns = ProjectionMap.builder()
    682             .add(Data.DATA1)
    683             .add(Data.DATA2)
    684             .add(Data.DATA3)
    685             .add(Data.DATA4)
    686             .add(Data.DATA5)
    687             .add(Data.DATA6)
    688             .add(Data.DATA7)
    689             .add(Data.DATA8)
    690             .add(Data.DATA9)
    691             .add(Data.DATA10)
    692             .add(Data.DATA11)
    693             .add(Data.DATA12)
    694             .add(Data.DATA13)
    695             .add(Data.DATA14)
    696             .add(Data.DATA15)
    697             .add(Data.DATA_VERSION)
    698             .add(Data.IS_PRIMARY)
    699             .add(Data.IS_SUPER_PRIMARY)
    700             .add(Data.MIMETYPE)
    701             .add(Data.RES_PACKAGE)
    702             .add(Data.SYNC1)
    703             .add(Data.SYNC2)
    704             .add(Data.SYNC3)
    705             .add(Data.SYNC4)
    706             .add(GroupMembership.GROUP_SOURCE_ID)
    707             .build();
    708 
    709     private static final ProjectionMap sContactPresenceColumns = ProjectionMap.builder()
    710             .add(Contacts.CONTACT_PRESENCE,
    711                     Tables.AGGREGATED_PRESENCE + '.' + StatusUpdates.PRESENCE)
    712             .add(Contacts.CONTACT_CHAT_CAPABILITY,
    713                     Tables.AGGREGATED_PRESENCE + '.' + StatusUpdates.CHAT_CAPABILITY)
    714             .add(Contacts.CONTACT_STATUS,
    715                     ContactsStatusUpdatesColumns.CONCRETE_STATUS)
    716             .add(Contacts.CONTACT_STATUS_TIMESTAMP,
    717                     ContactsStatusUpdatesColumns.CONCRETE_STATUS_TIMESTAMP)
    718             .add(Contacts.CONTACT_STATUS_RES_PACKAGE,
    719                     ContactsStatusUpdatesColumns.CONCRETE_STATUS_RES_PACKAGE)
    720             .add(Contacts.CONTACT_STATUS_LABEL,
    721                     ContactsStatusUpdatesColumns.CONCRETE_STATUS_LABEL)
    722             .add(Contacts.CONTACT_STATUS_ICON,
    723                     ContactsStatusUpdatesColumns.CONCRETE_STATUS_ICON)
    724             .build();
    725 
    726     private static final ProjectionMap sDataPresenceColumns = ProjectionMap.builder()
    727             .add(Data.PRESENCE, Tables.PRESENCE + "." + StatusUpdates.PRESENCE)
    728             .add(Data.CHAT_CAPABILITY, Tables.PRESENCE + "." + StatusUpdates.CHAT_CAPABILITY)
    729             .add(Data.STATUS, StatusUpdatesColumns.CONCRETE_STATUS)
    730             .add(Data.STATUS_TIMESTAMP, StatusUpdatesColumns.CONCRETE_STATUS_TIMESTAMP)
    731             .add(Data.STATUS_RES_PACKAGE, StatusUpdatesColumns.CONCRETE_STATUS_RES_PACKAGE)
    732             .add(Data.STATUS_LABEL, StatusUpdatesColumns.CONCRETE_STATUS_LABEL)
    733             .add(Data.STATUS_ICON, StatusUpdatesColumns.CONCRETE_STATUS_ICON)
    734             .build();
    735 
    736     private static final ProjectionMap sDataUsageColumns = ProjectionMap.builder()
    737             .add(Data.TIMES_USED, Tables.DATA_USAGE_STAT + "." + Data.TIMES_USED)
    738             .add(Data.LAST_TIME_USED, Tables.DATA_USAGE_STAT + "." + Data.LAST_TIME_USED)
    739             .build();
    740 
    741     /** Contains just BaseColumns._COUNT */
    742     private static final ProjectionMap sCountProjectionMap = ProjectionMap.builder()
    743             .add(BaseColumns._COUNT, "COUNT(*)")
    744             .build();
    745 
    746     /** Contains just the contacts columns */
    747     private static final ProjectionMap sContactsProjectionMap = ProjectionMap.builder()
    748             .add(Contacts._ID)
    749             .add(Contacts.HAS_PHONE_NUMBER)
    750             .add(Contacts.NAME_RAW_CONTACT_ID)
    751             .add(Contacts.IS_USER_PROFILE)
    752             .addAll(sContactsColumns)
    753             .addAll(sContactsPresenceColumns)
    754             .build();
    755 
    756     /** Contains just the contacts columns */
    757     private static final ProjectionMap sContactsProjectionWithSnippetMap = ProjectionMap.builder()
    758             .addAll(sContactsProjectionMap)
    759             .addAll(sSnippetColumns)
    760             .build();
    761 
    762     /** Used for pushing starred contacts to the top of a times contacted list **/
    763     private static final ProjectionMap sStrequentStarredProjectionMap = ProjectionMap.builder()
    764             .addAll(sContactsProjectionMap)
    765             .add(DataUsageStatColumns.TIMES_USED, String.valueOf(Long.MAX_VALUE))
    766             .add(DataUsageStatColumns.LAST_TIME_USED, String.valueOf(Long.MAX_VALUE))
    767             .build();
    768 
    769     private static final ProjectionMap sStrequentFrequentProjectionMap = ProjectionMap.builder()
    770             .addAll(sContactsProjectionMap)
    771             .add(DataUsageStatColumns.TIMES_USED,
    772                     "SUM(" + DataUsageStatColumns.CONCRETE_TIMES_USED + ")")
    773             .add(DataUsageStatColumns.LAST_TIME_USED,
    774                     "MAX(" + DataUsageStatColumns.CONCRETE_LAST_TIME_USED + ")")
    775             .build();
    776 
    777     /**
    778      * Used for Strequent URI with {@link ContactsContract#STREQUENT_PHONE_ONLY}, which allows
    779      * users to obtain part of Data columns. We hard-code {@link Contacts#IS_USER_PROFILE} to NULL,
    780      * because sContactsProjectionMap specifies a field that doesn't exist in the view behind the
    781      * query that uses this projection map.
    782      **/
    783     private static final ProjectionMap sStrequentPhoneOnlyProjectionMap
    784             = ProjectionMap.builder()
    785             .addAll(sContactsProjectionMap)
    786             .add(DataUsageStatColumns.TIMES_USED, DataUsageStatColumns.CONCRETE_TIMES_USED)
    787             .add(DataUsageStatColumns.LAST_TIME_USED, DataUsageStatColumns.CONCRETE_LAST_TIME_USED)
    788             .add(Phone.NUMBER)
    789             .add(Phone.TYPE)
    790             .add(Phone.LABEL)
    791             .add(Phone.IS_SUPER_PRIMARY)
    792             .add(Phone.CONTACT_ID)
    793             .add(Contacts.IS_USER_PROFILE, "NULL")
    794             .build();
    795 
    796     /** Contains just the contacts vCard columns */
    797     private static final ProjectionMap sContactsVCardProjectionMap = ProjectionMap.builder()
    798             .add(Contacts._ID)
    799             .add(OpenableColumns.DISPLAY_NAME, Contacts.DISPLAY_NAME + " || '.vcf'")
    800             .add(OpenableColumns.SIZE, "NULL")
    801             .build();
    802 
    803     /** Contains just the raw contacts columns */
    804     private static final ProjectionMap sRawContactsProjectionMap = ProjectionMap.builder()
    805             .add(RawContacts._ID)
    806             .add(RawContacts.CONTACT_ID)
    807             .add(RawContacts.DELETED)
    808             .add(RawContacts.DISPLAY_NAME_PRIMARY)
    809             .add(RawContacts.DISPLAY_NAME_ALTERNATIVE)
    810             .add(RawContacts.DISPLAY_NAME_SOURCE)
    811             .add(RawContacts.PHONETIC_NAME)
    812             .add(RawContacts.PHONETIC_NAME_STYLE)
    813             .add(RawContacts.SORT_KEY_PRIMARY)
    814             .add(RawContacts.SORT_KEY_ALTERNATIVE)
    815             .add(RawContactsColumns.PHONEBOOK_LABEL_PRIMARY)
    816             .add(RawContactsColumns.PHONEBOOK_BUCKET_PRIMARY)
    817             .add(RawContactsColumns.PHONEBOOK_LABEL_ALTERNATIVE)
    818             .add(RawContactsColumns.PHONEBOOK_BUCKET_ALTERNATIVE)
    819             .add(RawContacts.TIMES_CONTACTED)
    820             .add(RawContacts.LAST_TIME_CONTACTED)
    821             .add(RawContacts.CUSTOM_RINGTONE)
    822             .add(RawContacts.SEND_TO_VOICEMAIL)
    823             .add(RawContacts.STARRED)
    824             .add(RawContacts.PINNED)
    825             .add(RawContacts.AGGREGATION_MODE)
    826             .add(RawContacts.RAW_CONTACT_IS_USER_PROFILE)
    827             .addAll(sRawContactColumns)
    828             .addAll(sRawContactSyncColumns)
    829             .build();
    830 
    831     /** Contains the columns from the raw entity view*/
    832     private static final ProjectionMap sRawEntityProjectionMap = ProjectionMap.builder()
    833             .add(RawContacts._ID)
    834             .add(RawContacts.CONTACT_ID)
    835             .add(RawContacts.Entity.DATA_ID)
    836             .add(RawContacts.DELETED)
    837             .add(RawContacts.STARRED)
    838             .add(RawContacts.RAW_CONTACT_IS_USER_PROFILE)
    839             .addAll(sRawContactColumns)
    840             .addAll(sRawContactSyncColumns)
    841             .addAll(sDataColumns)
    842             .build();
    843 
    844     /** Contains the columns from the contact entity view*/
    845     private static final ProjectionMap sEntityProjectionMap = ProjectionMap.builder()
    846             .add(Contacts.Entity._ID)
    847             .add(Contacts.Entity.CONTACT_ID)
    848             .add(Contacts.Entity.RAW_CONTACT_ID)
    849             .add(Contacts.Entity.DATA_ID)
    850             .add(Contacts.Entity.NAME_RAW_CONTACT_ID)
    851             .add(Contacts.Entity.DELETED)
    852             .add(Contacts.IS_USER_PROFILE)
    853             .addAll(sContactsColumns)
    854             .addAll(sContactPresenceColumns)
    855             .addAll(sRawContactColumns)
    856             .addAll(sRawContactSyncColumns)
    857             .addAll(sDataColumns)
    858             .addAll(sDataPresenceColumns)
    859             .addAll(sDataUsageColumns)
    860             .build();
    861 
    862     /** Contains columns in PhoneLookup which are not contained in the data view. */
    863     private static final ProjectionMap sSipLookupColumns = ProjectionMap.builder()
    864             .add(PhoneLookup.NUMBER, SipAddress.SIP_ADDRESS)
    865             .add(PhoneLookup.TYPE, "0")
    866             .add(PhoneLookup.LABEL, "NULL")
    867             .add(PhoneLookup.NORMALIZED_NUMBER, "NULL")
    868             .build();
    869 
    870     /** Contains columns from the data view */
    871     private static final ProjectionMap sDataProjectionMap = ProjectionMap.builder()
    872             .add(Data._ID)
    873             .add(Data.RAW_CONTACT_ID)
    874             .add(Data.CONTACT_ID)
    875             .add(Data.NAME_RAW_CONTACT_ID)
    876             .add(RawContacts.RAW_CONTACT_IS_USER_PROFILE)
    877             .addAll(sDataColumns)
    878             .addAll(sDataPresenceColumns)
    879             .addAll(sRawContactColumns)
    880             .addAll(sContactsColumns)
    881             .addAll(sContactPresenceColumns)
    882             .addAll(sDataUsageColumns)
    883             .build();
    884 
    885     /** Contains columns from the data view used for SIP address lookup. */
    886     private static final ProjectionMap sDataSipLookupProjectionMap = ProjectionMap.builder()
    887             .addAll(sDataProjectionMap)
    888             .addAll(sSipLookupColumns)
    889             .build();
    890 
    891     /** Contains columns from the data view */
    892     private static final ProjectionMap sDistinctDataProjectionMap = ProjectionMap.builder()
    893             .add(Data._ID, "MIN(" + Data._ID + ")")
    894             .add(RawContacts.CONTACT_ID)
    895             .add(RawContacts.RAW_CONTACT_IS_USER_PROFILE)
    896             .addAll(sDataColumns)
    897             .addAll(sDataPresenceColumns)
    898             .addAll(sContactsColumns)
    899             .addAll(sContactPresenceColumns)
    900             .addAll(sDataUsageColumns)
    901             .build();
    902 
    903     /** Contains columns from the data view used for SIP address lookup. */
    904     private static final ProjectionMap sDistinctDataSipLookupProjectionMap = ProjectionMap.builder()
    905             .addAll(sDistinctDataProjectionMap)
    906             .addAll(sSipLookupColumns)
    907             .build();
    908 
    909     /** Contains the data and contacts columns, for joined tables */
    910     private static final ProjectionMap sPhoneLookupProjectionMap = ProjectionMap.builder()
    911             .add(PhoneLookup._ID, "contacts_view." + Contacts._ID)
    912             .add(PhoneLookup.LOOKUP_KEY, "contacts_view." + Contacts.LOOKUP_KEY)
    913             .add(PhoneLookup.DISPLAY_NAME, "contacts_view." + Contacts.DISPLAY_NAME)
    914             .add(PhoneLookup.LAST_TIME_CONTACTED, "contacts_view." + Contacts.LAST_TIME_CONTACTED)
    915             .add(PhoneLookup.TIMES_CONTACTED, "contacts_view." + Contacts.TIMES_CONTACTED)
    916             .add(PhoneLookup.STARRED, "contacts_view." + Contacts.STARRED)
    917             .add(PhoneLookup.IN_DEFAULT_DIRECTORY, "contacts_view." + Contacts.IN_DEFAULT_DIRECTORY)
    918             .add(PhoneLookup.IN_VISIBLE_GROUP, "contacts_view." + Contacts.IN_VISIBLE_GROUP)
    919             .add(PhoneLookup.PHOTO_ID, "contacts_view." + Contacts.PHOTO_ID)
    920             .add(PhoneLookup.PHOTO_FILE_ID, "contacts_view." + Contacts.PHOTO_FILE_ID)
    921             .add(PhoneLookup.PHOTO_URI, "contacts_view." + Contacts.PHOTO_URI)
    922             .add(PhoneLookup.PHOTO_THUMBNAIL_URI, "contacts_view." + Contacts.PHOTO_THUMBNAIL_URI)
    923             .add(PhoneLookup.CUSTOM_RINGTONE, "contacts_view." + Contacts.CUSTOM_RINGTONE)
    924             .add(PhoneLookup.HAS_PHONE_NUMBER, "contacts_view." + Contacts.HAS_PHONE_NUMBER)
    925             .add(PhoneLookup.SEND_TO_VOICEMAIL, "contacts_view." + Contacts.SEND_TO_VOICEMAIL)
    926             .add(PhoneLookup.NUMBER, Phone.NUMBER)
    927             .add(PhoneLookup.TYPE, Phone.TYPE)
    928             .add(PhoneLookup.LABEL, Phone.LABEL)
    929             .add(PhoneLookup.NORMALIZED_NUMBER, Phone.NORMALIZED_NUMBER)
    930             .build();
    931 
    932     /** Contains the just the {@link Groups} columns */
    933     private static final ProjectionMap sGroupsProjectionMap = ProjectionMap.builder()
    934             .add(Groups._ID)
    935             .add(Groups.ACCOUNT_NAME)
    936             .add(Groups.ACCOUNT_TYPE)
    937             .add(Groups.DATA_SET)
    938             .add(Groups.ACCOUNT_TYPE_AND_DATA_SET)
    939             .add(Groups.SOURCE_ID)
    940             .add(Groups.DIRTY)
    941             .add(Groups.VERSION)
    942             .add(Groups.RES_PACKAGE)
    943             .add(Groups.TITLE)
    944             .add(Groups.TITLE_RES)
    945             .add(Groups.GROUP_VISIBLE)
    946             .add(Groups.SYSTEM_ID)
    947             .add(Groups.DELETED)
    948             .add(Groups.NOTES)
    949             .add(Groups.SHOULD_SYNC)
    950             .add(Groups.FAVORITES)
    951             .add(Groups.AUTO_ADD)
    952             .add(Groups.GROUP_IS_READ_ONLY)
    953             .add(Groups.SYNC1)
    954             .add(Groups.SYNC2)
    955             .add(Groups.SYNC3)
    956             .add(Groups.SYNC4)
    957             .build();
    958 
    959     private static final ProjectionMap sDeletedContactsProjectionMap = ProjectionMap.builder()
    960             .add(DeletedContacts.CONTACT_ID)
    961             .add(DeletedContacts.CONTACT_DELETED_TIMESTAMP)
    962             .build();
    963 
    964     /**
    965      * Contains {@link Groups} columns along with summary details.
    966      *
    967      * Note {@link Groups#SUMMARY_COUNT} doesn't exist in groups/view_groups.
    968      * When we detect this column being requested, we join {@link Joins#GROUP_MEMBER_COUNT} to
    969      * generate it.
    970      *
    971      * TODO Support SUMMARY_GROUP_COUNT_PER_ACCOUNT too.  See also queryLocal().
    972      */
    973     private static final ProjectionMap sGroupsSummaryProjectionMap = ProjectionMap.builder()
    974             .addAll(sGroupsProjectionMap)
    975             .add(Groups.SUMMARY_COUNT, "ifnull(group_member_count, 0)")
    976             .add(Groups.SUMMARY_WITH_PHONES,
    977                     "(SELECT COUNT(" + ContactsColumns.CONCRETE_ID + ") FROM "
    978                         + Tables.CONTACTS_JOIN_RAW_CONTACTS_DATA_FILTERED_BY_GROUPMEMBERSHIP
    979                         + " WHERE " + Contacts.HAS_PHONE_NUMBER + ")")
    980             .add(Groups.SUMMARY_GROUP_COUNT_PER_ACCOUNT, "0") // Always returns 0 for now.
    981             .build();
    982 
    983     /** Contains the agg_exceptions columns */
    984     private static final ProjectionMap sAggregationExceptionsProjectionMap = ProjectionMap.builder()
    985             .add(AggregationExceptionColumns._ID, Tables.AGGREGATION_EXCEPTIONS + "._id")
    986             .add(AggregationExceptions.TYPE)
    987             .add(AggregationExceptions.RAW_CONTACT_ID1)
    988             .add(AggregationExceptions.RAW_CONTACT_ID2)
    989             .build();
    990 
    991     /** Contains the agg_exceptions columns */
    992     private static final ProjectionMap sSettingsProjectionMap = ProjectionMap.builder()
    993             .add(Settings.ACCOUNT_NAME)
    994             .add(Settings.ACCOUNT_TYPE)
    995             .add(Settings.DATA_SET)
    996             .add(Settings.UNGROUPED_VISIBLE)
    997             .add(Settings.SHOULD_SYNC)
    998             .add(Settings.ANY_UNSYNCED,
    999                     "(CASE WHEN MIN(" + Settings.SHOULD_SYNC
   1000                         + ",(SELECT "
   1001                                 + "(CASE WHEN MIN(" + Groups.SHOULD_SYNC + ") IS NULL"
   1002                                 + " THEN 1"
   1003                                 + " ELSE MIN(" + Groups.SHOULD_SYNC + ")"
   1004                                 + " END)"
   1005                             + " FROM " + Views.GROUPS
   1006                             + " WHERE " + ViewGroupsColumns.CONCRETE_ACCOUNT_NAME + "="
   1007                                     + SettingsColumns.CONCRETE_ACCOUNT_NAME
   1008                                 + " AND " + ViewGroupsColumns.CONCRETE_ACCOUNT_TYPE + "="
   1009                                     + SettingsColumns.CONCRETE_ACCOUNT_TYPE
   1010                                 + " AND ((" + ViewGroupsColumns.CONCRETE_DATA_SET + " IS NULL AND "
   1011                                     + SettingsColumns.CONCRETE_DATA_SET + " IS NULL) OR ("
   1012                                     + ViewGroupsColumns.CONCRETE_DATA_SET + "="
   1013                                     + SettingsColumns.CONCRETE_DATA_SET + "))))=0"
   1014                     + " THEN 1"
   1015                     + " ELSE 0"
   1016                     + " END)")
   1017             .add(Settings.UNGROUPED_COUNT,
   1018                     "(SELECT COUNT(*)"
   1019                     + " FROM (SELECT 1"
   1020                             + " FROM " + Tables.SETTINGS_JOIN_RAW_CONTACTS_DATA_MIMETYPES_CONTACTS
   1021                             + " GROUP BY " + Clauses.GROUP_BY_ACCOUNT_CONTACT_ID
   1022                             + " HAVING " + Clauses.HAVING_NO_GROUPS
   1023                     + "))")
   1024             .add(Settings.UNGROUPED_WITH_PHONES,
   1025                     "(SELECT COUNT(*)"
   1026                     + " FROM (SELECT 1"
   1027                             + " FROM " + Tables.SETTINGS_JOIN_RAW_CONTACTS_DATA_MIMETYPES_CONTACTS
   1028                             + " WHERE " + Contacts.HAS_PHONE_NUMBER
   1029                             + " GROUP BY " + Clauses.GROUP_BY_ACCOUNT_CONTACT_ID
   1030                             + " HAVING " + Clauses.HAVING_NO_GROUPS
   1031                     + "))")
   1032             .build();
   1033 
   1034     /** Contains StatusUpdates columns */
   1035     private static final ProjectionMap sStatusUpdatesProjectionMap = ProjectionMap.builder()
   1036             .add(PresenceColumns.RAW_CONTACT_ID)
   1037             .add(StatusUpdates.DATA_ID, DataColumns.CONCRETE_ID)
   1038             .add(StatusUpdates.IM_ACCOUNT)
   1039             .add(StatusUpdates.IM_HANDLE)
   1040             .add(StatusUpdates.PROTOCOL)
   1041             // We cannot allow a null in the custom protocol field, because SQLite3 does not
   1042             // properly enforce uniqueness of null values
   1043             .add(StatusUpdates.CUSTOM_PROTOCOL,
   1044                     "(CASE WHEN " + StatusUpdates.CUSTOM_PROTOCOL + "=''"
   1045                     + " THEN NULL"
   1046                     + " ELSE " + StatusUpdates.CUSTOM_PROTOCOL + " END)")
   1047             .add(StatusUpdates.PRESENCE)
   1048             .add(StatusUpdates.CHAT_CAPABILITY)
   1049             .add(StatusUpdates.STATUS)
   1050             .add(StatusUpdates.STATUS_TIMESTAMP)
   1051             .add(StatusUpdates.STATUS_RES_PACKAGE)
   1052             .add(StatusUpdates.STATUS_ICON)
   1053             .add(StatusUpdates.STATUS_LABEL)
   1054             .build();
   1055 
   1056     /** Contains StreamItems columns */
   1057     private static final ProjectionMap sStreamItemsProjectionMap = ProjectionMap.builder()
   1058             .add(StreamItems._ID)
   1059             .add(StreamItems.CONTACT_ID)
   1060             .add(StreamItems.CONTACT_LOOKUP_KEY)
   1061             .add(StreamItems.ACCOUNT_NAME)
   1062             .add(StreamItems.ACCOUNT_TYPE)
   1063             .add(StreamItems.DATA_SET)
   1064             .add(StreamItems.RAW_CONTACT_ID)
   1065             .add(StreamItems.RAW_CONTACT_SOURCE_ID)
   1066             .add(StreamItems.RES_PACKAGE)
   1067             .add(StreamItems.RES_ICON)
   1068             .add(StreamItems.RES_LABEL)
   1069             .add(StreamItems.TEXT)
   1070             .add(StreamItems.TIMESTAMP)
   1071             .add(StreamItems.COMMENTS)
   1072             .add(StreamItems.SYNC1)
   1073             .add(StreamItems.SYNC2)
   1074             .add(StreamItems.SYNC3)
   1075             .add(StreamItems.SYNC4)
   1076             .build();
   1077 
   1078     private static final ProjectionMap sStreamItemPhotosProjectionMap = ProjectionMap.builder()
   1079             .add(StreamItemPhotos._ID, StreamItemPhotosColumns.CONCRETE_ID)
   1080             .add(StreamItems.RAW_CONTACT_ID)
   1081             .add(StreamItems.RAW_CONTACT_SOURCE_ID, RawContactsColumns.CONCRETE_SOURCE_ID)
   1082             .add(StreamItemPhotos.STREAM_ITEM_ID)
   1083             .add(StreamItemPhotos.SORT_INDEX)
   1084             .add(StreamItemPhotos.PHOTO_FILE_ID)
   1085             .add(StreamItemPhotos.PHOTO_URI,
   1086                     "'" + DisplayPhoto.CONTENT_URI + "'||'/'||" + StreamItemPhotos.PHOTO_FILE_ID)
   1087             .add(PhotoFiles.HEIGHT)
   1088             .add(PhotoFiles.WIDTH)
   1089             .add(PhotoFiles.FILESIZE)
   1090             .add(StreamItemPhotos.SYNC1)
   1091             .add(StreamItemPhotos.SYNC2)
   1092             .add(StreamItemPhotos.SYNC3)
   1093             .add(StreamItemPhotos.SYNC4)
   1094             .build();
   1095 
   1096     /** Contains {@link Directory} columns */
   1097     private static final ProjectionMap sDirectoryProjectionMap = ProjectionMap.builder()
   1098             .add(Directory._ID)
   1099             .add(Directory.PACKAGE_NAME)
   1100             .add(Directory.TYPE_RESOURCE_ID)
   1101             .add(Directory.DISPLAY_NAME)
   1102             .add(Directory.DIRECTORY_AUTHORITY)
   1103             .add(Directory.ACCOUNT_TYPE)
   1104             .add(Directory.ACCOUNT_NAME)
   1105             .add(Directory.EXPORT_SUPPORT)
   1106             .add(Directory.SHORTCUT_SUPPORT)
   1107             .add(Directory.PHOTO_SUPPORT)
   1108             .build();
   1109 
   1110     // where clause to update the status_updates table
   1111     private static final String WHERE_CLAUSE_FOR_STATUS_UPDATES_TABLE =
   1112             StatusUpdatesColumns.DATA_ID + " IN (SELECT Distinct " + StatusUpdates.DATA_ID +
   1113             " FROM " + Tables.STATUS_UPDATES + " LEFT OUTER JOIN " + Tables.PRESENCE +
   1114             " ON " + StatusUpdatesColumns.DATA_ID + " = " + StatusUpdates.DATA_ID + " WHERE ";
   1115 
   1116     private static final String[] EMPTY_STRING_ARRAY = new String[0];
   1117 
   1118     private static final String DEFAULT_SNIPPET_ARG_START_MATCH = "[";
   1119     private static final String DEFAULT_SNIPPET_ARG_END_MATCH = "]";
   1120     private static final String DEFAULT_SNIPPET_ARG_ELLIPSIS = "\u2026";
   1121     private static final int DEFAULT_SNIPPET_ARG_MAX_TOKENS = 5;
   1122 
   1123     private final StringBuilder mSb = new StringBuilder();
   1124     private final String[] mSelectionArgs1 = new String[1];
   1125     private final String[] mSelectionArgs2 = new String[2];
   1126     private final String[] mSelectionArgs3 = new String[3];
   1127     private final String[] mSelectionArgs4 = new String[4];
   1128     private final ArrayList<String> mSelectionArgs = Lists.newArrayList();
   1129 
   1130     static {
   1131         // Contacts URI matching table
   1132         final UriMatcher matcher = sUriMatcher;
   1133 
   1134         // DO NOT use constants such as Contacts.CONTENT_URI here.  This is the only place
   1135         // where one can see all supported URLs at a glance, and using constants will reduce
   1136         // readability.
   1137         matcher.addURI(ContactsContract.AUTHORITY, "contacts", CONTACTS);
   1138         matcher.addURI(ContactsContract.AUTHORITY, "contacts/#", CONTACTS_ID);
   1139         matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/data", CONTACTS_ID_DATA);
   1140         matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/entities", CONTACTS_ID_ENTITIES);
   1141         matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/suggestions",
   1142                 AGGREGATION_SUGGESTIONS);
   1143         matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/suggestions/*",
   1144                 AGGREGATION_SUGGESTIONS);
   1145         matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/photo", CONTACTS_ID_PHOTO);
   1146         matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/display_photo",
   1147                 CONTACTS_ID_DISPLAY_PHOTO);
   1148 
   1149         // Special URIs that refer to contact pictures in the corp CP2.
   1150         matcher.addURI(ContactsContract.AUTHORITY, "contacts_corp/#/photo", CONTACTS_ID_PHOTO_CORP);
   1151         matcher.addURI(ContactsContract.AUTHORITY, "contacts_corp/#/display_photo",
   1152                 CONTACTS_ID_DISPLAY_PHOTO_CORP);
   1153 
   1154         matcher.addURI(ContactsContract.AUTHORITY, "contacts/#/stream_items",
   1155                 CONTACTS_ID_STREAM_ITEMS);
   1156         matcher.addURI(ContactsContract.AUTHORITY, "contacts/filter", CONTACTS_FILTER);
   1157         matcher.addURI(ContactsContract.AUTHORITY, "contacts/filter/*", CONTACTS_FILTER);
   1158         matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*", CONTACTS_LOOKUP);
   1159         matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/data", CONTACTS_LOOKUP_DATA);
   1160         matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/photo",
   1161                 CONTACTS_LOOKUP_PHOTO);
   1162         matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/#", CONTACTS_LOOKUP_ID);
   1163         matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/#/data",
   1164                 CONTACTS_LOOKUP_ID_DATA);
   1165         matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/#/photo",
   1166                 CONTACTS_LOOKUP_ID_PHOTO);
   1167         matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/display_photo",
   1168                 CONTACTS_LOOKUP_DISPLAY_PHOTO);
   1169         matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/#/display_photo",
   1170                 CONTACTS_LOOKUP_ID_DISPLAY_PHOTO);
   1171         matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/entities",
   1172                 CONTACTS_LOOKUP_ENTITIES);
   1173         matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/#/entities",
   1174                 CONTACTS_LOOKUP_ID_ENTITIES);
   1175         matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/stream_items",
   1176                 CONTACTS_LOOKUP_STREAM_ITEMS);
   1177         matcher.addURI(ContactsContract.AUTHORITY, "contacts/lookup/*/#/stream_items",
   1178                 CONTACTS_LOOKUP_ID_STREAM_ITEMS);
   1179         matcher.addURI(ContactsContract.AUTHORITY, "contacts/as_vcard/*", CONTACTS_AS_VCARD);
   1180         matcher.addURI(ContactsContract.AUTHORITY, "contacts/as_multi_vcard/*",
   1181                 CONTACTS_AS_MULTI_VCARD);
   1182         matcher.addURI(ContactsContract.AUTHORITY, "contacts/strequent/", CONTACTS_STREQUENT);
   1183         matcher.addURI(ContactsContract.AUTHORITY, "contacts/strequent/filter/*",
   1184                 CONTACTS_STREQUENT_FILTER);
   1185         matcher.addURI(ContactsContract.AUTHORITY, "contacts/group/*", CONTACTS_GROUP);
   1186         matcher.addURI(ContactsContract.AUTHORITY, "contacts/frequent", CONTACTS_FREQUENT);
   1187         matcher.addURI(ContactsContract.AUTHORITY, "contacts/delete_usage", CONTACTS_DELETE_USAGE);
   1188 
   1189         matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts", RAW_CONTACTS);
   1190         matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#", RAW_CONTACTS_ID);
   1191         matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#/data", RAW_CONTACTS_ID_DATA);
   1192         matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#/display_photo",
   1193                 RAW_CONTACTS_ID_DISPLAY_PHOTO);
   1194         matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#/entity", RAW_CONTACT_ID_ENTITY);
   1195         matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#/stream_items",
   1196                 RAW_CONTACTS_ID_STREAM_ITEMS);
   1197         matcher.addURI(ContactsContract.AUTHORITY, "raw_contacts/#/stream_items/#",
   1198                 RAW_CONTACTS_ID_STREAM_ITEMS_ID);
   1199 
   1200         matcher.addURI(ContactsContract.AUTHORITY, "raw_contact_entities", RAW_CONTACT_ENTITIES);
   1201 
   1202         matcher.addURI(ContactsContract.AUTHORITY, "data", DATA);
   1203         matcher.addURI(ContactsContract.AUTHORITY, "data/#", DATA_ID);
   1204         matcher.addURI(ContactsContract.AUTHORITY, "data/phones", PHONES);
   1205         matcher.addURI(ContactsContract.AUTHORITY, "data/phones/#", PHONES_ID);
   1206         matcher.addURI(ContactsContract.AUTHORITY, "data/phones/filter", PHONES_FILTER);
   1207         matcher.addURI(ContactsContract.AUTHORITY, "data/phones/filter/*", PHONES_FILTER);
   1208         matcher.addURI(ContactsContract.AUTHORITY, "data/emails", EMAILS);
   1209         matcher.addURI(ContactsContract.AUTHORITY, "data/emails/#", EMAILS_ID);
   1210         matcher.addURI(ContactsContract.AUTHORITY, "data/emails/lookup", EMAILS_LOOKUP);
   1211         matcher.addURI(ContactsContract.AUTHORITY, "data/emails/lookup/*", EMAILS_LOOKUP);
   1212         matcher.addURI(ContactsContract.AUTHORITY, "data/emails/filter", EMAILS_FILTER);
   1213         matcher.addURI(ContactsContract.AUTHORITY, "data/emails/filter/*", EMAILS_FILTER);
   1214         matcher.addURI(ContactsContract.AUTHORITY, "data/postals", POSTALS);
   1215         matcher.addURI(ContactsContract.AUTHORITY, "data/postals/#", POSTALS_ID);
   1216         /** "*" is in CSV form with data IDs ("123,456,789") */
   1217         matcher.addURI(ContactsContract.AUTHORITY, "data/usagefeedback/*", DATA_USAGE_FEEDBACK_ID);
   1218         matcher.addURI(ContactsContract.AUTHORITY, "data/callables/", CALLABLES);
   1219         matcher.addURI(ContactsContract.AUTHORITY, "data/callables/#", CALLABLES_ID);
   1220         matcher.addURI(ContactsContract.AUTHORITY, "data/callables/filter", CALLABLES_FILTER);
   1221         matcher.addURI(ContactsContract.AUTHORITY, "data/callables/filter/*", CALLABLES_FILTER);
   1222 
   1223         matcher.addURI(ContactsContract.AUTHORITY, "data/contactables/", CONTACTABLES);
   1224         matcher.addURI(ContactsContract.AUTHORITY, "data/contactables/filter", CONTACTABLES_FILTER);
   1225         matcher.addURI(ContactsContract.AUTHORITY, "data/contactables/filter/*",
   1226                 CONTACTABLES_FILTER);
   1227 
   1228         matcher.addURI(ContactsContract.AUTHORITY, "groups", GROUPS);
   1229         matcher.addURI(ContactsContract.AUTHORITY, "groups/#", GROUPS_ID);
   1230         matcher.addURI(ContactsContract.AUTHORITY, "groups_summary", GROUPS_SUMMARY);
   1231 
   1232         matcher.addURI(ContactsContract.AUTHORITY, SyncStateContentProviderHelper.PATH, SYNCSTATE);
   1233         matcher.addURI(ContactsContract.AUTHORITY, SyncStateContentProviderHelper.PATH + "/#",
   1234                 SYNCSTATE_ID);
   1235         matcher.addURI(ContactsContract.AUTHORITY, "profile/" + SyncStateContentProviderHelper.PATH,
   1236                 PROFILE_SYNCSTATE);
   1237         matcher.addURI(ContactsContract.AUTHORITY,
   1238                 "profile/" + SyncStateContentProviderHelper.PATH + "/#",
   1239                 PROFILE_SYNCSTATE_ID);
   1240 
   1241         matcher.addURI(ContactsContract.AUTHORITY, "phone_lookup/*", PHONE_LOOKUP);
   1242         matcher.addURI(ContactsContract.AUTHORITY, "phone_lookup_enterprise/*",
   1243                 PHONE_LOOKUP_ENTERPRISE);
   1244         matcher.addURI(ContactsContract.AUTHORITY, "aggregation_exceptions",
   1245                 AGGREGATION_EXCEPTIONS);
   1246         matcher.addURI(ContactsContract.AUTHORITY, "aggregation_exceptions/*",
   1247                 AGGREGATION_EXCEPTION_ID);
   1248 
   1249         matcher.addURI(ContactsContract.AUTHORITY, "settings", SETTINGS);
   1250 
   1251         matcher.addURI(ContactsContract.AUTHORITY, "status_updates", STATUS_UPDATES);
   1252         matcher.addURI(ContactsContract.AUTHORITY, "status_updates/#", STATUS_UPDATES_ID);
   1253 
   1254         matcher.addURI(ContactsContract.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY,
   1255                 SEARCH_SUGGESTIONS);
   1256         matcher.addURI(ContactsContract.AUTHORITY, SearchManager.SUGGEST_URI_PATH_QUERY + "/*",
   1257                 SEARCH_SUGGESTIONS);
   1258         matcher.addURI(ContactsContract.AUTHORITY, SearchManager.SUGGEST_URI_PATH_SHORTCUT + "/*",
   1259                 SEARCH_SHORTCUT);
   1260 
   1261         matcher.addURI(ContactsContract.AUTHORITY, "provider_status", PROVIDER_STATUS);
   1262 
   1263         matcher.addURI(ContactsContract.AUTHORITY, "directories", DIRECTORIES);
   1264         matcher.addURI(ContactsContract.AUTHORITY, "directories/#", DIRECTORIES_ID);
   1265 
   1266         matcher.addURI(ContactsContract.AUTHORITY, "complete_name", COMPLETE_NAME);
   1267 
   1268         matcher.addURI(ContactsContract.AUTHORITY, "profile", PROFILE);
   1269         matcher.addURI(ContactsContract.AUTHORITY, "profile/entities", PROFILE_ENTITIES);
   1270         matcher.addURI(ContactsContract.AUTHORITY, "profile/data", PROFILE_DATA);
   1271         matcher.addURI(ContactsContract.AUTHORITY, "profile/data/#", PROFILE_DATA_ID);
   1272         matcher.addURI(ContactsContract.AUTHORITY, "profile/photo", PROFILE_PHOTO);
   1273         matcher.addURI(ContactsContract.AUTHORITY, "profile/display_photo", PROFILE_DISPLAY_PHOTO);
   1274         matcher.addURI(ContactsContract.AUTHORITY, "profile/as_vcard", PROFILE_AS_VCARD);
   1275         matcher.addURI(ContactsContract.AUTHORITY, "profile/raw_contacts", PROFILE_RAW_CONTACTS);
   1276         matcher.addURI(ContactsContract.AUTHORITY, "profile/raw_contacts/#",
   1277                 PROFILE_RAW_CONTACTS_ID);
   1278         matcher.addURI(ContactsContract.AUTHORITY, "profile/raw_contacts/#/data",
   1279                 PROFILE_RAW_CONTACTS_ID_DATA);
   1280         matcher.addURI(ContactsContract.AUTHORITY, "profile/raw_contacts/#/entity",
   1281                 PROFILE_RAW_CONTACTS_ID_ENTITIES);
   1282         matcher.addURI(ContactsContract.AUTHORITY, "profile/status_updates",
   1283                 PROFILE_STATUS_UPDATES);
   1284         matcher.addURI(ContactsContract.AUTHORITY, "profile/raw_contact_entities",
   1285                 PROFILE_RAW_CONTACT_ENTITIES);
   1286 
   1287         matcher.addURI(ContactsContract.AUTHORITY, "stream_items", STREAM_ITEMS);
   1288         matcher.addURI(ContactsContract.AUTHORITY, "stream_items/photo", STREAM_ITEMS_PHOTOS);
   1289         matcher.addURI(ContactsContract.AUTHORITY, "stream_items/#", STREAM_ITEMS_ID);
   1290         matcher.addURI(ContactsContract.AUTHORITY, "stream_items/#/photo", STREAM_ITEMS_ID_PHOTOS);
   1291         matcher.addURI(ContactsContract.AUTHORITY, "stream_items/#/photo/#",
   1292                 STREAM_ITEMS_ID_PHOTOS_ID);
   1293         matcher.addURI(ContactsContract.AUTHORITY, "stream_items_limit", STREAM_ITEMS_LIMIT);
   1294 
   1295         matcher.addURI(ContactsContract.AUTHORITY, "display_photo/#", DISPLAY_PHOTO_ID);
   1296         matcher.addURI(ContactsContract.AUTHORITY, "photo_dimensions", PHOTO_DIMENSIONS);
   1297 
   1298         matcher.addURI(ContactsContract.AUTHORITY, "deleted_contacts", DELETED_CONTACTS);
   1299         matcher.addURI(ContactsContract.AUTHORITY, "deleted_contacts/#", DELETED_CONTACTS_ID);
   1300     }
   1301 
   1302     private static class DirectoryInfo {
   1303         String authority;
   1304         String accountName;
   1305         String accountType;
   1306     }
   1307 
   1308     /**
   1309      * An entry in group id cache.
   1310      *
   1311      * TODO: Move this and {@link #mGroupIdCache} to {@link DataRowHandlerForGroupMembership}.
   1312      */
   1313     public static class GroupIdCacheEntry {
   1314         long accountId;
   1315         String sourceId;
   1316         long groupId;
   1317     }
   1318 
   1319     /**
   1320      * The thread-local holder of the active transaction.  Shared between this and the profile
   1321      * provider, to keep transactions on both databases synchronized.
   1322      */
   1323     private final ThreadLocal<ContactsTransaction> mTransactionHolder =
   1324             new ThreadLocal<ContactsTransaction>();
   1325 
   1326     // This variable keeps track of whether the current operation is intended for the profile DB.
   1327     private final ThreadLocal<Boolean> mInProfileMode = new ThreadLocal<Boolean>();
   1328 
   1329     // Depending on whether the action being performed is for the profile, we will use one of two
   1330     // database helper instances.
   1331     private final ThreadLocal<ContactsDatabaseHelper> mDbHelper =
   1332             new ThreadLocal<ContactsDatabaseHelper>();
   1333 
   1334     // Depending on whether the action being performed is for the profile or not, we will use one of
   1335     // two aggregator instances.
   1336     private final ThreadLocal<ContactAggregator> mAggregator = new ThreadLocal<ContactAggregator>();
   1337 
   1338     // Depending on whether the action being performed is for the profile or not, we will use one of
   1339     // two photo store instances (with their files stored in separate sub-directories).
   1340     private final ThreadLocal<PhotoStore> mPhotoStore = new ThreadLocal<PhotoStore>();
   1341 
   1342     // The active transaction context will switch depending on the operation being performed.
   1343     // Both transaction contexts will be cleared out when a batch transaction is started, and
   1344     // each will be processed separately when a batch transaction completes.
   1345     private final TransactionContext mContactTransactionContext = new TransactionContext(false);
   1346     private final TransactionContext mProfileTransactionContext = new TransactionContext(true);
   1347     private final ThreadLocal<TransactionContext> mTransactionContext =
   1348             new ThreadLocal<TransactionContext>();
   1349 
   1350     // Map of single-use pre-authorized URIs to expiration times.
   1351     private final Map<Uri, Long> mPreAuthorizedUris = Maps.newHashMap();
   1352 
   1353     // Random number generator.
   1354     private final SecureRandom mRandom = new SecureRandom();
   1355 
   1356     private final HashMap<String, Boolean> mAccountWritability = Maps.newHashMap();
   1357 
   1358     private PhotoStore mContactsPhotoStore;
   1359     private PhotoStore mProfilePhotoStore;
   1360 
   1361     private ContactsDatabaseHelper mContactsHelper;
   1362     private ProfileDatabaseHelper mProfileHelper;
   1363 
   1364     // Separate data row handler instances for contact data and profile data.
   1365     private HashMap<String, DataRowHandler> mDataRowHandlers;
   1366     private HashMap<String, DataRowHandler> mProfileDataRowHandlers;
   1367 
   1368     /**
   1369      * Cached information about contact directories.
   1370      */
   1371     private HashMap<String, DirectoryInfo> mDirectoryCache = new HashMap<String, DirectoryInfo>();
   1372     private boolean mDirectoryCacheValid = false;
   1373 
   1374     /**
   1375      * Map from group source IDs to lists of {@link GroupIdCacheEntry}s.
   1376      *
   1377      * We don't need a soft cache for groups - the assumption is that there will only
   1378      * be a small number of contact groups. The cache is keyed off source ID.  The value
   1379      * is a list of groups with this group ID.
   1380      */
   1381     private HashMap<String, ArrayList<GroupIdCacheEntry>> mGroupIdCache = Maps.newHashMap();
   1382 
   1383     /**
   1384      * Sub-provider for handling profile requests against the profile database.
   1385      */
   1386     private ProfileProvider mProfileProvider;
   1387 
   1388     private NameSplitter mNameSplitter;
   1389     private NameLookupBuilder mNameLookupBuilder;
   1390 
   1391     private PostalSplitter mPostalSplitter;
   1392 
   1393     private ContactDirectoryManager mContactDirectoryManager;
   1394 
   1395     private boolean mIsPhoneInitialized;
   1396     private boolean mIsPhone;
   1397 
   1398     private Account mAccount;
   1399 
   1400     private ContactAggregator mContactAggregator;
   1401     private ContactAggregator mProfileAggregator;
   1402 
   1403     // Duration in milliseconds that pre-authorized URIs will remain valid.
   1404     private long mPreAuthorizedUriDuration;
   1405 
   1406     private LegacyApiSupport mLegacyApiSupport;
   1407     private GlobalSearchSupport mGlobalSearchSupport;
   1408     private CommonNicknameCache mCommonNicknameCache;
   1409     private SearchIndexManager mSearchIndexManager;
   1410 
   1411     private int mProviderStatus = ProviderStatus.STATUS_NORMAL;
   1412     private boolean mProviderStatusUpdateNeeded;
   1413     private long mEstimatedStorageRequirement = 0;
   1414     private volatile CountDownLatch mReadAccessLatch;
   1415     private volatile CountDownLatch mWriteAccessLatch;
   1416     private boolean mAccountUpdateListenerRegistered;
   1417     private boolean mOkToOpenAccess = true;
   1418 
   1419     private boolean mVisibleTouched = false;
   1420 
   1421     private boolean mSyncToNetwork;
   1422 
   1423     private LocaleSet mCurrentLocales;
   1424     private int mContactsAccountCount;
   1425 
   1426     private HandlerThread mBackgroundThread;
   1427     private Handler mBackgroundHandler;
   1428 
   1429     private long mLastPhotoCleanup = 0;
   1430 
   1431     private FastScrollingIndexCache mFastScrollingIndexCache;
   1432 
   1433     // Stats about FastScrollingIndex.
   1434     private int mFastScrollingIndexCacheRequestCount;
   1435     private int mFastScrollingIndexCacheMissCount;
   1436     private long mTotalTimeFastScrollingIndexGenerate;
   1437 
   1438     @Override
   1439     public boolean onCreate() {
   1440         if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) {
   1441             Log.d(Constants.PERFORMANCE_TAG, "ContactsProvider2.onCreate start");
   1442         }
   1443         super.onCreate();
   1444         setAppOps(AppOpsManager.OP_READ_CONTACTS, AppOpsManager.OP_WRITE_CONTACTS);
   1445         try {
   1446             return initialize();
   1447         } catch (RuntimeException e) {
   1448             Log.e(TAG, "Cannot start provider", e);
   1449             // In production code we don't want to throw here, so that phone will still work
   1450             // in low storage situations.
   1451             // See I5c88a3024ff1c5a06b5756b29a2d903f8f6a2531
   1452             if (shouldThrowExceptionForInitializationError()) {
   1453                 throw e;
   1454             }
   1455             return false;
   1456         } finally {
   1457             if (Log.isLoggable(Constants.PERFORMANCE_TAG, Log.DEBUG)) {
   1458                 Log.d(Constants.PERFORMANCE_TAG, "ContactsProvider2.onCreate finish");
   1459             }
   1460         }
   1461     }
   1462 
   1463     protected boolean shouldThrowExceptionForInitializationError() {
   1464         return false;
   1465     }
   1466 
   1467     private boolean initialize() {
   1468         StrictMode.setThreadPolicy(
   1469                 new StrictMode.ThreadPolicy.Builder().detectAll().penaltyLog().build());
   1470 
   1471         mFastScrollingIndexCache = FastScrollingIndexCache.getInstance(getContext());
   1472 
   1473         mContactsHelper = getDatabaseHelper(getContext());
   1474         mDbHelper.set(mContactsHelper);
   1475 
   1476         // Set up the DB helper for keeping transactions serialized.
   1477         setDbHelperToSerializeOn(mContactsHelper, CONTACTS_DB_TAG, this);
   1478 
   1479         mContactDirectoryManager = new ContactDirectoryManager(this);
   1480         mGlobalSearchSupport = new GlobalSearchSupport(this);
   1481 
   1482         // The provider is closed for business until fully initialized
   1483         mReadAccessLatch = new CountDownLatch(1);
   1484         mWriteAccessLatch = new CountDownLatch(1);
   1485 
   1486         mBackgroundThread = new HandlerThread("ContactsProviderWorker",
   1487                 Process.THREAD_PRIORITY_BACKGROUND);
   1488         mBackgroundThread.start();
   1489         mBackgroundHandler = new Handler(mBackgroundThread.getLooper()) {
   1490             @Override
   1491             public void handleMessage(Message msg) {
   1492                 performBackgroundTask(msg.what, msg.obj);
   1493             }
   1494         };
   1495 
   1496         // Set up the sub-provider for handling profiles.
   1497         mProfileProvider = newProfileProvider();
   1498         mProfileProvider.setDbHelperToSerializeOn(mContactsHelper, CONTACTS_DB_TAG, this);
   1499         ProviderInfo profileInfo = new ProviderInfo();
   1500         profileInfo.readPermission = "android.permission.READ_PROFILE";
   1501         profileInfo.writePermission = "android.permission.WRITE_PROFILE";
   1502         profileInfo.authority = ContactsContract.AUTHORITY;
   1503         mProfileProvider.attachInfo(getContext(), profileInfo);
   1504         mProfileHelper = mProfileProvider.getDatabaseHelper(getContext());
   1505 
   1506         // Initialize the pre-authorized URI duration.
   1507         mPreAuthorizedUriDuration = DEFAULT_PREAUTHORIZED_URI_EXPIRATION;
   1508 
   1509         scheduleBackgroundTask(BACKGROUND_TASK_INITIALIZE);
   1510         scheduleBackgroundTask(BACKGROUND_TASK_UPDATE_ACCOUNTS);
   1511         scheduleBackgroundTask(BACKGROUND_TASK_UPDATE_LOCALE);
   1512         scheduleBackgroundTask(BACKGROUND_TASK_UPGRADE_AGGREGATION_ALGORITHM);
   1513         scheduleBackgroundTask(BACKGROUND_TASK_UPDATE_SEARCH_INDEX);
   1514         scheduleBackgroundTask(BACKGROUND_TASK_UPDATE_PROVIDER_STATUS);
   1515         scheduleBackgroundTask(BACKGROUND_TASK_OPEN_WRITE_ACCESS);
   1516         scheduleBackgroundTask(BACKGROUND_TASK_CLEANUP_PHOTOS);
   1517         scheduleBackgroundTask(BACKGROUND_TASK_CLEAN_DELETE_LOG);
   1518 
   1519         return true;
   1520     }
   1521 
   1522     // Updates the locale set to reflect a new system locale.
   1523     private static LocaleSet updateLocaleSet(LocaleSet oldLocales, Locale newLocale) {
   1524         final Locale prevLocale = oldLocales.getPrimaryLocale();
   1525         // If primary locale is unchanged then no change to locale set.
   1526         if (newLocale.equals(prevLocale)) {
   1527             return oldLocales;
   1528         }
   1529         // Otherwise, construct a new locale set based on the new locale
   1530         // and the previous primary locale.
   1531         return new LocaleSet(newLocale, prevLocale).normalize();
   1532     }
   1533 
   1534     private static LocaleSet getProviderPrefLocales(SharedPreferences prefs) {
   1535         final String providerLocaleString = prefs.getString(PREF_LOCALE, null);
   1536         return LocaleSet.getLocaleSet(providerLocaleString);
   1537     }
   1538 
   1539     // Called by initForDefaultLocale. Returns an updated locale set using the
   1540     // current system locale.
   1541     private LocaleSet getLocaleSet() {
   1542         final Locale curLocale = getLocale();
   1543         if (mCurrentLocales != null) {
   1544             return updateLocaleSet(mCurrentLocales, curLocale);
   1545         }
   1546         // On startup need to reload the locale set from prefs for update.
   1547         final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
   1548         return updateLocaleSet(getProviderPrefLocales(prefs), curLocale);
   1549     }
   1550 
   1551     // Static routine called on startup by updateLocaleOffline.
   1552     private static LocaleSet getLocaleSet(SharedPreferences prefs, Locale curLocale) {
   1553         return updateLocaleSet(getProviderPrefLocales(prefs), curLocale);
   1554     }
   1555 
   1556     /**
   1557      * (Re)allocates all locale-sensitive structures.
   1558      */
   1559     private void initForDefaultLocale() {
   1560         Context context = getContext();
   1561         mLegacyApiSupport =
   1562                 new LegacyApiSupport(context, mContactsHelper, this, mGlobalSearchSupport);
   1563         mCurrentLocales = getLocaleSet();
   1564         mNameSplitter = mContactsHelper.createNameSplitter(mCurrentLocales.getPrimaryLocale());
   1565         mNameLookupBuilder = new StructuredNameLookupBuilder(mNameSplitter);
   1566         mPostalSplitter = new PostalSplitter(mCurrentLocales.getPrimaryLocale());
   1567         mCommonNicknameCache = new CommonNicknameCache(mContactsHelper.getReadableDatabase());
   1568         ContactLocaleUtils.setLocales(mCurrentLocales);
   1569         mContactAggregator = new ContactAggregator(this, mContactsHelper,
   1570                 createPhotoPriorityResolver(context), mNameSplitter, mCommonNicknameCache);
   1571         mContactAggregator.setEnabled(SystemProperties.getBoolean(AGGREGATE_CONTACTS, true));
   1572         mProfileAggregator = new ProfileAggregator(this, mProfileHelper,
   1573                 createPhotoPriorityResolver(context), mNameSplitter, mCommonNicknameCache);
   1574         mProfileAggregator.setEnabled(SystemProperties.getBoolean(AGGREGATE_CONTACTS, true));
   1575         mSearchIndexManager = new SearchIndexManager(this);
   1576         mContactsPhotoStore = new PhotoStore(getContext().getFilesDir(), mContactsHelper);
   1577         mProfilePhotoStore =
   1578                 new PhotoStore(new File(getContext().getFilesDir(), "profile"), mProfileHelper);
   1579 
   1580         mDataRowHandlers = new HashMap<String, DataRowHandler>();
   1581         initDataRowHandlers(mDataRowHandlers, mContactsHelper, mContactAggregator,
   1582                 mContactsPhotoStore);
   1583         mProfileDataRowHandlers = new HashMap<String, DataRowHandler>();
   1584         initDataRowHandlers(mProfileDataRowHandlers, mProfileHelper, mProfileAggregator,
   1585                 mProfilePhotoStore);
   1586 
   1587         // Set initial thread-local state variables for the Contacts DB.
   1588         switchToContactMode();
   1589     }
   1590 
   1591     private void initDataRowHandlers(Map<String, DataRowHandler> handlerMap,
   1592             ContactsDatabaseHelper dbHelper, ContactAggregator contactAggregator,
   1593             PhotoStore photoStore) {
   1594         Context context = getContext();
   1595         handlerMap.put(Email.CONTENT_ITEM_TYPE,
   1596                 new DataRowHandlerForEmail(context, dbHelper, contactAggregator));
   1597         handlerMap.put(Im.CONTENT_ITEM_TYPE,
   1598                 new DataRowHandlerForIm(context, dbHelper, contactAggregator));
   1599         handlerMap.put(Organization.CONTENT_ITEM_TYPE,
   1600                 new DataRowHandlerForOrganization(context, dbHelper, contactAggregator));
   1601         handlerMap.put(Phone.CONTENT_ITEM_TYPE,
   1602                 new DataRowHandlerForPhoneNumber(context, dbHelper, contactAggregator));
   1603         handlerMap.put(Nickname.CONTENT_ITEM_TYPE,
   1604                 new DataRowHandlerForNickname(context, dbHelper, contactAggregator));
   1605         handlerMap.put(StructuredName.CONTENT_ITEM_TYPE,
   1606                 new DataRowHandlerForStructuredName(context, dbHelper, contactAggregator,
   1607                         mNameSplitter, mNameLookupBuilder));
   1608         handlerMap.put(StructuredPostal.CONTENT_ITEM_TYPE,
   1609                 new DataRowHandlerForStructuredPostal(context, dbHelper, contactAggregator,
   1610                         mPostalSplitter));
   1611         handlerMap.put(GroupMembership.CONTENT_ITEM_TYPE,
   1612                 new DataRowHandlerForGroupMembership(context, dbHelper, contactAggregator,
   1613                         mGroupIdCache));
   1614         handlerMap.put(Photo.CONTENT_ITEM_TYPE,
   1615                 new DataRowHandlerForPhoto(context, dbHelper, contactAggregator, photoStore,
   1616                         getMaxDisplayPhotoDim(), getMaxThumbnailDim()));
   1617         handlerMap.put(Note.CONTENT_ITEM_TYPE,
   1618                 new DataRowHandlerForNote(context, dbHelper, contactAggregator));
   1619         handlerMap.put(Identity.CONTENT_ITEM_TYPE,
   1620                 new DataRowHandlerForIdentity(context, dbHelper, contactAggregator));
   1621     }
   1622 
   1623     @VisibleForTesting
   1624     PhotoPriorityResolver createPhotoPriorityResolver(Context context) {
   1625         return new PhotoPriorityResolver(context);
   1626     }
   1627 
   1628     protected void scheduleBackgroundTask(int task) {
   1629         mBackgroundHandler.sendEmptyMessage(task);
   1630     }
   1631 
   1632     protected void scheduleBackgroundTask(int task, Object arg) {
   1633         mBackgroundHandler.sendMessage(mBackgroundHandler.obtainMessage(task, arg));
   1634     }
   1635 
   1636     protected void performBackgroundTask(int task, Object arg) {
   1637         // Make sure we operate on the contacts db by default.
   1638         switchToContactMode();
   1639         switch (task) {
   1640             case BACKGROUND_TASK_INITIALIZE: {
   1641                 initForDefaultLocale();
   1642                 mReadAccessLatch.countDown();
   1643                 mReadAccessLatch = null;
   1644                 break;
   1645             }
   1646 
   1647             case BACKGROUND_TASK_OPEN_WRITE_ACCESS: {
   1648                 if (mOkToOpenAccess) {
   1649                     mWriteAccessLatch.countDown();
   1650                     mWriteAccessLatch = null;
   1651                 }
   1652                 break;
   1653             }
   1654 
   1655             case BACKGROUND_TASK_UPDATE_ACCOUNTS: {
   1656                 Context context = getContext();
   1657                 if (!mAccountUpdateListenerRegistered) {
   1658                     AccountManager.get(context).addOnAccountsUpdatedListener(this, null, false);
   1659                     mAccountUpdateListenerRegistered = true;
   1660                 }
   1661 
   1662                 // Update the accounts for both the contacts and profile DBs.
   1663                 Account[] accounts = AccountManager.get(context).getAccounts();
   1664                 switchToContactMode();
   1665                 boolean accountsChanged = updateAccountsInBackground(accounts);
   1666                 switchToProfileMode();
   1667                 accountsChanged |= updateAccountsInBackground(accounts);
   1668 
   1669                 switchToContactMode();
   1670 
   1671                 updateContactsAccountCount(accounts);
   1672                 updateDirectoriesInBackground(accountsChanged);
   1673                 break;
   1674             }
   1675 
   1676             case BACKGROUND_TASK_UPDATE_LOCALE: {
   1677                 updateLocaleInBackground();
   1678                 break;
   1679             }
   1680 
   1681             case BACKGROUND_TASK_CHANGE_LOCALE: {
   1682                 changeLocaleInBackground();
   1683                 break;
   1684             }
   1685 
   1686             case BACKGROUND_TASK_UPGRADE_AGGREGATION_ALGORITHM: {
   1687                 if (isAggregationUpgradeNeeded()) {
   1688                     upgradeAggregationAlgorithmInBackground();
   1689                     invalidateFastScrollingIndexCache();
   1690                 }
   1691                 break;
   1692             }
   1693 
   1694             case BACKGROUND_TASK_UPDATE_SEARCH_INDEX: {
   1695                 updateSearchIndexInBackground();
   1696                 break;
   1697             }
   1698 
   1699             case BACKGROUND_TASK_UPDATE_PROVIDER_STATUS: {
   1700                 updateProviderStatus();
   1701                 break;
   1702             }
   1703 
   1704             case BACKGROUND_TASK_UPDATE_DIRECTORIES: {
   1705                 if (arg != null) {
   1706                     mContactDirectoryManager.onPackageChanged((String) arg);
   1707                 }
   1708                 break;
   1709             }
   1710 
   1711             case BACKGROUND_TASK_CLEANUP_PHOTOS: {
   1712                 // Check rate limit.
   1713                 long now = System.currentTimeMillis();
   1714                 if (now - mLastPhotoCleanup > PHOTO_CLEANUP_RATE_LIMIT) {
   1715                     mLastPhotoCleanup = now;
   1716 
   1717                     // Clean up photo stores for both contacts and profiles.
   1718                     switchToContactMode();
   1719                     cleanupPhotoStore();
   1720                     switchToProfileMode();
   1721                     cleanupPhotoStore();
   1722 
   1723                     switchToContactMode(); // Switch to the default, just in case.
   1724                 }
   1725                 break;
   1726             }
   1727 
   1728             case BACKGROUND_TASK_CLEAN_DELETE_LOG: {
   1729                 final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
   1730                 DeletedContactsTableUtil.deleteOldLogs(db);
   1731                 break;
   1732             }
   1733         }
   1734     }
   1735 
   1736     public void onLocaleChanged() {
   1737         if (mProviderStatus != ProviderStatus.STATUS_NORMAL
   1738                 && mProviderStatus != ProviderStatus.STATUS_NO_ACCOUNTS_NO_CONTACTS) {
   1739             return;
   1740         }
   1741 
   1742         scheduleBackgroundTask(BACKGROUND_TASK_CHANGE_LOCALE);
   1743     }
   1744 
   1745     private static boolean needsToUpdateLocaleData(SharedPreferences prefs,
   1746             LocaleSet locales, ContactsDatabaseHelper contactsHelper,
   1747             ProfileDatabaseHelper profileHelper) {
   1748         final String providerLocales = prefs.getString(PREF_LOCALE, null);
   1749 
   1750         // If locale matches that of the provider, and neither DB needs
   1751         // updating, there's nothing to do. A DB might require updating
   1752         // as a result of a system upgrade.
   1753         if (!locales.toString().equals(providerLocales)) {
   1754             Log.i(TAG, "Locale has changed from " + providerLocales
   1755                     + " to " + locales);
   1756             return true;
   1757         }
   1758         if (contactsHelper.needsToUpdateLocaleData(locales) ||
   1759                 profileHelper.needsToUpdateLocaleData(locales)) {
   1760             return true;
   1761         }
   1762         return false;
   1763     }
   1764 
   1765     /**
   1766      * Verifies that the contacts database is properly configured for the current locale.
   1767      * If not, changes the database locale to the current locale using an asynchronous task.
   1768      * This needs to be done asynchronously because the process involves rebuilding
   1769      * large data structures (name lookup, sort keys), which can take minutes on
   1770      * a large set of contacts.
   1771      */
   1772     protected void updateLocaleInBackground() {
   1773 
   1774         // The process is already running - postpone the change
   1775         if (mProviderStatus == ProviderStatus.STATUS_CHANGING_LOCALE) {
   1776             return;
   1777         }
   1778 
   1779         final LocaleSet currentLocales = mCurrentLocales;
   1780         final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext());
   1781         if (!needsToUpdateLocaleData(prefs, currentLocales, mContactsHelper, mProfileHelper)) {
   1782             return;
   1783         }
   1784 
   1785         int providerStatus = mProviderStatus;
   1786         setProviderStatus(ProviderStatus.STATUS_CHANGING_LOCALE);
   1787         mContactsHelper.setLocale(currentLocales);
   1788         mProfileHelper.setLocale(currentLocales);
   1789         mSearchIndexManager.updateIndex(true);
   1790         prefs.edit().putString(PREF_LOCALE, currentLocales.toString()).commit();
   1791         setProviderStatus(providerStatus);
   1792     }
   1793 
   1794     // Static update routine for use by ContactsUpgradeReceiver during startup.
   1795     // This clears the search index and marks it to be rebuilt, but doesn't
   1796     // actually rebuild it. That is done later by
   1797     // BACKGROUND_TASK_UPDATE_SEARCH_INDEX.
   1798     protected static void updateLocaleOffline(
   1799             Context context,
   1800             ContactsDatabaseHelper contactsHelper,
   1801             ProfileDatabaseHelper profileHelper) {
   1802         final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
   1803         final LocaleSet currentLocales = getLocaleSet(prefs, Locale.getDefault());
   1804         if (!needsToUpdateLocaleData(prefs, currentLocales, contactsHelper, profileHelper)) {
   1805             return;
   1806         }
   1807 
   1808         contactsHelper.setLocale(currentLocales);
   1809         profileHelper.setLocale(currentLocales);
   1810         contactsHelper.rebuildSearchIndex();
   1811         prefs.edit().putString(PREF_LOCALE, currentLocales.toString()).commit();
   1812     }
   1813 
   1814     /**
   1815      * Reinitializes the provider for a new locale.
   1816      */
   1817     private void changeLocaleInBackground() {
   1818         // Re-initializing the provider without stopping it.
   1819         // Locking the database will prevent inserts/updates/deletes from
   1820         // running at the same time, but queries may still be running
   1821         // on other threads. Those queries may return inconsistent results.
   1822         SQLiteDatabase db = mContactsHelper.getWritableDatabase();
   1823         SQLiteDatabase profileDb = mProfileHelper.getWritableDatabase();
   1824         db.beginTransaction();
   1825         profileDb.beginTransaction();
   1826         try {
   1827             initForDefaultLocale();
   1828             db.setTransactionSuccessful();
   1829             profileDb.setTransactionSuccessful();
   1830         } finally {
   1831             db.endTransaction();
   1832             profileDb.endTransaction();
   1833         }
   1834 
   1835         updateLocaleInBackground();
   1836     }
   1837 
   1838     protected void updateSearchIndexInBackground() {
   1839         mSearchIndexManager.updateIndex(false);
   1840     }
   1841 
   1842     protected void updateDirectoriesInBackground(boolean rescan) {
   1843         mContactDirectoryManager.scanAllPackages(rescan);
   1844     }
   1845 
   1846     private void updateProviderStatus() {
   1847         if (mProviderStatus != ProviderStatus.STATUS_NORMAL
   1848                 && mProviderStatus != ProviderStatus.STATUS_NO_ACCOUNTS_NO_CONTACTS) {
   1849             return;
   1850         }
   1851 
   1852         // No accounts/no contacts status is true if there are no account and
   1853         // there are no contacts or one profile contact
   1854         if (mContactsAccountCount == 0) {
   1855             boolean isContactsEmpty = DatabaseUtils.queryIsEmpty(mContactsHelper.getReadableDatabase(), Tables.CONTACTS);
   1856             long profileNum = DatabaseUtils.queryNumEntries(mProfileHelper.getReadableDatabase(),
   1857                     Tables.CONTACTS, null);
   1858 
   1859             // TODO: Different status if there is a profile but no contacts?
   1860             if (isContactsEmpty && profileNum <= 1) {
   1861                 setProviderStatus(ProviderStatus.STATUS_NO_ACCOUNTS_NO_CONTACTS);
   1862             } else {
   1863                 setProviderStatus(ProviderStatus.STATUS_NORMAL);
   1864             }
   1865         } else {
   1866             setProviderStatus(ProviderStatus.STATUS_NORMAL);
   1867         }
   1868     }
   1869 
   1870     @VisibleForTesting
   1871     protected void cleanupPhotoStore() {
   1872         final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
   1873 
   1874         // Assemble the set of photo store file IDs that are in use, and send those to the photo
   1875         // store.  Any photos that aren't in that set will be deleted, and any photos that no
   1876         // longer exist in the photo store will be returned for us to clear out in the DB.
   1877         long photoMimeTypeId = mDbHelper.get().getMimeTypeId(Photo.CONTENT_ITEM_TYPE);
   1878         Cursor c = db.query(Views.DATA, new String[] {Data._ID, Photo.PHOTO_FILE_ID},
   1879                 DataColumns.MIMETYPE_ID + "=" + photoMimeTypeId + " AND "
   1880                         + Photo.PHOTO_FILE_ID + " IS NOT NULL", null, null, null, null);
   1881         Set<Long> usedPhotoFileIds = Sets.newHashSet();
   1882         Map<Long, Long> photoFileIdToDataId = Maps.newHashMap();
   1883         try {
   1884             while (c.moveToNext()) {
   1885                 long dataId = c.getLong(0);
   1886                 long photoFileId = c.getLong(1);
   1887                 usedPhotoFileIds.add(photoFileId);
   1888                 photoFileIdToDataId.put(photoFileId, dataId);
   1889             }
   1890         } finally {
   1891             c.close();
   1892         }
   1893 
   1894         // Also query for all social stream item photos.
   1895         c = db.query(Tables.STREAM_ITEM_PHOTOS + " JOIN " + Tables.STREAM_ITEMS
   1896                 + " ON " + StreamItemPhotos.STREAM_ITEM_ID + "=" + StreamItemsColumns.CONCRETE_ID,
   1897                 new String[] {
   1898                         StreamItemPhotosColumns.CONCRETE_ID,
   1899                         StreamItemPhotosColumns.CONCRETE_STREAM_ITEM_ID,
   1900                         StreamItemPhotos.PHOTO_FILE_ID
   1901                 },
   1902                 null, null, null, null, null);
   1903         Map<Long, Long> photoFileIdToStreamItemPhotoId = Maps.newHashMap();
   1904         Map<Long, Long> streamItemPhotoIdToStreamItemId = Maps.newHashMap();
   1905         try {
   1906             while (c.moveToNext()) {
   1907                 long streamItemPhotoId = c.getLong(0);
   1908                 long streamItemId = c.getLong(1);
   1909                 long photoFileId = c.getLong(2);
   1910                 usedPhotoFileIds.add(photoFileId);
   1911                 photoFileIdToStreamItemPhotoId.put(photoFileId, streamItemPhotoId);
   1912                 streamItemPhotoIdToStreamItemId.put(streamItemPhotoId, streamItemId);
   1913             }
   1914         } finally {
   1915             c.close();
   1916         }
   1917 
   1918         // Run the photo store cleanup.
   1919         Set<Long> missingPhotoIds = mPhotoStore.get().cleanup(usedPhotoFileIds);
   1920 
   1921         // If any of the keys we're using no longer exist, clean them up.  We need to do these
   1922         // using internal APIs or direct DB access to avoid permission errors.
   1923         if (!missingPhotoIds.isEmpty()) {
   1924             try {
   1925                 // Need to set the db listener because we need to run onCommit afterwards.
   1926                 // Make sure to use the proper listener depending on the current mode.
   1927                 db.beginTransactionWithListener(inProfileMode() ? mProfileProvider : this);
   1928                 for (long missingPhotoId : missingPhotoIds) {
   1929                     if (photoFileIdToDataId.containsKey(missingPhotoId)) {
   1930                         long dataId = photoFileIdToDataId.get(missingPhotoId);
   1931                         ContentValues updateValues = new ContentValues();
   1932                         updateValues.putNull(Photo.PHOTO_FILE_ID);
   1933                         updateData(ContentUris.withAppendedId(Data.CONTENT_URI, dataId),
   1934                                 updateValues, null, null, false);
   1935                     }
   1936                     if (photoFileIdToStreamItemPhotoId.containsKey(missingPhotoId)) {
   1937                         // For missing photos that were in stream item photos, just delete the
   1938                         // stream item photo.
   1939                         long streamItemPhotoId = photoFileIdToStreamItemPhotoId.get(missingPhotoId);
   1940                         db.delete(Tables.STREAM_ITEM_PHOTOS, StreamItemPhotos._ID + "=?",
   1941                                 new String[] {String.valueOf(streamItemPhotoId)});
   1942                     }
   1943                 }
   1944                 db.setTransactionSuccessful();
   1945             } catch (Exception e) {
   1946                 // Cleanup failure is not a fatal problem.  We'll try again later.
   1947                 Log.e(TAG, "Failed to clean up outdated photo references", e);
   1948             } finally {
   1949                 db.endTransaction();
   1950             }
   1951         }
   1952     }
   1953 
   1954     @Override
   1955     protected ContactsDatabaseHelper getDatabaseHelper(final Context context) {
   1956         return ContactsDatabaseHelper.getInstance(context);
   1957     }
   1958 
   1959     @Override
   1960     protected ThreadLocal<ContactsTransaction> getTransactionHolder() {
   1961         return mTransactionHolder;
   1962     }
   1963 
   1964     public ProfileProvider newProfileProvider() {
   1965         return new ProfileProvider(this);
   1966     }
   1967 
   1968     @VisibleForTesting
   1969     /* package */ PhotoStore getPhotoStore() {
   1970         return mContactsPhotoStore;
   1971     }
   1972 
   1973     @VisibleForTesting
   1974     /* package */ PhotoStore getProfilePhotoStore() {
   1975         return mProfilePhotoStore;
   1976     }
   1977 
   1978     /**
   1979      * Maximum dimension (height or width) of photo thumbnails.
   1980      */
   1981     public int getMaxThumbnailDim() {
   1982         return PhotoProcessor.getMaxThumbnailSize();
   1983     }
   1984 
   1985     /**
   1986      * Maximum dimension (height or width) of display photos.  Larger images will be scaled
   1987      * to fit.
   1988      */
   1989     public int getMaxDisplayPhotoDim() {
   1990         return PhotoProcessor.getMaxDisplayPhotoSize();
   1991     }
   1992 
   1993     @VisibleForTesting
   1994     public ContactDirectoryManager getContactDirectoryManagerForTest() {
   1995         return mContactDirectoryManager;
   1996     }
   1997 
   1998     @VisibleForTesting
   1999     protected Locale getLocale() {
   2000         return Locale.getDefault();
   2001     }
   2002 
   2003     @VisibleForTesting
   2004     final boolean inProfileMode() {
   2005         Boolean profileMode = mInProfileMode.get();
   2006         return profileMode != null && profileMode;
   2007     }
   2008 
   2009     /**
   2010      * Wipes all data from the contacts database.
   2011      */
   2012     @NeededForTesting
   2013     void wipeData() {
   2014         invalidateFastScrollingIndexCache();
   2015         mContactsHelper.wipeData();
   2016         mProfileHelper.wipeData();
   2017         mContactsPhotoStore.clear();
   2018         mProfilePhotoStore.clear();
   2019         mProviderStatus = ProviderStatus.STATUS_NO_ACCOUNTS_NO_CONTACTS;
   2020         initForDefaultLocale();
   2021     }
   2022 
   2023     /**
   2024      * During initialization, this content provider will block all attempts to change contacts data.
   2025      * In particular, it will hold up all contact syncs.  As soon as the import process is complete,
   2026      * all processes waiting to write to the provider are unblocked, and can proceed to compete for
   2027      * the database transaction monitor.
   2028      */
   2029     private void waitForAccess(CountDownLatch latch) {
   2030         if (latch == null) {
   2031             return;
   2032         }
   2033 
   2034         while (true) {
   2035             try {
   2036                 latch.await();
   2037                 return;
   2038             } catch (InterruptedException e) {
   2039                 Thread.currentThread().interrupt();
   2040             }
   2041         }
   2042     }
   2043 
   2044     private int getIntValue(ContentValues values, String key, int defaultValue) {
   2045         final Integer value = values.getAsInteger(key);
   2046         return value != null ? value : defaultValue;
   2047     }
   2048 
   2049     private boolean flagExists(ContentValues values, String key) {
   2050         return values.getAsInteger(key) != null;
   2051     }
   2052 
   2053     private boolean flagIsSet(ContentValues values, String key) {
   2054         return getIntValue(values, key, 0) != 0;
   2055     }
   2056 
   2057     private boolean flagIsClear(ContentValues values, String key) {
   2058         return getIntValue(values, key, 1) == 0;
   2059     }
   2060 
   2061     /**
   2062      * Determines whether the given URI should be directed to the profile
   2063      * database rather than the contacts database.  This is true under either
   2064      * of three conditions:
   2065      * 1. The URI itself is specifically for the profile.
   2066      * 2. The URI contains ID references that are in the profile ID-space.
   2067      * 3. The URI contains lookup key references that match the special profile lookup key.
   2068      * @param uri The URI to examine.
   2069      * @return Whether to direct the DB operation to the profile database.
   2070      */
   2071     private boolean mapsToProfileDb(Uri uri) {
   2072         return sUriMatcher.mapsToProfile(uri);
   2073     }
   2074 
   2075     /**
   2076      * Determines whether the given URI with the given values being inserted
   2077      * should be directed to the profile database rather than the contacts
   2078      * database.  This is true if the URI already maps to the profile DB from
   2079      * a call to {@link #mapsToProfileDb} or if the URI matches a URI that
   2080      * specifies parent IDs via the ContentValues, and the given ContentValues
   2081      * contains an ID in the profile ID-space.
   2082      * @param uri The URI to examine.
   2083      * @param values The values being inserted.
   2084      * @return Whether to direct the DB insert to the profile database.
   2085      */
   2086     private boolean mapsToProfileDbWithInsertedValues(Uri uri, ContentValues values) {
   2087         if (mapsToProfileDb(uri)) {
   2088             return true;
   2089         }
   2090         int match = sUriMatcher.match(uri);
   2091         if (INSERT_URI_ID_VALUE_MAP.containsKey(match)) {
   2092             String idField = INSERT_URI_ID_VALUE_MAP.get(match);
   2093             Long id = values.getAsLong(idField);
   2094             if (id != null && ContactsContract.isProfileId(id)) {
   2095                 return true;
   2096             }
   2097         }
   2098         return false;
   2099     }
   2100 
   2101     /**
   2102      * Switches the provider's thread-local context variables to prepare for performing
   2103      * a profile operation.
   2104      */
   2105     private void switchToProfileMode() {
   2106         if (ENABLE_TRANSACTION_LOG) {
   2107             Log.i(TAG, "switchToProfileMode", new RuntimeException("switchToProfileMode"));
   2108         }
   2109         mDbHelper.set(mProfileHelper);
   2110         mTransactionContext.set(mProfileTransactionContext);
   2111         mAggregator.set(mProfileAggregator);
   2112         mPhotoStore.set(mProfilePhotoStore);
   2113         mInProfileMode.set(true);
   2114     }
   2115 
   2116     /**
   2117      * Switches the provider's thread-local context variables to prepare for performing
   2118      * a contacts operation.
   2119      */
   2120     private void switchToContactMode() {
   2121         if (ENABLE_TRANSACTION_LOG) {
   2122             Log.i(TAG, "switchToContactMode", new RuntimeException("switchToContactMode"));
   2123         }
   2124         mDbHelper.set(mContactsHelper);
   2125         mTransactionContext.set(mContactTransactionContext);
   2126         mAggregator.set(mContactAggregator);
   2127         mPhotoStore.set(mContactsPhotoStore);
   2128         mInProfileMode.set(false);
   2129     }
   2130 
   2131     @Override
   2132     public Uri insert(Uri uri, ContentValues values) {
   2133         waitForAccess(mWriteAccessLatch);
   2134 
   2135         // Enforce stream items access check if applicable.
   2136         enforceSocialStreamWritePermission(uri);
   2137 
   2138         if (mapsToProfileDbWithInsertedValues(uri, values)) {
   2139             switchToProfileMode();
   2140             return mProfileProvider.insert(uri, values);
   2141         }
   2142         switchToContactMode();
   2143         return super.insert(uri, values);
   2144     }
   2145 
   2146     @Override
   2147     public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
   2148         if (mWriteAccessLatch != null) {
   2149             // Update on PROVIDER_STATUS used to be used as a trigger to re-start legacy contact
   2150             // import.  Now that we no longer support it, we just ignore it.
   2151             int match = sUriMatcher.match(uri);
   2152             if (match == PROVIDER_STATUS) {
   2153                 return 0;
   2154             }
   2155         }
   2156         waitForAccess(mWriteAccessLatch);
   2157 
   2158         // Enforce stream items access check if applicable.
   2159         enforceSocialStreamWritePermission(uri);
   2160 
   2161         if (mapsToProfileDb(uri)) {
   2162             switchToProfileMode();
   2163             return mProfileProvider.update(uri, values, selection, selectionArgs);
   2164         }
   2165         switchToContactMode();
   2166         return super.update(uri, values, selection, selectionArgs);
   2167     }
   2168 
   2169     @Override
   2170     public int delete(Uri uri, String selection, String[] selectionArgs) {
   2171         waitForAccess(mWriteAccessLatch);
   2172 
   2173         // Enforce stream items access check if applicable.
   2174         enforceSocialStreamWritePermission(uri);
   2175 
   2176         if (mapsToProfileDb(uri)) {
   2177             switchToProfileMode();
   2178             return mProfileProvider.delete(uri, selection, selectionArgs);
   2179         }
   2180         switchToContactMode();
   2181         return super.delete(uri, selection, selectionArgs);
   2182     }
   2183 
   2184     @Override
   2185     public Bundle call(String method, String arg, Bundle extras) {
   2186         waitForAccess(mReadAccessLatch);
   2187         switchToContactMode();
   2188         if (Authorization.AUTHORIZATION_METHOD.equals(method)) {
   2189             Uri uri = (Uri) extras.getParcelable(Authorization.KEY_URI_TO_AUTHORIZE);
   2190 
   2191             // Check permissions on the caller.  The URI can only be pre-authorized if the caller
   2192             // already has the necessary permissions.
   2193             enforceSocialStreamReadPermission(uri);
   2194             if (mapsToProfileDb(uri)) {
   2195                 mProfileProvider.enforceReadPermission(uri);
   2196             }
   2197 
   2198             // If there hasn't been a security violation yet, we're clear to pre-authorize the URI.
   2199             Uri authUri = preAuthorizeUri(uri);
   2200             Bundle response = new Bundle();
   2201             response.putParcelable(Authorization.KEY_AUTHORIZED_URI, authUri);
   2202             return response;
   2203         } else if (PinnedPositions.UNDEMOTE_METHOD.equals(method)) {
   2204             getContext().enforceCallingOrSelfPermission(WRITE_PERMISSION, null);
   2205             final long id;
   2206             try {
   2207                 id = Long.valueOf(arg);
   2208             } catch (NumberFormatException e) {
   2209                 throw new IllegalArgumentException("Contact ID must be a valid long number.");
   2210             }
   2211             undemoteContact(mDbHelper.get().getWritableDatabase(), id);
   2212             return null;
   2213         }
   2214         return null;
   2215     }
   2216 
   2217     /**
   2218      * Pre-authorizes the given URI, adding an expiring permission token to it and placing that
   2219      * in our map of pre-authorized URIs.
   2220      * @param uri The URI to pre-authorize.
   2221      * @return A pre-authorized URI that will not require special permissions to use.
   2222      */
   2223     private Uri preAuthorizeUri(Uri uri) {
   2224         String token = String.valueOf(mRandom.nextLong());
   2225         Uri authUri = uri.buildUpon()
   2226                 .appendQueryParameter(PREAUTHORIZED_URI_TOKEN, token)
   2227                 .build();
   2228         long expiration = SystemClock.elapsedRealtime() + mPreAuthorizedUriDuration;
   2229         mPreAuthorizedUris.put(authUri, expiration);
   2230 
   2231         return authUri;
   2232     }
   2233 
   2234     /**
   2235      * Checks whether the given URI has an unexpired permission token that would grant access to
   2236      * query the content.  If it does, the regular permission check should be skipped.
   2237      * @param uri The URI being accessed.
   2238      * @return Whether the URI is a pre-authorized URI that is still valid.
   2239      */
   2240     public boolean isValidPreAuthorizedUri(Uri uri) {
   2241         // Only proceed if the URI has a permission token parameter.
   2242         if (uri.getQueryParameter(PREAUTHORIZED_URI_TOKEN) != null) {
   2243             // First expire any pre-authorization URIs that are no longer valid.
   2244             long now = SystemClock.elapsedRealtime();
   2245             Set<Uri> expiredUris = Sets.newHashSet();
   2246             for (Uri preAuthUri : mPreAuthorizedUris.keySet()) {
   2247                 if (mPreAuthorizedUris.get(preAuthUri) < now) {
   2248                     expiredUris.add(preAuthUri);
   2249                 }
   2250             }
   2251             for (Uri expiredUri : expiredUris) {
   2252                 mPreAuthorizedUris.remove(expiredUri);
   2253             }
   2254 
   2255             // Now check to see if the pre-authorized URI map contains the URI.
   2256             if (mPreAuthorizedUris.containsKey(uri)) {
   2257                 // Unexpired token - skip the permission check.
   2258                 return true;
   2259             }
   2260         }
   2261         return false;
   2262     }
   2263 
   2264     @Override
   2265     protected boolean yield(ContactsTransaction transaction) {
   2266         // If there's a profile transaction in progress, and we're yielding, we need to
   2267         // end it.  Unlike the Contacts DB yield (which re-starts a transaction at its
   2268         // conclusion), we can just go back into a state in which we have no active
   2269         // profile transaction, and let it be re-created as needed.  We can't hold onto
   2270         // the transaction without risking a deadlock.
   2271         SQLiteDatabase profileDb = transaction.removeDbForTag(PROFILE_DB_TAG);
   2272         if (profileDb != null) {
   2273             profileDb.setTransactionSuccessful();
   2274             profileDb.endTransaction();
   2275         }
   2276 
   2277         // Now proceed with the Contacts DB yield.
   2278         SQLiteDatabase contactsDb = transaction.getDbForTag(CONTACTS_DB_TAG);
   2279         return contactsDb != null && contactsDb.yieldIfContendedSafely(SLEEP_AFTER_YIELD_DELAY);
   2280     }
   2281 
   2282     @Override
   2283     public ContentProviderResult[] applyBatch(ArrayList<ContentProviderOperation> operations)
   2284             throws OperationApplicationException {
   2285         waitForAccess(mWriteAccessLatch);
   2286         return super.applyBatch(operations);
   2287     }
   2288 
   2289     @Override
   2290     public int bulkInsert(Uri uri, ContentValues[] values) {
   2291         waitForAccess(mWriteAccessLatch);
   2292         return super.bulkInsert(uri, values);
   2293     }
   2294 
   2295     @Override
   2296     public void onBegin() {
   2297         onBeginTransactionInternal(false);
   2298     }
   2299 
   2300     protected void onBeginTransactionInternal(boolean forProfile) {
   2301         if (ENABLE_TRANSACTION_LOG) {
   2302             Log.i(TAG, "onBeginTransaction: " + (forProfile ? "profile" : "contacts"),
   2303                     new RuntimeException("onBeginTransactionInternal"));
   2304         }
   2305         if (forProfile) {
   2306             switchToProfileMode();
   2307             mProfileAggregator.clearPendingAggregations();
   2308             mProfileTransactionContext.clearExceptSearchIndexUpdates();
   2309         } else {
   2310             switchToContactMode();
   2311             mContactAggregator.clearPendingAggregations();
   2312             mContactTransactionContext.clearExceptSearchIndexUpdates();
   2313         }
   2314     }
   2315 
   2316     @Override
   2317     public void onCommit() {
   2318         onCommitTransactionInternal(false);
   2319     }
   2320 
   2321     protected void onCommitTransactionInternal(boolean forProfile) {
   2322         if (ENABLE_TRANSACTION_LOG) {
   2323             Log.i(TAG, "onCommitTransactionInternal: " + (forProfile ? "profile" : "contacts"),
   2324                     new RuntimeException("onCommitTransactionInternal"));
   2325         }
   2326         if (forProfile) {
   2327             switchToProfileMode();
   2328         } else {
   2329             switchToContactMode();
   2330         }
   2331 
   2332         flushTransactionalChanges();
   2333         final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
   2334         mAggregator.get().aggregateInTransaction(mTransactionContext.get(), db);
   2335         if (mVisibleTouched) {
   2336             mVisibleTouched = false;
   2337             mDbHelper.get().updateAllVisible();
   2338 
   2339             // Need to rebuild the fast-indxer bundle.
   2340             invalidateFastScrollingIndexCache();
   2341         }
   2342 
   2343         updateSearchIndexInTransaction();
   2344 
   2345         if (mProviderStatusUpdateNeeded) {
   2346             updateProviderStatus();
   2347             mProviderStatusUpdateNeeded = false;
   2348         }
   2349     }
   2350 
   2351     @Override
   2352     public void onRollback() {
   2353         onRollbackTransactionInternal(false);
   2354     }
   2355 
   2356     protected void onRollbackTransactionInternal(boolean forProfile) {
   2357         if (ENABLE_TRANSACTION_LOG) {
   2358             Log.i(TAG, "onRollbackTransactionInternal: " + (forProfile ? "profile" : "contacts"),
   2359                     new RuntimeException("onRollbackTransactionInternal"));
   2360         }
   2361         if (forProfile) {
   2362             switchToProfileMode();
   2363         } else {
   2364             switchToContactMode();
   2365         }
   2366 
   2367         mDbHelper.get().invalidateAllCache();
   2368     }
   2369 
   2370     private void updateSearchIndexInTransaction() {
   2371         Set<Long> staleContacts = mTransactionContext.get().getStaleSearchIndexContactIds();
   2372         Set<Long> staleRawContacts = mTransactionContext.get().getStaleSearchIndexRawContactIds();
   2373         if (!staleContacts.isEmpty() || !staleRawContacts.isEmpty()) {
   2374             mSearchIndexManager.updateIndexForRawContacts(staleContacts, staleRawContacts);
   2375             mTransactionContext.get().clearSearchIndexUpdates();
   2376         }
   2377     }
   2378 
   2379     private void flushTransactionalChanges() {
   2380         if (VERBOSE_LOGGING) {
   2381             Log.v(TAG, "flushTransactionalChanges: " + (inProfileMode() ? "profile" : "contacts"));
   2382         }
   2383 
   2384         final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
   2385         for (long rawContactId : mTransactionContext.get().getInsertedRawContactIds()) {
   2386             mDbHelper.get().updateRawContactDisplayName(db, rawContactId);
   2387             mAggregator.get().onRawContactInsert(mTransactionContext.get(), db, rawContactId);
   2388         }
   2389 
   2390         Set<Long> dirtyRawContacts = mTransactionContext.get().getDirtyRawContactIds();
   2391         if (!dirtyRawContacts.isEmpty()) {
   2392             mSb.setLength(0);
   2393             mSb.append(UPDATE_RAW_CONTACT_SET_DIRTY_SQL);
   2394             appendIds(mSb, dirtyRawContacts);
   2395             mSb.append(")");
   2396             db.execSQL(mSb.toString());
   2397         }
   2398 
   2399         Set<Long> updatedRawContacts = mTransactionContext.get().getUpdatedRawContactIds();
   2400         if (!updatedRawContacts.isEmpty()) {
   2401             mSb.setLength(0);
   2402             mSb.append(UPDATE_RAW_CONTACT_SET_VERSION_SQL);
   2403             appendIds(mSb, updatedRawContacts);
   2404             mSb.append(")");
   2405             db.execSQL(mSb.toString());
   2406         }
   2407 
   2408         final Set<Long> changedRawContacts = mTransactionContext.get().getChangedRawContactIds();
   2409         ContactsTableUtil.updateContactLastUpdateByRawContactId(db, changedRawContacts);
   2410 
   2411         // Update sync states.
   2412         for (Map.Entry<Long, Object> entry : mTransactionContext.get().getUpdatedSyncStates()) {
   2413             long id = entry.getKey();
   2414             if (mDbHelper.get().getSyncState().update(db, id, entry.getValue()) <= 0) {
   2415                 throw new IllegalStateException(
   2416                         "unable to update sync state, does it still exist?");
   2417             }
   2418         }
   2419 
   2420         mTransactionContext.get().clearExceptSearchIndexUpdates();
   2421     }
   2422 
   2423     /**
   2424      * Appends comma separated IDs.
   2425      * @param ids Should not be empty
   2426      */
   2427     private void appendIds(StringBuilder sb, Set<Long> ids) {
   2428         for (long id : ids) {
   2429             sb.append(id).append(',');
   2430         }
   2431 
   2432         sb.setLength(sb.length() - 1); // Yank the last comma
   2433     }
   2434 
   2435     @Override
   2436     protected void notifyChange() {
   2437         notifyChange(mSyncToNetwork);
   2438         mSyncToNetwork = false;
   2439     }
   2440 
   2441     protected void notifyChange(boolean syncToNetwork) {
   2442         getContext().getContentResolver().notifyChange(ContactsContract.AUTHORITY_URI, null,
   2443                 syncToNetwork);
   2444     }
   2445 
   2446     protected void setProviderStatus(int status) {
   2447         if (mProviderStatus != status) {
   2448             mProviderStatus = status;
   2449             getContext().getContentResolver().notifyChange(ProviderStatus.CONTENT_URI, null, false);
   2450         }
   2451     }
   2452 
   2453     public DataRowHandler getDataRowHandler(final String mimeType) {
   2454         if (inProfileMode()) {
   2455             return getDataRowHandlerForProfile(mimeType);
   2456         }
   2457         DataRowHandler handler = mDataRowHandlers.get(mimeType);
   2458         if (handler == null) {
   2459             handler = new DataRowHandlerForCustomMimetype(
   2460                     getContext(), mContactsHelper, mContactAggregator, mimeType);
   2461             mDataRowHandlers.put(mimeType, handler);
   2462         }
   2463         return handler;
   2464     }
   2465 
   2466     public DataRowHandler getDataRowHandlerForProfile(final String mimeType) {
   2467         DataRowHandler handler = mProfileDataRowHandlers.get(mimeType);
   2468         if (handler == null) {
   2469             handler = new DataRowHandlerForCustomMimetype(
   2470                     getContext(), mProfileHelper, mProfileAggregator, mimeType);
   2471             mProfileDataRowHandlers.put(mimeType, handler);
   2472         }
   2473         return handler;
   2474     }
   2475 
   2476     @Override
   2477     protected Uri insertInTransaction(Uri uri, ContentValues values) {
   2478         if (VERBOSE_LOGGING) {
   2479             Log.v(TAG, "insertInTransaction: uri=" + uri + "  values=[" + values + "]" +
   2480                     " CPID=" + Binder.getCallingPid());
   2481         }
   2482 
   2483         final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
   2484 
   2485         final boolean callerIsSyncAdapter =
   2486                 readBooleanQueryParameter(uri, ContactsContract.CALLER_IS_SYNCADAPTER, false);
   2487 
   2488         final int match = sUriMatcher.match(uri);
   2489         long id = 0;
   2490 
   2491         switch (match) {
   2492             case SYNCSTATE:
   2493             case PROFILE_SYNCSTATE:
   2494                 id = mDbHelper.get().getSyncState().insert(db, values);
   2495                 break;
   2496 
   2497             case CONTACTS: {
   2498                 invalidateFastScrollingIndexCache();
   2499                 insertContact(values);
   2500                 break;
   2501             }
   2502 
   2503             case PROFILE: {
   2504                 throw new UnsupportedOperationException(
   2505                         "The profile contact is created automatically");
   2506             }
   2507 
   2508             case RAW_CONTACTS:
   2509             case PROFILE_RAW_CONTACTS: {
   2510                 invalidateFastScrollingIndexCache();
   2511                 id = insertRawContact(uri, values, callerIsSyncAdapter);
   2512                 mSyncToNetwork |= !callerIsSyncAdapter;
   2513                 break;
   2514             }
   2515 
   2516             case RAW_CONTACTS_ID_DATA:
   2517             case PROFILE_RAW_CONTACTS_ID_DATA: {
   2518                 invalidateFastScrollingIndexCache();
   2519                 int segment = match == RAW_CONTACTS_ID_DATA ? 1 : 2;
   2520                 values.put(Data.RAW_CONTACT_ID, uri.getPathSegments().get(segment));
   2521                 id = insertData(values, callerIsSyncAdapter);
   2522                 mSyncToNetwork |= !callerIsSyncAdapter;
   2523                 break;
   2524             }
   2525 
   2526             case RAW_CONTACTS_ID_STREAM_ITEMS: {
   2527                 values.put(StreamItems.RAW_CONTACT_ID, uri.getPathSegments().get(1));
   2528                 id = insertStreamItem(uri, values);
   2529                 mSyncToNetwork |= !callerIsSyncAdapter;
   2530                 break;
   2531             }
   2532 
   2533             case DATA:
   2534             case PROFILE_DATA: {
   2535                 invalidateFastScrollingIndexCache();
   2536                 id = insertData(values, callerIsSyncAdapter);
   2537                 mSyncToNetwork |= !callerIsSyncAdapter;
   2538                 break;
   2539             }
   2540 
   2541             case GROUPS: {
   2542                 id = insertGroup(uri, values, callerIsSyncAdapter);
   2543                 mSyncToNetwork |= !callerIsSyncAdapter;
   2544                 break;
   2545             }
   2546 
   2547             case SETTINGS: {
   2548                 id = insertSettings(values);
   2549                 mSyncToNetwork |= !callerIsSyncAdapter;
   2550                 break;
   2551             }
   2552 
   2553             case STATUS_UPDATES:
   2554             case PROFILE_STATUS_UPDATES: {
   2555                 id = insertStatusUpdate(values);
   2556                 break;
   2557             }
   2558 
   2559             case STREAM_ITEMS: {
   2560                 id = insertStreamItem(uri, values);
   2561                 mSyncToNetwork |= !callerIsSyncAdapter;
   2562                 break;
   2563             }
   2564 
   2565             case STREAM_ITEMS_PHOTOS: {
   2566                 id = insertStreamItemPhoto(uri, values);
   2567                 mSyncToNetwork |= !callerIsSyncAdapter;
   2568                 break;
   2569             }
   2570 
   2571             case STREAM_ITEMS_ID_PHOTOS: {
   2572                 values.put(StreamItemPhotos.STREAM_ITEM_ID, uri.getPathSegments().get(1));
   2573                 id = insertStreamItemPhoto(uri, values);
   2574                 mSyncToNetwork |= !callerIsSyncAdapter;
   2575                 break;
   2576             }
   2577 
   2578             default:
   2579                 mSyncToNetwork = true;
   2580                 return mLegacyApiSupport.insert(uri, values);
   2581         }
   2582 
   2583         if (id < 0) {
   2584             return null;
   2585         }
   2586 
   2587         return ContentUris.withAppendedId(uri, id);
   2588     }
   2589 
   2590     /**
   2591      * If account is non-null then store it in the values. If the account is
   2592      * already specified in the values then it must be consistent with the
   2593      * account, if it is non-null.
   2594      *
   2595      * @param uri Current {@link Uri} being operated on.
   2596      * @param values {@link ContentValues} to read and possibly update.
   2597      * @throws IllegalArgumentException when only one of
   2598      *             {@link RawContacts#ACCOUNT_NAME} or
   2599      *             {@link RawContacts#ACCOUNT_TYPE} is specified, leaving the
   2600      *             other undefined.
   2601      * @throws IllegalArgumentException when {@link RawContacts#ACCOUNT_NAME}
   2602      *             and {@link RawContacts#ACCOUNT_TYPE} are inconsistent between
   2603      *             the given {@link Uri} and {@link ContentValues}.
   2604      */
   2605     private Account resolveAccount(Uri uri, ContentValues values) throws IllegalArgumentException {
   2606         String accountName = getQueryParameter(uri, RawContacts.ACCOUNT_NAME);
   2607         String accountType = getQueryParameter(uri, RawContacts.ACCOUNT_TYPE);
   2608         final boolean partialUri = TextUtils.isEmpty(accountName) ^ TextUtils.isEmpty(accountType);
   2609 
   2610         String valueAccountName = values.getAsString(RawContacts.ACCOUNT_NAME);
   2611         String valueAccountType = values.getAsString(RawContacts.ACCOUNT_TYPE);
   2612         final boolean partialValues = TextUtils.isEmpty(valueAccountName)
   2613                 ^ TextUtils.isEmpty(valueAccountType);
   2614 
   2615         if (partialUri || partialValues) {
   2616             // Throw when either account is incomplete.
   2617             throw new IllegalArgumentException(mDbHelper.get().exceptionMessage(
   2618                     "Must specify both or neither of ACCOUNT_NAME and ACCOUNT_TYPE", uri));
   2619         }
   2620 
   2621         // Accounts are valid by only checking one parameter, since we've
   2622         // already ruled out partial accounts.
   2623         final boolean validUri = !TextUtils.isEmpty(accountName);
   2624         final boolean validValues = !TextUtils.isEmpty(valueAccountName);
   2625 
   2626         if (validValues && validUri) {
   2627             // Check that accounts match when both present
   2628             final boolean accountMatch = TextUtils.equals(accountName, valueAccountName)
   2629                     && TextUtils.equals(accountType, valueAccountType);
   2630             if (!accountMatch) {
   2631                 throw new IllegalArgumentException(mDbHelper.get().exceptionMessage(
   2632                         "When both specified, ACCOUNT_NAME and ACCOUNT_TYPE must match", uri));
   2633             }
   2634         } else if (validUri) {
   2635             // Fill values from the URI when not present.
   2636             values.put(RawContacts.ACCOUNT_NAME, accountName);
   2637             values.put(RawContacts.ACCOUNT_TYPE, accountType);
   2638         } else if (validValues) {
   2639             accountName = valueAccountName;
   2640             accountType = valueAccountType;
   2641         } else {
   2642             return null;
   2643         }
   2644 
   2645         // Use cached Account object when matches, otherwise create
   2646         if (mAccount == null
   2647                 || !mAccount.name.equals(accountName)
   2648                 || !mAccount.type.equals(accountType)) {
   2649             mAccount = new Account(accountName, accountType);
   2650         }
   2651 
   2652         return mAccount;
   2653     }
   2654 
   2655     /**
   2656      * Resolves the account and builds an {@link AccountWithDataSet} based on the data set specified
   2657      * in the URI or values (if any).
   2658      * @param uri Current {@link Uri} being operated on.
   2659      * @param values {@link ContentValues} to read and possibly update.
   2660      */
   2661     private AccountWithDataSet resolveAccountWithDataSet(Uri uri, ContentValues values) {
   2662         final Account account = resolveAccount(uri, values);
   2663         AccountWithDataSet accountWithDataSet = null;
   2664         if (account != null) {
   2665             String dataSet = getQueryParameter(uri, RawContacts.DATA_SET);
   2666             if (dataSet == null) {
   2667                 dataSet = values.getAsString(RawContacts.DATA_SET);
   2668             } else {
   2669                 values.put(RawContacts.DATA_SET, dataSet);
   2670             }
   2671             accountWithDataSet = AccountWithDataSet.get(account.name, account.type, dataSet);
   2672         }
   2673         return accountWithDataSet;
   2674     }
   2675 
   2676     /**
   2677      * Inserts an item in the contacts table
   2678      *
   2679      * @param values the values for the new row
   2680      * @return the row ID of the newly created row
   2681      */
   2682     private long insertContact(ContentValues values) {
   2683         throw new UnsupportedOperationException("Aggregate contacts are created automatically");
   2684     }
   2685 
   2686     /**
   2687      * Inserts a new entry into the raw-contacts table.
   2688      *
   2689      * @param uri The insertion URI.
   2690      * @param inputValues The values for the new row.
   2691      * @param callerIsSyncAdapter True to identify the entity invoking this method as a SyncAdapter
   2692      *     and false otherwise.
   2693      * @return the ID of the newly-created row.
   2694      */
   2695     private long insertRawContact(
   2696             Uri uri, ContentValues inputValues, boolean callerIsSyncAdapter) {
   2697 
   2698         // Create a shallow copy and initialize the contact ID to null.
   2699         final ContentValues values = new ContentValues(inputValues);
   2700         values.putNull(RawContacts.CONTACT_ID);
   2701 
   2702         // Populate the relevant values before inserting the new entry into the database.
   2703         final long accountId = replaceAccountInfoByAccountId(uri, values);
   2704         if (flagIsSet(values, RawContacts.DELETED)) {
   2705             values.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DISABLED);
   2706         }
   2707 
   2708         // Databases that were created prior to the 906 upgrade have a default of Int.MAX_VALUE
   2709         // for RawContacts.PINNED. Manually set the value to the correct default (0) if it is not
   2710         // set.
   2711         if (!values.containsKey(RawContacts.PINNED)) {
   2712             values.put(RawContacts.PINNED, PinnedPositions.UNPINNED);
   2713         }
   2714 
   2715         // Insert the new entry.
   2716         final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
   2717         final long rawContactId = db.insert(Tables.RAW_CONTACTS, RawContacts.CONTACT_ID, values);
   2718 
   2719         final int aggregationMode = getIntValue(values, RawContacts.AGGREGATION_MODE,
   2720                 RawContacts.AGGREGATION_MODE_DEFAULT);
   2721         mAggregator.get().markNewForAggregation(rawContactId, aggregationMode);
   2722 
   2723         // Trigger creation of a Contact based on this RawContact at the end of transaction.
   2724         mTransactionContext.get().rawContactInserted(rawContactId, accountId);
   2725 
   2726         if (!callerIsSyncAdapter) {
   2727             addAutoAddMembership(rawContactId);
   2728             if (flagIsSet(values, RawContacts.STARRED)) {
   2729                 updateFavoritesMembership(rawContactId, true);
   2730             }
   2731         }
   2732 
   2733         mProviderStatusUpdateNeeded = true;
   2734         return rawContactId;
   2735     }
   2736 
   2737     private void addAutoAddMembership(long rawContactId) {
   2738         final Long groupId =
   2739                 findGroupByRawContactId(SELECTION_AUTO_ADD_GROUPS_BY_RAW_CONTACT_ID, rawContactId);
   2740         if (groupId != null) {
   2741             insertDataGroupMembership(rawContactId, groupId);
   2742         }
   2743     }
   2744 
   2745     private Long findGroupByRawContactId(String selection, long rawContactId) {
   2746         final SQLiteDatabase db = mDbHelper.get().getReadableDatabase();
   2747         Cursor c = db.query(Tables.GROUPS + "," + Tables.RAW_CONTACTS,
   2748                 PROJECTION_GROUP_ID, selection,
   2749                 new String[] {Long.toString(rawContactId)},
   2750                 null /* groupBy */, null /* having */, null /* orderBy */);
   2751         try {
   2752             while (c.moveToNext()) {
   2753                 return c.getLong(0);
   2754             }
   2755             return null;
   2756         } finally {
   2757             c.close();
   2758         }
   2759     }
   2760 
   2761     private void updateFavoritesMembership(long rawContactId, boolean isStarred) {
   2762         final Long groupId =
   2763                 findGroupByRawContactId(SELECTION_FAVORITES_GROUPS_BY_RAW_CONTACT_ID, rawContactId);
   2764         if (groupId != null) {
   2765             if (isStarred) {
   2766                 insertDataGroupMembership(rawContactId, groupId);
   2767             } else {
   2768                 deleteDataGroupMembership(rawContactId, groupId);
   2769             }
   2770         }
   2771     }
   2772 
   2773     private void insertDataGroupMembership(long rawContactId, long groupId) {
   2774         ContentValues groupMembershipValues = new ContentValues();
   2775         groupMembershipValues.put(GroupMembership.GROUP_ROW_ID, groupId);
   2776         groupMembershipValues.put(GroupMembership.RAW_CONTACT_ID, rawContactId);
   2777         groupMembershipValues.put(DataColumns.MIMETYPE_ID,
   2778                 mDbHelper.get().getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE));
   2779 
   2780         final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
   2781         db.insert(Tables.DATA, null, groupMembershipValues);
   2782     }
   2783 
   2784     private void deleteDataGroupMembership(long rawContactId, long groupId) {
   2785         final String[] selectionArgs = {
   2786                 Long.toString(mDbHelper.get().getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE)),
   2787                 Long.toString(groupId),
   2788                 Long.toString(rawContactId)};
   2789 
   2790         final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
   2791         db.delete(Tables.DATA, SELECTION_GROUPMEMBERSHIP_DATA, selectionArgs);
   2792     }
   2793 
   2794     /**
   2795      * Inserts a new entry into the (contact) data table.
   2796      *
   2797      * @param inputValues The values for the new row.
   2798      * @return The ID of the newly-created row.
   2799      */
   2800     private long insertData(ContentValues inputValues, boolean callerIsSyncAdapter) {
   2801         final Long rawContactId = inputValues.getAsLong(Data.RAW_CONTACT_ID);
   2802         if (rawContactId == null) {
   2803             throw new IllegalArgumentException(Data.RAW_CONTACT_ID + " is required");
   2804         }
   2805 
   2806         final String mimeType = inputValues.getAsString(Data.MIMETYPE);
   2807         if (TextUtils.isEmpty(mimeType)) {
   2808             throw new IllegalArgumentException(Data.MIMETYPE + " is required");
   2809         }
   2810 
   2811         if (Phone.CONTENT_ITEM_TYPE.equals(mimeType)) {
   2812             maybeTrimLongPhoneNumber(inputValues);
   2813         }
   2814 
   2815         // The input seem valid, create a shallow copy.
   2816         final ContentValues values = new ContentValues(inputValues);
   2817 
   2818         // Populate the relevant values before inserting the new entry into the database.
   2819         replacePackageNameByPackageId(values);
   2820 
   2821         // Replace the mimetype by the corresponding mimetype ID.
   2822         values.put(DataColumns.MIMETYPE_ID, mDbHelper.get().getMimeTypeId(mimeType));
   2823         values.remove(Data.MIMETYPE);
   2824 
   2825         // Insert the new entry.
   2826         final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
   2827         final TransactionContext context = mTransactionContext.get();
   2828         final long dataId = getDataRowHandler(mimeType).insert(db, context, rawContactId, values);
   2829         context.markRawContactDirtyAndChanged(rawContactId, callerIsSyncAdapter);
   2830         context.rawContactUpdated(rawContactId);
   2831 
   2832         return dataId;
   2833     }
   2834 
   2835     /**
   2836      * Inserts an item in the stream_items table.  The account is checked against the
   2837      * account in the raw contact for which the stream item is being inserted.  If the
   2838      * new stream item results in more stream items under this raw contact than the limit,
   2839      * the oldest one will be deleted (note that if the stream item inserted was the
   2840      * oldest, it will be immediately deleted, and this will return 0).
   2841      *
   2842      * @param uri the insertion URI
   2843      * @param inputValues the values for the new row
   2844      * @return the stream item _ID of the newly created row, or 0 if it was not created
   2845      */
   2846     private long insertStreamItem(Uri uri, ContentValues inputValues) {
   2847         Long rawContactId = inputValues.getAsLong(Data.RAW_CONTACT_ID);
   2848         if (rawContactId == null) {
   2849             throw new IllegalArgumentException(Data.RAW_CONTACT_ID + " is required");
   2850         }
   2851 
   2852         // The input seem valid, create a shallow copy.
   2853         final ContentValues values = new ContentValues(inputValues);
   2854 
   2855         // Update the relevant values before inserting the new entry into the database.  The
   2856         // account parameters are not added since they don't exist in the stream items table.
   2857         values.remove(RawContacts.ACCOUNT_NAME);
   2858         values.remove(RawContacts.ACCOUNT_TYPE);
   2859 
   2860         // Insert the new stream item.
   2861         final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
   2862         final long id = db.insert(Tables.STREAM_ITEMS, null, values);
   2863         if (id == -1) {
   2864             return 0;  // Insertion failed.
   2865         }
   2866 
   2867         // Check to see if we're over the limit for stream items under this raw contact.
   2868         // It's possible that the inserted stream item is older than the the existing
   2869         // ones, in which case it may be deleted immediately (resetting the ID to 0).
   2870         return cleanUpOldStreamItems(rawContactId, id);
   2871     }
   2872 
   2873     /**
   2874      * Inserts an item in the stream_item_photos table.  The account is checked against
   2875      * the account in the raw contact that owns the stream item being modified.
   2876      *
   2877      * @param uri the insertion URI.
   2878      * @param inputValues The values for the new row.
   2879      * @return The stream item photo _ID of the newly created row, or 0 if there was an issue
   2880      *     with processing the photo or creating the row.
   2881      */
   2882     private long insertStreamItemPhoto(Uri uri, ContentValues inputValues) {
   2883         final Long streamItemId = inputValues.getAsLong(StreamItemPhotos.STREAM_ITEM_ID);
   2884         if (streamItemId == null || streamItemId == 0) {
   2885             return 0;
   2886         }
   2887 
   2888         // The input seem valid, create a shallow copy.
   2889         final ContentValues values = new ContentValues(inputValues);
   2890 
   2891         // Update the relevant values before inserting the new entry into the database.  The
   2892         // account parameters are not added since they don't exist in the stream items table.
   2893         values.remove(RawContacts.ACCOUNT_NAME);
   2894         values.remove(RawContacts.ACCOUNT_TYPE);
   2895 
   2896         // Attempt to process and store the photo.
   2897         if (!processStreamItemPhoto(values, false)) {
   2898             return 0;
   2899         }
   2900 
   2901         // Insert the new entry and return its ID.
   2902         final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
   2903         return db.insert(Tables.STREAM_ITEM_PHOTOS, null, values);
   2904     }
   2905 
   2906     /**
   2907      * Processes the photo contained in the {@link StreamItemPhotos#PHOTO} field of the given
   2908      * values, attempting to store it in the photo store.  If successful, the resulting photo
   2909      * file ID will be added to the values for insert/update in the table.
   2910      * <p>
   2911      * If updating, it is valid for the picture to be empty or unspecified (the function will
   2912      * still return true).  If inserting, a valid picture must be specified.
   2913      * @param values The content values provided by the caller.
   2914      * @param forUpdate Whether this photo is being processed for update (vs. insert).
   2915      * @return Whether the insert or update should proceed.
   2916      */
   2917     private boolean processStreamItemPhoto(ContentValues values, boolean forUpdate) {
   2918         byte[] photoBytes = values.getAsByteArray(StreamItemPhotos.PHOTO);
   2919         if (photoBytes == null) {
   2920             return forUpdate;
   2921         }
   2922 
   2923         // Process the photo and store it.
   2924         IOException exception = null;
   2925         try {
   2926             final PhotoProcessor processor = new PhotoProcessor(
   2927                     photoBytes, getMaxDisplayPhotoDim(), getMaxThumbnailDim(), true);
   2928             long photoFileId = mPhotoStore.get().insert(processor, true);
   2929             if (photoFileId != 0) {
   2930                 values.put(StreamItemPhotos.PHOTO_FILE_ID, photoFileId);
   2931                 values.remove(StreamItemPhotos.PHOTO);
   2932                 return true;
   2933             }
   2934         } catch (IOException ioe) {
   2935             exception = ioe;
   2936         }
   2937 
   2938         Log.e(TAG, "Could not process stream item photo for insert", exception);
   2939         return false;
   2940     }
   2941 
   2942     /**
   2943      * If the given URI is reading stream items or stream photos, this will run a permission check
   2944      * for the android.permission.READ_SOCIAL_STREAM permission - otherwise it will do nothing.
   2945      * @param uri The URI to check.
   2946      */
   2947     private void enforceSocialStreamReadPermission(Uri uri) {
   2948         if (SOCIAL_STREAM_URIS.contains(sUriMatcher.match(uri))
   2949                 && !isValidPreAuthorizedUri(uri)) {
   2950             getContext().enforceCallingOrSelfPermission(
   2951                     "android.permission.READ_SOCIAL_STREAM", null);
   2952         }
   2953     }
   2954 
   2955     /**
   2956      * If the given URI is modifying stream items or stream photos, this will run a permission check
   2957      * for the android.permission.WRITE_SOCIAL_STREAM permission - otherwise it will do nothing.
   2958      * @param uri The URI to check.
   2959      */
   2960     private void enforceSocialStreamWritePermission(Uri uri) {
   2961         if (SOCIAL_STREAM_URIS.contains(sUriMatcher.match(uri))) {
   2962             getContext().enforceCallingOrSelfPermission(
   2963                     "android.permission.WRITE_SOCIAL_STREAM", null);
   2964         }
   2965     }
   2966 
   2967     /**
   2968      * Queries the database for stream items under the given raw contact.  If there are
   2969      * more entries than {@link ContactsProvider2#MAX_STREAM_ITEMS_PER_RAW_CONTACT},
   2970      * the oldest entries (as determined by timestamp) will be deleted.
   2971      * @param rawContactId The raw contact ID to examine for stream items.
   2972      * @param insertedStreamItemId The ID of the stream item that was just inserted,
   2973      *     prompting this cleanup.  Callers may pass 0 if no insertion prompted the
   2974      *     cleanup.
   2975      * @return The ID of the inserted stream item if it still exists after cleanup;
   2976      *     0 otherwise.
   2977      */
   2978     private long cleanUpOldStreamItems(long rawContactId, long insertedStreamItemId) {
   2979         long postCleanupInsertedStreamId = insertedStreamItemId;
   2980         final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
   2981         Cursor c = db.query(Tables.STREAM_ITEMS, new String[] {StreamItems._ID},
   2982                 StreamItems.RAW_CONTACT_ID + "=?", new String[] {String.valueOf(rawContactId)},
   2983                 null, null, StreamItems.TIMESTAMP + " DESC, " + StreamItems._ID + " DESC");
   2984         try {
   2985             int streamItemCount = c.getCount();
   2986             if (streamItemCount <= MAX_STREAM_ITEMS_PER_RAW_CONTACT) {
   2987                 // Still under the limit - nothing to clean up!
   2988                 return insertedStreamItemId;
   2989             }
   2990 
   2991             c.moveToLast();
   2992             while (c.getPosition() >= MAX_STREAM_ITEMS_PER_RAW_CONTACT) {
   2993                 long streamItemId = c.getLong(0);
   2994                 if (insertedStreamItemId == streamItemId) {
   2995                     // The stream item just inserted is being deleted.
   2996                     postCleanupInsertedStreamId = 0;
   2997                 }
   2998                 deleteStreamItem(db, c.getLong(0));
   2999                 c.moveToPrevious();
   3000             }
   3001         } finally {
   3002             c.close();
   3003         }
   3004         return postCleanupInsertedStreamId;
   3005     }
   3006 
   3007     /**
   3008      * Delete data row by row so that fixing of primaries etc work correctly.
   3009      */
   3010     private int deleteData(String selection, String[] selectionArgs, boolean callerIsSyncAdapter) {
   3011         int count = 0;
   3012 
   3013         final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
   3014 
   3015         // Note that the query will return data according to the access restrictions,
   3016         // so we don't need to worry about deleting data we don't have permission to read.
   3017         Uri dataUri = inProfileMode()
   3018                 ? Uri.withAppendedPath(Profile.CONTENT_URI, RawContacts.Data.CONTENT_DIRECTORY)
   3019                 : Data.CONTENT_URI;
   3020         Cursor c = query(dataUri, DataRowHandler.DataDeleteQuery.COLUMNS,
   3021                 selection, selectionArgs, null);
   3022         try {
   3023             while(c.moveToNext()) {
   3024                 long rawContactId = c.getLong(DataRowHandler.DataDeleteQuery.RAW_CONTACT_ID);
   3025                 String mimeType = c.getString(DataRowHandler.DataDeleteQuery.MIMETYPE);
   3026                 DataRowHandler rowHandler = getDataRowHandler(mimeType);
   3027                 count += rowHandler.delete(db, mTransactionContext.get(), c);
   3028                 mTransactionContext.get().markRawContactDirtyAndChanged(
   3029                         rawContactId, callerIsSyncAdapter);
   3030             }
   3031         } finally {
   3032             c.close();
   3033         }
   3034 
   3035         return count;
   3036     }
   3037 
   3038     /**
   3039      * Delete a data row provided that it is one of the allowed mime types.
   3040      */
   3041     public int deleteData(long dataId, String[] allowedMimeTypes) {
   3042 
   3043         final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
   3044 
   3045         // Note that the query will return data according to the access restrictions,
   3046         // so we don't need to worry about deleting data we don't have permission to read.
   3047         mSelectionArgs1[0] = String.valueOf(dataId);
   3048         Cursor c = query(Data.CONTENT_URI, DataRowHandler.DataDeleteQuery.COLUMNS, Data._ID + "=?",
   3049                 mSelectionArgs1, null);
   3050 
   3051         try {
   3052             if (!c.moveToFirst()) {
   3053                 return 0;
   3054             }
   3055 
   3056             String mimeType = c.getString(DataRowHandler.DataDeleteQuery.MIMETYPE);
   3057             boolean valid = false;
   3058             for (String type : allowedMimeTypes) {
   3059                 if (TextUtils.equals(mimeType, type)) {
   3060                     valid = true;
   3061                     break;
   3062                 }
   3063             }
   3064 
   3065             if (!valid) {
   3066                 throw new IllegalArgumentException("Data type mismatch: expected "
   3067                         + Lists.newArrayList(allowedMimeTypes));
   3068             }
   3069             DataRowHandler rowHandler = getDataRowHandler(mimeType);
   3070             return rowHandler.delete(db, mTransactionContext.get(), c);
   3071         } finally {
   3072             c.close();
   3073         }
   3074     }
   3075 
   3076     /**
   3077      * Inserts a new entry into the groups table.
   3078      *
   3079      * @param uri The insertion URI.
   3080      * @param inputValues The values for the new row.
   3081      * @param callerIsSyncAdapter True to identify the entity invoking this method as a SyncAdapter
   3082      *     and false otherwise.
   3083      * @return the ID of the newly-created row.
   3084      */
   3085     private long insertGroup(Uri uri, ContentValues inputValues, boolean callerIsSyncAdapter) {
   3086         // Create a shallow copy.
   3087         final ContentValues values = new ContentValues(inputValues);
   3088 
   3089         // Populate the relevant values before inserting the new entry into the database.
   3090         final long accountId = replaceAccountInfoByAccountId(uri, values);
   3091         replacePackageNameByPackageId(values);
   3092         if (!callerIsSyncAdapter) {
   3093             values.put(Groups.DIRTY, 1);
   3094         }
   3095 
   3096         // Insert the new entry.
   3097         final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
   3098         final long groupId = db.insert(Tables.GROUPS, Groups.TITLE, values);
   3099 
   3100         final boolean isFavoritesGroup = flagIsSet(values, Groups.FAVORITES);
   3101         if (!callerIsSyncAdapter && isFavoritesGroup) {
   3102             // Favorite group, add all starred raw contacts to it.
   3103             mSelectionArgs1[0] = Long.toString(accountId);
   3104             Cursor c = db.query(Tables.RAW_CONTACTS,
   3105                     new String[] {RawContacts._ID, RawContacts.STARRED},
   3106                     RawContactsColumns.CONCRETE_ACCOUNT_ID + "=?", mSelectionArgs1,
   3107                     null, null, null);
   3108             try {
   3109                 while (c.moveToNext()) {
   3110                     if (c.getLong(1) != 0) {
   3111                         final long rawContactId = c.getLong(0);
   3112                         insertDataGroupMembership(rawContactId, groupId);
   3113                         mTransactionContext.get().markRawContactDirtyAndChanged(
   3114                                 rawContactId, callerIsSyncAdapter);
   3115                     }
   3116                 }
   3117             } finally {
   3118                 c.close();
   3119             }
   3120         }
   3121 
   3122         if (values.containsKey(Groups.GROUP_VISIBLE)) {
   3123             mVisibleTouched = true;
   3124         }
   3125         return groupId;
   3126     }
   3127 
   3128     private long insertSettings(ContentValues values) {
   3129         // Before inserting, ensure that no settings record already exists for the
   3130         // values being inserted (this used to be enforced by a primary key, but that no
   3131         // longer works with the nullable data_set field added).
   3132         String accountName = values.getAsString(Settings.ACCOUNT_NAME);
   3133         String accountType = values.getAsString(Settings.ACCOUNT_TYPE);
   3134         String dataSet = values.getAsString(Settings.DATA_SET);
   3135         Uri.Builder settingsUri = Settings.CONTENT_URI.buildUpon();
   3136         if (accountName != null) {
   3137             settingsUri.appendQueryParameter(Settings.ACCOUNT_NAME, accountName);
   3138         }
   3139         if (accountType != null) {
   3140             settingsUri.appendQueryParameter(Settings.ACCOUNT_TYPE, accountType);
   3141         }
   3142         if (dataSet != null) {
   3143             settingsUri.appendQueryParameter(Settings.DATA_SET, dataSet);
   3144         }
   3145         Cursor c = queryLocal(settingsUri.build(), null, null, null, null, 0, null);
   3146         try {
   3147             if (c.getCount() > 0) {
   3148                 // If a record was found, replace it with the new values.
   3149                 String selection = null;
   3150                 String[] selectionArgs = null;
   3151                 if (accountName != null && accountType != null) {
   3152                     selection = Settings.ACCOUNT_NAME + "=? AND " + Settings.ACCOUNT_TYPE + "=?";
   3153                     if (dataSet == null) {
   3154                         selection += " AND " + Settings.DATA_SET + " IS NULL";
   3155                         selectionArgs = new String[] {accountName, accountType};
   3156                     } else {
   3157                         selection += " AND " + Settings.DATA_SET + "=?";
   3158                         selectionArgs = new String[] {accountName, accountType, dataSet};
   3159                     }
   3160                 }
   3161                 return updateSettings(values, selection, selectionArgs);
   3162             }
   3163         } finally {
   3164             c.close();
   3165         }
   3166 
   3167         final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
   3168 
   3169         // If we didn't find a duplicate, we're fine to insert.
   3170         final long id = db.insert(Tables.SETTINGS, null, values);
   3171 
   3172         if (values.containsKey(Settings.UNGROUPED_VISIBLE)) {
   3173             mVisibleTouched = true;
   3174         }
   3175 
   3176         return id;
   3177     }
   3178 
   3179     /**
   3180      * Inserts a status update.
   3181      */
   3182     private long insertStatusUpdate(ContentValues inputValues) {
   3183         final String handle = inputValues.getAsString(StatusUpdates.IM_HANDLE);
   3184         final Integer protocol = inputValues.getAsInteger(StatusUpdates.PROTOCOL);
   3185         String customProtocol = null;
   3186 
   3187         final ContactsDatabaseHelper dbHelper = mDbHelper.get();
   3188         final SQLiteDatabase db = dbHelper.getWritableDatabase();
   3189 
   3190         if (protocol != null && protocol == Im.PROTOCOL_CUSTOM) {
   3191             customProtocol = inputValues.getAsString(StatusUpdates.CUSTOM_PROTOCOL);
   3192             if (TextUtils.isEmpty(customProtocol)) {
   3193                 throw new IllegalArgumentException(
   3194                         "CUSTOM_PROTOCOL is required when PROTOCOL=PROTOCOL_CUSTOM");
   3195             }
   3196         }
   3197 
   3198         long rawContactId = -1;
   3199         long contactId = -1;
   3200         Long dataId = inputValues.getAsLong(StatusUpdates.DATA_ID);
   3201         String accountType = null;
   3202         String accountName = null;
   3203         mSb.setLength(0);
   3204         mSelectionArgs.clear();
   3205         if (dataId != null) {
   3206             // Lookup the contact info for the given data row.
   3207 
   3208             mSb.append(Tables.DATA + "." + Data._ID + "=?");
   3209             mSelectionArgs.add(String.valueOf(dataId));
   3210         } else {
   3211             // Lookup the data row to attach this presence update to
   3212 
   3213             if (TextUtils.isEmpty(handle) || protocol == null) {
   3214                 throw new IllegalArgumentException("PROTOCOL and IM_HANDLE are required");
   3215             }
   3216 
   3217             // TODO: generalize to allow other providers to match against email.
   3218             boolean matchEmail = Im.PROTOCOL_GOOGLE_TALK == protocol;
   3219 
   3220             String mimeTypeIdIm = String.valueOf(dbHelper.getMimeTypeIdForIm());
   3221             if (matchEmail) {
   3222                 String mimeTypeIdEmail = String.valueOf(dbHelper.getMimeTypeIdForEmail());
   3223 
   3224                 // The following hack forces SQLite to use the (mimetype_id,data1) index, otherwise
   3225                 // the "OR" conjunction confuses it and it switches to a full scan of
   3226                 // the raw_contacts table.
   3227 
   3228                 // This code relies on the fact that Im.DATA and Email.DATA are in fact the same
   3229                 // column - Data.DATA1
   3230                 mSb.append(DataColumns.MIMETYPE_ID + " IN (?,?)" +
   3231                         " AND " + Data.DATA1 + "=?" +
   3232                         " AND ((" + DataColumns.MIMETYPE_ID + "=? AND " + Im.PROTOCOL + "=?");
   3233                 mSelectionArgs.add(mimeTypeIdEmail);
   3234                 mSelectionArgs.add(mimeTypeIdIm);
   3235                 mSelectionArgs.add(handle);
   3236                 mSelectionArgs.add(mimeTypeIdIm);
   3237                 mSelectionArgs.add(String.valueOf(protocol));
   3238                 if (customProtocol != null) {
   3239                     mSb.append(" AND " + Im.CUSTOM_PROTOCOL + "=?");
   3240                     mSelectionArgs.add(customProtocol);
   3241                 }
   3242                 mSb.append(") OR (" + DataColumns.MIMETYPE_ID + "=?))");
   3243                 mSelectionArgs.add(mimeTypeIdEmail);
   3244             } else {
   3245                 mSb.append(DataColumns.MIMETYPE_ID + "=?" +
   3246                         " AND " + Im.PROTOCOL + "=?" +
   3247                         " AND " + Im.DATA + "=?");
   3248                 mSelectionArgs.add(mimeTypeIdIm);
   3249                 mSelectionArgs.add(String.valueOf(protocol));
   3250                 mSelectionArgs.add(handle);
   3251                 if (customProtocol != null) {
   3252                     mSb.append(" AND " + Im.CUSTOM_PROTOCOL + "=?");
   3253                     mSelectionArgs.add(customProtocol);
   3254                 }
   3255             }
   3256 
   3257             final String dataID = inputValues.getAsString(StatusUpdates.DATA_ID);
   3258             if (dataID != null) {
   3259                 mSb.append(" AND " + DataColumns.CONCRETE_ID + "=?");
   3260                 mSelectionArgs.add(dataID);
   3261             }
   3262         }
   3263 
   3264         Cursor cursor = null;
   3265         try {
   3266             cursor = db.query(DataContactsQuery.TABLE, DataContactsQuery.PROJECTION,
   3267                     mSb.toString(), mSelectionArgs.toArray(EMPTY_STRING_ARRAY), null, null,
   3268                     Clauses.CONTACT_VISIBLE + " DESC, " + Data.RAW_CONTACT_ID);
   3269             if (cursor.moveToFirst()) {
   3270                 dataId = cursor.getLong(DataContactsQuery.DATA_ID);
   3271                 rawContactId = cursor.getLong(DataContactsQuery.RAW_CONTACT_ID);
   3272                 accountType = cursor.getString(DataContactsQuery.ACCOUNT_TYPE);
   3273                 accountName = cursor.getString(DataContactsQuery.ACCOUNT_NAME);
   3274                 contactId = cursor.getLong(DataContactsQuery.CONTACT_ID);
   3275             } else {
   3276                 // No contact found, return a null URI.
   3277                 return -1;
   3278             }
   3279         } finally {
   3280             if (cursor != null) {
   3281                 cursor.close();
   3282             }
   3283         }
   3284 
   3285         final String presence = inputValues.getAsString(StatusUpdates.PRESENCE);
   3286         if (presence != null) {
   3287             if (customProtocol == null) {
   3288                 // We cannot allow a null in the custom protocol field, because SQLite3 does not
   3289                 // properly enforce uniqueness of null values
   3290                 customProtocol = "";
   3291             }
   3292 
   3293             final ContentValues values = new ContentValues();
   3294             values.put(StatusUpdates.DATA_ID, dataId);
   3295             values.put(PresenceColumns.RAW_CONTACT_ID, rawContactId);
   3296             values.put(PresenceColumns.CONTACT_ID, contactId);
   3297             values.put(StatusUpdates.PROTOCOL, protocol);
   3298             values.put(StatusUpdates.CUSTOM_PROTOCOL, customProtocol);
   3299             values.put(StatusUpdates.IM_HANDLE, handle);
   3300             final String imAccount = inputValues.getAsString(StatusUpdates.IM_ACCOUNT);
   3301             if (imAccount != null) {
   3302                 values.put(StatusUpdates.IM_ACCOUNT, imAccount);
   3303             }
   3304             values.put(StatusUpdates.PRESENCE, presence);
   3305             values.put(StatusUpdates.CHAT_CAPABILITY,
   3306                     inputValues.getAsString(StatusUpdates.CHAT_CAPABILITY));
   3307 
   3308             // Insert the presence update.
   3309             db.replace(Tables.PRESENCE, null, values);
   3310         }
   3311 
   3312         if (inputValues.containsKey(StatusUpdates.STATUS)) {
   3313             String status = inputValues.getAsString(StatusUpdates.STATUS);
   3314             String resPackage = inputValues.getAsString(StatusUpdates.STATUS_RES_PACKAGE);
   3315             Resources resources = getContext().getResources();
   3316             if (!TextUtils.isEmpty(resPackage)) {
   3317                 PackageManager pm = getContext().getPackageManager();
   3318                 try {
   3319                     resources = pm.getResourcesForApplication(resPackage);
   3320                 } catch (NameNotFoundException e) {
   3321                     Log.w(TAG, "Contact status update resource package not found: " + resPackage);
   3322                 }
   3323             }
   3324             Integer labelResourceId = inputValues.getAsInteger(StatusUpdates.STATUS_LABEL);
   3325 
   3326             if ((labelResourceId == null || labelResourceId == 0) && protocol != null) {
   3327                 labelResourceId = Im.getProtocolLabelResource(protocol);
   3328             }
   3329             String labelResource = getResourceName(resources, "string", labelResourceId);
   3330 
   3331             Integer iconResourceId = inputValues.getAsInteger(StatusUpdates.STATUS_ICON);
   3332             // TODO compute the default icon based on the protocol
   3333 
   3334             String iconResource = getResourceName(resources, "drawable", iconResourceId);
   3335 
   3336             if (TextUtils.isEmpty(status)) {
   3337                 dbHelper.deleteStatusUpdate(dataId);
   3338             } else {
   3339                 Long timestamp = inputValues.getAsLong(StatusUpdates.STATUS_TIMESTAMP);
   3340                 if (timestamp != null) {
   3341                     dbHelper.replaceStatusUpdate(
   3342                             dataId, timestamp, status, resPackage, iconResourceId, labelResourceId);
   3343                 } else {
   3344                     dbHelper.insertStatusUpdate(
   3345                             dataId, status, resPackage, iconResourceId, labelResourceId);
   3346                 }
   3347 
   3348                 // For forward compatibility with the new stream item API, insert this status update
   3349                 // there as well.  If we already have a stream item from this source, update that
   3350                 // one instead of inserting a new one (since the semantics of the old status update
   3351                 // API is to only have a single record).
   3352                 if (rawContactId != -1 && !TextUtils.isEmpty(status)) {
   3353                     ContentValues streamItemValues = new ContentValues();
   3354                     streamItemValues.put(StreamItems.RAW_CONTACT_ID, rawContactId);
   3355                     // Status updates are text only but stream items are HTML.
   3356                     streamItemValues.put(StreamItems.TEXT, statusUpdateToHtml(status));
   3357                     streamItemValues.put(StreamItems.COMMENTS, "");
   3358                     streamItemValues.put(StreamItems.RES_PACKAGE, resPackage);
   3359                     streamItemValues.put(StreamItems.RES_ICON, iconResource);
   3360                     streamItemValues.put(StreamItems.RES_LABEL, labelResource);
   3361                     streamItemValues.put(StreamItems.TIMESTAMP,
   3362                             timestamp == null ? System.currentTimeMillis() : timestamp);
   3363 
   3364                     // Note: The following is basically a workaround for the fact that status
   3365                     // updates didn't do any sort of account enforcement, while social stream item
   3366                     // updates do.  We can't expect callers of the old API to start passing account
   3367                     // information along, so we just populate the account params appropriately for
   3368                     // the raw contact.  Data set is not relevant here, as we only check account
   3369                     // name and type.
   3370                     if (accountName != null && accountType != null) {
   3371                         streamItemValues.put(RawContacts.ACCOUNT_NAME, accountName);
   3372                         streamItemValues.put(RawContacts.ACCOUNT_TYPE, accountType);
   3373                     }
   3374 
   3375                     // Check for an existing stream item from this source, and insert or update.
   3376                     Uri streamUri = StreamItems.CONTENT_URI;
   3377                     Cursor c = queryLocal(streamUri, new String[] {StreamItems._ID},
   3378                             StreamItems.RAW_CONTACT_ID + "=?",
   3379                             new String[] {String.valueOf(rawContactId)},
   3380                             null, -1 /* directory ID */, null);
   3381                     try {
   3382                         if (c.getCount() > 0) {
   3383                             c.moveToFirst();
   3384                             updateInTransaction(ContentUris.withAppendedId(streamUri, c.getLong(0)),
   3385                                     streamItemValues, null, null);
   3386                         } else {
   3387                             insertInTransaction(streamUri, streamItemValues);
   3388                         }
   3389                     } finally {
   3390                         c.close();
   3391                     }
   3392                 }
   3393             }
   3394         }
   3395 
   3396         if (contactId != -1) {
   3397             mAggregator.get().updateLastStatusUpdateId(contactId);
   3398         }
   3399 
   3400         return dataId;
   3401     }
   3402 
   3403     /** Converts a status update to HTML. */
   3404     private String statusUpdateToHtml(String status) {
   3405         return TextUtils.htmlEncode(status);
   3406     }
   3407 
   3408     private String getResourceName(Resources resources, String expectedType, Integer resourceId) {
   3409         try {
   3410             if (resourceId == null || resourceId == 0) {
   3411                 return null;
   3412             }
   3413 
   3414             // Resource has an invalid type (e.g. a string as icon)? ignore
   3415             final String resourceEntryName = resources.getResourceEntryName(resourceId);
   3416             final String resourceTypeName = resources.getResourceTypeName(resourceId);
   3417             if (!expectedType.equals(resourceTypeName)) {
   3418                 Log.w(TAG, "Resource " + resourceId + " (" + resourceEntryName + ") is of type " +
   3419                         resourceTypeName + " but " + expectedType + " is required.");
   3420                 return null;
   3421             }
   3422 
   3423             return resourceEntryName;
   3424         } catch (NotFoundException e) {
   3425             return null;
   3426         }
   3427     }
   3428 
   3429     @Override
   3430     protected int deleteInTransaction(Uri uri, String selection, String[] selectionArgs) {
   3431         if (VERBOSE_LOGGING) {
   3432             Log.v(TAG, "deleteInTransaction: uri=" + uri +
   3433                     "  selection=[" + selection + "]  args=" + Arrays.toString(selectionArgs) +
   3434                     " CPID=" + Binder.getCallingPid() +
   3435                     " User=" + UserUtils.getCurrentUserHandle(getContext()));
   3436         }
   3437 
   3438         final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
   3439 
   3440         flushTransactionalChanges();
   3441         final boolean callerIsSyncAdapter =
   3442                 readBooleanQueryParameter(uri, ContactsContract.CALLER_IS_SYNCADAPTER, false);
   3443         final int match = sUriMatcher.match(uri);
   3444         switch (match) {
   3445             case SYNCSTATE:
   3446             case PROFILE_SYNCSTATE:
   3447                 return mDbHelper.get().getSyncState().delete(db, selection, selectionArgs);
   3448 
   3449             case SYNCSTATE_ID: {
   3450                 String selectionWithId =
   3451                         (SyncStateContract.Columns._ID + "=" + ContentUris.parseId(uri) + " ")
   3452                         + (selection == null ? "" : " AND (" + selection + ")");
   3453                 return mDbHelper.get().getSyncState().delete(db, selectionWithId, selectionArgs);
   3454             }
   3455 
   3456             case PROFILE_SYNCSTATE_ID: {
   3457                 String selectionWithId =
   3458                         (SyncStateContract.Columns._ID + "=" + ContentUris.parseId(uri) + " ")
   3459                         + (selection == null ? "" : " AND (" + selection + ")");
   3460                 return mProfileHelper.getSyncState().delete(db, selectionWithId, selectionArgs);
   3461             }
   3462 
   3463             case CONTACTS: {
   3464                 invalidateFastScrollingIndexCache();
   3465                 // TODO
   3466                 return 0;
   3467             }
   3468 
   3469             case CONTACTS_ID: {
   3470                 invalidateFastScrollingIndexCache();
   3471                 long contactId = ContentUris.parseId(uri);
   3472                 return deleteContact(contactId, callerIsSyncAdapter);
   3473             }
   3474 
   3475             case CONTACTS_LOOKUP: {
   3476                 invalidateFastScrollingIndexCache();
   3477                 final List<String> pathSegments = uri.getPathSegments();
   3478                 final int segmentCount = pathSegments.size();
   3479                 if (segmentCount < 3) {
   3480                     throw new IllegalArgumentException(
   3481                             mDbHelper.get().exceptionMessage("Missing a lookup key", uri));
   3482                 }
   3483                 final String lookupKey = pathSegments.get(2);
   3484                 final long contactId = lookupContactIdByLookupKey(db, lookupKey);
   3485                 return deleteContact(contactId, callerIsSyncAdapter);
   3486             }
   3487 
   3488             case CONTACTS_LOOKUP_ID: {
   3489                 invalidateFastScrollingIndexCache();
   3490                 // lookup contact by ID and lookup key to see if they still match the actual record
   3491                 final List<String> pathSegments = uri.getPathSegments();
   3492                 final String lookupKey = pathSegments.get(2);
   3493                 SQLiteQueryBuilder lookupQb = new SQLiteQueryBuilder();
   3494                 setTablesAndProjectionMapForContacts(lookupQb, null);
   3495                 long contactId = ContentUris.parseId(uri);
   3496                 String[] args;
   3497                 if (selectionArgs == null) {
   3498                     args = new String[2];
   3499                 } else {
   3500                     args = new String[selectionArgs.length + 2];
   3501                     System.arraycopy(selectionArgs, 0, args, 2, selectionArgs.length);
   3502                 }
   3503                 args[0] = String.valueOf(contactId);
   3504                 args[1] = Uri.encode(lookupKey);
   3505                 lookupQb.appendWhere(Contacts._ID + "=? AND " + Contacts.LOOKUP_KEY + "=?");
   3506                 Cursor c = query(db, lookupQb, null, selection, args, null, null, null, null, null);
   3507                 try {
   3508                     if (c.getCount() == 1) {
   3509                         // Contact was unmodified so go ahead and delete it.
   3510                         return deleteContact(contactId, callerIsSyncAdapter);
   3511                     }
   3512 
   3513                     // The row was changed (e.g. the merging might have changed), we got multiple
   3514                     // rows or the supplied selection filtered the record out.
   3515                     return 0;
   3516 
   3517                 } finally {
   3518                     c.close();
   3519                 }
   3520             }
   3521 
   3522             case CONTACTS_DELETE_USAGE: {
   3523                 return deleteDataUsage();
   3524             }
   3525 
   3526             case RAW_CONTACTS:
   3527             case PROFILE_RAW_CONTACTS: {
   3528                 invalidateFastScrollingIndexCache();
   3529                 int numDeletes = 0;
   3530                 Cursor c = db.query(Views.RAW_CONTACTS,
   3531                         new String[] {RawContacts._ID, RawContacts.CONTACT_ID},
   3532                         appendAccountIdToSelection(
   3533                                 uri, selection), selectionArgs, null, null, null);
   3534                 try {
   3535                     while (c.moveToNext()) {
   3536                         final long rawContactId = c.getLong(0);
   3537                         long contactId = c.getLong(1);
   3538                         numDeletes += deleteRawContact(
   3539                                 rawContactId, contactId, callerIsSyncAdapter);
   3540                     }
   3541                 } finally {
   3542                     c.close();
   3543                 }
   3544                 return numDeletes;
   3545             }
   3546 
   3547             case RAW_CONTACTS_ID:
   3548             case PROFILE_RAW_CONTACTS_ID: {
   3549                 invalidateFastScrollingIndexCache();
   3550                 final long rawContactId = ContentUris.parseId(uri);
   3551                 return deleteRawContact(rawContactId, mDbHelper.get().getContactId(rawContactId),
   3552                         callerIsSyncAdapter);
   3553             }
   3554 
   3555             case DATA:
   3556             case PROFILE_DATA: {
   3557                 invalidateFastScrollingIndexCache();
   3558                 mSyncToNetwork |= !callerIsSyncAdapter;
   3559                 return deleteData(appendAccountToSelection(
   3560                         uri, selection), selectionArgs, callerIsSyncAdapter);
   3561             }
   3562 
   3563             case DATA_ID:
   3564             case PHONES_ID:
   3565             case EMAILS_ID:
   3566             case CALLABLES_ID:
   3567             case POSTALS_ID:
   3568             case PROFILE_DATA_ID: {
   3569                 invalidateFastScrollingIndexCache();
   3570                 long dataId = ContentUris.parseId(uri);
   3571                 mSyncToNetwork |= !callerIsSyncAdapter;
   3572                 mSelectionArgs1[0] = String.valueOf(dataId);
   3573                 return deleteData(Data._ID + "=?", mSelectionArgs1, callerIsSyncAdapter);
   3574             }
   3575 
   3576             case GROUPS_ID: {
   3577                 mSyncToNetwork |= !callerIsSyncAdapter;
   3578                 return deleteGroup(uri, ContentUris.parseId(uri), callerIsSyncAdapter);
   3579             }
   3580 
   3581             case GROUPS: {
   3582                 int numDeletes = 0;
   3583                 Cursor c = db.query(Views.GROUPS, Projections.ID,
   3584                         appendAccountIdToSelection(uri, selection), selectionArgs,
   3585                         null, null, null);
   3586                 try {
   3587                     while (c.moveToNext()) {
   3588                         numDeletes += deleteGroup(uri, c.getLong(0), callerIsSyncAdapter);
   3589                     }
   3590                 } finally {
   3591                     c.close();
   3592                 }
   3593                 if (numDeletes > 0) {
   3594                     mSyncToNetwork |= !callerIsSyncAdapter;
   3595                 }
   3596                 return numDeletes;
   3597             }
   3598 
   3599             case SETTINGS: {
   3600                 mSyncToNetwork |= !callerIsSyncAdapter;
   3601                 return deleteSettings(appendAccountToSelection(uri, selection), selectionArgs);
   3602             }
   3603 
   3604             case STATUS_UPDATES:
   3605             case PROFILE_STATUS_UPDATES: {
   3606                 return deleteStatusUpdates(selection, selectionArgs);
   3607             }
   3608 
   3609             case STREAM_ITEMS: {
   3610                 mSyncToNetwork |= !callerIsSyncAdapter;
   3611                 return deleteStreamItems(selection, selectionArgs);
   3612             }
   3613 
   3614             case STREAM_ITEMS_ID: {
   3615                 mSyncToNetwork |= !callerIsSyncAdapter;
   3616                 return deleteStreamItems(
   3617                         StreamItems._ID + "=?", new String[] {uri.getLastPathSegment()});
   3618             }
   3619 
   3620             case RAW_CONTACTS_ID_STREAM_ITEMS_ID: {
   3621                 mSyncToNetwork |= !callerIsSyncAdapter;
   3622                 String rawContactId = uri.getPathSegments().get(1);
   3623                 String streamItemId = uri.getLastPathSegment();
   3624                 return deleteStreamItems(
   3625                         StreamItems.RAW_CONTACT_ID + "=? AND " + StreamItems._ID + "=?",
   3626                         new String[] {rawContactId, streamItemId});
   3627             }
   3628 
   3629             case STREAM_ITEMS_ID_PHOTOS: {
   3630                 mSyncToNetwork |= !callerIsSyncAdapter;
   3631                 String streamItemId = uri.getPathSegments().get(1);
   3632                 String selectionWithId =
   3633                         (StreamItemPhotos.STREAM_ITEM_ID + "=" + streamItemId + " ")
   3634                                 + (selection == null ? "" : " AND (" + selection + ")");
   3635                 return deleteStreamItemPhotos(selectionWithId, selectionArgs);
   3636             }
   3637 
   3638             case STREAM_ITEMS_ID_PHOTOS_ID: {
   3639                 mSyncToNetwork |= !callerIsSyncAdapter;
   3640                 String streamItemId = uri.getPathSegments().get(1);
   3641                 String streamItemPhotoId = uri.getPathSegments().get(3);
   3642                 return deleteStreamItemPhotos(
   3643                         StreamItemPhotosColumns.CONCRETE_ID + "=? AND "
   3644                                 + StreamItemPhotos.STREAM_ITEM_ID + "=?",
   3645                         new String[] {streamItemPhotoId, streamItemId});
   3646             }
   3647 
   3648             default: {
   3649                 mSyncToNetwork = true;
   3650                 return mLegacyApiSupport.delete(uri, selection, selectionArgs);
   3651             }
   3652         }
   3653     }
   3654 
   3655     public int deleteGroup(Uri uri, long groupId, boolean callerIsSyncAdapter) {
   3656         final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
   3657         mGroupIdCache.clear();
   3658         final long groupMembershipMimetypeId = mDbHelper.get()
   3659                 .getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE);
   3660         db.delete(Tables.DATA, DataColumns.MIMETYPE_ID + "="
   3661                 + groupMembershipMimetypeId + " AND " + GroupMembership.GROUP_ROW_ID + "="
   3662                 + groupId, null);
   3663 
   3664         try {
   3665             if (callerIsSyncAdapter) {
   3666                 return db.delete(Tables.GROUPS, Groups._ID + "=" + groupId, null);
   3667             }
   3668 
   3669             final ContentValues values = new ContentValues();
   3670             values.put(Groups.DELETED, 1);
   3671             values.put(Groups.DIRTY, 1);
   3672             return db.update(Tables.GROUPS, values, Groups._ID + "=" + groupId, null);
   3673         } finally {
   3674             mVisibleTouched = true;
   3675         }
   3676     }
   3677 
   3678     private int deleteSettings(String selection, String[] selectionArgs) {
   3679         final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
   3680         final int count = db.delete(Tables.SETTINGS, selection, selectionArgs);
   3681         mVisibleTouched = true;
   3682         return count;
   3683     }
   3684 
   3685     private int deleteContact(long contactId, boolean callerIsSyncAdapter) {
   3686         final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
   3687         mSelectionArgs1[0] = Long.toString(contactId);
   3688         Cursor c = db.query(Tables.RAW_CONTACTS, new String[] {RawContacts._ID},
   3689                 RawContacts.CONTACT_ID + "=?", mSelectionArgs1,
   3690                 null, null, null);
   3691         try {
   3692             while (c.moveToNext()) {
   3693                 long rawContactId = c.getLong(0);
   3694                 markRawContactAsDeleted(db, rawContactId, callerIsSyncAdapter);
   3695             }
   3696         } finally {
   3697             c.close();
   3698         }
   3699 
   3700         mProviderStatusUpdateNeeded = true;
   3701 
   3702         int result = ContactsTableUtil.deleteContact(db, contactId);
   3703         scheduleBackgroundTask(BACKGROUND_TASK_CLEAN_DELETE_LOG);
   3704         return result;
   3705     }
   3706 
   3707     public int deleteRawContact(long rawContactId, long contactId, boolean callerIsSyncAdapter) {
   3708         mAggregator.get().invalidateAggregationExceptionCache();
   3709         mProviderStatusUpdateNeeded = true;
   3710 
   3711         final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
   3712 
   3713         // Find and delete stream items associated with the raw contact.
   3714         Cursor c = db.query(Tables.STREAM_ITEMS,
   3715                 new String[] {StreamItems._ID},
   3716                 StreamItems.RAW_CONTACT_ID + "=?", new String[] {String.valueOf(rawContactId)},
   3717                 null, null, null);
   3718         try {
   3719             while (c.moveToNext()) {
   3720                 deleteStreamItem(db, c.getLong(0));
   3721             }
   3722         } finally {
   3723             c.close();
   3724         }
   3725 
   3726         if (callerIsSyncAdapter || rawContactIsLocal(rawContactId)) {
   3727             // When a raw contact is deleted, a SQLite trigger deletes the parent contact.
   3728             // TODO: all contact deletes was consolidated into ContactTableUtil but this one can't
   3729             // because it's in a trigger.  Consider removing trigger and replacing with java code.
   3730             // This has to happen before the raw contact is deleted since it relies on the number
   3731             // of raw contacts.
   3732             ContactsTableUtil.deleteContactIfSingleton(db, rawContactId);
   3733 
   3734             db.delete(Tables.PRESENCE, PresenceColumns.RAW_CONTACT_ID + "=" + rawContactId, null);
   3735             int count = db.delete(Tables.RAW_CONTACTS, RawContacts._ID + "=" + rawContactId, null);
   3736 
   3737             mAggregator.get().updateAggregateData(mTransactionContext.get(), contactId);
   3738             mTransactionContext.get().markRawContactChangedOrDeletedOrInserted(rawContactId);
   3739             return count;
   3740         }
   3741 
   3742         ContactsTableUtil.deleteContactIfSingleton(db, rawContactId);
   3743         return markRawContactAsDeleted(db, rawContactId, callerIsSyncAdapter);
   3744     }
   3745 
   3746     /**
   3747      * Returns whether the given raw contact ID is local (i.e. has no account associated with it).
   3748      */
   3749     private boolean rawContactIsLocal(long rawContactId) {
   3750         final SQLiteDatabase db = mDbHelper.get().getReadableDatabase();
   3751         Cursor c = db.query(Tables.RAW_CONTACTS, Projections.LITERAL_ONE,
   3752                 RawContactsColumns.CONCRETE_ID + "=? AND " +
   3753                 RawContactsColumns.ACCOUNT_ID + "=" + Clauses.LOCAL_ACCOUNT_ID,
   3754                 new String[] {String.valueOf(rawContactId)}, null, null, null);
   3755         try {
   3756             return c.getCount() > 0;
   3757         } finally {
   3758             c.close();
   3759         }
   3760     }
   3761 
   3762     private int deleteStatusUpdates(String selection, String[] selectionArgs) {
   3763       // delete from both tables: presence and status_updates
   3764       // TODO should account type/name be appended to the where clause?
   3765       if (VERBOSE_LOGGING) {
   3766           Log.v(TAG, "deleting data from status_updates for " + selection);
   3767       }
   3768       final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
   3769       db.delete(Tables.STATUS_UPDATES, getWhereClauseForStatusUpdatesTable(selection),
   3770               selectionArgs);
   3771 
   3772       return db.delete(Tables.PRESENCE, selection, selectionArgs);
   3773     }
   3774 
   3775     private int deleteStreamItems(String selection, String[] selectionArgs) {
   3776         final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
   3777         int count = 0;
   3778         final Cursor c = db.query(
   3779                 Views.STREAM_ITEMS, Projections.ID, selection, selectionArgs, null, null, null);
   3780         try {
   3781             c.moveToPosition(-1);
   3782             while (c.moveToNext()) {
   3783                 count += deleteStreamItem(db, c.getLong(0));
   3784             }
   3785         } finally {
   3786             c.close();
   3787         }
   3788         return count;
   3789     }
   3790 
   3791     private int deleteStreamItem(SQLiteDatabase db, long streamItemId) {
   3792         deleteStreamItemPhotos(streamItemId);
   3793         return db.delete(Tables.STREAM_ITEMS, StreamItems._ID + "=?",
   3794                 new String[] {String.valueOf(streamItemId)});
   3795     }
   3796 
   3797     private int deleteStreamItemPhotos(String selection, String[] selectionArgs) {
   3798         final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
   3799         return db.delete(Tables.STREAM_ITEM_PHOTOS, selection, selectionArgs);
   3800     }
   3801 
   3802     private int deleteStreamItemPhotos(long streamItemId) {
   3803         final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
   3804         // Note that this does not enforce the modifying account.
   3805         return db.delete(Tables.STREAM_ITEM_PHOTOS,
   3806                 StreamItemPhotos.STREAM_ITEM_ID + "=?",
   3807                 new String[] {String.valueOf(streamItemId)});
   3808     }
   3809 
   3810     private int markRawContactAsDeleted(
   3811             SQLiteDatabase db, long rawContactId, boolean callerIsSyncAdapter) {
   3812 
   3813         mSyncToNetwork = true;
   3814 
   3815         final ContentValues values = new ContentValues();
   3816         values.put(RawContacts.DELETED, 1);
   3817         values.put(RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DISABLED);
   3818         values.put(RawContactsColumns.AGGREGATION_NEEDED, 1);
   3819         values.putNull(RawContacts.CONTACT_ID);
   3820         values.put(RawContacts.DIRTY, 1);
   3821         return updateRawContact(db, rawContactId, values, callerIsSyncAdapter);
   3822     }
   3823 
   3824     private int deleteDataUsage() {
   3825         final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
   3826         db.execSQL("UPDATE " + Tables.RAW_CONTACTS + " SET " +
   3827                 Contacts.TIMES_CONTACTED + "=0," +
   3828                 Contacts.LAST_TIME_CONTACTED + "=NULL");
   3829 
   3830         db.execSQL("UPDATE " + Tables.CONTACTS + " SET " +
   3831                 Contacts.TIMES_CONTACTED + "=0," +
   3832                 Contacts.LAST_TIME_CONTACTED + "=NULL");
   3833 
   3834         db.delete(Tables.DATA_USAGE_STAT, null, null);
   3835         return 1;
   3836     }
   3837 
   3838     @Override
   3839     protected int updateInTransaction(
   3840             Uri uri, ContentValues values, String selection, String[] selectionArgs) {
   3841 
   3842         if (VERBOSE_LOGGING) {
   3843             Log.v(TAG, "updateInTransaction: uri=" + uri +
   3844                     "  selection=[" + selection + "]  args=" + Arrays.toString(selectionArgs) +
   3845                     "  values=[" + values + "] CPID=" + Binder.getCallingPid() +
   3846                     " User=" + UserUtils.getCurrentUserHandle(getContext()));
   3847         }
   3848 
   3849         final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
   3850         int count = 0;
   3851 
   3852         final int match = sUriMatcher.match(uri);
   3853         if (match == SYNCSTATE_ID && selection == null) {
   3854             long rowId = ContentUris.parseId(uri);
   3855             Object data = values.get(ContactsContract.SyncState.DATA);
   3856             mTransactionContext.get().syncStateUpdated(rowId, data);
   3857             return 1;
   3858         }
   3859         flushTransactionalChanges();
   3860         final boolean callerIsSyncAdapter =
   3861                 readBooleanQueryParameter(uri, ContactsContract.CALLER_IS_SYNCADAPTER, false);
   3862         switch(match) {
   3863             case SYNCSTATE:
   3864             case PROFILE_SYNCSTATE:
   3865                 return mDbHelper.get().getSyncState().update(db, values,
   3866                         appendAccountToSelection(uri, selection), selectionArgs);
   3867 
   3868             case SYNCSTATE_ID: {
   3869                 selection = appendAccountToSelection(uri, selection);
   3870                 String selectionWithId =
   3871                         (SyncStateContract.Columns._ID + "=" + ContentUris.parseId(uri) + " ")
   3872                         + (selection == null ? "" : " AND (" + selection + ")");
   3873                 return mDbHelper.get().getSyncState().update(db, values,
   3874                         selectionWithId, selectionArgs);
   3875             }
   3876 
   3877             case PROFILE_SYNCSTATE_ID: {
   3878                 selection = appendAccountToSelection(uri, selection);
   3879                 String selectionWithId =
   3880                         (SyncStateContract.Columns._ID + "=" + ContentUris.parseId(uri) + " ")
   3881                         + (selection == null ? "" : " AND (" + selection + ")");
   3882                 return mProfileHelper.getSyncState().update(db, values,
   3883                         selectionWithId, selectionArgs);
   3884             }
   3885 
   3886             case CONTACTS:
   3887             case PROFILE: {
   3888                 invalidateFastScrollingIndexCache();
   3889                 count = updateContactOptions(values, selection, selectionArgs, callerIsSyncAdapter);
   3890                 break;
   3891             }
   3892 
   3893             case CONTACTS_ID: {
   3894                 invalidateFastScrollingIndexCache();
   3895                 count = updateContactOptions(db, ContentUris.parseId(uri), values,
   3896                         callerIsSyncAdapter);
   3897                 break;
   3898             }
   3899 
   3900             case CONTACTS_LOOKUP:
   3901             case CONTACTS_LOOKUP_ID: {
   3902                 invalidateFastScrollingIndexCache();
   3903                 final List<String> pathSegments = uri.getPathSegments();
   3904                 final int segmentCount = pathSegments.size();
   3905                 if (segmentCount < 3) {
   3906                     throw new IllegalArgumentException(
   3907                             mDbHelper.get().exceptionMessage("Missing a lookup key", uri));
   3908                 }
   3909                 final String lookupKey = pathSegments.get(2);
   3910                 final long contactId = lookupContactIdByLookupKey(db, lookupKey);
   3911                 count = updateContactOptions(db, contactId, values, callerIsSyncAdapter);
   3912                 break;
   3913             }
   3914 
   3915             case RAW_CONTACTS_ID_DATA:
   3916             case PROFILE_RAW_CONTACTS_ID_DATA: {
   3917                 invalidateFastScrollingIndexCache();
   3918                 int segment = match == RAW_CONTACTS_ID_DATA ? 1 : 2;
   3919                 final String rawContactId = uri.getPathSegments().get(segment);
   3920                 String selectionWithId = (Data.RAW_CONTACT_ID + "=" + rawContactId + " ")
   3921                     + (selection == null ? "" : " AND " + selection);
   3922 
   3923                 count = updateData(uri, values, selectionWithId, selectionArgs, callerIsSyncAdapter);
   3924                 break;
   3925             }
   3926 
   3927             case DATA:
   3928             case PROFILE_DATA: {
   3929                 invalidateFastScrollingIndexCache();
   3930                 count = updateData(uri, values, appendAccountToSelection(uri, selection),
   3931                         selectionArgs, callerIsSyncAdapter);
   3932                 if (count > 0) {
   3933                     mSyncToNetwork |= !callerIsSyncAdapter;
   3934                 }
   3935                 break;
   3936             }
   3937 
   3938             case DATA_ID:
   3939             case PHONES_ID:
   3940             case EMAILS_ID:
   3941             case CALLABLES_ID:
   3942             case POSTALS_ID: {
   3943                 invalidateFastScrollingIndexCache();
   3944                 count = updateData(uri, values, selection, selectionArgs, callerIsSyncAdapter);
   3945                 if (count > 0) {
   3946                     mSyncToNetwork |= !callerIsSyncAdapter;
   3947                 }
   3948                 break;
   3949             }
   3950 
   3951             case RAW_CONTACTS:
   3952             case PROFILE_RAW_CONTACTS: {
   3953                 invalidateFastScrollingIndexCache();
   3954                 selection = appendAccountIdToSelection(uri, selection);
   3955                 count = updateRawContacts(values, selection, selectionArgs, callerIsSyncAdapter);
   3956                 break;
   3957             }
   3958 
   3959             case RAW_CONTACTS_ID: {
   3960                 invalidateFastScrollingIndexCache();
   3961                 long rawContactId = ContentUris.parseId(uri);
   3962                 if (selection != null) {
   3963                     selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId));
   3964                     count = updateRawContacts(values, RawContacts._ID + "=?"
   3965                                     + " AND(" + selection + ")", selectionArgs,
   3966                             callerIsSyncAdapter);
   3967                 } else {
   3968                     mSelectionArgs1[0] = String.valueOf(rawContactId);
   3969                     count = updateRawContacts(values, RawContacts._ID + "=?", mSelectionArgs1,
   3970                             callerIsSyncAdapter);
   3971                 }
   3972                 break;
   3973             }
   3974 
   3975             case GROUPS: {
   3976                count = updateGroups(values, appendAccountIdToSelection(uri, selection),
   3977                         selectionArgs, callerIsSyncAdapter);
   3978                 if (count > 0) {
   3979                     mSyncToNetwork |= !callerIsSyncAdapter;
   3980                 }
   3981                 break;
   3982             }
   3983 
   3984             case GROUPS_ID: {
   3985                 long groupId = ContentUris.parseId(uri);
   3986                 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(groupId));
   3987                 String selectionWithId = Groups._ID + "=? "
   3988                         + (selection == null ? "" : " AND " + selection);
   3989                 count = updateGroups(values, selectionWithId, selectionArgs, callerIsSyncAdapter);
   3990                 if (count > 0) {
   3991                     mSyncToNetwork |= !callerIsSyncAdapter;
   3992                 }
   3993                 break;
   3994             }
   3995 
   3996             case AGGREGATION_EXCEPTIONS: {
   3997                 count = updateAggregationException(db, values);
   3998                 invalidateFastScrollingIndexCache();
   3999                 break;
   4000             }
   4001 
   4002             case SETTINGS: {
   4003                 count = updateSettings(
   4004                         values, appendAccountToSelection(uri, selection), selectionArgs);
   4005                 mSyncToNetwork |= !callerIsSyncAdapter;
   4006                 break;
   4007             }
   4008 
   4009             case STATUS_UPDATES:
   4010             case PROFILE_STATUS_UPDATES: {
   4011                 count = updateStatusUpdate(values, selection, selectionArgs);
   4012                 break;
   4013             }
   4014 
   4015             case STREAM_ITEMS: {
   4016                 count = updateStreamItems(values, selection, selectionArgs);
   4017                 break;
   4018             }
   4019 
   4020             case STREAM_ITEMS_ID: {
   4021                 count = updateStreamItems(values, StreamItems._ID + "=?",
   4022                         new String[] {uri.getLastPathSegment()});
   4023                 break;
   4024             }
   4025 
   4026             case RAW_CONTACTS_ID_STREAM_ITEMS_ID: {
   4027                 String rawContactId = uri.getPathSegments().get(1);
   4028                 String streamItemId = uri.getLastPathSegment();
   4029                 count = updateStreamItems(values,
   4030                         StreamItems.RAW_CONTACT_ID + "=? AND " + StreamItems._ID + "=?",
   4031                         new String[] {rawContactId, streamItemId});
   4032                 break;
   4033             }
   4034 
   4035             case STREAM_ITEMS_PHOTOS: {
   4036                 count = updateStreamItemPhotos(values, selection, selectionArgs);
   4037                 break;
   4038             }
   4039 
   4040             case STREAM_ITEMS_ID_PHOTOS: {
   4041                 String streamItemId = uri.getPathSegments().get(1);
   4042                 count = updateStreamItemPhotos(values,
   4043                         StreamItemPhotos.STREAM_ITEM_ID + "=?", new String[] {streamItemId});
   4044                 break;
   4045             }
   4046 
   4047             case STREAM_ITEMS_ID_PHOTOS_ID: {
   4048                 String streamItemId = uri.getPathSegments().get(1);
   4049                 String streamItemPhotoId = uri.getPathSegments().get(3);
   4050                 count = updateStreamItemPhotos(values,
   4051                         StreamItemPhotosColumns.CONCRETE_ID + "=? AND " +
   4052                                 StreamItemPhotosColumns.CONCRETE_STREAM_ITEM_ID + "=?",
   4053                         new String[] {streamItemPhotoId, streamItemId});
   4054                 break;
   4055             }
   4056 
   4057             case DIRECTORIES: {
   4058                 mContactDirectoryManager.scanPackagesByUid(Binder.getCallingUid());
   4059                 count = 1;
   4060                 break;
   4061             }
   4062 
   4063             case DATA_USAGE_FEEDBACK_ID: {
   4064                 count = handleDataUsageFeedback(uri) ? 1 : 0;
   4065                 break;
   4066             }
   4067 
   4068             default: {
   4069                 mSyncToNetwork = true;
   4070                 return mLegacyApiSupport.update(uri, values, selection, selectionArgs);
   4071             }
   4072         }
   4073 
   4074         return count;
   4075     }
   4076 
   4077     private int updateStatusUpdate(ContentValues values, String selection, String[] selectionArgs) {
   4078         final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
   4079         // update status_updates table, if status is provided
   4080         // TODO should account type/name be appended to the where clause?
   4081         int updateCount = 0;
   4082         ContentValues settableValues = getSettableColumnsForStatusUpdatesTable(values);
   4083         if (settableValues.size() > 0) {
   4084           updateCount = db.update(Tables.STATUS_UPDATES,
   4085                     settableValues,
   4086                     getWhereClauseForStatusUpdatesTable(selection),
   4087                     selectionArgs);
   4088         }
   4089 
   4090         // now update the Presence table
   4091         settableValues = getSettableColumnsForPresenceTable(values);
   4092         if (settableValues.size() > 0) {
   4093             updateCount = db.update(Tables.PRESENCE, settableValues, selection, selectionArgs);
   4094         }
   4095         // TODO updateCount is not entirely a valid count of updated rows because 2 tables could
   4096         // potentially get updated in this method.
   4097         return updateCount;
   4098     }
   4099 
   4100     private int updateStreamItems(ContentValues values, String selection, String[] selectionArgs) {
   4101         // Stream items can't be moved to a new raw contact.
   4102         values.remove(StreamItems.RAW_CONTACT_ID);
   4103 
   4104         // Don't attempt to update accounts params - they don't exist in the stream items table.
   4105         values.remove(RawContacts.ACCOUNT_NAME);
   4106         values.remove(RawContacts.ACCOUNT_TYPE);
   4107 
   4108         final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
   4109 
   4110         // If there's been no exception, the update should be fine.
   4111         return db.update(Tables.STREAM_ITEMS, values, selection, selectionArgs);
   4112     }
   4113 
   4114     private int updateStreamItemPhotos(
   4115             ContentValues values, String selection, String[] selectionArgs) {
   4116 
   4117         // Stream item photos can't be moved to a new stream item.
   4118         values.remove(StreamItemPhotos.STREAM_ITEM_ID);
   4119 
   4120         // Don't attempt to update accounts params - they don't exist in the stream item
   4121         // photos table.
   4122         values.remove(RawContacts.ACCOUNT_NAME);
   4123         values.remove(RawContacts.ACCOUNT_TYPE);
   4124 
   4125         final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
   4126 
   4127         // Process the photo (since we're updating, it's valid for the photo to not be present).
   4128         if (processStreamItemPhoto(values, true)) {
   4129             // If there's been no exception, the update should be fine.
   4130             return db.update(Tables.STREAM_ITEM_PHOTOS, values, selection, selectionArgs);
   4131         }
   4132         return 0;
   4133     }
   4134 
   4135     /**
   4136      * Build a where clause to select the rows to be updated in status_updates table.
   4137      */
   4138     private String getWhereClauseForStatusUpdatesTable(String selection) {
   4139         mSb.setLength(0);
   4140         mSb.append(WHERE_CLAUSE_FOR_STATUS_UPDATES_TABLE);
   4141         mSb.append(selection);
   4142         mSb.append(")");
   4143         return mSb.toString();
   4144     }
   4145 
   4146     private ContentValues getSettableColumnsForStatusUpdatesTable(ContentValues inputValues) {
   4147         final ContentValues values = new ContentValues();
   4148 
   4149         ContactsDatabaseHelper.copyStringValue(
   4150                 values, StatusUpdates.STATUS,
   4151                 inputValues, StatusUpdates.STATUS);
   4152         ContactsDatabaseHelper.copyStringValue(
   4153                 values, StatusUpdates.STATUS_TIMESTAMP,
   4154                 inputValues, StatusUpdates.STATUS_TIMESTAMP);
   4155         ContactsDatabaseHelper.copyStringValue(
   4156                 values, StatusUpdates.STATUS_RES_PACKAGE,
   4157                 inputValues, StatusUpdates.STATUS_RES_PACKAGE);
   4158         ContactsDatabaseHelper.copyStringValue(
   4159                 values, StatusUpdates.STATUS_LABEL,
   4160                 inputValues, StatusUpdates.STATUS_LABEL);
   4161         ContactsDatabaseHelper.copyStringValue(
   4162                 values, StatusUpdates.STATUS_ICON,
   4163                 inputValues, StatusUpdates.STATUS_ICON);
   4164 
   4165         return values;
   4166     }
   4167 
   4168     private ContentValues getSettableColumnsForPresenceTable(ContentValues inputValues) {
   4169         final ContentValues values = new ContentValues();
   4170 
   4171         ContactsDatabaseHelper.copyStringValue(
   4172               values, StatusUpdates.PRESENCE, inputValues, StatusUpdates.PRESENCE);
   4173         ContactsDatabaseHelper.copyStringValue(
   4174               values, StatusUpdates.CHAT_CAPABILITY, inputValues, StatusUpdates.CHAT_CAPABILITY);
   4175 
   4176         return values;
   4177     }
   4178 
   4179     private interface GroupAccountQuery {
   4180         String TABLE = Views.GROUPS;
   4181         String[] COLUMNS = new String[] {
   4182                 Groups._ID,
   4183                 Groups.ACCOUNT_TYPE,
   4184                 Groups.ACCOUNT_NAME,
   4185                 Groups.DATA_SET,
   4186         };
   4187         int ID = 0;
   4188         int ACCOUNT_TYPE = 1;
   4189         int ACCOUNT_NAME = 2;
   4190         int DATA_SET = 3;
   4191     }
   4192 
   4193     private int updateGroups(ContentValues originalValues, String selectionWithId,
   4194             String[] selectionArgs, boolean callerIsSyncAdapter) {
   4195         mGroupIdCache.clear();
   4196 
   4197         final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
   4198         final ContactsDatabaseHelper dbHelper = mDbHelper.get();
   4199 
   4200         final ContentValues updatedValues = new ContentValues();
   4201         updatedValues.putAll(originalValues);
   4202 
   4203         if (!callerIsSyncAdapter && !updatedValues.containsKey(Groups.DIRTY)) {
   4204             updatedValues.put(Groups.DIRTY, 1);
   4205         }
   4206         if (updatedValues.containsKey(Groups.GROUP_VISIBLE)) {
   4207             mVisibleTouched = true;
   4208         }
   4209 
   4210         // Prepare for account change
   4211         final boolean isAccountNameChanging = updatedValues.containsKey(Groups.ACCOUNT_NAME);
   4212         final boolean isAccountTypeChanging = updatedValues.containsKey(Groups.ACCOUNT_TYPE);
   4213         final boolean isDataSetChanging = updatedValues.containsKey(Groups.DATA_SET);
   4214         final boolean isAccountChanging =
   4215                 isAccountNameChanging || isAccountTypeChanging || isDataSetChanging;
   4216         final String updatedAccountName = updatedValues.getAsString(Groups.ACCOUNT_NAME);
   4217         final String updatedAccountType = updatedValues.getAsString(Groups.ACCOUNT_TYPE);
   4218         final String updatedDataSet = updatedValues.getAsString(Groups.DATA_SET);
   4219 
   4220         updatedValues.remove(Groups.ACCOUNT_NAME);
   4221         updatedValues.remove(Groups.ACCOUNT_TYPE);
   4222         updatedValues.remove(Groups.DATA_SET);
   4223 
   4224         // We later call requestSync() on all affected accounts.
   4225         final Set<Account> affectedAccounts = Sets.newHashSet();
   4226 
   4227         // Look for all affected rows, and change them row by row.
   4228         final Cursor c = db.query(GroupAccountQuery.TABLE, GroupAccountQuery.COLUMNS,
   4229                 selectionWithId, selectionArgs, null, null, null);
   4230         int returnCount = 0;
   4231         try {
   4232             c.moveToPosition(-1);
   4233             while (c.moveToNext()) {
   4234                 final long groupId = c.getLong(GroupAccountQuery.ID);
   4235 
   4236                 mSelectionArgs1[0] = Long.toString(groupId);
   4237 
   4238                 final String accountName = isAccountNameChanging
   4239                         ? updatedAccountName : c.getString(GroupAccountQuery.ACCOUNT_NAME);
   4240                 final String accountType = isAccountTypeChanging
   4241                         ? updatedAccountType : c.getString(GroupAccountQuery.ACCOUNT_TYPE);
   4242                 final String dataSet = isDataSetChanging
   4243                         ? updatedDataSet : c.getString(GroupAccountQuery.DATA_SET);
   4244 
   4245                 if (isAccountChanging) {
   4246                     final long accountId = dbHelper.getOrCreateAccountIdInTransaction(
   4247                             AccountWithDataSet.get(accountName, accountType, dataSet));
   4248                     updatedValues.put(GroupsColumns.ACCOUNT_ID, accountId);
   4249                 }
   4250 
   4251                 // Finally do the actual update.
   4252                 final int count = db.update(Tables.GROUPS, updatedValues,
   4253                         GroupsColumns.CONCRETE_ID + "=?", mSelectionArgs1);
   4254 
   4255                 if ((count > 0)
   4256                         && !TextUtils.isEmpty(accountName)
   4257                         && !TextUtils.isEmpty(accountType)) {
   4258                     affectedAccounts.add(new Account(accountName, accountType));
   4259                 }
   4260 
   4261                 returnCount += count;
   4262             }
   4263         } finally {
   4264             c.close();
   4265         }
   4266 
   4267         // TODO: This will not work for groups that have a data set specified, since the content
   4268         // resolver will not be able to request a sync for the right source (unless it is updated
   4269         // to key off account with data set).
   4270         // i.e. requestSync only takes Account, not AccountWithDataSet.
   4271         if (flagIsSet(updatedValues, Groups.SHOULD_SYNC)) {
   4272             for (Account account : affectedAccounts) {
   4273                 ContentResolver.requestSync(account, ContactsContract.AUTHORITY, new Bundle());
   4274             }
   4275         }
   4276         return returnCount;
   4277     }
   4278 
   4279     private int updateSettings(ContentValues values, String selection, String[] selectionArgs) {
   4280         final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
   4281         final int count = db.update(Tables.SETTINGS, values, selection, selectionArgs);
   4282         if (values.containsKey(Settings.UNGROUPED_VISIBLE)) {
   4283             mVisibleTouched = true;
   4284         }
   4285         return count;
   4286     }
   4287 
   4288     private int updateRawContacts(ContentValues values, String selection, String[] selectionArgs,
   4289             boolean callerIsSyncAdapter) {
   4290         if (values.containsKey(RawContacts.CONTACT_ID)) {
   4291             throw new IllegalArgumentException(RawContacts.CONTACT_ID + " should not be included " +
   4292                     "in content values. Contact IDs are assigned automatically");
   4293         }
   4294 
   4295         if (!callerIsSyncAdapter) {
   4296             selection = DatabaseUtils.concatenateWhere(selection,
   4297                     RawContacts.RAW_CONTACT_IS_READ_ONLY + "=0");
   4298         }
   4299 
   4300         int count = 0;
   4301         final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
   4302         Cursor cursor = db.query(Views.RAW_CONTACTS,
   4303                 Projections.ID, selection,
   4304                 selectionArgs, null, null, null);
   4305         try {
   4306             while (cursor.moveToNext()) {
   4307                 long rawContactId = cursor.getLong(0);
   4308                 updateRawContact(db, rawContactId, values, callerIsSyncAdapter);
   4309                 count++;
   4310             }
   4311         } finally {
   4312             cursor.close();
   4313         }
   4314 
   4315         return count;
   4316     }
   4317 
   4318     private int updateRawContact(SQLiteDatabase db, long rawContactId, ContentValues values,
   4319             boolean callerIsSyncAdapter) {
   4320         final String selection = RawContactsColumns.CONCRETE_ID + " = ?";
   4321         mSelectionArgs1[0] = Long.toString(rawContactId);
   4322 
   4323         final ContactsDatabaseHelper dbHelper = mDbHelper.get();
   4324 
   4325         final boolean requestUndoDelete = flagIsClear(values, RawContacts.DELETED);
   4326 
   4327         final boolean isAccountNameChanging = values.containsKey(RawContacts.ACCOUNT_NAME);
   4328         final boolean isAccountTypeChanging = values.containsKey(RawContacts.ACCOUNT_TYPE);
   4329         final boolean isDataSetChanging = values.containsKey(RawContacts.DATA_SET);
   4330         final boolean isAccountChanging =
   4331                 isAccountNameChanging || isAccountTypeChanging || isDataSetChanging;
   4332 
   4333         int previousDeleted = 0;
   4334         long accountId = 0;
   4335         String oldAccountType = null;
   4336         String oldAccountName = null;
   4337         String oldDataSet = null;
   4338 
   4339         if (requestUndoDelete || isAccountChanging) {
   4340             Cursor cursor = db.query(RawContactsQuery.TABLE, RawContactsQuery.COLUMNS,
   4341                     selection, mSelectionArgs1, null, null, null);
   4342             try {
   4343                 if (cursor.moveToFirst()) {
   4344                     previousDeleted = cursor.getInt(RawContactsQuery.DELETED);
   4345                     accountId = cursor.getLong(RawContactsQuery.ACCOUNT_ID);
   4346                     oldAccountType = cursor.getString(RawContactsQuery.ACCOUNT_TYPE);
   4347                     oldAccountName = cursor.getString(RawContactsQuery.ACCOUNT_NAME);
   4348                     oldDataSet = cursor.getString(RawContactsQuery.DATA_SET);
   4349                 }
   4350             } finally {
   4351                 cursor.close();
   4352             }
   4353             if (isAccountChanging) {
   4354                 // We can't change the original ContentValues, as it'll be re-used over all
   4355                 // updateRawContact invocations in a transaction, so we need to create a new one.
   4356                 final ContentValues originalValues = values;
   4357                 values = new ContentValues();
   4358                 values.clear();
   4359                 values.putAll(originalValues);
   4360 
   4361                 final AccountWithDataSet newAccountWithDataSet = AccountWithDataSet.get(
   4362                         isAccountNameChanging
   4363                             ? values.getAsString(RawContacts.ACCOUNT_NAME) : oldAccountName,
   4364                         isAccountTypeChanging
   4365                             ? values.getAsString(RawContacts.ACCOUNT_TYPE) : oldAccountType,
   4366                         isDataSetChanging
   4367                             ? values.getAsString(RawContacts.DATA_SET) : oldDataSet
   4368                         );
   4369                 accountId = dbHelper.getOrCreateAccountIdInTransaction(newAccountWithDataSet);
   4370 
   4371                 values.put(RawContactsColumns.ACCOUNT_ID, accountId);
   4372 
   4373                 values.remove(RawContacts.ACCOUNT_NAME);
   4374                 values.remove(RawContacts.ACCOUNT_TYPE);
   4375                 values.remove(RawContacts.DATA_SET);
   4376             }
   4377         }
   4378         if (requestUndoDelete) {
   4379             values.put(ContactsContract.RawContacts.AGGREGATION_MODE,
   4380                     ContactsContract.RawContacts.AGGREGATION_MODE_DEFAULT);
   4381         }
   4382 
   4383         int count = db.update(Tables.RAW_CONTACTS, values, selection, mSelectionArgs1);
   4384         if (count != 0) {
   4385             final ContactAggregator aggregator = mAggregator.get();
   4386             int aggregationMode = getIntValue(
   4387                     values, RawContacts.AGGREGATION_MODE, RawContacts.AGGREGATION_MODE_DEFAULT);
   4388 
   4389             // As per ContactsContract documentation, changing aggregation mode
   4390             // to DEFAULT should not trigger aggregation
   4391             if (aggregationMode != RawContacts.AGGREGATION_MODE_DEFAULT) {
   4392                 aggregator.markForAggregation(rawContactId, aggregationMode, false);
   4393             }
   4394             if (flagExists(values, RawContacts.STARRED)) {
   4395                 if (!callerIsSyncAdapter) {
   4396                     updateFavoritesMembership(rawContactId, flagIsSet(values, RawContacts.STARRED));
   4397                 }
   4398                 aggregator.updateStarred(rawContactId);
   4399                 aggregator.updatePinned(rawContactId);
   4400             } else {
   4401                 // if this raw contact is being associated with an account, then update the
   4402                 // favorites group membership based on whether or not this contact is starred.
   4403                 // If it is starred, add a group membership, if one doesn't already exist
   4404                 // otherwise delete any matching group memberships.
   4405                 if (!callerIsSyncAdapter && isAccountChanging) {
   4406                     boolean starred = 0 != DatabaseUtils.longForQuery(db,
   4407                             SELECTION_STARRED_FROM_RAW_CONTACTS,
   4408                             new String[] {Long.toString(rawContactId)});
   4409                     updateFavoritesMembership(rawContactId, starred);
   4410                 }
   4411             }
   4412 
   4413             // if this raw contact is being associated with an account, then add a
   4414             // group membership to the group marked as AutoAdd, if any.
   4415             if (!callerIsSyncAdapter && isAccountChanging) {
   4416                 addAutoAddMembership(rawContactId);
   4417             }
   4418 
   4419             if (values.containsKey(RawContacts.SOURCE_ID)) {
   4420                 aggregator.updateLookupKeyForRawContact(db, rawContactId);
   4421             }
   4422             if (flagExists(values, RawContacts.NAME_VERIFIED)) {
   4423                 // If setting NAME_VERIFIED for this raw contact, reset it for all
   4424                 // other raw contacts in the same aggregate
   4425                 if (flagIsSet(values, RawContacts.NAME_VERIFIED)) {
   4426                     mDbHelper.get().resetNameVerifiedForOtherRawContacts(rawContactId);
   4427                 }
   4428                 aggregator.updateDisplayNameForRawContact(db, rawContactId);
   4429             }
   4430             if (requestUndoDelete && previousDeleted == 1) {
   4431                 // Note before the accounts refactoring, we used to use the *old* account here,
   4432                 // which doesn't make sense, so now we pass the *new* account.
   4433                 // (In practice it doesn't matter because there's probably no apps that undo-delete
   4434                 // and change accounts at the same time.)
   4435                 mTransactionContext.get().rawContactInserted(rawContactId, accountId);
   4436             }
   4437             mTransactionContext.get().markRawContactChangedOrDeletedOrInserted(rawContactId);
   4438         }
   4439         return count;
   4440     }
   4441 
   4442     private int updateData(Uri uri, ContentValues inputValues, String selection,
   4443             String[] selectionArgs, boolean callerIsSyncAdapter) {
   4444 
   4445         final ContentValues values = new ContentValues(inputValues);
   4446         values.remove(Data._ID);
   4447         values.remove(Data.RAW_CONTACT_ID);
   4448         values.remove(Data.MIMETYPE);
   4449 
   4450         String packageName = inputValues.getAsString(Data.RES_PACKAGE);
   4451         if (packageName != null) {
   4452             values.remove(Data.RES_PACKAGE);
   4453             values.put(DataColumns.PACKAGE_ID, mDbHelper.get().getPackageId(packageName));
   4454         }
   4455 
   4456         if (!callerIsSyncAdapter) {
   4457             selection = DatabaseUtils.concatenateWhere(selection, Data.IS_READ_ONLY + "=0");
   4458         }
   4459 
   4460         int count = 0;
   4461 
   4462         // Note that the query will return data according to the access restrictions,
   4463         // so we don't need to worry about updating data we don't have permission to read.
   4464         Cursor c = queryLocal(uri,
   4465                 DataRowHandler.DataUpdateQuery.COLUMNS,
   4466                 selection, selectionArgs, null, -1 /* directory ID */, null);
   4467         try {
   4468             while(c.moveToNext()) {
   4469                 count += updateData(values, c, callerIsSyncAdapter);
   4470             }
   4471         } finally {
   4472             c.close();
   4473         }
   4474 
   4475         return count;
   4476     }
   4477 
   4478     private void maybeTrimLongPhoneNumber(ContentValues values) {
   4479         final String data1 = values.getAsString(Data.DATA1);
   4480         if (data1 != null && data1.length() > PHONE_NUMBER_LENGTH_LIMIT) {
   4481             values.put(Data.DATA1, data1.substring(0, PHONE_NUMBER_LENGTH_LIMIT));
   4482         }
   4483     }
   4484 
   4485     private int updateData(ContentValues values, Cursor c, boolean callerIsSyncAdapter) {
   4486         if (values.size() == 0) {
   4487             return 0;
   4488         }
   4489 
   4490         final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
   4491 
   4492         final String mimeType = c.getString(DataRowHandler.DataUpdateQuery.MIMETYPE);
   4493         if (Phone.CONTENT_ITEM_TYPE.equals(mimeType)) {
   4494             maybeTrimLongPhoneNumber(values);
   4495         }
   4496 
   4497         DataRowHandler rowHandler = getDataRowHandler(mimeType);
   4498         boolean updated =
   4499                 rowHandler.update(db, mTransactionContext.get(), values, c,
   4500                         callerIsSyncAdapter);
   4501         if (Photo.CONTENT_ITEM_TYPE.equals(mimeType)) {
   4502             scheduleBackgroundTask(BACKGROUND_TASK_CLEANUP_PHOTOS);
   4503         }
   4504         return updated ? 1 : 0;
   4505     }
   4506 
   4507     private int updateContactOptions(ContentValues values, String selection,
   4508             String[] selectionArgs, boolean callerIsSyncAdapter) {
   4509         int count = 0;
   4510         final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
   4511 
   4512         Cursor cursor = db.query(Views.CONTACTS,
   4513                 new String[] { Contacts._ID }, selection, selectionArgs, null, null, null);
   4514         try {
   4515             while (cursor.moveToNext()) {
   4516                 long contactId = cursor.getLong(0);
   4517 
   4518                 updateContactOptions(db, contactId, values, callerIsSyncAdapter);
   4519                 count++;
   4520             }
   4521         } finally {
   4522             cursor.close();
   4523         }
   4524 
   4525         return count;
   4526     }
   4527 
   4528     private int updateContactOptions(
   4529             SQLiteDatabase db, long contactId, ContentValues inputValues, boolean callerIsSyncAdapter) {
   4530 
   4531         final ContentValues values = new ContentValues();
   4532         ContactsDatabaseHelper.copyStringValue(
   4533                 values, RawContacts.CUSTOM_RINGTONE,
   4534                 inputValues, Contacts.CUSTOM_RINGTONE);
   4535         ContactsDatabaseHelper.copyLongValue(
   4536                 values, RawContacts.SEND_TO_VOICEMAIL,
   4537                 inputValues, Contacts.SEND_TO_VOICEMAIL);
   4538         ContactsDatabaseHelper.copyLongValue(
   4539                 values, RawContacts.LAST_TIME_CONTACTED,
   4540                 inputValues, Contacts.LAST_TIME_CONTACTED);
   4541         ContactsDatabaseHelper.copyLongValue(
   4542                 values, RawContacts.TIMES_CONTACTED,
   4543                 inputValues, Contacts.TIMES_CONTACTED);
   4544         ContactsDatabaseHelper.copyLongValue(
   4545                 values, RawContacts.STARRED,
   4546                 inputValues, Contacts.STARRED);
   4547         ContactsDatabaseHelper.copyLongValue(
   4548                 values, RawContacts.PINNED,
   4549                 inputValues, Contacts.PINNED);
   4550 
   4551         if (values.size() == 0) {
   4552             return 0;  // Nothing to update, bail out.
   4553         }
   4554 
   4555         boolean hasStarredValue = flagExists(values, RawContacts.STARRED);
   4556         if (hasStarredValue) {
   4557             // Mark dirty when changing starred to trigger sync.
   4558             values.put(RawContacts.DIRTY, 1);
   4559         }
   4560 
   4561         mSelectionArgs1[0] = String.valueOf(contactId);
   4562         db.update(Tables.RAW_CONTACTS, values, RawContacts.CONTACT_ID + "=?"
   4563                 + " AND " + RawContacts.RAW_CONTACT_IS_READ_ONLY + "=0", mSelectionArgs1);
   4564 
   4565         if (hasStarredValue && !callerIsSyncAdapter) {
   4566             Cursor cursor = db.query(Views.RAW_CONTACTS,
   4567                     new String[] { RawContacts._ID }, RawContacts.CONTACT_ID + "=?",
   4568                     mSelectionArgs1, null, null, null);
   4569             try {
   4570                 while (cursor.moveToNext()) {
   4571                     long rawContactId = cursor.getLong(0);
   4572                     updateFavoritesMembership(rawContactId,
   4573                             flagIsSet(values, RawContacts.STARRED));
   4574                 }
   4575             } finally {
   4576                 cursor.close();
   4577             }
   4578         }
   4579 
   4580         // Copy changeable values to prevent automatically managed fields from being explicitly
   4581         // updated by clients.
   4582         values.clear();
   4583         ContactsDatabaseHelper.copyStringValue(
   4584                 values, RawContacts.CUSTOM_RINGTONE,
   4585                 inputValues, Contacts.CUSTOM_RINGTONE);
   4586         ContactsDatabaseHelper.copyLongValue(
   4587                 values, RawContacts.SEND_TO_VOICEMAIL,
   4588                 inputValues, Contacts.SEND_TO_VOICEMAIL);
   4589         ContactsDatabaseHelper.copyLongValue(
   4590                 values, RawContacts.LAST_TIME_CONTACTED,
   4591                 inputValues, Contacts.LAST_TIME_CONTACTED);
   4592         ContactsDatabaseHelper.copyLongValue(
   4593                 values, RawContacts.TIMES_CONTACTED,
   4594                 inputValues, Contacts.TIMES_CONTACTED);
   4595         ContactsDatabaseHelper.copyLongValue(
   4596                 values, RawContacts.STARRED,
   4597                 inputValues, Contacts.STARRED);
   4598         ContactsDatabaseHelper.copyLongValue(
   4599                 values, RawContacts.PINNED,
   4600                 inputValues, Contacts.PINNED);
   4601 
   4602         values.put(Contacts.CONTACT_LAST_UPDATED_TIMESTAMP,
   4603                 Clock.getInstance().currentTimeMillis());
   4604 
   4605         int rslt = db.update(Tables.CONTACTS, values, Contacts._ID + "=?",
   4606                 mSelectionArgs1);
   4607 
   4608         if (inputValues.containsKey(Contacts.LAST_TIME_CONTACTED) &&
   4609                 !inputValues.containsKey(Contacts.TIMES_CONTACTED)) {
   4610             db.execSQL(UPDATE_TIMES_CONTACTED_CONTACTS_TABLE, mSelectionArgs1);
   4611             db.execSQL(UPDATE_TIMES_CONTACTED_RAWCONTACTS_TABLE, mSelectionArgs1);
   4612         }
   4613         return rslt;
   4614     }
   4615 
   4616     private int updateAggregationException(SQLiteDatabase db, ContentValues values) {
   4617         Integer exceptionType = values.getAsInteger(AggregationExceptions.TYPE);
   4618         Long rcId1 = values.getAsLong(AggregationExceptions.RAW_CONTACT_ID1);
   4619         Long rcId2 = values.getAsLong(AggregationExceptions.RAW_CONTACT_ID2);
   4620         if (exceptionType == null || rcId1 == null || rcId2 == null) {
   4621             return 0;
   4622         }
   4623 
   4624         long rawContactId1;
   4625         long rawContactId2;
   4626         if (rcId1 < rcId2) {
   4627             rawContactId1 = rcId1;
   4628             rawContactId2 = rcId2;
   4629         } else {
   4630             rawContactId2 = rcId1;
   4631             rawContactId1 = rcId2;
   4632         }
   4633 
   4634         if (exceptionType == AggregationExceptions.TYPE_AUTOMATIC) {
   4635             mSelectionArgs2[0] = String.valueOf(rawContactId1);
   4636             mSelectionArgs2[1] = String.valueOf(rawContactId2);
   4637             db.delete(Tables.AGGREGATION_EXCEPTIONS,
   4638                     AggregationExceptions.RAW_CONTACT_ID1 + "=? AND "
   4639                     + AggregationExceptions.RAW_CONTACT_ID2 + "=?", mSelectionArgs2);
   4640         } else {
   4641             ContentValues exceptionValues = new ContentValues(3);
   4642             exceptionValues.put(AggregationExceptions.TYPE, exceptionType);
   4643             exceptionValues.put(AggregationExceptions.RAW_CONTACT_ID1, rawContactId1);
   4644             exceptionValues.put(AggregationExceptions.RAW_CONTACT_ID2, rawContactId2);
   4645             db.replace(Tables.AGGREGATION_EXCEPTIONS, AggregationExceptions._ID, exceptionValues);
   4646         }
   4647 
   4648         final ContactAggregator aggregator = mAggregator.get();
   4649         aggregator.invalidateAggregationExceptionCache();
   4650         aggregator.markForAggregation(rawContactId1, RawContacts.AGGREGATION_MODE_DEFAULT, true);
   4651         aggregator.markForAggregation(rawContactId2, RawContacts.AGGREGATION_MODE_DEFAULT, true);
   4652 
   4653         aggregator.aggregateContact(mTransactionContext.get(), db, rawContactId1);
   4654         aggregator.aggregateContact(mTransactionContext.get(), db, rawContactId2);
   4655 
   4656         // The return value is fake - we just confirm that we made a change, not count actual
   4657         // rows changed.
   4658         return 1;
   4659     }
   4660 
   4661     @Override
   4662     public void onAccountsUpdated(Account[] accounts) {
   4663         scheduleBackgroundTask(BACKGROUND_TASK_UPDATE_ACCOUNTS);
   4664     }
   4665 
   4666     /** return serialized version of {@code accounts} */
   4667     @VisibleForTesting
   4668     static String accountsToString(Set<Account> accounts) {
   4669         final StringBuilder sb = new StringBuilder();
   4670         for (Account account : accounts) {
   4671             if (sb.length() > 0) {
   4672                 sb.append(ACCOUNT_STRING_SEPARATOR_OUTER);
   4673             }
   4674             sb.append(account.name);
   4675             sb.append(ACCOUNT_STRING_SEPARATOR_INNER);
   4676             sb.append(account.type);
   4677         }
   4678         return sb.toString();
   4679     }
   4680 
   4681     /**
   4682      * de-serialize string returned by {@link #accountsToString} and return it.
   4683      * If {@code accountsString} is malformed it'll throw {@link IllegalArgumentException}.
   4684      */
   4685     @VisibleForTesting
   4686     static Set<Account> stringToAccounts(String accountsString) {
   4687         final Set<Account> ret = Sets.newHashSet();
   4688         if (accountsString.length() == 0) return ret; // no accounts
   4689         try {
   4690             for (String accountString : accountsString.split(ACCOUNT_STRING_SEPARATOR_OUTER)) {
   4691                 String[] nameAndType = accountString.split(ACCOUNT_STRING_SEPARATOR_INNER);
   4692                 ret.add(new Account(nameAndType[0], nameAndType[1]));
   4693             }
   4694             return ret;
   4695         } catch (RuntimeException ex) {
   4696             throw new IllegalArgumentException("Malformed string", ex);
   4697         }
   4698     }
   4699 
   4700     /**
   4701      * @return {@code true} if the given {@code currentSystemAccounts} are different from the
   4702      *    accounts we know, which are stored in the {@link DbProperties#KNOWN_ACCOUNTS} property.
   4703      */
   4704     @VisibleForTesting
   4705     boolean haveAccountsChanged(Account[] currentSystemAccounts) {
   4706         final ContactsDatabaseHelper dbHelper = mDbHelper.get();
   4707         final Set<Account> knownAccountSet;
   4708         try {
   4709             knownAccountSet =
   4710                     stringToAccounts(dbHelper.getProperty(DbProperties.KNOWN_ACCOUNTS, ""));
   4711         } catch (IllegalArgumentException e) {
   4712             // Failed to get the last known accounts for an unknown reason.  Let's just
   4713             // treat as if accounts have changed.
   4714             return true;
   4715         }
   4716         final Set<Account> currentAccounts = Sets.newHashSet(currentSystemAccounts);
   4717         return !knownAccountSet.equals(currentAccounts);
   4718     }
   4719 
   4720     @VisibleForTesting
   4721     void saveAccounts(Account[] systemAccounts) {
   4722         final ContactsDatabaseHelper dbHelper = mDbHelper.get();
   4723         dbHelper.setProperty(
   4724                 DbProperties.KNOWN_ACCOUNTS, accountsToString(Sets.newHashSet(systemAccounts)));
   4725     }
   4726 
   4727     private boolean updateAccountsInBackground(Account[] systemAccounts) {
   4728         if (!haveAccountsChanged(systemAccounts)) {
   4729             return false;
   4730         }
   4731         if ("1".equals(SystemProperties.get(DEBUG_PROPERTY_KEEP_STALE_ACCOUNT_DATA))) {
   4732             Log.w(TAG, "Accounts changed, but not removing stale data for " +
   4733                     DEBUG_PROPERTY_KEEP_STALE_ACCOUNT_DATA);
   4734             return true;
   4735         }
   4736         Log.i(TAG, "Accounts changed");
   4737 
   4738         invalidateFastScrollingIndexCache();
   4739 
   4740         final ContactsDatabaseHelper dbHelper = mDbHelper.get();
   4741         final SQLiteDatabase db = dbHelper.getWritableDatabase();
   4742         db.beginTransaction();
   4743 
   4744         // WARNING: This method can be run in either contacts mode or profile mode.  It is
   4745         // absolutely imperative that no calls be made inside the following try block that can
   4746         // interact with a specific contacts or profile DB.  Otherwise it is quite possible for a
   4747         // deadlock to occur.  i.e. always use the current database in mDbHelper and do not access
   4748         // mContactsHelper or mProfileHelper directly.
   4749         //
   4750         // The problem may be a bit more subtle if you also access something that stores the current
   4751         // db instance in its constructor.  updateSearchIndexInTransaction relies on the
   4752         // SearchIndexManager which upon construction, stores the current db. In this case,
   4753         // SearchIndexManager always contains the contact DB. This is why the
   4754         // updateSearchIndexInTransaction is protected with !isInProfileMode now.
   4755         try {
   4756             // First, remove stale rows from raw_contacts, groups, and related tables.
   4757 
   4758             // All accounts that are used in raw_contacts and/or groups.
   4759             final Set<AccountWithDataSet> knownAccountsWithDataSets
   4760                     = dbHelper.getAllAccountsWithDataSets();
   4761 
   4762             // Find the accounts that have been removed.
   4763             final List<AccountWithDataSet> accountsWithDataSetsToDelete = Lists.newArrayList();
   4764             for (AccountWithDataSet knownAccountWithDataSet : knownAccountsWithDataSets) {
   4765                 if (knownAccountWithDataSet.isLocalAccount()
   4766                         || knownAccountWithDataSet.inSystemAccounts(systemAccounts)) {
   4767                     continue;
   4768                 }
   4769                 accountsWithDataSetsToDelete.add(knownAccountWithDataSet);
   4770             }
   4771 
   4772             if (!accountsWithDataSetsToDelete.isEmpty()) {
   4773                 for (AccountWithDataSet accountWithDataSet : accountsWithDataSetsToDelete) {
   4774                     Log.d(TAG, "removing data for removed account " + accountWithDataSet);
   4775                     final Long accountIdOrNull = dbHelper.getAccountIdOrNull(accountWithDataSet);
   4776 
   4777                     // getAccountIdOrNull() really shouldn't return null here, but just in case...
   4778                     if (accountIdOrNull != null) {
   4779                         final String accountId = Long.toString(accountIdOrNull);
   4780                         final String[] accountIdParams =
   4781                                 new String[] {accountId};
   4782                         db.execSQL(
   4783                                 "DELETE FROM " + Tables.GROUPS +
   4784                                 " WHERE " + GroupsColumns.ACCOUNT_ID + " = ?",
   4785                                 accountIdParams);
   4786                         db.execSQL(
   4787                                 "DELETE FROM " + Tables.PRESENCE +
   4788                                 " WHERE " + PresenceColumns.RAW_CONTACT_ID + " IN (" +
   4789                                         "SELECT " + RawContacts._ID +
   4790                                         " FROM " + Tables.RAW_CONTACTS +
   4791                                         " WHERE " + RawContactsColumns.ACCOUNT_ID + " = ?)",
   4792                                         accountIdParams);
   4793                         db.execSQL(
   4794                                 "DELETE FROM " + Tables.STREAM_ITEM_PHOTOS +
   4795                                 " WHERE " + StreamItemPhotos.STREAM_ITEM_ID + " IN (" +
   4796                                         "SELECT " + StreamItems._ID +
   4797                                         " FROM " + Tables.STREAM_ITEMS +
   4798                                         " WHERE " + StreamItems.RAW_CONTACT_ID + " IN (" +
   4799                                                 "SELECT " + RawContacts._ID +
   4800                                                 " FROM " + Tables.RAW_CONTACTS +
   4801                                                 " WHERE " + RawContactsColumns.ACCOUNT_ID + "=?))",
   4802                                                 accountIdParams);
   4803                         db.execSQL(
   4804                                 "DELETE FROM " + Tables.STREAM_ITEMS +
   4805                                 " WHERE " + StreamItems.RAW_CONTACT_ID + " IN (" +
   4806                                         "SELECT " + RawContacts._ID +
   4807                                         " FROM " + Tables.RAW_CONTACTS +
   4808                                         " WHERE " + RawContactsColumns.ACCOUNT_ID + " = ?)",
   4809                                         accountIdParams);
   4810 
   4811                         // Delta API is only needed for regular contacts.
   4812                         if (!inProfileMode()) {
   4813                             // Contacts are deleted by a trigger on the raw_contacts table.
   4814                             // But we also need to insert the contact into the delete log.
   4815                             // This logic is being consolidated into the ContactsTableUtil.
   4816 
   4817                             // deleteContactIfSingleton() does not work in this case because raw
   4818                             // contacts will be deleted in a single batch below.  Contacts with
   4819                             // multiple raw contacts in the same account will be missed.
   4820 
   4821                             // Find all contacts that do not have raw contacts in other accounts.
   4822                             // These should be deleted.
   4823                             Cursor cursor = db.rawQuery(
   4824                                     "SELECT " + RawContactsColumns.CONCRETE_CONTACT_ID +
   4825                                             " FROM " + Tables.RAW_CONTACTS +
   4826                                             " WHERE " + RawContactsColumns.ACCOUNT_ID + " = ?1" +
   4827                                             " AND " + RawContactsColumns.CONCRETE_CONTACT_ID +
   4828                                             " IS NOT NULL" +
   4829                                             " AND " + RawContactsColumns.CONCRETE_CONTACT_ID +
   4830                                             " NOT IN (" +
   4831                                             "    SELECT " + RawContactsColumns.CONCRETE_CONTACT_ID +
   4832                                             "    FROM " + Tables.RAW_CONTACTS +
   4833                                             "    WHERE " + RawContactsColumns.ACCOUNT_ID + " != ?1"
   4834                                             + "  AND " + RawContactsColumns.CONCRETE_CONTACT_ID +
   4835                                             "    IS NOT NULL"
   4836                                             + ")", accountIdParams);
   4837                             try {
   4838                                 while (cursor.moveToNext()) {
   4839                                     final long contactId = cursor.getLong(0);
   4840                                     ContactsTableUtil.deleteContact(db, contactId);
   4841                                 }
   4842                             } finally {
   4843                                 MoreCloseables.closeQuietly(cursor);
   4844                             }
   4845 
   4846                             // If the contact was not deleted, its last updated timestamp needs to
   4847                             // be refreshed since one of its raw contacts got removed.
   4848                             // Find all contacts that will not be deleted (i.e. contacts with
   4849                             // raw contacts in other accounts)
   4850                             cursor = db.rawQuery(
   4851                                     "SELECT DISTINCT " + RawContactsColumns.CONCRETE_CONTACT_ID +
   4852                                             " FROM " + Tables.RAW_CONTACTS +
   4853                                             " WHERE " + RawContactsColumns.ACCOUNT_ID + " = ?1" +
   4854                                             " AND " + RawContactsColumns.CONCRETE_CONTACT_ID +
   4855                                             " IN (" +
   4856                                             "    SELECT " + RawContactsColumns.CONCRETE_CONTACT_ID +
   4857                                             "    FROM " + Tables.RAW_CONTACTS +
   4858                                             "    WHERE " + RawContactsColumns.ACCOUNT_ID + " != ?1"
   4859                                             + ")", accountIdParams);
   4860                             try {
   4861                                 while (cursor.moveToNext()) {
   4862                                     final long contactId = cursor.getLong(0);
   4863                                     ContactsTableUtil.updateContactLastUpdateByContactId(
   4864                                             db, contactId);
   4865                                 }
   4866                             } finally {
   4867                                 MoreCloseables.closeQuietly(cursor);
   4868                             }
   4869                         }
   4870 
   4871                         db.execSQL(
   4872                                 "DELETE FROM " + Tables.RAW_CONTACTS +
   4873                                 " WHERE " + RawContactsColumns.ACCOUNT_ID + " = ?",
   4874                                 accountIdParams);
   4875                         db.execSQL(
   4876                                 "DELETE FROM " + Tables.ACCOUNTS +
   4877                                 " WHERE " + AccountsColumns._ID + "=?",
   4878                                 accountIdParams);
   4879                     }
   4880                 }
   4881 
   4882                 // Find all aggregated contacts that used to contain the raw contacts
   4883                 // we have just deleted and see if they are still referencing the deleted
   4884                 // names or photos.  If so, fix up those contacts.
   4885                 HashSet<Long> orphanContactIds = Sets.newHashSet();
   4886                 Cursor cursor = db.rawQuery("SELECT " + Contacts._ID +
   4887                         " FROM " + Tables.CONTACTS +
   4888                         " WHERE (" + Contacts.NAME_RAW_CONTACT_ID + " NOT NULL AND " +
   4889                                 Contacts.NAME_RAW_CONTACT_ID + " NOT IN " +
   4890                                         "(SELECT " + RawContacts._ID +
   4891                                         " FROM " + Tables.RAW_CONTACTS + "))" +
   4892                         " OR (" + Contacts.PHOTO_ID + " NOT NULL AND " +
   4893                                 Contacts.PHOTO_ID + " NOT IN " +
   4894                                         "(SELECT " + Data._ID +
   4895                                         " FROM " + Tables.DATA + "))", null);
   4896                 try {
   4897                     while (cursor.moveToNext()) {
   4898                         orphanContactIds.add(cursor.getLong(0));
   4899                     }
   4900                 } finally {
   4901                     cursor.close();
   4902                 }
   4903 
   4904                 for (Long contactId : orphanContactIds) {
   4905                     mAggregator.get().updateAggregateData(mTransactionContext.get(), contactId);
   4906                 }
   4907                 dbHelper.updateAllVisible();
   4908 
   4909                 // Don't bother updating the search index if we're in profile mode - there is no
   4910                 // search index for the profile DB, and updating it for the contacts DB in this case
   4911                 // makes no sense and risks a deadlock.
   4912                 if (!inProfileMode()) {
   4913                     // TODO Fix it.  It only updates index for contacts/raw_contacts that the
   4914                     // current transaction context knows updated, but here in this method we don't
   4915                     // update that information, so effectively it's no-op.
   4916                     // We can probably just schedule BACKGROUND_TASK_UPDATE_SEARCH_INDEX.
   4917                     // (But make sure it's not scheduled yet. We schedule this task in initialize()
   4918                     // too.)
   4919                     updateSearchIndexInTransaction();
   4920                 }
   4921             }
   4922 
   4923             // Second, remove stale rows from Tables.SETTINGS and Tables.DIRECTORIES
   4924             removeStaleAccountRows(
   4925                     Tables.SETTINGS, Settings.ACCOUNT_NAME, Settings.ACCOUNT_TYPE, systemAccounts);
   4926             removeStaleAccountRows(Tables.DIRECTORIES, Directory.ACCOUNT_NAME,
   4927                     Directory.ACCOUNT_TYPE, systemAccounts);
   4928 
   4929             // Third, remaining tasks that must be done in a transaction.
   4930             // TODO: Should sync state take data set into consideration?
   4931             dbHelper.getSyncState().onAccountsChanged(db, systemAccounts);
   4932 
   4933             saveAccounts(systemAccounts);
   4934 
   4935             db.setTransactionSuccessful();
   4936         } finally {
   4937             db.endTransaction();
   4938         }
   4939         mAccountWritability.clear();
   4940 
   4941         updateContactsAccountCount(systemAccounts);
   4942         updateProviderStatus();
   4943         return true;
   4944     }
   4945 
   4946     private void updateContactsAccountCount(Account[] accounts) {
   4947         int count = 0;
   4948         for (Account account : accounts) {
   4949             if (isContactsAccount(account)) {
   4950                 count++;
   4951             }
   4952         }
   4953         mContactsAccountCount = count;
   4954     }
   4955 
   4956     // Overridden in SynchronousContactsProvider2.java
   4957     protected boolean isContactsAccount(Account account) {
   4958         final IContentService cs = ContentResolver.getContentService();
   4959         try {
   4960             return cs.getIsSyncable(account, ContactsContract.AUTHORITY) > 0;
   4961         } catch (RemoteException e) {
   4962             Log.e(TAG, "Cannot obtain sync flag for account: " + account, e);
   4963             return false;
   4964         }
   4965     }
   4966 
   4967     public void onPackageChanged(String packageName) {
   4968         scheduleBackgroundTask(BACKGROUND_TASK_UPDATE_DIRECTORIES, packageName);
   4969     }
   4970 
   4971     private void removeStaleAccountRows(String table, String accountNameColumn,
   4972             String accountTypeColumn, Account[] systemAccounts) {
   4973         final SQLiteDatabase db = mDbHelper.get().getWritableDatabase();
   4974         final Cursor c = db.rawQuery(
   4975                 "SELECT DISTINCT " + accountNameColumn +
   4976                 "," + accountTypeColumn +
   4977                 " FROM " + table, null);
   4978         try {
   4979             c.moveToPosition(-1);
   4980             while (c.moveToNext()) {
   4981                 final AccountWithDataSet accountWithDataSet = AccountWithDataSet.get(
   4982                         c.getString(0), c.getString(1), null);
   4983                 if (accountWithDataSet.isLocalAccount()
   4984                         || accountWithDataSet.inSystemAccounts(systemAccounts)) {
   4985                     // Account still exists.
   4986                     continue;
   4987                 }
   4988 
   4989                 db.execSQL("DELETE FROM " + table +
   4990                         " WHERE " + accountNameColumn + "=? AND " +
   4991                         accountTypeColumn + "=?",
   4992                         new String[] {accountWithDataSet.getAccountName(),
   4993                                 accountWithDataSet.getAccountType()});
   4994             }
   4995         } finally {
   4996             c.close();
   4997         }
   4998     }
   4999 
   5000     @Override
   5001     public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
   5002             String sortOrder) {
   5003         return query(uri, projection, selection, selectionArgs, sortOrder, null);
   5004     }
   5005 
   5006     @Override
   5007     public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,
   5008             String sortOrder, CancellationSignal cancellationSignal) {
   5009         if (VERBOSE_LOGGING) {
   5010             Log.v(TAG, "query: uri=" + uri + "  projection=" + Arrays.toString(projection) +
   5011                     "  selection=[" + selection + "]  args=" + Arrays.toString(selectionArgs) +
   5012                     "  order=[" + sortOrder + "] CPID=" + Binder.getCallingPid() +
   5013                     " User=" + UserUtils.getCurrentUserHandle(getContext()));
   5014         }
   5015 
   5016         waitForAccess(mReadAccessLatch);
   5017 
   5018         // Enforce stream items access check if applicable.
   5019         enforceSocialStreamReadPermission(uri);
   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 = getContext().getContentResolver().query(
   5072                 directoryUri, projection, selection, selectionArgs, sortOrder);
   5073         if (cursor == null) {
   5074             return null;
   5075         }
   5076 
   5077         // Load the cursor contents into a memory cursor (backed by a cursor window) and close the
   5078         // underlying cursor.
   5079         try {
   5080             MemoryCursor memCursor = new MemoryCursor(null, cursor.getColumnNames());
   5081             memCursor.fillFromCursor(cursor);
   5082             return memCursor;
   5083         } finally {
   5084             cursor.close();
   5085         }
   5086     }
   5087 
   5088     private Cursor addSnippetExtrasToCursor(Uri uri, Cursor cursor) {
   5089 
   5090         // If the cursor doesn't contain a snippet column, don't bother wrapping it.
   5091         if (cursor.getColumnIndex(SearchSnippets.SNIPPET) < 0) {
   5092             return cursor;
   5093         }
   5094 
   5095         String query = uri.getLastPathSegment();
   5096 
   5097         // Snippet data is needed for the snippeting on the client side, so store it in the cursor
   5098         if (cursor instanceof AbstractCursor && deferredSnippetingRequested(uri)){
   5099             Bundle oldExtras = cursor.getExtras();
   5100             Bundle extras = new Bundle();
   5101             if (oldExtras != null) {
   5102                 extras.putAll(oldExtras);
   5103             }
   5104             extras.putString(ContactsContract.DEFERRED_SNIPPETING_QUERY, query);
   5105 
   5106             ((AbstractCursor) cursor).setExtras(extras);
   5107         }
   5108         return cursor;
   5109     }
   5110 
   5111     private Cursor addDeferredSnippetingExtra(Cursor cursor) {
   5112         if (cursor instanceof AbstractCursor){
   5113             Bundle oldExtras = cursor.getExtras();
   5114             Bundle extras = new Bundle();
   5115             if (oldExtras != null) {
   5116                 extras.putAll(oldExtras);
   5117             }
   5118             extras.putBoolean(ContactsContract.DEFERRED_SNIPPETING, true);
   5119             ((AbstractCursor) cursor).setExtras(extras);
   5120         }
   5121         return cursor;
   5122     }
   5123 
   5124     private static final class DirectoryQuery {
   5125         public static final String[] COLUMNS = new String[] {
   5126                 Directory._ID,
   5127                 Directory.DIRECTORY_AUTHORITY,
   5128                 Directory.ACCOUNT_NAME,
   5129                 Directory.ACCOUNT_TYPE
   5130         };
   5131 
   5132         public static final int DIRECTORY_ID = 0;
   5133         public static final int AUTHORITY = 1;
   5134         public static final int ACCOUNT_NAME = 2;
   5135         public static final int ACCOUNT_TYPE = 3;
   5136     }
   5137 
   5138     /**
   5139      * Reads and caches directory information for the database.
   5140      */
   5141     private DirectoryInfo getDirectoryAuthority(String directoryId) {
   5142         synchronized (mDirectoryCache) {
   5143             if (!mDirectoryCacheValid) {
   5144                 mDirectoryCache.clear();
   5145                 SQLiteDatabase db = mDbHelper.get().getReadableDatabase();
   5146                 Cursor cursor = db.query(
   5147                         Tables.DIRECTORIES, DirectoryQuery.COLUMNS, null, null, null, null, null);
   5148                 try {
   5149                     while (cursor.moveToNext()) {
   5150                         DirectoryInfo info = new DirectoryInfo();
   5151                         String id = cursor.getString(DirectoryQuery.DIRECTORY_ID);
   5152                         info.authority = cursor.getString(DirectoryQuery.AUTHORITY);
   5153                         info.accountName = cursor.getString(DirectoryQuery.ACCOUNT_NAME);
   5154                         info.accountType = cursor.getString(DirectoryQuery.ACCOUNT_TYPE);
   5155                         mDirectoryCache.put(id, info);
   5156                     }
   5157                 } finally {
   5158                     cursor.close();
   5159                 }
   5160                 mDirectoryCacheValid = true;
   5161             }
   5162 
   5163             return mDirectoryCache.get(directoryId);
   5164         }
   5165     }
   5166 
   5167     public void resetDirectoryCache() {
   5168         synchronized(mDirectoryCache) {
   5169             mDirectoryCacheValid = false;
   5170         }
   5171     }
   5172 
   5173     protected Cursor queryLocal(final Uri uri, final String[] projection, String selection,
   5174             String[] selectionArgs, String sortOrder, final long directoryId,
   5175             final CancellationSignal cancellationSignal) {
   5176 
   5177         final SQLiteDatabase db = mDbHelper.get().getReadableDatabase();
   5178 
   5179         SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
   5180         String groupBy = null;
   5181         String having = null;
   5182         String limit = getLimit(uri);
   5183         boolean snippetDeferred = false;
   5184 
   5185         // The expression used in bundleLetterCountExtras() to get count.
   5186         String addressBookIndexerCountExpression = null;
   5187 
   5188         final int match = sUriMatcher.match(uri);
   5189         switch (match) {
   5190             case SYNCSTATE:
   5191             case PROFILE_SYNCSTATE:
   5192                 return mDbHelper.get().getSyncState().query(db, projection, selection,
   5193                         selectionArgs, sortOrder);
   5194 
   5195             case CONTACTS: {
   5196                 setTablesAndProjectionMapForContacts(qb, projection);
   5197                 appendLocalDirectoryAndAccountSelectionIfNeeded(qb, directoryId, uri);
   5198                 break;
   5199             }
   5200 
   5201             case CONTACTS_ID: {
   5202                 long contactId = ContentUris.parseId(uri);
   5203                 setTablesAndProjectionMapForContacts(qb, projection);
   5204                 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(contactId));
   5205                 qb.appendWhere(Contacts._ID + "=?");
   5206                 break;
   5207             }
   5208 
   5209             case CONTACTS_LOOKUP:
   5210             case CONTACTS_LOOKUP_ID: {
   5211                 List<String> pathSegments = uri.getPathSegments();
   5212                 int segmentCount = pathSegments.size();
   5213                 if (segmentCount < 3) {
   5214                     throw new IllegalArgumentException(mDbHelper.get().exceptionMessage(
   5215                             "Missing a lookup key", uri));
   5216                 }
   5217 
   5218                 String lookupKey = pathSegments.get(2);
   5219                 if (segmentCount == 4) {
   5220                     long contactId = Long.parseLong(pathSegments.get(3));
   5221                     SQLiteQueryBuilder lookupQb = new SQLiteQueryBuilder();
   5222                     setTablesAndProjectionMapForContacts(lookupQb, projection);
   5223 
   5224                     Cursor c = queryWithContactIdAndLookupKey(lookupQb, db,
   5225                             projection, selection, selectionArgs, sortOrder, groupBy, limit,
   5226                             Contacts._ID, contactId, Contacts.LOOKUP_KEY, lookupKey,
   5227                             cancellationSignal);
   5228                     if (c != null) {
   5229                         return c;
   5230                     }
   5231                 }
   5232 
   5233                 setTablesAndProjectionMapForContacts(qb, projection);
   5234                 selectionArgs = insertSelectionArg(selectionArgs,
   5235                         String.valueOf(lookupContactIdByLookupKey(db, lookupKey)));
   5236                 qb.appendWhere(Contacts._ID + "=?");
   5237                 break;
   5238             }
   5239 
   5240             case CONTACTS_LOOKUP_DATA:
   5241             case CONTACTS_LOOKUP_ID_DATA:
   5242             case CONTACTS_LOOKUP_PHOTO:
   5243             case CONTACTS_LOOKUP_ID_PHOTO: {
   5244                 List<String> pathSegments = uri.getPathSegments();
   5245                 int segmentCount = pathSegments.size();
   5246                 if (segmentCount < 4) {
   5247                     throw new IllegalArgumentException(mDbHelper.get().exceptionMessage(
   5248                             "Missing a lookup key", uri));
   5249                 }
   5250                 String lookupKey = pathSegments.get(2);
   5251                 if (segmentCount == 5) {
   5252                     long contactId = Long.parseLong(pathSegments.get(3));
   5253                     SQLiteQueryBuilder lookupQb = new SQLiteQueryBuilder();
   5254                     setTablesAndProjectionMapForData(lookupQb, uri, projection, false);
   5255                     if (match == CONTACTS_LOOKUP_PHOTO || match == CONTACTS_LOOKUP_ID_PHOTO) {
   5256                         lookupQb.appendWhere(" AND " + Data._ID + "=" + Contacts.PHOTO_ID);
   5257                     }
   5258                     lookupQb.appendWhere(" AND ");
   5259                     Cursor c = queryWithContactIdAndLookupKey(lookupQb, db,
   5260                             projection, selection, selectionArgs, sortOrder, groupBy, limit,
   5261                             Data.CONTACT_ID, contactId, Data.LOOKUP_KEY, lookupKey,
   5262                             cancellationSignal);
   5263                     if (c != null) {
   5264                         return c;
   5265                     }
   5266 
   5267                     // TODO see if the contact exists but has no data rows (rare)
   5268                 }
   5269 
   5270                 setTablesAndProjectionMapForData(qb, uri, projection, false);
   5271                 long contactId = lookupContactIdByLookupKey(db, lookupKey);
   5272                 selectionArgs = insertSelectionArg(selectionArgs,
   5273                         String.valueOf(contactId));
   5274                 if (match == CONTACTS_LOOKUP_PHOTO || match == CONTACTS_LOOKUP_ID_PHOTO) {
   5275                     qb.appendWhere(" AND " + Data._ID + "=" + Contacts.PHOTO_ID);
   5276                 }
   5277                 qb.appendWhere(" AND " + Data.CONTACT_ID + "=?");
   5278                 break;
   5279             }
   5280 
   5281             case CONTACTS_ID_STREAM_ITEMS: {
   5282                 long contactId = Long.parseLong(uri.getPathSegments().get(1));
   5283                 setTablesAndProjectionMapForStreamItems(qb);
   5284                 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(contactId));
   5285                 qb.appendWhere(StreamItems.CONTACT_ID + "=?");
   5286                 break;
   5287             }
   5288 
   5289             case CONTACTS_LOOKUP_STREAM_ITEMS:
   5290             case CONTACTS_LOOKUP_ID_STREAM_ITEMS: {
   5291                 List<String> pathSegments = uri.getPathSegments();
   5292                 int segmentCount = pathSegments.size();
   5293                 if (segmentCount < 4) {
   5294                     throw new IllegalArgumentException(mDbHelper.get().exceptionMessage(
   5295                             "Missing a lookup key", uri));
   5296                 }
   5297                 String lookupKey = pathSegments.get(2);
   5298                 if (segmentCount == 5) {
   5299                     long contactId = Long.parseLong(pathSegments.get(3));
   5300                     SQLiteQueryBuilder lookupQb = new SQLiteQueryBuilder();
   5301                     setTablesAndProjectionMapForStreamItems(lookupQb);
   5302                     Cursor c = queryWithContactIdAndLookupKey(lookupQb, db,
   5303                             projection, selection, selectionArgs, sortOrder, groupBy, limit,
   5304                             StreamItems.CONTACT_ID, contactId,
   5305                             StreamItems.CONTACT_LOOKUP_KEY, lookupKey,
   5306                             cancellationSignal);
   5307                     if (c != null) {
   5308                         return c;
   5309                     }
   5310                 }
   5311 
   5312                 setTablesAndProjectionMapForStreamItems(qb);
   5313                 long contactId = lookupContactIdByLookupKey(db, lookupKey);
   5314                 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(contactId));
   5315                 qb.appendWhere(RawContacts.CONTACT_ID + "=?");
   5316                 break;
   5317             }
   5318 
   5319             case CONTACTS_AS_VCARD: {
   5320                 final String lookupKey = Uri.encode(uri.getPathSegments().get(2));
   5321                 long contactId = lookupContactIdByLookupKey(db, lookupKey);
   5322                 qb.setTables(Views.CONTACTS);
   5323                 qb.setProjectionMap(sContactsVCardProjectionMap);
   5324                 selectionArgs = insertSelectionArg(selectionArgs,
   5325                         String.valueOf(contactId));
   5326                 qb.appendWhere(Contacts._ID + "=?");
   5327                 break;
   5328             }
   5329 
   5330             case CONTACTS_AS_MULTI_VCARD: {
   5331                 SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US);
   5332                 String currentDateString = dateFormat.format(new Date()).toString();
   5333                 return db.rawQuery(
   5334                     "SELECT" +
   5335                     " 'vcards_' || ? || '.vcf' AS " + OpenableColumns.DISPLAY_NAME + "," +
   5336                     " NULL AS " + OpenableColumns.SIZE,
   5337                     new String[] { currentDateString });
   5338             }
   5339 
   5340             case CONTACTS_FILTER: {
   5341                 String filterParam = "";
   5342                 boolean deferredSnipRequested = deferredSnippetingRequested(uri);
   5343                 if (uri.getPathSegments().size() > 2) {
   5344                     filterParam = uri.getLastPathSegment();
   5345                 }
   5346 
   5347                 // If the query consists of a single word, we can do snippetizing after-the-fact for
   5348                 // a performance boost.  Otherwise, we can't defer.
   5349                 snippetDeferred = isSingleWordQuery(filterParam)
   5350                     && deferredSnipRequested && snippetNeeded(projection);
   5351                 setTablesAndProjectionMapForContactsWithSnippet(
   5352                         qb, uri, projection, filterParam, directoryId,
   5353                         snippetDeferred);
   5354                 break;
   5355             }
   5356 
   5357             case CONTACTS_STREQUENT_FILTER:
   5358             case CONTACTS_STREQUENT: {
   5359                 // Basically the resultant SQL should look like this:
   5360                 // (SQL for listing starred items)
   5361                 // UNION ALL
   5362                 // (SQL for listing frequently contacted items)
   5363                 // ORDER BY ...
   5364 
   5365                 final boolean phoneOnly = readBooleanQueryParameter(
   5366                         uri, ContactsContract.STREQUENT_PHONE_ONLY, false);
   5367                 if (match == CONTACTS_STREQUENT_FILTER && uri.getPathSegments().size() > 3) {
   5368                     String filterParam = uri.getLastPathSegment();
   5369                     StringBuilder sb = new StringBuilder();
   5370                     sb.append(Contacts._ID + " IN ");
   5371                     appendContactFilterAsNestedQuery(sb, filterParam);
   5372                     selection = DbQueryUtils.concatenateClauses(selection, sb.toString());
   5373                 }
   5374 
   5375                 String[] subProjection = null;
   5376                 if (projection != null) {
   5377                     subProjection = new String[projection.length + 2];
   5378                     System.arraycopy(projection, 0, subProjection, 0, projection.length);
   5379                     subProjection[projection.length + 0] = DataUsageStatColumns.TIMES_USED;
   5380                     subProjection[projection.length + 1] = DataUsageStatColumns.LAST_TIME_USED;
   5381                 }
   5382 
   5383                 // String that will store the query for starred contacts. For phone only queries,
   5384                 // these will return a list of all phone numbers that belong to starred contacts.
   5385                 final String starredInnerQuery;
   5386                 // String that will store the query for frequents. These JOINS can be very slow
   5387                 // if assembled in the wrong order. Be sure to test changes against huge databases.
   5388                 final String frequentInnerQuery;
   5389 
   5390                 if (phoneOnly) {
   5391                     final StringBuilder tableBuilder = new StringBuilder();
   5392                     // In phone only mode, we need to look at view_data instead of
   5393                     // contacts/raw_contacts to obtain actual phone numbers. One problem is that
   5394                     // view_data is much larger than view_contacts, so our query might become much
   5395                     // slower.
   5396 
   5397                     // For starred phone numbers, we select only phone numbers that belong to
   5398                     // starred contacts, and then do an outer join against the data usage table,
   5399                     // to make sure that even if a starred number hasn't been previously used,
   5400                     // it is included in the list of strequent numbers.
   5401                     tableBuilder.append("(SELECT * FROM " + Views.DATA + " WHERE "
   5402                             + Contacts.STARRED + "=1)" + " AS " + Tables.DATA
   5403                         + " LEFT OUTER JOIN " + Tables.DATA_USAGE_STAT
   5404                             + " ON (" + DataUsageStatColumns.CONCRETE_DATA_ID + "="
   5405                                 + DataColumns.CONCRETE_ID + " AND "
   5406                             + DataUsageStatColumns.CONCRETE_USAGE_TYPE + "="
   5407                                 + DataUsageStatColumns.USAGE_TYPE_INT_CALL + ")");
   5408                     appendContactPresenceJoin(tableBuilder, projection, RawContacts.CONTACT_ID);
   5409                     appendContactStatusUpdateJoin(tableBuilder, projection,
   5410                             ContactsColumns.LAST_STATUS_UPDATE_ID);
   5411                     qb.setTables(tableBuilder.toString());
   5412                     qb.setProjectionMap(sStrequentPhoneOnlyProjectionMap);
   5413                     final long phoneMimeTypeId =
   5414                             mDbHelper.get().getMimeTypeId(Phone.CONTENT_ITEM_TYPE);
   5415                     final long sipMimeTypeId =
   5416                             mDbHelper.get().getMimeTypeId(SipAddress.CONTENT_ITEM_TYPE);
   5417 
   5418                     qb.appendWhere(DbQueryUtils.concatenateClauses(
   5419                             selection,
   5420                                 "(" + Contacts.STARRED + "=1",
   5421                                 DataColumns.MIMETYPE_ID + " IN (" +
   5422                             phoneMimeTypeId + ", " + sipMimeTypeId + ")) AND (" +
   5423                             RawContacts.CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY + ")"));
   5424                     starredInnerQuery = qb.buildQuery(subProjection, null, null,
   5425                         null, Data.IS_SUPER_PRIMARY + " DESC," + SORT_BY_DATA_USAGE, null);
   5426 
   5427                     qb = new SQLiteQueryBuilder();
   5428                     qb.setStrict(true);
   5429                     // Construct the query string for frequent phone numbers
   5430                     tableBuilder.setLength(0);
   5431                     // For frequent phone numbers, we start from data usage table and join
   5432                     // view_data to the table, assuming data usage table is quite smaller than
   5433                     // data rows (almost always it should be), and we don't want any phone
   5434                     // numbers not used by the user. This way sqlite is able to drop a number of
   5435                     // rows in view_data in the early stage of data lookup.
   5436                     tableBuilder.append(Tables.DATA_USAGE_STAT
   5437                             + " INNER JOIN " + Views.DATA + " " + Tables.DATA
   5438                             + " ON (" + DataUsageStatColumns.CONCRETE_DATA_ID + "="
   5439                                 + DataColumns.CONCRETE_ID + " AND "
   5440                             + DataUsageStatColumns.CONCRETE_USAGE_TYPE + "="
   5441                                 + DataUsageStatColumns.USAGE_TYPE_INT_CALL + ")");
   5442                     appendContactPresenceJoin(tableBuilder, projection, RawContacts.CONTACT_ID);
   5443                     appendContactStatusUpdateJoin(tableBuilder, projection,
   5444                             ContactsColumns.LAST_STATUS_UPDATE_ID);
   5445                     qb.setTables(tableBuilder.toString());
   5446                     qb.setProjectionMap(sStrequentPhoneOnlyProjectionMap);
   5447                     qb.appendWhere(DbQueryUtils.concatenateClauses(
   5448                             selection,
   5449                             "(" + Contacts.STARRED + "=0 OR " + Contacts.STARRED + " IS NULL",
   5450                             DataColumns.MIMETYPE_ID + " IN (" +
   5451                             phoneMimeTypeId + ", " + sipMimeTypeId + ")) AND (" +
   5452                             RawContacts.CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY + ")"));
   5453                     frequentInnerQuery = qb.buildQuery(subProjection, null, null, null,
   5454                             SORT_BY_DATA_USAGE, "25");
   5455 
   5456                 } else {
   5457                     // Build the first query for starred contacts
   5458                     qb.setStrict(true);
   5459                     setTablesAndProjectionMapForContacts(qb, projection, false);
   5460                     qb.setProjectionMap(sStrequentStarredProjectionMap);
   5461 
   5462                     starredInnerQuery = qb.buildQuery(subProjection,
   5463                             DbQueryUtils.concatenateClauses(selection, Contacts.STARRED + "=1"),
   5464                             Contacts._ID, null, Contacts.DISPLAY_NAME + " COLLATE LOCALIZED ASC",
   5465                             null);
   5466 
   5467                     // Reset the builder, and build the second query for frequents contacts
   5468                     qb = new SQLiteQueryBuilder();
   5469                     qb.setStrict(true);
   5470 
   5471                     setTablesAndProjectionMapForContacts(qb, projection, true);
   5472                     qb.setProjectionMap(sStrequentFrequentProjectionMap);
   5473                     qb.appendWhere(DbQueryUtils.concatenateClauses(
   5474                             selection,
   5475                             "(" + Contacts.STARRED + " =0 OR " + Contacts.STARRED + " IS NULL)"));
   5476                     // Note frequentInnerQuery is a grouping query, so the "IN default_directory"
   5477                     // selection needs to be in HAVING, not in WHERE.
   5478                     final String HAVING =
   5479                             RawContacts.CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY;
   5480                     frequentInnerQuery = qb.buildQuery(subProjection,
   5481                             null, Contacts._ID, HAVING, SORT_BY_DATA_USAGE, "25");
   5482                 }
   5483 
   5484                 // We need to wrap the inner queries in an extra select, because they contain
   5485                 // their own SORT and LIMIT
   5486 
   5487                 // Phone numbers that were used more than 30 days ago are dropped from frequents
   5488                 final String frequentQuery = "SELECT * FROM (" + frequentInnerQuery + ") WHERE " +
   5489                         TIME_SINCE_LAST_USED_SEC + "<" + LAST_TIME_USED_30_DAYS_SEC;
   5490                 final String starredQuery = "SELECT * FROM (" + starredInnerQuery + ")";
   5491 
   5492                 // Put them together
   5493                 final String unionQuery =
   5494                         qb.buildUnionQuery(new String[] {starredQuery, frequentQuery}, null, null);
   5495 
   5496                 // Here, we need to use selection / selectionArgs (supplied from users) "twice",
   5497                 // as we want them both for starred items and for frequently contacted items.
   5498                 //
   5499                 // e.g. if the user specify selection = "starred =?" and selectionArgs = "0",
   5500                 // the resultant SQL should be like:
   5501                 // SELECT ... WHERE starred =? AND ...
   5502                 // UNION ALL
   5503                 // SELECT ... WHERE starred =? AND ...
   5504                 String[] doubledSelectionArgs = null;
   5505                 if (selectionArgs != null) {
   5506                     final int length = selectionArgs.length;
   5507                     doubledSelectionArgs = new String[length * 2];
   5508                     System.arraycopy(selectionArgs, 0, doubledSelectionArgs, 0, length);
   5509                     System.arraycopy(selectionArgs, 0, doubledSelectionArgs, length, length);
   5510                 }
   5511 
   5512                 Cursor cursor = db.rawQuery(unionQuery, doubledSelectionArgs);
   5513                 if (cursor != null) {
   5514                     cursor.setNotificationUri(
   5515                             getContext().getContentResolver(), ContactsContract.AUTHORITY_URI);
   5516                 }
   5517                 return cursor;
   5518             }
   5519 
   5520             case CONTACTS_FREQUENT: {
   5521                 setTablesAndProjectionMapForContacts(qb, projection, true);
   5522                 qb.setProjectionMap(sStrequentFrequentProjectionMap);
   5523                 groupBy = Contacts._ID;
   5524                 having = Contacts._ID + " IN " + Tables.DEFAULT_DIRECTORY;
   5525                 if (!TextUtils.isEmpty(sortOrder)) {
   5526                     sortOrder = FREQUENT_ORDER_BY + ", " + sortOrder;
   5527                 } else {
   5528                     sortOrder = FREQUENT_ORDER_BY;
   5529                 }
   5530                 break;
   5531             }
   5532 
   5533             case CONTACTS_GROUP: {
   5534                 setTablesAndProjectionMapForContacts(qb, projection);
   5535                 if (uri.getPathSegments().size() > 2) {
   5536                     qb.appendWhere(CONTACTS_IN_GROUP_SELECT);
   5537                     String groupMimeTypeId = String.valueOf(
   5538                             mDbHelper.get().getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE));
   5539                     selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
   5540                     selectionArgs = insertSelectionArg(selectionArgs, groupMimeTypeId);
   5541                 }
   5542                 break;
   5543             }
   5544 
   5545             case PROFILE: {
   5546                 setTablesAndProjectionMapForContacts(qb, projection);
   5547                 break;
   5548             }
   5549 
   5550             case PROFILE_ENTITIES: {
   5551                 setTablesAndProjectionMapForEntities(qb, uri, projection);
   5552                 break;
   5553             }
   5554 
   5555             case PROFILE_AS_VCARD: {
   5556                 qb.setTables(Views.CONTACTS);
   5557                 qb.setProjectionMap(sContactsVCardProjectionMap);
   5558                 break;
   5559             }
   5560 
   5561             case CONTACTS_ID_DATA: {
   5562                 long contactId = Long.parseLong(uri.getPathSegments().get(1));
   5563                 setTablesAndProjectionMapForData(qb, uri, projection, false);
   5564                 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(contactId));
   5565                 qb.appendWhere(" AND " + RawContacts.CONTACT_ID + "=?");
   5566                 break;
   5567             }
   5568 
   5569             case CONTACTS_ID_PHOTO: {
   5570                 long contactId = Long.parseLong(uri.getPathSegments().get(1));
   5571                 setTablesAndProjectionMapForData(qb, uri, projection, false);
   5572                 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(contactId));
   5573                 qb.appendWhere(" AND " + RawContacts.CONTACT_ID + "=?");
   5574                 qb.appendWhere(" AND " + Data._ID + "=" + Contacts.PHOTO_ID);
   5575                 break;
   5576             }
   5577 
   5578             case CONTACTS_ID_ENTITIES: {
   5579                 long contactId = Long.parseLong(uri.getPathSegments().get(1));
   5580                 setTablesAndProjectionMapForEntities(qb, uri, projection);
   5581                 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(contactId));
   5582                 qb.appendWhere(" AND " + RawContacts.CONTACT_ID + "=?");
   5583                 break;
   5584             }
   5585 
   5586             case CONTACTS_LOOKUP_ENTITIES:
   5587             case CONTACTS_LOOKUP_ID_ENTITIES: {
   5588                 List<String> pathSegments = uri.getPathSegments();
   5589                 int segmentCount = pathSegments.size();
   5590                 if (segmentCount < 4) {
   5591                     throw new IllegalArgumentException(mDbHelper.get().exceptionMessage(
   5592                             "Missing a lookup key", uri));
   5593                 }
   5594                 String lookupKey = pathSegments.get(2);
   5595                 if (segmentCount == 5) {
   5596                     long contactId = Long.parseLong(pathSegments.get(3));
   5597                     SQLiteQueryBuilder lookupQb = new SQLiteQueryBuilder();
   5598                     setTablesAndProjectionMapForEntities(lookupQb, uri, projection);
   5599                     lookupQb.appendWhere(" AND ");
   5600 
   5601                     Cursor c = queryWithContactIdAndLookupKey(lookupQb, db,
   5602                             projection, selection, selectionArgs, sortOrder, groupBy, limit,
   5603                             Contacts.Entity.CONTACT_ID, contactId,
   5604                             Contacts.Entity.LOOKUP_KEY, lookupKey,
   5605                             cancellationSignal);
   5606                     if (c != null) {
   5607                         return c;
   5608                     }
   5609                 }
   5610 
   5611                 setTablesAndProjectionMapForEntities(qb, uri, projection);
   5612                 selectionArgs = insertSelectionArg(
   5613                         selectionArgs, String.valueOf(lookupContactIdByLookupKey(db, lookupKey)));
   5614                 qb.appendWhere(" AND " + Contacts.Entity.CONTACT_ID + "=?");
   5615                 break;
   5616             }
   5617 
   5618             case STREAM_ITEMS: {
   5619                 setTablesAndProjectionMapForStreamItems(qb);
   5620                 break;
   5621             }
   5622 
   5623             case STREAM_ITEMS_ID: {
   5624                 setTablesAndProjectionMapForStreamItems(qb);
   5625                 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
   5626                 qb.appendWhere(StreamItems._ID + "=?");
   5627                 break;
   5628             }
   5629 
   5630             case STREAM_ITEMS_LIMIT: {
   5631                 return buildSingleRowResult(projection, new String[] {StreamItems.MAX_ITEMS},
   5632                         new Object[] {MAX_STREAM_ITEMS_PER_RAW_CONTACT});
   5633             }
   5634 
   5635             case STREAM_ITEMS_PHOTOS: {
   5636                 setTablesAndProjectionMapForStreamItemPhotos(qb);
   5637                 break;
   5638             }
   5639 
   5640             case STREAM_ITEMS_ID_PHOTOS: {
   5641                 setTablesAndProjectionMapForStreamItemPhotos(qb);
   5642                 String streamItemId = uri.getPathSegments().get(1);
   5643                 selectionArgs = insertSelectionArg(selectionArgs, streamItemId);
   5644                 qb.appendWhere(StreamItemPhotosColumns.CONCRETE_STREAM_ITEM_ID + "=?");
   5645                 break;
   5646             }
   5647 
   5648             case STREAM_ITEMS_ID_PHOTOS_ID: {
   5649                 setTablesAndProjectionMapForStreamItemPhotos(qb);
   5650                 String streamItemId = uri.getPathSegments().get(1);
   5651                 String streamItemPhotoId = uri.getPathSegments().get(3);
   5652                 selectionArgs = insertSelectionArg(selectionArgs, streamItemPhotoId);
   5653                 selectionArgs = insertSelectionArg(selectionArgs, streamItemId);
   5654                 qb.appendWhere(StreamItemPhotosColumns.CONCRETE_STREAM_ITEM_ID + "=? AND " +
   5655                         StreamItemPhotosColumns.CONCRETE_ID + "=?");
   5656                 break;
   5657             }
   5658 
   5659             case PHOTO_DIMENSIONS: {
   5660                 return buildSingleRowResult(projection,
   5661                         new String[] {DisplayPhoto.DISPLAY_MAX_DIM, DisplayPhoto.THUMBNAIL_MAX_DIM},
   5662                         new Object[] {getMaxDisplayPhotoDim(), getMaxThumbnailDim()});
   5663             }
   5664 
   5665             case PHONES:
   5666             case CALLABLES: {
   5667                 final String mimeTypeIsPhoneExpression =
   5668                         DataColumns.MIMETYPE_ID + "=" + mDbHelper.get().getMimeTypeIdForPhone();
   5669                 final String mimeTypeIsSipExpression =
   5670                         DataColumns.MIMETYPE_ID + "=" + mDbHelper.get().getMimeTypeIdForSip();
   5671                 setTablesAndProjectionMapForData(qb, uri, projection, false);
   5672                 if (match == CALLABLES) {
   5673                     qb.appendWhere(" AND ((" + mimeTypeIsPhoneExpression +
   5674                             ") OR (" + mimeTypeIsSipExpression + "))");
   5675                 } else {
   5676                     qb.appendWhere(" AND " + mimeTypeIsPhoneExpression);
   5677                 }
   5678 
   5679                 final boolean removeDuplicates = readBooleanQueryParameter(
   5680                         uri, ContactsContract.REMOVE_DUPLICATE_ENTRIES, false);
   5681                 if (removeDuplicates) {
   5682                     groupBy = RawContacts.CONTACT_ID + ", " + Data.DATA1;
   5683 
   5684                     // In this case, because we dedupe phone numbers, the address book indexer needs
   5685                     // to take it into account too.  (Otherwise headers will appear in wrong
   5686                     // positions.)
   5687                     // So use count(distinct pair(CONTACT_ID, PHONE NUMBER)) instead of count(*).
   5688                     // But because there's no such thing as pair() on sqlite, we use
   5689                     // CONTACT_ID || ',' || PHONE NUMBER instead.
   5690                     // This only slows down the query by 14% with 10,000 contacts.
   5691                     addressBookIndexerCountExpression = "DISTINCT "
   5692                             + RawContacts.CONTACT_ID + "||','||" + Data.DATA1;
   5693                 }
   5694                 break;
   5695             }
   5696 
   5697             case PHONES_ID:
   5698             case CALLABLES_ID: {
   5699                 final String mimeTypeIsPhoneExpression =
   5700                         DataColumns.MIMETYPE_ID + "=" + mDbHelper.get().getMimeTypeIdForPhone();
   5701                 final String mimeTypeIsSipExpression =
   5702                         DataColumns.MIMETYPE_ID + "=" + mDbHelper.get().getMimeTypeIdForSip();
   5703                 setTablesAndProjectionMapForData(qb, uri, projection, false);
   5704                 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
   5705                 if (match == CALLABLES_ID) {
   5706                     qb.appendWhere(" AND ((" + mimeTypeIsPhoneExpression +
   5707                             ") OR (" + mimeTypeIsSipExpression + "))");
   5708                 } else {
   5709                     qb.appendWhere(" AND " + mimeTypeIsPhoneExpression);
   5710                 }
   5711                 qb.appendWhere(" AND " + Data._ID + "=?");
   5712                 break;
   5713             }
   5714 
   5715             case PHONES_FILTER:
   5716             case CALLABLES_FILTER: {
   5717                 final String mimeTypeIsPhoneExpression =
   5718                         DataColumns.MIMETYPE_ID + "=" + mDbHelper.get().getMimeTypeIdForPhone();
   5719                 final String mimeTypeIsSipExpression =
   5720                         DataColumns.MIMETYPE_ID + "=" + mDbHelper.get().getMimeTypeIdForSip();
   5721 
   5722                 String typeParam = uri.getQueryParameter(DataUsageFeedback.USAGE_TYPE);
   5723                 final int typeInt = getDataUsageFeedbackType(typeParam,
   5724                         DataUsageStatColumns.USAGE_TYPE_INT_CALL);
   5725                 setTablesAndProjectionMapForData(qb, uri, projection, true, typeInt);
   5726                 if (match == CALLABLES_FILTER) {
   5727                     qb.appendWhere(" AND ((" + mimeTypeIsPhoneExpression +
   5728                             ") OR (" + mimeTypeIsSipExpression + "))");
   5729                 } else {
   5730                     qb.appendWhere(" AND " + mimeTypeIsPhoneExpression);
   5731                 }
   5732 
   5733                 if (uri.getPathSegments().size() > 2) {
   5734                     final String filterParam = uri.getLastPathSegment();
   5735                     final boolean searchDisplayName = uri.getBooleanQueryParameter(
   5736                             Phone.SEARCH_DISPLAY_NAME_KEY, true);
   5737                     final boolean searchPhoneNumber = uri.getBooleanQueryParameter(
   5738                             Phone.SEARCH_PHONE_NUMBER_KEY, true);
   5739 
   5740                     final StringBuilder sb = new StringBuilder();
   5741                     sb.append(" AND (");
   5742 
   5743                     boolean hasCondition = false;
   5744                     // This searches the name, nickname and organization fields.
   5745                     final String ftsMatchQuery =
   5746                             searchDisplayName
   5747                             ? SearchIndexManager.getFtsMatchQuery(filterParam,
   5748                                     FtsQueryBuilder.UNSCOPED_NORMALIZING)
   5749                             : null;
   5750                     if (!TextUtils.isEmpty(ftsMatchQuery)) {
   5751                         sb.append(Data.RAW_CONTACT_ID + " IN " +
   5752                                 "(SELECT " + RawContactsColumns.CONCRETE_ID +
   5753                                 " FROM " + Tables.SEARCH_INDEX +
   5754                                 " JOIN " + Tables.RAW_CONTACTS +
   5755                                 " ON (" + Tables.SEARCH_INDEX + "." + SearchIndexColumns.CONTACT_ID
   5756                                         + "=" + RawContactsColumns.CONCRETE_CONTACT_ID + ")" +
   5757                                 " WHERE " + SearchIndexColumns.NAME + " MATCH '");
   5758                         sb.append(ftsMatchQuery);
   5759                         sb.append("')");
   5760                         hasCondition = true;
   5761                     }
   5762 
   5763                     if (searchPhoneNumber) {
   5764                         final String number = PhoneNumberUtils.normalizeNumber(filterParam);
   5765                         if (!TextUtils.isEmpty(number)) {
   5766                             if (hasCondition) {
   5767                                 sb.append(" OR ");
   5768                             }
   5769                             sb.append(Data._ID +
   5770                                     " IN (SELECT DISTINCT " + PhoneLookupColumns.DATA_ID
   5771                                     + " FROM " + Tables.PHONE_LOOKUP
   5772                                     + " WHERE " + PhoneLookupColumns.NORMALIZED_NUMBER + " LIKE '");
   5773                             sb.append(number);
   5774                             sb.append("%')");
   5775                             hasCondition = true;
   5776                         }
   5777 
   5778                         if (!TextUtils.isEmpty(filterParam) && match == CALLABLES_FILTER) {
   5779                             // If the request is via Callable URI, Sip addresses matching the filter
   5780                             // parameter should be returned.
   5781                             if (hasCondition) {
   5782                                 sb.append(" OR ");
   5783                             }
   5784                             sb.append("(");
   5785                             sb.append(mimeTypeIsSipExpression);
   5786                             sb.append(" AND ((" + Data.DATA1 + " LIKE ");
   5787                             DatabaseUtils.appendEscapedSQLString(sb, filterParam + '%');
   5788                             sb.append(") OR (" + Data.DATA1 + " LIKE ");
   5789                             // Users may want SIP URIs starting from "sip:"
   5790                             DatabaseUtils.appendEscapedSQLString(sb, "sip:"+ filterParam + '%');
   5791                             sb.append(")))");
   5792                             hasCondition = true;
   5793                         }
   5794                     }
   5795 
   5796                     if (!hasCondition) {
   5797                         // If it is neither a phone number nor a name, the query should return
   5798                         // an empty cursor.  Let's ensure that.
   5799                         sb.append("0");
   5800                     }
   5801                     sb.append(")");
   5802                     qb.appendWhere(sb);
   5803                 }
   5804                 if (match == CALLABLES_FILTER) {
   5805                     // If the row is for a phone number that has a normalized form, we should use
   5806                     // the normalized one as PHONES_FILTER does, while we shouldn't do that
   5807                     // if the row is for a sip address.
   5808                     String isPhoneAndHasNormalized = "("
   5809                         + mimeTypeIsPhoneExpression + " AND "
   5810                         + Phone.NORMALIZED_NUMBER + " IS NOT NULL)";
   5811                     groupBy = "(CASE WHEN " + isPhoneAndHasNormalized
   5812                         + " THEN " + Phone.NORMALIZED_NUMBER
   5813                         + " ELSE " + Phone.NUMBER + " END), " + RawContacts.CONTACT_ID;
   5814                 } else {
   5815                     groupBy = "(CASE WHEN " + Phone.NORMALIZED_NUMBER
   5816                         + " IS NOT NULL THEN " + Phone.NORMALIZED_NUMBER
   5817                         + " ELSE " + Phone.NUMBER + " END), " + RawContacts.CONTACT_ID;
   5818                 }
   5819                 if (sortOrder == null) {
   5820                     final String accountPromotionSortOrder = getAccountPromotionSortOrder(uri);
   5821                     if (!TextUtils.isEmpty(accountPromotionSortOrder)) {
   5822                         sortOrder = accountPromotionSortOrder + ", " + PHONE_FILTER_SORT_ORDER;
   5823                     } else {
   5824                         sortOrder = PHONE_FILTER_SORT_ORDER;
   5825                     }
   5826                 }
   5827                 break;
   5828             }
   5829 
   5830             case EMAILS: {
   5831                 setTablesAndProjectionMapForData(qb, uri, projection, false);
   5832                 qb.appendWhere(" AND " + DataColumns.MIMETYPE_ID + " = "
   5833                         + mDbHelper.get().getMimeTypeIdForEmail());
   5834 
   5835                 final boolean removeDuplicates = readBooleanQueryParameter(
   5836                         uri, ContactsContract.REMOVE_DUPLICATE_ENTRIES, false);
   5837                 if (removeDuplicates) {
   5838                     groupBy = RawContacts.CONTACT_ID + ", " + Data.DATA1;
   5839 
   5840                     // See PHONES for more detail.
   5841                     addressBookIndexerCountExpression = "DISTINCT "
   5842                             + RawContacts.CONTACT_ID + "||','||" + Data.DATA1;
   5843                 }
   5844                 break;
   5845             }
   5846 
   5847             case EMAILS_ID: {
   5848                 setTablesAndProjectionMapForData(qb, uri, projection, false);
   5849                 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
   5850                 qb.appendWhere(" AND " + DataColumns.MIMETYPE_ID + " = "
   5851                         + mDbHelper.get().getMimeTypeIdForEmail()
   5852                         + " AND " + Data._ID + "=?");
   5853                 break;
   5854             }
   5855 
   5856             case EMAILS_LOOKUP: {
   5857                 setTablesAndProjectionMapForData(qb, uri, projection, false);
   5858                 qb.appendWhere(" AND " + DataColumns.MIMETYPE_ID + " = "
   5859                         + mDbHelper.get().getMimeTypeIdForEmail());
   5860                 if (uri.getPathSegments().size() > 2) {
   5861                     String email = uri.getLastPathSegment();
   5862                     String address = mDbHelper.get().extractAddressFromEmailAddress(email);
   5863                     selectionArgs = insertSelectionArg(selectionArgs, address);
   5864                     qb.appendWhere(" AND UPPER(" + Email.DATA + ")=UPPER(?)");
   5865                 }
   5866                 // unless told otherwise, we'll return visible before invisible contacts
   5867                 if (sortOrder == null) {
   5868                     sortOrder = "(" + RawContacts.CONTACT_ID + " IN " +
   5869                             Tables.DEFAULT_DIRECTORY + ") DESC";
   5870                 }
   5871                 break;
   5872             }
   5873 
   5874             case EMAILS_FILTER: {
   5875                 String typeParam = uri.getQueryParameter(DataUsageFeedback.USAGE_TYPE);
   5876                 final int typeInt = getDataUsageFeedbackType(typeParam,
   5877                         DataUsageStatColumns.USAGE_TYPE_INT_LONG_TEXT);
   5878                 setTablesAndProjectionMapForData(qb, uri, projection, true, typeInt);
   5879                 String filterParam = null;
   5880 
   5881                 if (uri.getPathSegments().size() > 3) {
   5882                     filterParam = uri.getLastPathSegment();
   5883                     if (TextUtils.isEmpty(filterParam)) {
   5884                         filterParam = null;
   5885                     }
   5886                 }
   5887 
   5888                 if (filterParam == null) {
   5889                     // If the filter is unspecified, return nothing
   5890                     qb.appendWhere(" AND 0");
   5891                 } else {
   5892                     StringBuilder sb = new StringBuilder();
   5893                     sb.append(" AND " + Data._ID + " IN (");
   5894                     sb.append(
   5895                             "SELECT " + Data._ID +
   5896                             " FROM " + Tables.DATA +
   5897                             " WHERE " + DataColumns.MIMETYPE_ID + "=");
   5898                     sb.append(mDbHelper.get().getMimeTypeIdForEmail());
   5899                     sb.append(" AND " + Data.DATA1 + " LIKE ");
   5900                     DatabaseUtils.appendEscapedSQLString(sb, filterParam + '%');
   5901                     if (!filterParam.contains("@")) {
   5902                         sb.append(
   5903                                 " UNION SELECT " + Data._ID +
   5904                                 " FROM " + Tables.DATA +
   5905                                 " WHERE +" + DataColumns.MIMETYPE_ID + "=");
   5906                         sb.append(mDbHelper.get().getMimeTypeIdForEmail());
   5907                         sb.append(" AND " + Data.RAW_CONTACT_ID + " IN " +
   5908                                 "(SELECT " + RawContactsColumns.CONCRETE_ID +
   5909                                 " FROM " + Tables.SEARCH_INDEX +
   5910                                 " JOIN " + Tables.RAW_CONTACTS +
   5911                                 " ON (" + Tables.SEARCH_INDEX + "." + SearchIndexColumns.CONTACT_ID
   5912                                         + "=" + RawContactsColumns.CONCRETE_CONTACT_ID + ")" +
   5913                                 " WHERE " + SearchIndexColumns.NAME + " MATCH '");
   5914                         final String ftsMatchQuery = SearchIndexManager.getFtsMatchQuery(
   5915                                 filterParam, FtsQueryBuilder.UNSCOPED_NORMALIZING);
   5916                         sb.append(ftsMatchQuery);
   5917                         sb.append("')");
   5918                     }
   5919                     sb.append(")");
   5920                     qb.appendWhere(sb);
   5921                 }
   5922 
   5923                 // Group by a unique email address on a per account basis, to make sure that
   5924                 // account promotion sort order correctly ranks email addresses that are in
   5925                 // multiple accounts
   5926                 groupBy = Email.DATA + "," + RawContacts.CONTACT_ID + "," +
   5927                         RawContacts.ACCOUNT_NAME + "," + RawContacts.ACCOUNT_TYPE;
   5928                 if (sortOrder == null) {
   5929                     final String accountPromotionSortOrder = getAccountPromotionSortOrder(uri);
   5930                     if (!TextUtils.isEmpty(accountPromotionSortOrder)) {
   5931                         sortOrder = accountPromotionSortOrder + ", " + EMAIL_FILTER_SORT_ORDER;
   5932                     } else {
   5933                         sortOrder = EMAIL_FILTER_SORT_ORDER;
   5934                     }
   5935 
   5936                     final String primaryAccountName =
   5937                             uri.getQueryParameter(ContactsContract.PRIMARY_ACCOUNT_NAME);
   5938                     if (!TextUtils.isEmpty(primaryAccountName)) {
   5939                         final int index = primaryAccountName.indexOf('@');
   5940                         if (index != -1) {
   5941                             // Purposely include '@' in matching.
   5942                             final String domain = primaryAccountName.substring(index);
   5943                             final char escapeChar = '\\';
   5944 
   5945                             final StringBuilder likeValue = new StringBuilder();
   5946                             likeValue.append('%');
   5947                             DbQueryUtils.escapeLikeValue(likeValue, domain, escapeChar);
   5948                             selectionArgs = appendSelectionArg(selectionArgs, likeValue.toString());
   5949 
   5950                             // similar email domains is the last sort preference.
   5951                             sortOrder += ", (CASE WHEN " + Data.DATA1 + " like ? ESCAPE '" +
   5952                                     escapeChar + "' THEN 0 ELSE 1 END)";
   5953                         }
   5954                     }
   5955                 }
   5956                 break;
   5957             }
   5958 
   5959             case CONTACTABLES:
   5960             case CONTACTABLES_FILTER: {
   5961                 setTablesAndProjectionMapForData(qb, uri, projection, false);
   5962 
   5963                 String filterParam = null;
   5964 
   5965                 final int uriPathSize = uri.getPathSegments().size();
   5966                 if (uriPathSize > 3) {
   5967                     filterParam = uri.getLastPathSegment();
   5968                     if (TextUtils.isEmpty(filterParam)) {
   5969                         filterParam = null;
   5970                     }
   5971                 }
   5972 
   5973                 // CONTACTABLES_FILTER but no query provided, return an empty cursor
   5974                 if (uriPathSize > 2 && filterParam == null) {
   5975                     qb.appendWhere(" AND 0");
   5976                     break;
   5977                 }
   5978 
   5979                 if (uri.getBooleanQueryParameter(Contactables.VISIBLE_CONTACTS_ONLY, false)) {
   5980                     qb.appendWhere(" AND " + Data.CONTACT_ID + " in " +
   5981                             Tables.DEFAULT_DIRECTORY);
   5982                     }
   5983 
   5984                 final StringBuilder sb = new StringBuilder();
   5985 
   5986                 // we only want data items that are either email addresses or phone numbers
   5987                 sb.append(" AND (");
   5988                 sb.append(DataColumns.MIMETYPE_ID + " IN (");
   5989                 sb.append(mDbHelper.get().getMimeTypeIdForEmail());
   5990                 sb.append(",");
   5991                 sb.append(mDbHelper.get().getMimeTypeIdForPhone());
   5992                 sb.append("))");
   5993 
   5994                 // Rest of the query is only relevant if we are handling CONTACTABLES_FILTER
   5995                 if (uriPathSize < 3) {
   5996                     qb.appendWhere(sb);
   5997                     break;
   5998                 }
   5999 
   6000                 // but we want all the email addresses and phone numbers that belong to
   6001                 // all contacts that have any data items (or name) that match the query
   6002                 sb.append(" AND ");
   6003                 sb.append("(" + Data.CONTACT_ID + " IN (");
   6004 
   6005                 // All contacts where the email address data1 column matches the query
   6006                 sb.append(
   6007                         "SELECT " + RawContacts.CONTACT_ID +
   6008                         " FROM " + Tables.DATA + " JOIN " + Tables.RAW_CONTACTS +
   6009                         " ON " + Tables.DATA + "." + Data.RAW_CONTACT_ID + "=" +
   6010                         Tables.RAW_CONTACTS + "." + RawContacts._ID +
   6011                         " WHERE (" + DataColumns.MIMETYPE_ID + "=");
   6012                 sb.append(mDbHelper.get().getMimeTypeIdForEmail());
   6013 
   6014                 sb.append(" AND " + Data.DATA1 + " LIKE ");
   6015                 DatabaseUtils.appendEscapedSQLString(sb, filterParam + '%');
   6016                 sb.append(")");
   6017 
   6018                 // All contacts where the phone number matches the query (determined by checking
   6019                 // Tables.PHONE_LOOKUP
   6020                 final String number = PhoneNumberUtils.normalizeNumber(filterParam);
   6021                 if (!TextUtils.isEmpty(number)) {
   6022                     sb.append("UNION SELECT DISTINCT " + RawContacts.CONTACT_ID +
   6023                             " FROM " + Tables.PHONE_LOOKUP + " JOIN " + Tables.RAW_CONTACTS +
   6024                             " ON (" + Tables.PHONE_LOOKUP + "." +
   6025                             PhoneLookupColumns.RAW_CONTACT_ID + "=" +
   6026                             Tables.RAW_CONTACTS + "." + RawContacts._ID + ")" +
   6027                             " WHERE " + PhoneLookupColumns.NORMALIZED_NUMBER + " LIKE '");
   6028                     sb.append(number);
   6029                     sb.append("%'");
   6030                 }
   6031 
   6032                 // All contacts where the name matches the query (determined by checking
   6033                 // Tables.SEARCH_INDEX
   6034                 sb.append(
   6035                         " UNION SELECT " + Data.CONTACT_ID +
   6036                         " FROM " + Tables.DATA + " JOIN " + Tables.RAW_CONTACTS +
   6037                         " ON " + Tables.DATA + "." + Data.RAW_CONTACT_ID + "=" +
   6038                         Tables.RAW_CONTACTS + "." + RawContacts._ID +
   6039 
   6040                         " WHERE " + Data.RAW_CONTACT_ID + " IN " +
   6041 
   6042                         "(SELECT " + RawContactsColumns.CONCRETE_ID +
   6043                         " FROM " + Tables.SEARCH_INDEX +
   6044                         " JOIN " + Tables.RAW_CONTACTS +
   6045                         " ON (" + Tables.SEARCH_INDEX + "." + SearchIndexColumns.CONTACT_ID
   6046                         + "=" + RawContactsColumns.CONCRETE_CONTACT_ID + ")" +
   6047 
   6048                         " WHERE " + SearchIndexColumns.NAME + " MATCH '");
   6049 
   6050                 final String ftsMatchQuery = SearchIndexManager.getFtsMatchQuery(
   6051                         filterParam, FtsQueryBuilder.UNSCOPED_NORMALIZING);
   6052                 sb.append(ftsMatchQuery);
   6053                 sb.append("')");
   6054 
   6055                 sb.append("))");
   6056                 qb.appendWhere(sb);
   6057 
   6058                 break;
   6059             }
   6060 
   6061             case POSTALS: {
   6062                 setTablesAndProjectionMapForData(qb, uri, projection, false);
   6063                 qb.appendWhere(" AND " + DataColumns.MIMETYPE_ID + " = "
   6064                         + mDbHelper.get().getMimeTypeIdForStructuredPostal());
   6065 
   6066                 final boolean removeDuplicates = readBooleanQueryParameter(
   6067                         uri, ContactsContract.REMOVE_DUPLICATE_ENTRIES, false);
   6068                 if (removeDuplicates) {
   6069                     groupBy = RawContacts.CONTACT_ID + ", " + Data.DATA1;
   6070 
   6071                     // See PHONES for more detail.
   6072                     addressBookIndexerCountExpression = "DISTINCT "
   6073                             + RawContacts.CONTACT_ID + "||','||" + Data.DATA1;
   6074                 }
   6075                 break;
   6076             }
   6077 
   6078             case POSTALS_ID: {
   6079                 setTablesAndProjectionMapForData(qb, uri, projection, false);
   6080                 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
   6081                 qb.appendWhere(" AND " + DataColumns.MIMETYPE_ID + " = "
   6082                         + mDbHelper.get().getMimeTypeIdForStructuredPostal());
   6083                 qb.appendWhere(" AND " + Data._ID + "=?");
   6084                 break;
   6085             }
   6086 
   6087             case RAW_CONTACTS:
   6088             case PROFILE_RAW_CONTACTS: {
   6089                 setTablesAndProjectionMapForRawContacts(qb, uri);
   6090                 break;
   6091             }
   6092 
   6093             case RAW_CONTACTS_ID:
   6094             case PROFILE_RAW_CONTACTS_ID: {
   6095                 long rawContactId = ContentUris.parseId(uri);
   6096                 setTablesAndProjectionMapForRawContacts(qb, uri);
   6097                 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId));
   6098                 qb.appendWhere(" AND " + RawContacts._ID + "=?");
   6099                 break;
   6100             }
   6101 
   6102             case RAW_CONTACTS_ID_DATA:
   6103             case PROFILE_RAW_CONTACTS_ID_DATA: {
   6104                 int segment = match == RAW_CONTACTS_ID_DATA ? 1 : 2;
   6105                 long rawContactId = Long.parseLong(uri.getPathSegments().get(segment));
   6106                 setTablesAndProjectionMapForData(qb, uri, projection, false);
   6107                 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId));
   6108                 qb.appendWhere(" AND " + Data.RAW_CONTACT_ID + "=?");
   6109                 break;
   6110             }
   6111 
   6112             case RAW_CONTACTS_ID_STREAM_ITEMS: {
   6113                 long rawContactId = Long.parseLong(uri.getPathSegments().get(1));
   6114                 setTablesAndProjectionMapForStreamItems(qb);
   6115                 selectionArgs = insertSelectionArg(