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