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