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