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