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