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                     // Peek at the results of the first query (which attempts to use fully
   5775                     // normalized and internationalized numbers for comparison).  If no results
   5776                     // were returned, fall back to doing a match of the trailing 7 digits.
   5777                     qb.setStrict(true);
   5778                     boolean foundResult = false;
   5779                     Cursor cursor = query(mActiveDb.get(), qb, projection, selection, selectionArgs,
   5780                             sortOrder, groupBy, limit);
   5781                     try {
   5782                         if (cursor.getCount() > 0) {
   5783                             foundResult = true;
   5784                             return cursor;
   5785                         } else {
   5786                             qb = new SQLiteQueryBuilder();
   5787                             mDbHelper.get().buildMinimalPhoneLookupAndContactQuery(
   5788                                     qb, normalizedNumber);
   5789                             qb.setProjectionMap(sPhoneLookupProjectionMap);
   5790                         }
   5791                     } finally {
   5792                         if (!foundResult) {
   5793                             // We'll be returning a different cursor, so close this one.
   5794                             cursor.close();
   5795                         }
   5796                     }
   5797                 }
   5798                 break;
   5799             }
   5800 
   5801             case GROUPS: {
   5802                 qb.setTables(Views.GROUPS);
   5803                 qb.setProjectionMap(sGroupsProjectionMap);
   5804                 appendAccountFromParameter(qb, uri);
   5805                 break;
   5806             }
   5807 
   5808             case GROUPS_ID: {
   5809                 qb.setTables(Views.GROUPS);
   5810                 qb.setProjectionMap(sGroupsProjectionMap);
   5811                 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
   5812                 qb.appendWhere(Groups._ID + "=?");
   5813                 break;
   5814             }
   5815 
   5816             case GROUPS_SUMMARY: {
   5817                 final boolean returnGroupCountPerAccount =
   5818                         readBooleanQueryParameter(uri, Groups.PARAM_RETURN_GROUP_COUNT_PER_ACCOUNT,
   5819                                 false);
   5820                 String tables = Views.GROUPS + " AS " + Tables.GROUPS;
   5821                 if (hasColumn(projection, Groups.SUMMARY_COUNT)) {
   5822                     tables = tables + Joins.GROUP_MEMBER_COUNT;
   5823                 }
   5824                 qb.setTables(tables);
   5825                 qb.setProjectionMap(returnGroupCountPerAccount ?
   5826                         sGroupsSummaryProjectionMapWithGroupCountPerAccount
   5827                         : sGroupsSummaryProjectionMap);
   5828                 appendAccountFromParameter(qb, uri);
   5829                 groupBy = GroupsColumns.CONCRETE_ID;
   5830                 break;
   5831             }
   5832 
   5833             case AGGREGATION_EXCEPTIONS: {
   5834                 qb.setTables(Tables.AGGREGATION_EXCEPTIONS);
   5835                 qb.setProjectionMap(sAggregationExceptionsProjectionMap);
   5836                 break;
   5837             }
   5838 
   5839             case AGGREGATION_SUGGESTIONS: {
   5840                 long contactId = Long.parseLong(uri.getPathSegments().get(1));
   5841                 String filter = null;
   5842                 if (uri.getPathSegments().size() > 3) {
   5843                     filter = uri.getPathSegments().get(3);
   5844                 }
   5845                 final int maxSuggestions;
   5846                 if (limit != null) {
   5847                     maxSuggestions = Integer.parseInt(limit);
   5848                 } else {
   5849                     maxSuggestions = DEFAULT_MAX_SUGGESTIONS;
   5850                 }
   5851 
   5852                 ArrayList<AggregationSuggestionParameter> parameters = null;
   5853                 List<String> query = uri.getQueryParameters("query");
   5854                 if (query != null && !query.isEmpty()) {
   5855                     parameters = new ArrayList<AggregationSuggestionParameter>(query.size());
   5856                     for (String parameter : query) {
   5857                         int offset = parameter.indexOf(':');
   5858                         parameters.add(offset == -1
   5859                                 ? new AggregationSuggestionParameter(
   5860                                         AggregationSuggestions.PARAMETER_MATCH_NAME,
   5861                                         parameter)
   5862                                 : new AggregationSuggestionParameter(
   5863                                         parameter.substring(0, offset),
   5864                                         parameter.substring(offset + 1)));
   5865                     }
   5866                 }
   5867 
   5868                 setTablesAndProjectionMapForContacts(qb, uri, projection);
   5869 
   5870                 return mAggregator.get().queryAggregationSuggestions(qb, projection, contactId,
   5871                         maxSuggestions, filter, parameters);
   5872             }
   5873 
   5874             case SETTINGS: {
   5875                 qb.setTables(Tables.SETTINGS);
   5876                 qb.setProjectionMap(sSettingsProjectionMap);
   5877                 appendAccountFromParameter(qb, uri);
   5878 
   5879                 // When requesting specific columns, this query requires
   5880                 // late-binding of the GroupMembership MIME-type.
   5881                 final String groupMembershipMimetypeId = Long.toString(mDbHelper.get()
   5882                         .getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE));
   5883                 if (projection != null && projection.length != 0 &&
   5884                         mDbHelper.get().isInProjection(projection, Settings.UNGROUPED_COUNT)) {
   5885                     selectionArgs = insertSelectionArg(selectionArgs, groupMembershipMimetypeId);
   5886                 }
   5887                 if (projection != null && projection.length != 0 &&
   5888                         mDbHelper.get().isInProjection(
   5889                                 projection, Settings.UNGROUPED_WITH_PHONES)) {
   5890                     selectionArgs = insertSelectionArg(selectionArgs, groupMembershipMimetypeId);
   5891                 }
   5892 
   5893                 break;
   5894             }
   5895 
   5896             case STATUS_UPDATES:
   5897             case PROFILE_STATUS_UPDATES: {
   5898                 setTableAndProjectionMapForStatusUpdates(qb, projection);
   5899                 break;
   5900             }
   5901 
   5902             case STATUS_UPDATES_ID: {
   5903                 setTableAndProjectionMapForStatusUpdates(qb, projection);
   5904                 selectionArgs = insertSelectionArg(selectionArgs, uri.getLastPathSegment());
   5905                 qb.appendWhere(DataColumns.CONCRETE_ID + "=?");
   5906                 break;
   5907             }
   5908 
   5909             case SEARCH_SUGGESTIONS: {
   5910                 return mGlobalSearchSupport.handleSearchSuggestionsQuery(
   5911                         mActiveDb.get(), uri, projection, limit);
   5912             }
   5913 
   5914             case SEARCH_SHORTCUT: {
   5915                 String lookupKey = uri.getLastPathSegment();
   5916                 String filter = getQueryParameter(
   5917                         uri, SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA);
   5918                 return mGlobalSearchSupport.handleSearchShortcutRefresh(
   5919                         mActiveDb.get(), projection, lookupKey, filter);
   5920             }
   5921 
   5922             case RAW_CONTACT_ENTITIES:
   5923             case PROFILE_RAW_CONTACT_ENTITIES: {
   5924                 setTablesAndProjectionMapForRawEntities(qb, uri);
   5925                 break;
   5926             }
   5927 
   5928             case RAW_CONTACT_ENTITY_ID: {
   5929                 long rawContactId = Long.parseLong(uri.getPathSegments().get(1));
   5930                 setTablesAndProjectionMapForRawEntities(qb, uri);
   5931                 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(rawContactId));
   5932                 qb.appendWhere(" AND " + RawContacts._ID + "=?");
   5933                 break;
   5934             }
   5935 
   5936             case PROVIDER_STATUS: {
   5937                 return queryProviderStatus(uri, projection);
   5938             }
   5939 
   5940             case DIRECTORIES : {
   5941                 qb.setTables(Tables.DIRECTORIES);
   5942                 qb.setProjectionMap(sDirectoryProjectionMap);
   5943                 break;
   5944             }
   5945 
   5946             case DIRECTORIES_ID : {
   5947                 long id = ContentUris.parseId(uri);
   5948                 qb.setTables(Tables.DIRECTORIES);
   5949                 qb.setProjectionMap(sDirectoryProjectionMap);
   5950                 selectionArgs = insertSelectionArg(selectionArgs, String.valueOf(id));
   5951                 qb.appendWhere(Directory._ID + "=?");
   5952                 break;
   5953             }
   5954 
   5955             case COMPLETE_NAME: {
   5956                 return completeName(uri, projection);
   5957             }
   5958 
   5959             default:
   5960                 return mLegacyApiSupport.query(uri, projection, selection, selectionArgs,
   5961                         sortOrder, limit);
   5962         }
   5963 
   5964         qb.setStrict(true);
   5965 
   5966         Cursor cursor =
   5967                 query(mActiveDb.get(), qb, projection, selection, selectionArgs, sortOrder, groupBy,
   5968                         limit);
   5969         if (readBooleanQueryParameter(uri, ContactCounts.ADDRESS_BOOK_INDEX_EXTRAS, false)) {
   5970             cursor = bundleLetterCountExtras(cursor, mActiveDb.get(), qb, selection,
   5971                     selectionArgs, sortOrder, addressBookIndexerCountExpression);
   5972         }
   5973         if (snippetDeferred) {
   5974             cursor = addDeferredSnippetingExtra(cursor);
   5975         }
   5976         return cursor;
   5977     }
   5978 
   5979     private Cursor query(final SQLiteDatabase db, SQLiteQueryBuilder qb, String[] projection,
   5980             String selection, String[] selectionArgs, String sortOrder, String groupBy,
   5981             String limit) {
   5982         if (projection != null && projection.length == 1
   5983                 && BaseColumns._COUNT.equals(projection[0])) {
   5984             qb.setProjectionMap(sCountProjectionMap);
   5985         }
   5986         final Cursor c = qb.query(db, projection, selection, selectionArgs, groupBy, null,
   5987                 sortOrder, limit);
   5988         if (c != null) {
   5989             c.setNotificationUri(getContext().getContentResolver(), ContactsContract.AUTHORITY_URI);
   5990         }
   5991         return c;
   5992     }
   5993 
   5994     /**
   5995      * Creates a single-row cursor containing the current status of the provider.
   5996      */
   5997     private Cursor queryProviderStatus(Uri uri, String[] projection) {
   5998         MatrixCursor cursor = new MatrixCursor(projection);
   5999         RowBuilder row = cursor.newRow();
   6000         for (int i = 0; i < projection.length; i++) {
   6001             if (ProviderStatus.STATUS.equals(projection[i])) {
   6002                 row.add(mProviderStatus);
   6003             } else if (ProviderStatus.DATA1.equals(projection[i])) {
   6004                 row.add(mEstimatedStorageRequirement);
   6005             }
   6006         }
   6007         return cursor;
   6008     }
   6009 
   6010     /**
   6011      * Runs the query with the supplied contact ID and lookup ID.  If the query succeeds,
   6012      * it returns the resulting cursor, otherwise it returns null and the calling
   6013      * method needs to resolve the lookup key and rerun the query.
   6014      */
   6015     private Cursor queryWithContactIdAndLookupKey(SQLiteQueryBuilder lookupQb,
   6016             SQLiteDatabase db, Uri uri,
   6017             String[] projection, String selection, String[] selectionArgs,
   6018             String sortOrder, String groupBy, String limit,
   6019             String contactIdColumn, long contactId, String lookupKeyColumn, String lookupKey) {
   6020         String[] args;
   6021         if (selectionArgs == null) {
   6022             args = new String[2];
   6023         } else {
   6024             args = new String[selectionArgs.length + 2];
   6025             System.arraycopy(selectionArgs, 0, args, 2, selectionArgs.length);
   6026         }
   6027         args[0] = String.valueOf(contactId);
   6028         args[1] = Uri.encode(lookupKey);
   6029         lookupQb.appendWhere(contactIdColumn + "=? AND " + lookupKeyColumn + "=?");
   6030         Cursor c = query(db, lookupQb, projection, selection, args, sortOrder,
   6031                 groupBy, limit);
   6032         if (c.getCount() != 0) {
   6033             return c;
   6034         }
   6035 
   6036         c.close();
   6037         return null;
   6038     }
   6039 
   6040     private static final class AddressBookIndexQuery {
   6041         public static final String LETTER = "letter";
   6042         public static final String TITLE = "title";
   6043         public static final String COUNT = "count";
   6044 
   6045         public static final String[] COLUMNS = new String[] {
   6046                 LETTER, TITLE, COUNT
   6047         };
   6048 
   6049         public static final int COLUMN_LETTER = 0;
   6050         public static final int COLUMN_TITLE = 1;
   6051         public static final int COLUMN_COUNT = 2;
   6052 
   6053         // The first letter of the sort key column is what is used for the index headings.
   6054         public static final String SECTION_HEADING = "SUBSTR(%1$s,1,1)";
   6055 
   6056         public static final String ORDER_BY = LETTER + " COLLATE " + PHONEBOOK_COLLATOR_NAME;
   6057     }
   6058 
   6059     /**
   6060      * Computes counts by the address book index titles and adds the resulting tally
   6061      * to the returned cursor as a bundle of extras.
   6062      */
   6063     private Cursor bundleLetterCountExtras(Cursor cursor, final SQLiteDatabase db,
   6064             SQLiteQueryBuilder qb, String selection, String[] selectionArgs, String sortOrder,
   6065             String countExpression) {
   6066         if (!(cursor instanceof AbstractCursor)) {
   6067             Log.w(TAG, "Unable to bundle extras.  Cursor is not AbstractCursor.");
   6068             return cursor;
   6069         }
   6070         String sortKey;
   6071 
   6072         // The sort order suffix could be something like "DESC".
   6073         // We want to preserve it in the query even though we will change
   6074         // the sort column itself.
   6075         String sortOrderSuffix = "";
   6076         if (sortOrder != null) {
   6077             int spaceIndex = sortOrder.indexOf(' ');
   6078             if (spaceIndex != -1) {
   6079                 sortKey = sortOrder.substring(0, spaceIndex);
   6080                 sortOrderSuffix = sortOrder.substring(spaceIndex);
   6081             } else {
   6082                 sortKey = sortOrder;
   6083             }
   6084         } else {
   6085             sortKey = Contacts.SORT_KEY_PRIMARY;
   6086         }
   6087 
   6088         String locale = getLocale().toString();
   6089         HashMap<String, String> projectionMap = Maps.newHashMap();
   6090         String sectionHeading = String.format(Locale.US, AddressBookIndexQuery.SECTION_HEADING,
   6091                 sortKey);
   6092         projectionMap.put(AddressBookIndexQuery.LETTER,
   6093                 sectionHeading + " AS " + AddressBookIndexQuery.LETTER);
   6094 
   6095         // If "what to count" is not specified, we just count all records.
   6096         if (TextUtils.isEmpty(countExpression)) {
   6097             countExpression = "*";
   6098         }
   6099 
   6100         /**
   6101          * Use the GET_PHONEBOOK_INDEX function, which is an android extension for SQLite3,
   6102          * to map the first letter of the sort key to a character that is traditionally
   6103          * used in phonebooks to represent that letter.  For example, in Korean it will
   6104          * be the first consonant in the letter; for Japanese it will be Hiragana rather
   6105          * than Katakana.
   6106          */
   6107         projectionMap.put(AddressBookIndexQuery.TITLE,
   6108                 "GET_PHONEBOOK_INDEX(" + sectionHeading + ",'" + locale + "')"
   6109                         + " AS " + AddressBookIndexQuery.TITLE);
   6110         projectionMap.put(AddressBookIndexQuery.COUNT,
   6111                 "COUNT(" + countExpression + ") AS " + AddressBookIndexQuery.COUNT);
   6112         qb.setProjectionMap(projectionMap);
   6113 
   6114         Cursor indexCursor = qb.query(db, AddressBookIndexQuery.COLUMNS, selection, selectionArgs,
   6115                 AddressBookIndexQuery.ORDER_BY, null /* having */,
   6116                 AddressBookIndexQuery.ORDER_BY + sortOrderSuffix);
   6117 
   6118         try {
   6119             int groupCount = indexCursor.getCount();
   6120             String titles[] = new String[groupCount];
   6121             int counts[] = new int[groupCount];
   6122             int indexCount = 0;
   6123             String currentTitle = null;
   6124 
   6125             // Since GET_PHONEBOOK_INDEX is a many-to-1 function, we may end up
   6126             // with multiple entries for the same title.  The following code
   6127             // collapses those duplicates.
   6128             for (int i = 0; i < groupCount; i++) {
   6129                 indexCursor.moveToNext();
   6130                 String title = indexCursor.getString(AddressBookIndexQuery.COLUMN_TITLE);
   6131                 int count = indexCursor.getInt(AddressBookIndexQuery.COLUMN_COUNT);
   6132                 if (indexCount == 0 || !TextUtils.equals(title, currentTitle)) {
   6133                     titles[indexCount] = currentTitle = title;
   6134                     counts[indexCount] = count;
   6135                     indexCount++;
   6136                 } else {
   6137                     counts[indexCount - 1] += count;
   6138                 }
   6139             }
   6140 
   6141             if (indexCount < groupCount) {
   6142                 String[] newTitles = new String[indexCount];
   6143                 System.arraycopy(titles, 0, newTitles, 0, indexCount);
   6144                 titles = newTitles;
   6145 
   6146                 int[] newCounts = new int[indexCount];
   6147                 System.arraycopy(counts, 0, newCounts, 0, indexCount);
   6148                 counts = newCounts;
   6149             }
   6150 
   6151             final Bundle bundle = new Bundle();
   6152             bundle.putStringArray(ContactCounts.EXTRA_ADDRESS_BOOK_INDEX_TITLES, titles);
   6153             bundle.putIntArray(ContactCounts.EXTRA_ADDRESS_BOOK_INDEX_COUNTS, counts);
   6154 
   6155             ((AbstractCursor) cursor).setExtras(bundle);
   6156             return cursor;
   6157         } finally {
   6158             indexCursor.close();
   6159         }
   6160     }
   6161 
   6162     /**
   6163      * Returns the contact Id for the contact identified by the lookupKey.
   6164      * Robust against changes in the lookup key: if the key has changed, will
   6165      * look up the contact by the raw contact IDs or name encoded in the lookup
   6166      * key.
   6167      */
   6168     public long lookupContactIdByLookupKey(SQLiteDatabase db, String lookupKey) {
   6169         ContactLookupKey key = new ContactLookupKey();
   6170         ArrayList<LookupKeySegment> segments = key.parse(lookupKey);
   6171 
   6172         long contactId = -1;
   6173         if (lookupKeyContainsType(segments, ContactLookupKey.LOOKUP_TYPE_PROFILE)) {
   6174             // We should already be in a profile database context, so just look up a single contact.
   6175            contactId = lookupSingleContactId(db);
   6176         }
   6177 
   6178         if (lookupKeyContainsType(segments, ContactLookupKey.LOOKUP_TYPE_SOURCE_ID)) {
   6179             contactId = lookupContactIdBySourceIds(db, segments);
   6180             if (contactId != -1) {
   6181                 return contactId;
   6182             }
   6183         }
   6184 
   6185         boolean hasRawContactIds =
   6186                 lookupKeyContainsType(segments, ContactLookupKey.LOOKUP_TYPE_RAW_CONTACT_ID);
   6187         if (hasRawContactIds) {
   6188             contactId = lookupContactIdByRawContactIds(db, segments);
   6189             if (contactId != -1) {
   6190                 return contactId;
   6191             }
   6192         }
   6193 
   6194         if (hasRawContactIds
   6195                 || lookupKeyContainsType(segments, ContactLookupKey.LOOKUP_TYPE_DISPLAY_NAME)) {
   6196             contactId = lookupContactIdByDisplayNames(db, segments);
   6197         }
   6198 
   6199         return contactId;
   6200     }
   6201 
   6202     private long lookupSingleContactId(SQLiteDatabase db) {
   6203         Cursor c = db.query(Tables.CONTACTS, new String[] {Contacts._ID},
   6204                 null, null, null, null, null, "1");
   6205         try {
   6206             if (c.moveToFirst()) {
   6207                 return c.getLong(0);
   6208             } else {
   6209                 return -1;
   6210             }
   6211         } finally {
   6212             c.close();
   6213         }
   6214     }
   6215 
   6216     private interface LookupBySourceIdQuery {
   6217         String TABLE = Views.RAW_CONTACTS;
   6218 
   6219         String COLUMNS[] = {
   6220                 RawContacts.CONTACT_ID,
   6221                 RawContacts.ACCOUNT_TYPE_AND_DATA_SET,
   6222                 RawContacts.ACCOUNT_NAME,
   6223                 RawContacts.SOURCE_ID
   6224         };
   6225 
   6226         int CONTACT_ID = 0;
   6227         int ACCOUNT_TYPE_AND_DATA_SET = 1;
   6228         int ACCOUNT_NAME = 2;
   6229         int SOURCE_ID = 3;
   6230     }
   6231 
   6232     private long lookupContactIdBySourceIds(SQLiteDatabase db,
   6233                 ArrayList<LookupKeySegment> segments) {
   6234         StringBuilder sb = new StringBuilder();
   6235         sb.append(RawContacts.SOURCE_ID + " IN (");
   6236         for (int i = 0; i < segments.size(); i++) {
   6237             LookupKeySegment segment = segments.get(i);
   6238             if (segment.lookupType == ContactLookupKey.LOOKUP_TYPE_SOURCE_ID) {
   6239                 DatabaseUtils.appendEscapedSQLString(sb, segment.key);
   6240                 sb.append(",");
   6241             }
   6242         }
   6243         sb.setLength(sb.length() - 1);      // Last comma
   6244         sb.append(") AND " + RawContacts.CONTACT_ID + " NOT NULL");
   6245 
   6246         Cursor c = db.query(LookupBySourceIdQuery.TABLE, LookupBySourceIdQuery.COLUMNS,
   6247                  sb.toString(), null, null, null, null);
   6248         try {
   6249             while (c.moveToNext()) {
   6250                 String accountTypeAndDataSet =
   6251                         c.getString(LookupBySourceIdQuery.ACCOUNT_TYPE_AND_DATA_SET);
   6252                 String accountName = c.getString(LookupBySourceIdQuery.ACCOUNT_NAME);
   6253                 int accountHashCode =
   6254                         ContactLookupKey.getAccountHashCode(accountTypeAndDataSet, accountName);
   6255                 String sourceId = c.getString(LookupBySourceIdQuery.SOURCE_ID);
   6256                 for (int i = 0; i < segments.size(); i++) {
   6257                     LookupKeySegment segment = segments.get(i);
   6258                     if (segment.lookupType == ContactLookupKey.LOOKUP_TYPE_SOURCE_ID
   6259                             && accountHashCode == segment.accountHashCode
   6260                             && segment.key.equals(sourceId)) {
   6261                         segment.contactId = c.getLong(LookupBySourceIdQuery.CONTACT_ID);
   6262                         break;
   6263                     }
   6264                 }
   6265             }
   6266         } finally {
   6267             c.close();
   6268         }
   6269 
   6270         return getMostReferencedContactId(segments);
   6271     }
   6272 
   6273     private interface LookupByRawContactIdQuery {
   6274         String TABLE = Views.RAW_CONTACTS;
   6275 
   6276         String COLUMNS[] = {
   6277                 RawContacts.CONTACT_ID,
   6278                 RawContacts.ACCOUNT_TYPE_AND_DATA_SET,
   6279                 RawContacts.ACCOUNT_NAME,
   6280                 RawContacts._ID,
   6281         };
   6282 
   6283         int CONTACT_ID = 0;
   6284         int ACCOUNT_TYPE_AND_DATA_SET = 1;
   6285         int ACCOUNT_NAME = 2;
   6286         int ID = 3;
   6287     }
   6288 
   6289     private long lookupContactIdByRawContactIds(SQLiteDatabase db,
   6290             ArrayList<LookupKeySegment> segments) {
   6291         StringBuilder sb = new StringBuilder();
   6292         sb.append(RawContacts._ID + " IN (");
   6293         for (int i = 0; i < segments.size(); i++) {
   6294             LookupKeySegment segment = segments.get(i);
   6295             if (segment.lookupType == ContactLookupKey.LOOKUP_TYPE_RAW_CONTACT_ID) {
   6296                 sb.append(segment.rawContactId);
   6297                 sb.append(",");
   6298             }
   6299         }
   6300         sb.setLength(sb.length() - 1);      // Last comma
   6301         sb.append(") AND " + RawContacts.CONTACT_ID + " NOT NULL");
   6302 
   6303         Cursor c = db.query(LookupByRawContactIdQuery.TABLE, LookupByRawContactIdQuery.COLUMNS,
   6304                  sb.toString(), null, null, null, null);
   6305         try {
   6306             while (c.moveToNext()) {
   6307                 String accountTypeAndDataSet = c.getString(
   6308                         LookupByRawContactIdQuery.ACCOUNT_TYPE_AND_DATA_SET);
   6309                 String accountName = c.getString(LookupByRawContactIdQuery.ACCOUNT_NAME);
   6310                 int accountHashCode =
   6311                         ContactLookupKey.getAccountHashCode(accountTypeAndDataSet, accountName);
   6312                 String rawContactId = c.getString(LookupByRawContactIdQuery.ID);
   6313                 for (int i = 0; i < segments.size(); i++) {
   6314                     LookupKeySegment segment = segments.get(i);
   6315                     if (segment.lookupType == ContactLookupKey.LOOKUP_TYPE_RAW_CONTACT_ID
   6316                             && accountHashCode == segment.accountHashCode
   6317                             && segment.rawContactId.equals(rawContactId)) {
   6318                         segment.contactId = c.getLong(LookupByRawContactIdQuery.CONTACT_ID);
   6319                         break;
   6320                     }
   6321                 }
   6322             }
   6323         } finally {
   6324             c.close();
   6325         }
   6326 
   6327         return getMostReferencedContactId(segments);
   6328     }
   6329 
   6330     private interface LookupByDisplayNameQuery {
   6331         String TABLE = Tables.NAME_LOOKUP_JOIN_RAW_CONTACTS;
   6332 
   6333         String COLUMNS[] = {
   6334                 RawContacts.CONTACT_ID,
   6335                 RawContacts.ACCOUNT_TYPE_AND_DATA_SET,
   6336                 RawContacts.ACCOUNT_NAME,
   6337                 NameLookupColumns.NORMALIZED_NAME
   6338         };
   6339 
   6340         int CONTACT_ID = 0;
   6341         int ACCOUNT_TYPE_AND_DATA_SET = 1;
   6342         int ACCOUNT_NAME = 2;
   6343         int NORMALIZED_NAME = 3;
   6344     }
   6345 
   6346     private long lookupContactIdByDisplayNames(SQLiteDatabase db,
   6347                 ArrayList<LookupKeySegment> segments) {
   6348         StringBuilder sb = new StringBuilder();
   6349         sb.append(NameLookupColumns.NORMALIZED_NAME + " IN (");
   6350         for (int i = 0; i < segments.size(); i++) {
   6351             LookupKeySegment segment = segments.get(i);
   6352             if (segment.lookupType == ContactLookupKey.LOOKUP_TYPE_DISPLAY_NAME
   6353                     || segment.lookupType == ContactLookupKey.LOOKUP_TYPE_RAW_CONTACT_ID) {
   6354                 DatabaseUtils.appendEscapedSQLString(sb, segment.key);
   6355                 sb.append(",");
   6356             }
   6357         }
   6358         sb.setLength(sb.length() - 1);      // Last comma
   6359         sb.append(") AND " + NameLookupColumns.NAME_TYPE + "=" + NameLookupType.NAME_COLLATION_KEY
   6360                 + " AND " + RawContacts.CONTACT_ID + " NOT NULL");
   6361 
   6362         Cursor c = db.query(LookupByDisplayNameQuery.TABLE, LookupByDisplayNameQuery.COLUMNS,
   6363                  sb.toString(), null, null, null, null);
   6364         try {
   6365             while (c.moveToNext()) {
   6366                 String accountTypeAndDataSet =
   6367                         c.getString(LookupByDisplayNameQuery.ACCOUNT_TYPE_AND_DATA_SET);
   6368                 String accountName = c.getString(LookupByDisplayNameQuery.ACCOUNT_NAME);
   6369                 int accountHashCode =
   6370                         ContactLookupKey.getAccountHashCode(accountTypeAndDataSet, accountName);
   6371                 String name = c.getString(LookupByDisplayNameQuery.NORMALIZED_NAME);
   6372                 for (int i = 0; i < segments.size(); i++) {
   6373                     LookupKeySegment segment = segments.get(i);
   6374                     if ((segment.lookupType == ContactLookupKey.LOOKUP_TYPE_DISPLAY_NAME
   6375                             || segment.lookupType == ContactLookupKey.LOOKUP_TYPE_RAW_CONTACT_ID)
   6376                             && accountHashCode == segment.accountHashCode
   6377                             && segment.key.equals(name)) {
   6378                         segment.contactId = c.getLong(LookupByDisplayNameQuery.CONTACT_ID);
   6379                         break;
   6380                     }
   6381                 }
   6382             }
   6383         } finally {
   6384             c.close();
   6385         }
   6386 
   6387         return getMostReferencedContactId(segments);
   6388     }
   6389 
   6390     private boolean lookupKeyContainsType(ArrayList<LookupKeySegment> segments, int lookupType) {
   6391         for (int i = 0; i < segments.size(); i++) {
   6392             LookupKeySegment segment = segments.get(i);
   6393             if (segment.lookupType == lookupType) {
   6394                 return true;
   6395             }
   6396         }
   6397 
   6398         return false;
   6399     }
   6400 
   6401     public void updateLookupKeyForRawContact(SQLiteDatabase db, long rawContactId) {
   6402         mAggregator.get().updateLookupKeyForRawContact(db, rawContactId);
   6403     }
   6404 
   6405     /**
   6406      * Returns the contact ID that is mentioned the highest number of times.
   6407      */
   6408     private long getMostReferencedContactId(ArrayList<LookupKeySegment> segments) {
   6409         Collections.sort(segments);
   6410 
   6411         long bestContactId = -1;
   6412         int bestRefCount = 0;
   6413 
   6414         long contactId = -1;
   6415         int count = 0;
   6416 
   6417         int segmentCount = segments.size();
   6418         for (int i = 0; i < segmentCount; i++) {
   6419             LookupKeySegment segment = segments.get(i);
   6420             if (segment.contactId != -1) {
   6421                 if (segment.contactId == contactId) {
   6422                     count++;
   6423                 } else {
   6424                     if (count > bestRefCount) {
   6425                         bestContactId = contactId;
   6426                         bestRefCount = count;
   6427                     }
   6428                     contactId = segment.contactId;
   6429                     count = 1;
   6430                 }
   6431             }
   6432         }
   6433         if (count > bestRefCount) {
   6434             return contactId;
   6435         } else {
   6436             return bestContactId;
   6437         }
   6438     }
   6439 
   6440     private void setTablesAndProjectionMapForContacts(SQLiteQueryBuilder qb, Uri uri,
   6441             String[] projection) {
   6442         setTablesAndProjectionMapForContacts(qb, uri, projection, false);
   6443     }
   6444 
   6445     /**
   6446      * @param includeDataUsageStat true when the table should include DataUsageStat table.
   6447      * Note that this uses INNER JOIN instead of LEFT OUTER JOIN, so some of data in Contacts
   6448      * may be dropped.
   6449      */
   6450     private void setTablesAndProjectionMapForContacts(SQLiteQueryBuilder qb, Uri uri,
   6451             String[] projection, boolean includeDataUsageStat) {
   6452         StringBuilder sb = new StringBuilder();
   6453         if (includeDataUsageStat) {
   6454             sb.append(Views.DATA_USAGE_STAT + " AS " + Tables.DATA_USAGE_STAT);
   6455             sb.append(" INNER JOIN ");
   6456         }
   6457 
   6458         sb.append(Views.CONTACTS);
   6459 
   6460         // Just for frequently contacted contacts in Strequent Uri handling.
   6461         if (includeDataUsageStat) {
   6462             sb.append(" ON (" +
   6463                     DbQueryUtils.concatenateClauses(
   6464                             DataUsageStatColumns.CONCRETE_TIMES_USED + " > 0",
   6465                             RawContacts.CONTACT_ID + "=" + Views.CONTACTS + "." + Contacts._ID) +
   6466                     ")");
   6467         }
   6468 
   6469         appendContactPresenceJoin(sb, projection, Contacts._ID);
   6470         appendContactStatusUpdateJoin(sb, projection, ContactsColumns.LAST_STATUS_UPDATE_ID);
   6471         qb.setTables(sb.toString());
   6472         qb.setProjectionMap(sContactsProjectionMap);
   6473     }
   6474 
   6475     /**
   6476      * Finds name lookup records matching the supplied filter, picks one arbitrary match per
   6477      * contact and joins that with other contacts tables.
   6478      */
   6479     private void setTablesAndProjectionMapForContactsWithSnippet(SQLiteQueryBuilder qb, Uri uri,
   6480             String[] projection, String filter, long directoryId, boolean deferredSnippeting) {
   6481 
   6482         StringBuilder sb = new StringBuilder();
   6483         sb.append(Views.CONTACTS);
   6484 
   6485         if (filter != null) {
   6486             filter = filter.trim();
   6487         }
   6488 
   6489         if (TextUtils.isEmpty(filter) || (directoryId != -1 && directoryId != Directory.DEFAULT)) {
   6490             sb.append(" JOIN (SELECT NULL AS " + SearchSnippetColumns.SNIPPET + " WHERE 0)");
   6491         } else {
   6492             appendSearchIndexJoin(sb, uri, projection, filter, deferredSnippeting);
   6493         }
   6494         appendContactPresenceJoin(sb, projection, Contacts._ID);
   6495         appendContactStatusUpdateJoin(sb, projection, ContactsColumns.LAST_STATUS_UPDATE_ID);
   6496         qb.setTables(sb.toString());
   6497         qb.setProjectionMap(sContactsProjectionWithSnippetMap);
   6498     }
   6499 
   6500     private void appendSearchIndexJoin(
   6501             StringBuilder sb, Uri uri, String[] projection, String filter,
   6502             boolean  deferredSnippeting) {
   6503 
   6504         if (snippetNeeded(projection)) {
   6505             String[] args = null;
   6506             String snippetArgs =
   6507                     getQueryParameter(uri, SearchSnippetColumns.SNIPPET_ARGS_PARAM_KEY);
   6508             if (snippetArgs != null) {
   6509                 args = snippetArgs.split(",");
   6510             }
   6511 
   6512             String startMatch = args != null && args.length > 0 ? args[0]
   6513                     : DEFAULT_SNIPPET_ARG_START_MATCH;
   6514             String endMatch = args != null && args.length > 1 ? args[1]
   6515                     : DEFAULT_SNIPPET_ARG_END_MATCH;
   6516             String ellipsis = args != null && args.length > 2 ? args[2]
   6517                     : DEFAULT_SNIPPET_ARG_ELLIPSIS;
   6518             int maxTokens = args != null && args.length > 3 ? Integer.parseInt(args[3])
   6519                     : DEFAULT_SNIPPET_ARG_MAX_TOKENS;
   6520 
   6521             appendSearchIndexJoin(
   6522                     sb, filter, true, startMatch, endMatch, ellipsis, maxTokens,
   6523                     deferredSnippeting);
   6524         } else {
   6525             appendSearchIndexJoin(sb, filter, false, null, null, null, 0, false);
   6526         }
   6527     }
   6528 
   6529     public void appendSearchIndexJoin(StringBuilder sb, String filter,
   6530             boolean snippetNeeded, String startMatch, String endMatch, String ellipsis,
   6531             int maxTokens, boolean deferredSnippeting) {
   6532         boolean isEmailAddress = false;
   6533         String emailAddress = null;
   6534         boolean isPhoneNumber = false;
   6535         String phoneNumber = null;
   6536         String numberE164 = null;
   6537 
   6538         // If the query consists of a single word, we can do snippetizing after-the-fact for a
   6539         // performance boost.
   6540         boolean singleTokenSearch = isSingleWordQuery(filter);
   6541 
   6542         if (filter.indexOf('@') != -1) {
   6543             emailAddress = mDbHelper.get().extractAddressFromEmailAddress(filter);
   6544             isEmailAddress = !TextUtils.isEmpty(emailAddress);
   6545         } else {
   6546             isPhoneNumber = isPhoneNumber(filter);
   6547             if (isPhoneNumber) {
   6548                 phoneNumber = PhoneNumberUtils.normalizeNumber(filter);
   6549                 numberE164 = PhoneNumberUtils.formatNumberToE164(phoneNumber,
   6550                         mDbHelper.get().getCountryIso());
   6551             }
   6552         }
   6553 
   6554         final String SNIPPET_CONTACT_ID = "snippet_contact_id";
   6555         sb.append(" JOIN (SELECT " + SearchIndexColumns.CONTACT_ID + " AS " + SNIPPET_CONTACT_ID);
   6556         if (snippetNeeded) {
   6557             sb.append(", ");
   6558             if (isEmailAddress) {
   6559                 sb.append("ifnull(");
   6560                 DatabaseUtils.appendEscapedSQLString(sb, startMatch);
   6561                 sb.append("||(SELECT MIN(" + Email.ADDRESS + ")");
   6562                 sb.append(" FROM " + Tables.DATA_JOIN_RAW_CONTACTS);
   6563                 sb.append(" WHERE  " + Tables.SEARCH_INDEX + "." + SearchIndexColumns.CONTACT_ID);
   6564                 sb.append("=" + RawContacts.CONTACT_ID + " AND " + Email.ADDRESS + " LIKE ");
   6565                 DatabaseUtils.appendEscapedSQLString(sb, filter + "%");
   6566                 sb.append(")||");
   6567                 DatabaseUtils.appendEscapedSQLString(sb, endMatch);
   6568                 sb.append(",");
   6569 
   6570                 // Optimization for single-token search (do only if requested).
   6571                 if (singleTokenSearch && deferredSnippeting) {
   6572                     sb.append(SearchIndexColumns.CONTENT);
   6573                 } else {
   6574                     appendSnippetFunction(sb, startMatch, endMatch, ellipsis, maxTokens);
   6575                 }
   6576                 sb.append(")");
   6577             } else if (isPhoneNumber) {
   6578                 sb.append("ifnull(");
   6579                 DatabaseUtils.appendEscapedSQLString(sb, startMatch);
   6580                 sb.append("||(SELECT MIN(" + Phone.NUMBER + ")");
   6581                 sb.append(" FROM " +
   6582                         Tables.DATA_JOIN_RAW_CONTACTS + " JOIN " + Tables.PHONE_LOOKUP);
   6583                 sb.append(" ON " + DataColumns.CONCRETE_ID);
   6584                 sb.append("=" + Tables.PHONE_LOOKUP + "." + PhoneLookupColumns.DATA_ID);
   6585                 sb.append(" WHERE  " + Tables.SEARCH_INDEX + "." + SearchIndexColumns.CONTACT_ID);
   6586                 sb.append("=" + RawContacts.CONTACT_ID);
   6587                 sb.append(" AND " + PhoneLookupColumns.NORMALIZED_NUMBER + " LIKE '");
   6588                 sb.append(phoneNumber);
   6589                 sb.append("%'");
   6590                 if (!TextUtils.isEmpty(numberE164)) {
   6591                     sb.append(" OR " + PhoneLookupColumns.NORMALIZED_NUMBER + " LIKE '");
   6592                     sb.append(numberE164);
   6593                     sb.append("%'");
   6594                 }
   6595                 sb.append(")||");
   6596                 DatabaseUtils.appendEscapedSQLString(sb, endMatch);
   6597                 sb.append(",");
   6598 
   6599                 // Optimization for single-token search (do only if requested).
   6600                 if (singleTokenSearch && deferredSnippeting) {
   6601                     sb.append(SearchIndexColumns.CONTENT);
   6602                 } else {
   6603                     appendSnippetFunction(sb, startMatch, endMatch, ellipsis, maxTokens);
   6604                 }
   6605                 sb.append(")");
   6606             } else {
   6607                 final String normalizedFilter = NameNormalizer.normalize(filter);
   6608                 if (!TextUtils.isEmpty(normalizedFilter)) {
   6609                     // Optimization for single-token search (do only if requested)..
   6610                     if (singleTokenSearch && deferredSnippeting) {
   6611                         sb.append(SearchIndexColumns.CONTENT);
   6612                     } else {
   6613                         sb.append("(CASE WHEN EXISTS (SELECT 1 FROM ");
   6614                         sb.append(Tables.RAW_CONTACTS + " AS rc INNER JOIN ");
   6615                         sb.append(Tables.NAME_LOOKUP + " AS nl ON (rc." + RawContacts._ID);
   6616                         sb.append("=nl." + NameLookupColumns.RAW_CONTACT_ID);
   6617                         sb.append(") WHERE nl." + NameLookupColumns.NORMALIZED_NAME);
   6618                         sb.append(" GLOB '" + normalizedFilter + "*' AND ");
   6619                         sb.append("nl." + NameLookupColumns.NAME_TYPE + "=");
   6620                         sb.append(NameLookupType.NAME_COLLATION_KEY + " AND ");
   6621                         sb.append(Tables.SEARCH_INDEX + "." + SearchIndexColumns.CONTACT_ID);
   6622                         sb.append("=rc." + RawContacts.CONTACT_ID);
   6623                         sb.append(") THEN NULL ELSE ");
   6624                         appendSnippetFunction(sb, startMatch, endMatch, ellipsis, maxTokens);
   6625                         sb.append(" END)");
   6626                     }
   6627                 } else {
   6628                     sb.append("NULL");
   6629                 }
   6630             }
   6631             sb.append(" AS " + SearchSnippetColumns.SNIPPET);
   6632         }
   6633 
   6634         sb.append(" FROM " + Tables.SEARCH_INDEX);
   6635         sb.append(" WHERE ");
   6636         sb.append(Tables.SEARCH_INDEX + " MATCH '");
   6637         if (isEmailAddress) {
   6638             // we know that the emailAddress contains a @. This phrase search should be
   6639             // scoped against "content:" only, but unfortunately SQLite doesn't support
   6640             // phrases and scoped columns at once. This is fine in this case however, because:
   6641             //  - We can't erronously match against name, as name is all-hex (so the @ can't match)
   6642             //  - We can't match against tokens, because phone-numbers can't contain @
   6643             final String sanitizedEmailAddress =
   6644                     emailAddress == null ? "" : sanitizeMatch(emailAddress);
   6645             sb.append("\"");
   6646             sb.append(sanitizedEmailAddress);
   6647             sb.append("*\"");
   6648         } else if (isPhoneNumber) {
   6649             // normalized version of the phone number (phoneNumber can only have + and digits)
   6650             final String phoneNumberCriteria = " OR tokens:" + phoneNumber + "*";
   6651 
   6652             // international version of this number (numberE164 can only have + and digits)
   6653             final String numberE164Criteria =
   6654                     (numberE164 != null && !TextUtils.equals(numberE164, phoneNumber))
   6655                     ? " OR tokens:" + numberE164 + "*"
   6656                     : "";
   6657 
   6658             // combine all criteria
   6659             final String commonCriteria =
   6660                     phoneNumberCriteria + numberE164Criteria;
   6661 
   6662             // search in content
   6663             sb.append(SearchIndexManager.getFtsMatchQuery(filter,
   6664                     FtsQueryBuilder.getDigitsQueryBuilder(commonCriteria)));
   6665         } else {
   6666             // general case: not a phone number, not an email-address
   6667             sb.append(SearchIndexManager.getFtsMatchQuery(filter,
   6668                     FtsQueryBuilder.SCOPED_NAME_NORMALIZING));
   6669         }
   6670         // Omit results in "Other Contacts".
   6671         sb.append("' AND " + SNIPPET_CONTACT_ID + " IN " + Tables.DEFAULT_DIRECTORY + ")");
   6672         sb.append(" ON (" + Contacts._ID + "=" + SNIPPET_CONTACT_ID + ")");
   6673     }
   6674 
   6675     private static String sanitizeMatch(String filter) {
   6676         return filter.replace("'", "").replace("*", "").replace("-", "").replace("\"", "");
   6677     }
   6678 
   6679     private void appendSnippetFunction(
   6680             StringBuilder sb, String startMatch, String endMatch, String ellipsis, int maxTokens) {
   6681         sb.append("snippet(" + Tables.SEARCH_INDEX + ",");
   6682         DatabaseUtils.appendEscapedSQLString(sb, startMatch);
   6683         sb.append(",");
   6684         DatabaseUtils.appendEscapedSQLString(sb, endMatch);
   6685         sb.append(",");
   6686         DatabaseUtils.appendEscapedSQLString(sb, ellipsis);
   6687 
   6688         // The index of the column used for the snippet, "content"
   6689         sb.append(",1,");
   6690         sb.append(maxTokens);
   6691         sb.append(")");
   6692     }
   6693 
   6694     private void setTablesAndProjectionMapForRawContacts(SQLiteQueryBuilder qb, Uri uri) {
   6695         StringBuilder sb = new StringBuilder();
   6696         sb.append(Views.RAW_CONTACTS);
   6697         qb.setTables(sb.toString());
   6698         qb.setProjectionMap(sRawContactsProjectionMap);
   6699         appendAccountFromParameter(qb, uri);
   6700     }
   6701 
   6702     private void setTablesAndProjectionMapForRawEntities(SQLiteQueryBuilder qb, Uri uri) {
   6703         qb.setTables(Views.RAW_ENTITIES);
   6704         qb.setProjectionMap(sRawEntityProjectionMap);
   6705         appendAccountFromParameter(qb, uri);
   6706     }
   6707 
   6708     private void setTablesAndProjectionMapForData(SQLiteQueryBuilder qb, Uri uri,
   6709             String[] projection, boolean distinct) {
   6710         setTablesAndProjectionMapForData(qb, uri, projection, distinct, false, null);
   6711     }
   6712 
   6713     private void setTablesAndProjectionMapForData(SQLiteQueryBuilder qb, Uri uri,
   6714             String[] projection, boolean distinct, boolean addSipLookupColumns) {
   6715         setTablesAndProjectionMapForData(qb, uri, projection, distinct, addSipLookupColumns, null);
   6716     }
   6717 
   6718     /**
   6719      * @param usageType when non-null {@link Tables#DATA_USAGE_STAT} is joined with the specified
   6720      * type.
   6721      */
   6722     private void setTablesAndProjectionMapForData(SQLiteQueryBuilder qb, Uri uri,
   6723             String[] projection, boolean distinct, Integer usageType) {
   6724         setTablesAndProjectionMapForData(qb, uri, projection, distinct, false, usageType);
   6725     }
   6726 
   6727     private void setTablesAndProjectionMapForData(SQLiteQueryBuilder qb, Uri uri,
   6728             String[] projection, boolean distinct, boolean addSipLookupColumns, Integer usageType) {
   6729         StringBuilder sb = new StringBuilder();
   6730         sb.append(Views.DATA);
   6731         sb.append(" data");
   6732 
   6733         appendContactPresenceJoin(sb, projection, RawContacts.CONTACT_ID);
   6734         appendContactStatusUpdateJoin(sb, projection, ContactsColumns.LAST_STATUS_UPDATE_ID);
   6735         appendDataPresenceJoin(sb, projection, DataColumns.CONCRETE_ID);
   6736         appendDataStatusUpdateJoin(sb, projection, DataColumns.CONCRETE_ID);
   6737 
   6738         if (usageType != null) {
   6739             appendDataUsageStatJoin(sb, usageType, DataColumns.CONCRETE_ID);
   6740         }
   6741 
   6742         qb.setTables(sb.toString());
   6743 
   6744         boolean useDistinct = distinct
   6745                 || !mDbHelper.get().isInProjection(projection, DISTINCT_DATA_PROHIBITING_COLUMNS);
   6746         qb.setDistinct(useDistinct);
   6747 
   6748         final ProjectionMap projectionMap;
   6749         if (addSipLookupColumns) {
   6750             projectionMap = useDistinct
   6751                     ? sDistinctDataSipLookupProjectionMap : sDataSipLookupProjectionMap;
   6752         } else {
   6753             projectionMap = useDistinct ? sDistinctDataProjectionMap : sDataProjectionMap;
   6754         }
   6755 
   6756         qb.setProjectionMap(projectionMap);
   6757         appendAccountFromParameter(qb, uri);
   6758     }
   6759 
   6760     private void setTableAndProjectionMapForStatusUpdates(SQLiteQueryBuilder qb,
   6761             String[] projection) {
   6762         StringBuilder sb = new StringBuilder();
   6763         sb.append(Views.DATA);
   6764         sb.append(" data");
   6765         appendDataPresenceJoin(sb, projection, DataColumns.CONCRETE_ID);
   6766         appendDataStatusUpdateJoin(sb, projection, DataColumns.CONCRETE_ID);
   6767 
   6768         qb.setTables(sb.toString());
   6769         qb.setProjectionMap(sStatusUpdatesProjectionMap);
   6770     }
   6771 
   6772     private void setTablesAndProjectionMapForStreamItems(SQLiteQueryBuilder qb) {
   6773         qb.setTables(Views.STREAM_ITEMS);
   6774         qb.setProjectionMap(sStreamItemsProjectionMap);
   6775     }
   6776 
   6777     private void setTablesAndProjectionMapForStreamItemPhotos(SQLiteQueryBuilder qb) {
   6778         qb.setTables(Tables.PHOTO_FILES
   6779                 + " JOIN " + Tables.STREAM_ITEM_PHOTOS + " ON ("
   6780                 + StreamItemPhotosColumns.CONCRETE_PHOTO_FILE_ID + "="
   6781                 + PhotoFilesColumns.CONCRETE_ID
   6782                 + ") JOIN " + Tables.STREAM_ITEMS + " ON ("
   6783                 + StreamItemPhotosColumns.CONCRETE_STREAM_ITEM_ID + "="
   6784                 + StreamItemsColumns.CONCRETE_ID + ")"
   6785                 + " JOIN " + Tables.RAW_CONTACTS + " ON ("
   6786                 + StreamItemsColumns.CONCRETE_RAW_CONTACT_ID + "=" + RawContactsColumns.CONCRETE_ID
   6787                 + ")");
   6788         qb.setProjectionMap(sStreamItemPhotosProjectionMap);
   6789     }
   6790 
   6791     private void setTablesAndProjectionMapForEntities(SQLiteQueryBuilder qb, Uri uri,
   6792             String[] projection) {
   6793         StringBuilder sb = new StringBuilder();
   6794         sb.append(Views.ENTITIES);
   6795         sb.append(" data");
   6796 
   6797         appendContactPresenceJoin(sb, projection, Contacts.Entity.CONTACT_ID);
   6798         appendContactStatusUpdateJoin(sb, projection, ContactsColumns.LAST_STATUS_UPDATE_ID);
   6799         appendDataPresenceJoin(sb, projection, Contacts.Entity.DATA_ID);
   6800         appendDataStatusUpdateJoin(sb, projection, Contacts.Entity.DATA_ID);
   6801 
   6802         qb.setTables(sb.toString());
   6803         qb.setProjectionMap(sEntityProjectionMap);
   6804         appendAccountFromParameter(qb, uri);
   6805     }
   6806 
   6807     private void appendContactStatusUpdateJoin(StringBuilder sb, String[] projection,
   6808             String lastStatusUpdateIdColumn) {
   6809         if (mDbHelper.get().isInProjection(projection,
   6810                 Contacts.CONTACT_STATUS,
   6811                 Contacts.CONTACT_STATUS_RES_PACKAGE,
   6812                 Contacts.CONTACT_STATUS_ICON,
   6813                 Contacts.CONTACT_STATUS_LABEL,
   6814                 Contacts.CONTACT_STATUS_TIMESTAMP)) {
   6815             sb.append(" LEFT OUTER JOIN " + Tables.STATUS_UPDATES + " "
   6816                     + ContactsStatusUpdatesColumns.ALIAS +
   6817                     " ON (" + lastStatusUpdateIdColumn + "="
   6818                             + ContactsStatusUpdatesColumns.CONCRETE_DATA_ID + ")");
   6819         }
   6820     }
   6821 
   6822     private void appendDataStatusUpdateJoin(StringBuilder sb, String[] projection,
   6823             String dataIdColumn) {
   6824         if (mDbHelper.get().isInProjection(projection,
   6825                 StatusUpdates.STATUS,
   6826                 StatusUpdates.STATUS_RES_PACKAGE,
   6827                 StatusUpdates.STATUS_ICON,
   6828                 StatusUpdates.STATUS_LABEL,
   6829                 StatusUpdates.STATUS_TIMESTAMP)) {
   6830             sb.append(" LEFT OUTER JOIN " + Tables.STATUS_UPDATES +
   6831                     " ON (" + StatusUpdatesColumns.CONCRETE_DATA_ID + "="
   6832                             + dataIdColumn + ")");
   6833         }
   6834     }
   6835 
   6836     private void appendDataUsageStatJoin(StringBuilder sb, int usageType, String dataIdColumn) {
   6837         sb.append(" LEFT OUTER JOIN " + Tables.DATA_USAGE_STAT +
   6838                 " ON (" + DataUsageStatColumns.CONCRETE_DATA_ID + "=" + dataIdColumn +
   6839                 " AND " + DataUsageStatColumns.CONCRETE_USAGE_TYPE + "=" + usageType + ")");
   6840     }
   6841 
   6842     private void appendContactPresenceJoin(StringBuilder sb, String[] projection,
   6843             String contactIdColumn) {
   6844         if (mDbHelper.get().isInProjection(projection,
   6845                 Contacts.CONTACT_PRESENCE, Contacts.CONTACT_CHAT_CAPABILITY)) {
   6846             sb.append(" LEFT OUTER JOIN " + Tables.AGGREGATED_PRESENCE +
   6847                     " ON (" + contactIdColumn + " = "
   6848                             + AggregatedPresenceColumns.CONCRETE_CONTACT_ID + ")");
   6849         }
   6850     }
   6851 
   6852     private void appendDataPresenceJoin(StringBuilder sb, String[] projection,
   6853             String dataIdColumn) {
   6854         if (mDbHelper.get().isInProjection(projection, Data.PRESENCE, Data.CHAT_CAPABILITY)) {
   6855             sb.append(" LEFT OUTER JOIN " + Tables.PRESENCE +
   6856                     " ON (" + StatusUpdates.DATA_ID + "=" + dataIdColumn + ")");
   6857         }
   6858     }
   6859 
   6860     private boolean appendLocalDirectorySelectionIfNeeded(SQLiteQueryBuilder qb, long directoryId) {
   6861         if (directoryId == Directory.DEFAULT) {
   6862             qb.appendWhere(Contacts._ID + " IN " + Tables.DEFAULT_DIRECTORY);
   6863             return true;
   6864         } else if (directoryId == Directory.LOCAL_INVISIBLE){
   6865             qb.appendWhere(Contacts._ID + " NOT IN " + Tables.DEFAULT_DIRECTORY);
   6866             return true;
   6867         }
   6868         return false;
   6869     }
   6870 
   6871     private void appendAccountFromParameter(SQLiteQueryBuilder qb, Uri uri) {
   6872         final String accountName = getQueryParameter(uri, RawContacts.ACCOUNT_NAME);
   6873         final String accountType = getQueryParameter(uri, RawContacts.ACCOUNT_TYPE);
   6874         final String dataSet = getQueryParameter(uri, RawContacts.DATA_SET);
   6875 
   6876         final boolean partialUri = TextUtils.isEmpty(accountName) ^ TextUtils.isEmpty(accountType);
   6877         if (partialUri) {
   6878             // Throw when either account is incomplete
   6879             throw new IllegalArgumentException(mDbHelper.get().exceptionMessage(
   6880                     "Must specify both or neither of ACCOUNT_NAME and ACCOUNT_TYPE", uri));
   6881         }
   6882 
   6883         // Accounts are valid by only checking one parameter, since we've
   6884         // already ruled out partial accounts.
   6885         final boolean validAccount = !TextUtils.isEmpty(accountName);
   6886         if (validAccount) {
   6887             String toAppend = RawContacts.ACCOUNT_NAME + "="
   6888                     + DatabaseUtils.sqlEscapeString(accountName) + " AND "
   6889                     + RawContacts.ACCOUNT_TYPE + "="
   6890                     + DatabaseUtils.sqlEscapeString(accountType);
   6891             if (dataSet == null) {
   6892                 toAppend += " AND " + RawContacts.DATA_SET + " IS NULL";
   6893             } else {
   6894                 toAppend += " AND " + RawContacts.DATA_SET + "=" +
   6895                         DatabaseUtils.sqlEscapeString(dataSet);
   6896             }
   6897             qb.appendWhere(toAppend);
   6898         } else {
   6899             qb.appendWhere("1");
   6900         }
   6901     }
   6902 
   6903     private String appendAccountToSelection(Uri uri, String selection) {
   6904         final String accountName = getQueryParameter(uri, RawContacts.ACCOUNT_NAME);
   6905         final String accountType = getQueryParameter(uri, RawContacts.ACCOUNT_TYPE);
   6906         final String dataSet = getQueryParameter(uri, RawContacts.DATA_SET);
   6907 
   6908         final boolean partialUri = TextUtils.isEmpty(accountName) ^ TextUtils.isEmpty(accountType);
   6909         if (partialUri) {
   6910             // Throw when either account is incomplete
   6911             throw new IllegalArgumentException(mDbHelper.get().exceptionMessage(
   6912                     "Must specify both or neither of ACCOUNT_NAME and ACCOUNT_TYPE", uri));
   6913         }
   6914 
   6915         // Accounts are valid by only checking one parameter, since we've
   6916         // already ruled out partial accounts.
   6917         final boolean validAccount = !TextUtils.isEmpty(accountName);
   6918         if (validAccount) {
   6919             StringBuilder selectionSb = new StringBuilder(RawContacts.ACCOUNT_NAME + "="
   6920                     + DatabaseUtils.sqlEscapeString(accountName) + " AND "
   6921                     + RawContacts.ACCOUNT_TYPE + "="
   6922                     + DatabaseUtils.sqlEscapeString(accountType));
   6923             if (dataSet == null) {
   6924                 selectionSb.append(" AND " + RawContacts.DATA_SET + " IS NULL");
   6925             } else {
   6926                 selectionSb.append(" AND " + RawContacts.DATA_SET + "=")
   6927                         .append(DatabaseUtils.sqlEscapeString(dataSet));
   6928             }
   6929             if (!TextUtils.isEmpty(selection)) {
   6930                 selectionSb.append(" AND (");
   6931                 selectionSb.append(selection);
   6932                 selectionSb.append(')');
   6933             }
   6934             return selectionSb.toString();
   6935         } else {
   6936             return selection;
   6937         }
   6938     }
   6939 
   6940     /**
   6941      * Gets the value of the "limit" URI query parameter.
   6942      *
   6943      * @return A string containing a non-negative integer, or <code>null</code> if
   6944      *         the parameter is not set, or is set to an invalid value.
   6945      */
   6946     private String getLimit(Uri uri) {
   6947         String limitParam = getQueryParameter(uri, ContactsContract.LIMIT_PARAM_KEY);
   6948         if (limitParam == null) {
   6949             return null;
   6950         }
   6951         // make sure that the limit is a non-negative integer
   6952         try {
   6953             int l = Integer.parseInt(limitParam);
   6954             if (l < 0) {
   6955                 Log.w(TAG, "Invalid limit parameter: " + limitParam);
   6956                 return null;
   6957             }
   6958             return String.valueOf(l);
   6959         } catch (NumberFormatException ex) {
   6960             Log.w(TAG, "Invalid limit parameter: " + limitParam);
   6961             return null;
   6962         }
   6963     }
   6964 
   6965     @Override
   6966     public AssetFileDescriptor openAssetFile(Uri uri, String mode) throws FileNotFoundException {
   6967         if (mode.equals("r")) {
   6968             waitForAccess(mReadAccessLatch);
   6969         } else {
   6970             waitForAccess(mWriteAccessLatch);
   6971         }
   6972         if (mapsToProfileDb(uri)) {
   6973             switchToProfileMode();
   6974             return mProfileProvider.openAssetFile(uri, mode);
   6975         } else {
   6976             switchToContactMode();
   6977             return openAssetFileLocal(uri, mode);
   6978         }
   6979     }
   6980 
   6981     public AssetFileDescriptor openAssetFileLocal(Uri uri, String mode)
   6982             throws FileNotFoundException {
   6983 
   6984         // Default active DB to the contacts DB if none has been set.
   6985         if (mActiveDb.get() == null) {
   6986             if (mode.equals("r")) {
   6987                 mActiveDb.set(mContactsHelper.getReadableDatabase());
   6988             } else {
   6989                 mActiveDb.set(mContactsHelper.getWritableDatabase());
   6990             }
   6991         }
   6992 
   6993         int match = sUriMatcher.match(uri);
   6994         switch (match) {
   6995             case CONTACTS_ID_PHOTO: {
   6996                 long contactId = Long.parseLong(uri.getPathSegments().get(1));
   6997                 return openPhotoAssetFile(mActiveDb.get(), uri, mode,
   6998                         Data._ID + "=" + Contacts.PHOTO_ID + " AND " +
   6999                                 RawContacts.CONTACT_ID + "=?",
   7000                         new String[]{String.valueOf(contactId)});
   7001             }
   7002 
   7003             case CONTACTS_ID_DISPLAY_PHOTO: {
   7004                 if (!mode.equals("r")) {
   7005                     throw new IllegalArgumentException(
   7006                             "Display photos retrieved by contact ID can only be read.");
   7007                 }
   7008                 long contactId = Long.parseLong(uri.getPathSegments().get(1));
   7009                 Cursor c = mActiveDb.get().query(Tables.CONTACTS,
   7010                         new String[]{Contacts.PHOTO_FILE_ID},
   7011                         Contacts._ID + "=?", new String[]{String.valueOf(contactId)},
   7012                         null, null, null);
   7013                 try {
   7014                     if (c.moveToFirst()) {
   7015                         long photoFileId = c.getLong(0);
   7016                         return openDisplayPhotoForRead(photoFileId);
   7017                     } else {
   7018                         // No contact for this ID.
   7019                         throw new FileNotFoundException(uri.toString());
   7020                     }
   7021                 } finally {
   7022                     c.close();
   7023                 }
   7024             }
   7025 
   7026             case PROFILE_DISPLAY_PHOTO: {
   7027                 if (!mode.equals("r")) {
   7028                     throw new IllegalArgumentException(
   7029                             "Display photos retrieved by contact ID can only be read.");
   7030                 }
   7031                 Cursor c = mActiveDb.get().query(Tables.CONTACTS,
   7032                         new String[]{Contacts.PHOTO_FILE_ID}, null, null, null, null, null);
   7033                 try {
   7034                     if (c.moveToFirst()) {
   7035                         long photoFileId = c.getLong(0);
   7036                         return openDisplayPhotoForRead(photoFileId);
   7037                     } else {
   7038                         // No profile record.
   7039                         throw new FileNotFoundException(uri.toString());
   7040                     }
   7041                 } finally {
   7042                     c.close();
   7043                 }
   7044             }
   7045 
   7046             case CONTACTS_LOOKUP_PHOTO:
   7047             case CONTACTS_LOOKUP_ID_PHOTO:
   7048             case CONTACTS_LOOKUP_DISPLAY_PHOTO:
   7049             case CONTACTS_LOOKUP_ID_DISPLAY_PHOTO: {
   7050                 if (!mode.equals("r")) {
   7051                     throw new IllegalArgumentException(
   7052                             "Photos retrieved by contact lookup key can only be read.");
   7053                 }
   7054                 List<String> pathSegments = uri.getPathSegments();
   7055                 int segmentCount = pathSegments.size();
   7056                 if (segmentCount < 4) {
   7057                     throw new IllegalArgumentException(mDbHelper.get().exceptionMessage(
   7058                             "Missing a lookup key", uri));
   7059                 }
   7060 
   7061                 boolean forDisplayPhoto = (match == CONTACTS_LOOKUP_ID_DISPLAY_PHOTO
   7062                         || match == CONTACTS_LOOKUP_DISPLAY_PHOTO);
   7063                 String lookupKey = pathSegments.get(2);
   7064                 String[] projection = new String[]{Contacts.PHOTO_ID, Contacts.PHOTO_FILE_ID};
   7065                 if (segmentCount == 5) {
   7066                     long contactId = Long.parseLong(pathSegments.get(3));
   7067                     SQLiteQueryBuilder lookupQb = new SQLiteQueryBuilder();
   7068                     setTablesAndProjectionMapForContacts(lookupQb, uri, projection);
   7069                     Cursor c = queryWithContactIdAndLookupKey(lookupQb, mActiveDb.get(), uri,
   7070                             projection, null, null, null, null, null,
   7071                             Contacts._ID, contactId, Contacts.LOOKUP_KEY, lookupKey);
   7072                     if (c != null) {
   7073                         try {
   7074                             c.moveToFirst();
   7075                             if (forDisplayPhoto) {
   7076                                 long photoFileId =
   7077                                         c.getLong(c.getColumnIndex(Contacts.PHOTO_FILE_ID));
   7078                                 return openDisplayPhotoForRead(photoFileId);
   7079                             } else {
   7080                                 long photoId = c.getLong(c.getColumnIndex(Contacts.PHOTO_ID));
   7081                                 return openPhotoAssetFile(mActiveDb.get(), uri, mode,
   7082                                         Data._ID + "=?", new String[]{String.valueOf(photoId)});
   7083                             }
   7084                         } finally {
   7085                             c.close();
   7086                         }
   7087                     }
   7088                 }
   7089 
   7090                 SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
   7091                 setTablesAndProjectionMapForContacts(qb, uri, projection);
   7092                 long contactId = lookupContactIdByLookupKey(mActiveDb.get(), lookupKey);
   7093                 Cursor c = qb.query(mActiveDb.get(), projection, Contacts._ID + "=?",
   7094                         new String[]{String.valueOf(contactId)}, null, null, null);
   7095                 try {
   7096                     c.moveToFirst();
   7097                     if (forDisplayPhoto) {
   7098                         long photoFileId = c.getLong(c.getColumnIndex(Contacts.PHOTO_FILE_ID));
   7099                         return openDisplayPhotoForRead(photoFileId);
   7100                     } else {
   7101                         long photoId = c.getLong(c.getColumnIndex(Contacts.PHOTO_ID));
   7102                         return openPhotoAssetFile(mActiveDb.get(), uri, mode,
   7103                                 Data._ID + "=?", new String[]{String.valueOf(photoId)});
   7104                     }
   7105                 } finally {
   7106                     c.close();
   7107                 }
   7108             }
   7109 
   7110             case RAW_CONTACTS_ID_DISPLAY_PHOTO: {
   7111                 long rawContactId = Long.parseLong(uri.getPathSegments().get(1));
   7112                 boolean writeable = !mode.equals("r");
   7113 
   7114                 // Find the primary photo data record for this raw contact.
   7115                 SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
   7116                 String[] projection = new String[]{Data._ID, Photo.PHOTO_FILE_ID};
   7117                 setTablesAndProjectionMapForData(qb, uri, projection, false);
   7118                 long photoMimetypeId = mDbHelper.get().getMimeTypeId(Photo.CONTENT_ITEM_TYPE);
   7119                 Cursor c = qb.query(mActiveDb.get(), projection,
   7120                         Data.RAW_CONTACT_ID + "=? AND " + DataColumns.MIMETYPE_ID + "=?",
   7121                         new String[]{String.valueOf(rawContactId), String.valueOf(photoMimetypeId)},
   7122                         null, null, Data.IS_PRIMARY + " DESC");
   7123                 long dataId = 0;
   7124                 long photoFileId = 0;
   7125                 try {
   7126                     if (c.getCount() >= 1) {
   7127                         c.moveToFirst();
   7128                         dataId = c.getLong(0);
   7129                         photoFileId = c.getLong(1);
   7130                     }
   7131                 } finally {
   7132                     c.close();
   7133                 }
   7134 
   7135                 // If writeable, open a writeable file descriptor that we can monitor.
   7136                 // When the caller finishes writing content, we'll process the photo and
   7137                 // update the data record.
   7138                 if (writeable) {
   7139                     return openDisplayPhotoForWrite(rawContactId, dataId, uri, mode);
   7140                 } else {
   7141                     return openDisplayPhotoForRead(photoFileId);
   7142                 }
   7143             }
   7144 
   7145             case DISPLAY_PHOTO: {
   7146                 long photoFileId = ContentUris.parseId(uri);
   7147                 if (!mode.equals("r")) {
   7148                     throw new IllegalArgumentException(
   7149                             "Display photos retrieved by key can only be read.");
   7150                 }
   7151                 return openDisplayPhotoForRead(photoFileId);
   7152             }
   7153 
   7154             case DATA_ID: {
   7155                 long dataId = Long.parseLong(uri.getPathSegments().get(1));
   7156                 long photoMimetypeId = mDbHelper.get().getMimeTypeId(Photo.CONTENT_ITEM_TYPE);
   7157                 return openPhotoAssetFile(mActiveDb.get(), uri, mode,
   7158                         Data._ID + "=? AND " + DataColumns.MIMETYPE_ID + "=" + photoMimetypeId,
   7159                         new String[]{String.valueOf(dataId)});
   7160             }
   7161 
   7162             case PROFILE_AS_VCARD: {
   7163                 // When opening a contact as file, we pass back contents as a
   7164                 // vCard-encoded stream. We build into a local buffer first,
   7165                 // then pipe into MemoryFile once the exact size is known.
   7166                 final ByteArrayOutputStream localStream = new ByteArrayOutputStream();
   7167                 outputRawContactsAsVCard(uri, localStream, null, null);
   7168                 return buildAssetFileDescriptor(localStream);
   7169             }
   7170 
   7171             case CONTACTS_AS_VCARD: {
   7172                 // When opening a contact as file, we pass back contents as a
   7173                 // vCard-encoded stream. We build into a local buffer first,
   7174                 // then pipe into MemoryFile once the exact size is known.
   7175                 final ByteArrayOutputStream localStream = new ByteArrayOutputStream();
   7176                 outputRawContactsAsVCard(uri, localStream, null, null);
   7177                 return buildAssetFileDescriptor(localStream);
   7178             }
   7179 
   7180             case CONTACTS_AS_MULTI_VCARD: {
   7181                 final String lookupKeys = uri.getPathSegments().get(2);
   7182                 final String[] loopupKeyList = lookupKeys.split(":");
   7183                 final StringBuilder inBuilder = new StringBuilder();
   7184                 Uri queryUri = Contacts.CONTENT_URI;
   7185                 int index = 0;
   7186 
   7187                 // SQLite has limits on how many parameters can be used
   7188                 // so the IDs are concatenated to a query string here instead
   7189                 for (String lookupKey : loopupKeyList) {
   7190                     if (index == 0) {
   7191                         inBuilder.append("(");
   7192                     } else {
   7193                         inBuilder.append(",");
   7194                     }
   7195                     // TODO: Figure out what to do if the profile contact is in the list.
   7196                     long contactId = lookupContactIdByLookupKey(mActiveDb.get(), lookupKey);
   7197                     inBuilder.append(contactId);
   7198                     index++;
   7199                 }
   7200                 inBuilder.append(')');
   7201                 final String selection = Contacts._ID + " IN " + inBuilder.toString();
   7202 
   7203                 // When opening a contact as file, we pass back contents as a
   7204                 // vCard-encoded stream. We build into a local buffer first,
   7205                 // then pipe into MemoryFile once the exact size is known.
   7206                 final ByteArrayOutputStream localStream = new ByteArrayOutputStream();
   7207                 outputRawContactsAsVCard(queryUri, localStream, selection, null);
   7208                 return buildAssetFileDescriptor(localStream);
   7209             }
   7210 
   7211             default:
   7212                 throw new FileNotFoundException(mDbHelper.get().exceptionMessage(
   7213                         "File does not exist", uri));
   7214         }
   7215     }
   7216 
   7217     private AssetFileDescriptor openPhotoAssetFile(SQLiteDatabase db, Uri uri, String mode,
   7218             String selection, String[] selectionArgs)
   7219             throws FileNotFoundException {
   7220         if (!"r".equals(mode)) {
   7221             throw new FileNotFoundException(mDbHelper.get().exceptionMessage("Mode " + mode
   7222                     + " not supported.", uri));
   7223         }
   7224 
   7225         String sql =
   7226                 "SELECT " + Photo.PHOTO + " FROM " + Views.DATA +
   7227                 " WHERE " + selection;
   7228         try {
   7229             return makeAssetFileDescriptor(
   7230                     DatabaseUtils.blobFileDescriptorForQuery(db, sql, selectionArgs));
   7231         } catch (SQLiteDoneException e) {
   7232             // this will happen if the DB query returns no rows (i.e. contact does not exist)
   7233             throw new FileNotFoundException(uri.toString());
   7234         }
   7235     }
   7236 
   7237     /**
   7238      * Opens a display photo from the photo store for reading.
   7239      * @param photoFileId The display photo file ID
   7240      * @return An asset file descriptor that allows the file to be read.
   7241      * @throws FileNotFoundException If no photo file for the given ID exists.
   7242      */
   7243     private AssetFileDescriptor openDisplayPhotoForRead(long photoFileId)
   7244             throws FileNotFoundException {
   7245         PhotoStore.Entry entry = mPhotoStore.get().get(photoFileId);
   7246         if (entry != null) {
   7247             try {
   7248                 return makeAssetFileDescriptor(
   7249                         ParcelFileDescriptor.open(new File(entry.path),
   7250                                 ParcelFileDescriptor.MODE_READ_ONLY),
   7251                         entry.size);
   7252             } catch (FileNotFoundException fnfe) {
   7253                 scheduleBackgroundTask(BACKGROUND_TASK_CLEANUP_PHOTOS);
   7254                 throw fnfe;
   7255             }
   7256         } else {
   7257             scheduleBackgroundTask(BACKGROUND_TASK_CLEANUP_PHOTOS);
   7258             throw new FileNotFoundException("No photo file found for ID " + photoFileId);
   7259         }
   7260     }
   7261 
   7262     /**
   7263      * Opens a file descriptor for a photo to be written.  When the caller completes writing
   7264      * to the file (closing the output stream), the image will be parsed out and processed.
   7265      * If processing succeeds, the given raw contact ID's primary photo record will be
   7266      * populated with the inserted image (if no primary photo record exists, the data ID can
   7267      * be left as 0, and a new data record will be inserted).
   7268      * @param rawContactId Raw contact ID this photo entry should be associated with.
   7269      * @param dataId Data ID for a photo mimetype that will be updated with the inserted
   7270      *     image.  May be set to 0, in which case the inserted image will trigger creation
   7271      *     of a new primary photo image data row for the raw contact.
   7272      * @param uri The URI being used to access this file.
   7273      * @param mode Read/write mode string.
   7274      * @return An asset file descriptor the caller can use to write an image file for the
   7275      *     raw contact.
   7276      */
   7277     private AssetFileDescriptor openDisplayPhotoForWrite(long rawContactId, long dataId, Uri uri,
   7278             String mode) {
   7279         try {
   7280             ParcelFileDescriptor[] pipeFds = ParcelFileDescriptor.createPipe();
   7281             PipeMonitor pipeMonitor = new PipeMonitor(rawContactId, dataId, pipeFds[0]);
   7282             pipeMonitor.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, (Object[]) null);
   7283             return new AssetFileDescriptor(pipeFds[1], 0, AssetFileDescriptor.UNKNOWN_LENGTH);
   7284         } catch (IOException ioe) {
   7285             Log.e(TAG, "Could not create temp image file in mode " + mode);
   7286             return null;
   7287         }
   7288     }
   7289 
   7290     /**
   7291      * Async task that monitors the given file descriptor (the read end of a pipe) for
   7292      * the writer finishing.  If the data from the pipe contains a valid image, the image
   7293      * is either inserted into the given raw contact or updated in the given data row.
   7294      */
   7295     private class PipeMonitor extends AsyncTask<Object, Object, Object> {
   7296         private final ParcelFileDescriptor mDescriptor;
   7297         private final long mRawContactId;
   7298         private final long mDataId;
   7299         private PipeMonitor(long rawContactId, long dataId, ParcelFileDescriptor descriptor) {
   7300             mRawContactId = rawContactId;
   7301             mDataId = dataId;
   7302             mDescriptor = descriptor;
   7303         }
   7304 
   7305         @Override
   7306         protected Object doInBackground(Object... params) {
   7307             AutoCloseInputStream is = new AutoCloseInputStream(mDescriptor);
   7308             try {
   7309                 Bitmap b = BitmapFactory.decodeStream(is);
   7310                 if (b != null) {
   7311                     waitForAccess(mWriteAccessLatch);
   7312                     PhotoProcessor processor = new PhotoProcessor(b, mMaxDisplayPhotoDim,
   7313                             mMaxThumbnailPhotoDim);
   7314 
   7315                     // Store the compressed photo in the photo store.
   7316                     PhotoStore photoStore = ContactsContract.isProfileId(mRawContactId)
   7317                             ? mProfilePhotoStore
   7318                             : mContactsPhotoStore;
   7319                     long photoFileId = photoStore.insert(processor);
   7320 
   7321                     // Depending on whether we already had a data row to attach the photo
   7322                     // to, do an update or insert.
   7323                     if (mDataId != 0) {
   7324                         // Update the data record with the new photo.
   7325                         ContentValues updateValues = new ContentValues();
   7326 
   7327                         // Signal that photo processing has already been handled.
   7328                         updateValues.put(DataRowHandlerForPhoto.SKIP_PROCESSING_KEY, true);
   7329 
   7330                         if (photoFileId != 0) {
   7331                             updateValues.put(Photo.PHOTO_FILE_ID, photoFileId);
   7332                         }
   7333                         updateValues.put(Photo.PHOTO, processor.getThumbnailPhotoBytes());
   7334                         update(ContentUris.withAppendedId(Data.CONTENT_URI, mDataId),
   7335                                 updateValues, null, null);
   7336                     } else {
   7337                         // Insert a new primary data record with the photo.
   7338                         ContentValues insertValues = new ContentValues();
   7339 
   7340                         // Signal that photo processing has already been handled.
   7341                         insertValues.put(DataRowHandlerForPhoto.SKIP_PROCESSING_KEY, true);
   7342 
   7343                         insertValues.put(Data.MIMETYPE, Photo.CONTENT_ITEM_TYPE);
   7344                         insertValues.put(Data.IS_PRIMARY, 1);
   7345                         if (photoFileId != 0) {
   7346                             insertValues.put(Photo.PHOTO_FILE_ID, photoFileId);
   7347                         }
   7348                         insertValues.put(Photo.PHOTO, processor.getThumbnailPhotoBytes());
   7349                         insert(RawContacts.CONTENT_URI.buildUpon()
   7350                                 .appendPath(String.valueOf(mRawContactId))
   7351                                 .appendPath(RawContacts.Data.CONTENT_DIRECTORY).build(),
   7352                                 insertValues);
   7353                     }
   7354 
   7355                 }
   7356             } catch (IOException e) {
   7357                 throw new RuntimeException(e);
   7358             }
   7359             return null;
   7360         }
   7361     }
   7362 
   7363     private static final String CONTACT_MEMORY_FILE_NAME = "contactAssetFile";
   7364 
   7365     /**
   7366      * Returns an {@link AssetFileDescriptor} backed by the
   7367      * contents of the given {@link ByteArrayOutputStream}.
   7368      */
   7369     private AssetFileDescriptor buildAssetFileDescriptor(ByteArrayOutputStream stream) {
   7370         try {
   7371             stream.flush();
   7372 
   7373             final byte[] byteData = stream.toByteArray();
   7374 
   7375             return makeAssetFileDescriptor(
   7376                     ParcelFileDescriptor.fromData(byteData, CONTACT_MEMORY_FILE_NAME),
   7377                     byteData.length);
   7378         } catch (IOException e) {
   7379             Log.w(TAG, "Problem writing stream into an ParcelFileDescriptor: " + e.toString());
   7380             return null;
   7381         }
   7382     }
   7383 
   7384     private AssetFileDescriptor makeAssetFileDescriptor(ParcelFileDescriptor fd) {
   7385         return makeAssetFileDescriptor(fd, AssetFileDescriptor.UNKNOWN_LENGTH);
   7386     }
   7387 
   7388     private AssetFileDescriptor makeAssetFileDescriptor(ParcelFileDescriptor fd, long length) {
   7389         return fd != null ? new AssetFileDescriptor(fd, 0, length) : null;
   7390     }
   7391 
   7392     /**
   7393      * Output {@link RawContacts} matching the requested selection in the vCard
   7394      * format to the given {@link OutputStream}. This method returns silently if
   7395      * any errors encountered.
   7396      */
   7397     private void outputRawContactsAsVCard(Uri uri, OutputStream stream,
   7398             String selection, String[] selectionArgs) {
   7399         final Context context = this.getContext();
   7400         int vcardconfig = VCardConfig.VCARD_TYPE_DEFAULT;
   7401         if(uri.getBooleanQueryParameter(
   7402                 Contacts.QUERY_PARAMETER_VCARD_NO_PHOTO, false)) {
   7403             vcardconfig |= VCardConfig.FLAG_REFRAIN_IMAGE_EXPORT;
   7404         }
   7405         final VCardComposer composer =
   7406                 new VCardComposer(context, vcardconfig, false);
   7407         Writer writer = null;
   7408         final Uri rawContactsUri;
   7409         if (mapsToProfileDb(uri)) {
   7410             // Pre-authorize the URI, since the caller would have already gone through the
   7411             // permission check to get here, but the pre-authorization at the top level wouldn't
   7412             // carry over to the raw contact.
   7413             rawContactsUri = preAuthorizeUri(RawContactsEntity.PROFILE_CONTENT_URI);
   7414         } else {
   7415             rawContactsUri = RawContactsEntity.CONTENT_URI;
   7416         }
   7417         try {
   7418             writer = new BufferedWriter(new OutputStreamWriter(stream));
   7419             if (!composer.init(uri, selection, selectionArgs, null, rawContactsUri)) {
   7420                 Log.w(TAG, "Failed to init VCardComposer");
   7421                 return;
   7422             }
   7423 
   7424             while (!composer.isAfterLast()) {
   7425                 writer.write(composer.createOneEntry());
   7426             }
   7427         } catch (IOException e) {
   7428             Log.e(TAG, "IOException: " + e);
   7429         } finally {
   7430             composer.terminate();
   7431             if (writer != null) {
   7432                 try {
   7433                     writer.close();
   7434                 } catch (IOException e) {
   7435                     Log.w(TAG, "IOException during closing output stream: " + e);
   7436                 }
   7437             }
   7438         }
   7439     }
   7440 
   7441     @Override
   7442     public String getType(Uri uri) {
   7443 
   7444         waitForAccess(mReadAccessLatch);
   7445 
   7446         final int match = sUriMatcher.match(uri);
   7447         switch (match) {
   7448             case CONTACTS:
   7449                 return Contacts.CONTENT_TYPE;
   7450             case CONTACTS_LOOKUP:
   7451             case CONTACTS_ID:
   7452             case CONTACTS_LOOKUP_ID:
   7453             case PROFILE:
   7454                 return Contacts.CONTENT_ITEM_TYPE;
   7455             case CONTACTS_AS_VCARD:
   7456             case CONTACTS_AS_MULTI_VCARD:
   7457             case PROFILE_AS_VCARD:
   7458                 return Contacts.CONTENT_VCARD_TYPE;
   7459             case CONTACTS_ID_PHOTO:
   7460             case CONTACTS_LOOKUP_PHOTO:
   7461             case CONTACTS_LOOKUP_ID_PHOTO:
   7462             case CONTACTS_ID_DISPLAY_PHOTO:
   7463             case CONTACTS_LOOKUP_DISPLAY_PHOTO:
   7464             case CONTACTS_LOOKUP_ID_DISPLAY_PHOTO:
   7465             case RAW_CONTACTS_ID_DISPLAY_PHOTO:
   7466             case DISPLAY_PHOTO:
   7467                 return "image/jpeg";
   7468             case RAW_CONTACTS:
   7469             case PROFILE_RAW_CONTACTS:
   7470                 return RawContacts.CONTENT_TYPE;
   7471             case RAW_CONTACTS_ID:
   7472             case PROFILE_RAW_CONTACTS_ID:
   7473                 return RawContacts.CONTENT_ITEM_TYPE;
   7474             case DATA:
   7475             case PROFILE_DATA:
   7476                 return Data.CONTENT_TYPE;
   7477             case DATA_ID:
   7478                 long id = ContentUris.parseId(uri);
   7479                 if (ContactsContract.isProfileId(id)) {
   7480                     return mProfileHelper.getDataMimeType(id);
   7481                 } else {
   7482                     return mContactsHelper.getDataMimeType(id);
   7483                 }
   7484             case PHONES:
   7485                 return Phone.CONTENT_TYPE;
   7486             case PHONES_ID:
   7487                 return Phone.CONTENT_ITEM_TYPE;
   7488             case PHONE_LOOKUP:
   7489                 return PhoneLookup.CONTENT_TYPE;
   7490             case EMAILS:
   7491                 return Email.CONTENT_TYPE;
   7492             case EMAILS_ID:
   7493                 return Email.CONTENT_ITEM_TYPE;
   7494             case POSTALS:
   7495                 return StructuredPostal.CONTENT_TYPE;
   7496             case POSTALS_ID:
   7497                 return StructuredPostal.CONTENT_ITEM_TYPE;
   7498             case AGGREGATION_EXCEPTIONS:
   7499                 return AggregationExceptions.CONTENT_TYPE;
   7500             case AGGREGATION_EXCEPTION_ID:
   7501                 return AggregationExceptions.CONTENT_ITEM_TYPE;
   7502             case SETTINGS:
   7503                 return Settings.CONTENT_TYPE;
   7504             case AGGREGATION_SUGGESTIONS:
   7505                 return Contacts.CONTENT_TYPE;
   7506             case SEARCH_SUGGESTIONS:
   7507                 return SearchManager.SUGGEST_MIME_TYPE;
   7508             case SEARCH_SHORTCUT:
   7509                 return SearchManager.SHORTCUT_MIME_TYPE;
   7510             case DIRECTORIES:
   7511                 return Directory.CONTENT_TYPE;
   7512             case DIRECTORIES_ID:
   7513                 return Directory.CONTENT_ITEM_TYPE;
   7514             case STREAM_ITEMS:
   7515                 return StreamItems.CONTENT_TYPE;
   7516             case STREAM_ITEMS_ID:
   7517                 return StreamItems.CONTENT_ITEM_TYPE;
   7518             case STREAM_ITEMS_ID_PHOTOS:
   7519                 return StreamItems.StreamItemPhotos.CONTENT_TYPE;
   7520             case STREAM_ITEMS_ID_PHOTOS_ID:
   7521                 return StreamItems.StreamItemPhotos.CONTENT_ITEM_TYPE;
   7522             case STREAM_ITEMS_PHOTOS:
   7523                 throw new UnsupportedOperationException("Not supported for write-only URI " + uri);
   7524             default:
   7525                 return mLegacyApiSupport.getType(uri);
   7526         }
   7527     }
   7528 
   7529     public String[] getDefaultProjection(Uri uri) {
   7530         final int match = sUriMatcher.match(uri);
   7531         switch (match) {
   7532             case CONTACTS:
   7533             case CONTACTS_LOOKUP:
   7534             case CONTACTS_ID:
   7535             case CONTACTS_LOOKUP_ID:
   7536             case AGGREGATION_SUGGESTIONS:
   7537             case PROFILE:
   7538                 return sContactsProjectionMap.getColumnNames();
   7539 
   7540             case CONTACTS_ID_ENTITIES:
   7541             case PROFILE_ENTITIES:
   7542                 return sEntityProjectionMap.getColumnNames();
   7543 
   7544             case CONTACTS_AS_VCARD:
   7545             case CONTACTS_AS_MULTI_VCARD:
   7546             case PROFILE_AS_VCARD:
   7547                 return sContactsVCardProjectionMap.getColumnNames();
   7548 
   7549             case RAW_CONTACTS:
   7550             case RAW_CONTACTS_ID:
   7551             case PROFILE_RAW_CONTACTS:
   7552             case PROFILE_RAW_CONTACTS_ID:
   7553                 return sRawContactsProjectionMap.getColumnNames();
   7554 
   7555             case DATA_ID:
   7556             case PHONES:
   7557             case PHONES_ID:
   7558             case EMAILS:
   7559             case EMAILS_ID:
   7560             case POSTALS:
   7561             case POSTALS_ID:
   7562             case PROFILE_DATA:
   7563                 return sDataProjectionMap.getColumnNames();
   7564 
   7565             case PHONE_LOOKUP:
   7566                 return sPhoneLookupProjectionMap.getColumnNames();
   7567 
   7568             case AGGREGATION_EXCEPTIONS:
   7569             case AGGREGATION_EXCEPTION_ID:
   7570                 return sAggregationExceptionsProjectionMap.getColumnNames();
   7571 
   7572             case SETTINGS:
   7573                 return sSettingsProjectionMap.getColumnNames();
   7574 
   7575             case DIRECTORIES:
   7576             case DIRECTORIES_ID:
   7577                 return sDirectoryProjectionMap.getColumnNames();
   7578 
   7579             default:
   7580                 return null;
   7581         }
   7582     }
   7583 
   7584     private class StructuredNameLookupBuilder extends NameLookupBuilder {
   7585 
   7586         public StructuredNameLookupBuilder(NameSplitter splitter) {
   7587             super(splitter);
   7588         }
   7589 
   7590         @Override
   7591         protected void insertNameLookup(long rawContactId, long dataId, int lookupType,
   7592                 String name) {
   7593             mDbHelper.get().insertNameLookup(rawContactId, dataId, lookupType, name);
   7594         }
   7595 
   7596         @Override
   7597         protected String[] getCommonNicknameClusters(String normalizedName) {
   7598             return mCommonNicknameCache.getCommonNicknameClusters(normalizedName);
   7599         }
   7600     }
   7601 
   7602     public void appendContactFilterAsNestedQuery(StringBuilder sb, String filterParam) {
   7603         sb.append("(" +
   7604                 "SELECT DISTINCT " + RawContacts.CONTACT_ID +
   7605                 " FROM " + Tables.RAW_CONTACTS +
   7606                 " JOIN " + Tables.NAME_LOOKUP +
   7607                 " ON(" + RawContactsColumns.CONCRETE_ID + "="
   7608                         + NameLookupColumns.RAW_CONTACT_ID + ")" +
   7609                 " WHERE normalized_name GLOB '");
   7610         sb.append(NameNormalizer.normalize(filterParam));
   7611         sb.append("*' AND " + NameLookupColumns.NAME_TYPE +
   7612                     " IN(" + CONTACT_LOOKUP_NAME_TYPES + "))");
   7613     }
   7614 
   7615     public boolean isPhoneNumber(String filter) {
   7616         boolean atLeastOneDigit = false;
   7617         int len = filter.length();
   7618         for (int i = 0; i < len; i++) {
   7619             char c = filter.charAt(i);
   7620             if (c >= '0' && c <= '9') {
   7621                 atLeastOneDigit = true;
   7622             } else if (c != '*' && c != '#' && c != '+' && c != 'N' && c != '.' && c != ';'
   7623                     && c != '-' && c != '(' && c != ')' && c != ' ') {
   7624                 return false;
   7625             }
   7626         }
   7627         return atLeastOneDigit;
   7628     }
   7629 
   7630     /**
   7631      * Takes components of a name from the query parameters and returns a cursor with those
   7632      * components as well as all missing components.  There is no database activity involved
   7633      * in this so the call can be made on the UI thread.
   7634      */
   7635     private Cursor completeName(Uri uri, String[] projection) {
   7636         if (projection == null) {
   7637             projection = sDataProjectionMap.getColumnNames();
   7638         }
   7639 
   7640         ContentValues values = new ContentValues();
   7641         DataRowHandlerForStructuredName handler = (DataRowHandlerForStructuredName)
   7642                 getDataRowHandler(StructuredName.CONTENT_ITEM_TYPE);
   7643 
   7644         copyQueryParamsToContentValues(values, uri,
   7645                 StructuredName.DISPLAY_NAME,
   7646                 StructuredName.PREFIX,
   7647                 StructuredName.GIVEN_NAME,
   7648                 StructuredName.MIDDLE_NAME,
   7649                 StructuredName.FAMILY_NAME,
   7650                 StructuredName.SUFFIX,
   7651                 StructuredName.PHONETIC_NAME,
   7652                 StructuredName.PHONETIC_FAMILY_NAME,
   7653                 StructuredName.PHONETIC_MIDDLE_NAME,
   7654                 StructuredName.PHONETIC_GIVEN_NAME
   7655         );
   7656 
   7657         handler.fixStructuredNameComponents(values, values);
   7658 
   7659         MatrixCursor cursor = new MatrixCursor(projection);
   7660         Object[] row = new Object[projection.length];
   7661         for (int i = 0; i < projection.length; i++) {
   7662             row[i] = values.get(projection[i]);
   7663         }
   7664         cursor.addRow(row);
   7665         return cursor;
   7666     }
   7667 
   7668     private void copyQueryParamsToContentValues(ContentValues values, Uri uri, String... columns) {
   7669         for (String column : columns) {
   7670             String param = uri.getQueryParameter(column);
   7671             if (param != null) {
   7672                 values.put(column, param);
   7673             }
   7674         }
   7675     }
   7676 
   7677 
   7678     /**
   7679      * Inserts an argument at the beginning of the selection arg list.
   7680      */
   7681     private String[] insertSelectionArg(String[] selectionArgs, String arg) {
   7682         if (selectionArgs == null) {
   7683             return new String[] {arg};
   7684         } else {
   7685             int newLength = selectionArgs.length + 1;
   7686             String[] newSelectionArgs = new String[newLength];
   7687             newSelectionArgs[0] = arg;
   7688             System.arraycopy(selectionArgs, 0, newSelectionArgs, 1, selectionArgs.length);
   7689             return newSelectionArgs;
   7690         }
   7691     }
   7692 
   7693     private String[] appendProjectionArg(String[] projection, String arg) {
   7694         if (projection == null) {
   7695             return null;
   7696         }
   7697         final int length = projection.length;
   7698         String[] newProjection = new String[length + 1];
   7699         System.arraycopy(projection, 0, newProjection, 0, length);
   7700         newProjection[length] = arg;
   7701         return newProjection;
   7702     }
   7703 
   7704     protected Account getDefaultAccount() {
   7705         AccountManager accountManager = AccountManager.get(getContext());
   7706         try {
   7707             Account[] accounts = accountManager.getAccountsByType(DEFAULT_ACCOUNT_TYPE);
   7708             if (accounts != null && accounts.length > 0) {
   7709                 return accounts[0];
   7710             }
   7711         } catch (Throwable e) {
   7712             Log.e(TAG, "Cannot determine the default account for contacts compatibility", e);
   7713         }
   7714         return null;
   7715     }
   7716 
   7717     /**
   7718      * Returns true if the specified account type and data set is writable.
   7719      */
   7720     protected boolean isWritableAccountWithDataSet(String accountTypeAndDataSet) {
   7721         if (accountTypeAndDataSet == null) {
   7722             return true;
   7723         }
   7724 
   7725         Boolean writable = mAccountWritability.get(accountTypeAndDataSet);
   7726         if (writable != null) {
   7727             return writable;
   7728         }
   7729 
   7730         IContentService contentService = ContentResolver.getContentService();
   7731         try {
   7732             // TODO(dsantoro): Need to update this logic to allow for sub-accounts.
   7733             for (SyncAdapterType sync : contentService.getSyncAdapterTypes()) {
   7734                 if (ContactsContract.AUTHORITY.equals(sync.authority) &&
   7735                         accountTypeAndDataSet.equals(sync.accountType)) {
   7736                     writable = sync.supportsUploading();
   7737                     break;
   7738                 }
   7739             }
   7740         } catch (RemoteException e) {
   7741             Log.e(TAG, "Could not acquire sync adapter types");
   7742         }
   7743 
   7744         if (writable == null) {
   7745             writable = false;
   7746         }
   7747 
   7748         mAccountWritability.put(accountTypeAndDataSet, writable);
   7749         return writable;
   7750     }
   7751 
   7752 
   7753     /* package */ static boolean readBooleanQueryParameter(Uri uri, String parameter,
   7754             boolean defaultValue) {
   7755 
   7756         // Manually parse the query, which is much faster than calling uri.getQueryParameter
   7757         String query = uri.getEncodedQuery();
   7758         if (query == null) {
   7759             return defaultValue;
   7760         }
   7761 
   7762         int index = query.indexOf(parameter);
   7763         if (index == -1) {
   7764             return defaultValue;
   7765         }
   7766 
   7767         index += parameter.length();
   7768 
   7769         return !matchQueryParameter(query, index, "=0", false)
   7770                 && !matchQueryParameter(query, index, "=false", true);
   7771     }
   7772 
   7773     private static boolean matchQueryParameter(String query, int index, String value,
   7774             boolean ignoreCase) {
   7775         int length = value.length();
   7776         return query.regionMatches(ignoreCase, index, value, 0, length)
   7777                 && (query.length() == index + length || query.charAt(index + length) == '&');
   7778     }
   7779 
   7780     /**
   7781      * A fast re-implementation of {@link Uri#getQueryParameter}
   7782      */
   7783     /* package */ static String getQueryParameter(Uri uri, String parameter) {
   7784         String query = uri.getEncodedQuery();
   7785         if (query == null) {
   7786             return null;
   7787         }
   7788 
   7789         int queryLength = query.length();
   7790         int parameterLength = parameter.length();
   7791 
   7792         String value;
   7793         int index = 0;
   7794         while (true) {
   7795             index = query.indexOf(parameter, index);
   7796             if (index == -1) {
   7797                 return null;
   7798             }
   7799 
   7800             // Should match against the whole parameter instead of its suffix.
   7801             // e.g. The parameter "param" must not be found in "some_param=val".
   7802             if (index > 0) {
   7803                 char prevChar = query.charAt(index - 1);
   7804                 if (prevChar != '?' && prevChar != '&') {
   7805                     // With "some_param=val1&param=val2", we should find second "param" occurrence.
   7806                     index += parameterLength;
   7807                     continue;
   7808                 }
   7809             }
   7810 
   7811             index += parameterLength;
   7812 
   7813             if (queryLength == index) {
   7814                 return null;
   7815             }
   7816 
   7817             if (query.charAt(index) == '=') {
   7818                 index++;
   7819                 break;
   7820             }
   7821         }
   7822 
   7823         int ampIndex = query.indexOf('&', index);
   7824         if (ampIndex == -1) {
   7825             value = query.substring(index);
   7826         } else {
   7827             value = query.substring(index, ampIndex);
   7828         }
   7829 
   7830         return Uri.decode(value);
   7831     }
   7832 
   7833     protected boolean isAggregationUpgradeNeeded() {
   7834         if (!mContactAggregator.isEnabled()) {
   7835             return false;
   7836         }
   7837 
   7838         int version = Integer.parseInt(mContactsHelper.getProperty(
   7839                 PROPERTY_AGGREGATION_ALGORITHM, "1"));
   7840         return version < PROPERTY_AGGREGATION_ALGORITHM_VERSION;
   7841     }
   7842 
   7843     protected void upgradeAggregationAlgorithmInBackground() {
   7844         // This upgrade will affect very few contacts, so it can be performed on the
   7845         // main thread during the initial boot after an OTA
   7846 
   7847         Log.i(TAG, "Upgrading aggregation algorithm");
   7848         int count = 0;
   7849         long start = SystemClock.currentThreadTimeMillis();
   7850         SQLiteDatabase db = null;
   7851         try {
   7852             switchToContactMode();
   7853             db = mContactsHelper.getWritableDatabase();
   7854             mActiveDb.set(db);
   7855             db.beginTransaction();
   7856             Cursor cursor = db.query(true,
   7857                     Tables.RAW_CONTACTS + " r1 JOIN " + Tables.RAW_CONTACTS + " r2",
   7858                     new String[]{"r1." + RawContacts._ID},
   7859                     "r1." + RawContacts._ID + "!=r2." + RawContacts._ID +
   7860                     " AND r1." + RawContacts.CONTACT_ID + "=r2." + RawContacts.CONTACT_ID +
   7861                     " AND r1." + RawContacts.ACCOUNT_NAME + "=r2." + RawContacts.ACCOUNT_NAME +
   7862                     " AND r1." + RawContacts.ACCOUNT_TYPE + "=r2." + RawContacts.ACCOUNT_TYPE +
   7863                     " AND r1." + RawContacts.DATA_SET + "=r2." + RawContacts.DATA_SET,
   7864                     null, null, null, null, null);
   7865             try {
   7866                 while (cursor.moveToNext()) {
   7867                     long rawContactId = cursor.getLong(0);
   7868                     mContactAggregator.markForAggregation(rawContactId,
   7869                             RawContacts.AGGREGATION_MODE_DEFAULT, true);
   7870                     count++;
   7871                 }
   7872             } finally {
   7873                 cursor.close();
   7874             }
   7875             mContactAggregator.aggregateInTransaction(mTransactionContext.get(), db);
   7876             updateSearchIndexInTransaction();
   7877             db.setTransactionSuccessful();
   7878             mContactsHelper.setProperty(PROPERTY_AGGREGATION_ALGORITHM,
   7879                     String.valueOf(PROPERTY_AGGREGATION_ALGORITHM_VERSION));
   7880         } finally {
   7881             if (db != null) {
   7882                 db.endTransaction();
   7883             }
   7884             long end = SystemClock.currentThreadTimeMillis();
   7885             Log.i(TAG, "Aggregation algorithm upgraded for " + count
   7886                     + " contacts, in " + (end - start) + "ms");
   7887         }
   7888     }
   7889 
   7890     /* Visible for testing */
   7891     boolean isPhone() {
   7892         if (!sIsPhoneInitialized) {
   7893             sIsPhone = new TelephonyManager(getContext()).isVoiceCapable();
   7894             sIsPhoneInitialized = true;
   7895         }
   7896         return sIsPhone;
   7897     }
   7898 
   7899     private boolean handleDataUsageFeedback(Uri uri) {
   7900         final long currentTimeMillis = System.currentTimeMillis();
   7901         final String usageType = uri.getQueryParameter(DataUsageFeedback.USAGE_TYPE);
   7902         final String[] ids = uri.getLastPathSegment().trim().split(",");
   7903         final ArrayList<Long> dataIds = new ArrayList<Long>();
   7904 
   7905         for (String id : ids) {
   7906             dataIds.add(Long.valueOf(id));
   7907         }
   7908         final boolean successful;
   7909         if (TextUtils.isEmpty(usageType)) {
   7910             Log.w(TAG, "Method for data usage feedback isn't specified. Ignoring.");
   7911             successful = false;
   7912         } else {
   7913             successful = updateDataUsageStat(dataIds, usageType, currentTimeMillis) > 0;
   7914         }
   7915 
   7916         // Handle old API. This doesn't affect the result of this entire method.
   7917         final String[] questionMarks = new String[ids.length];
   7918         Arrays.fill(questionMarks, "?");
   7919         final String where = Data._ID + " IN (" + TextUtils.join(",", questionMarks) + ")";
   7920         final Cursor cursor = mActiveDb.get().query(
   7921                 Views.DATA,
   7922                 new String[] { Data.CONTACT_ID },
   7923                 where, ids, null, null, null);
   7924         try {
   7925             while (cursor.moveToNext()) {
   7926                 mSelectionArgs1[0] = cursor.getString(0);
   7927                 ContentValues values2 = new ContentValues();
   7928                 values2.put(Contacts.LAST_TIME_CONTACTED, currentTimeMillis);
   7929                 mActiveDb.get().update(Tables.CONTACTS, values2, Contacts._ID + "=?",
   7930                         mSelectionArgs1);
   7931                 mActiveDb.get().execSQL(UPDATE_TIMES_CONTACTED_CONTACTS_TABLE, mSelectionArgs1);
   7932                 mActiveDb.get().execSQL(UPDATE_TIMES_CONTACTED_RAWCONTACTS_TABLE, mSelectionArgs1);
   7933             }
   7934         } finally {
   7935             cursor.close();
   7936         }
   7937 
   7938         return successful;
   7939     }
   7940 
   7941     /**
   7942      * Update {@link Tables#DATA_USAGE_STAT}.
   7943      *
   7944      * @return the number of rows affected.
   7945      */
   7946     @VisibleForTesting
   7947     /* package */ int updateDataUsageStat(
   7948             List<Long> dataIds, String type, long currentTimeMillis) {
   7949         final int typeInt = sDataUsageTypeMap.get(type);
   7950         final String where = DataUsageStatColumns.DATA_ID + " =? AND "
   7951                 + DataUsageStatColumns.USAGE_TYPE_INT + " =?";
   7952         final String[] columns =
   7953                 new String[] { DataUsageStatColumns._ID, DataUsageStatColumns.TIMES_USED };
   7954         final ContentValues values = new ContentValues();
   7955         for (Long dataId : dataIds) {
   7956             final String[] args = new String[] { dataId.toString(), String.valueOf(typeInt) };
   7957             mActiveDb.get().beginTransaction();
   7958             try {
   7959                 final Cursor cursor = mActiveDb.get().query(Tables.DATA_USAGE_STAT, columns, where,
   7960                         args, null, null, null);
   7961                 try {
   7962                     if (cursor.getCount() > 0) {
   7963                         if (!cursor.moveToFirst()) {
   7964                             Log.e(TAG,
   7965                                     "moveToFirst() failed while getAccount() returned non-zero.");
   7966                         } else {
   7967                             values.clear();
   7968                             values.put(DataUsageStatColumns.TIMES_USED, cursor.getInt(1) + 1);
   7969                             values.put(DataUsageStatColumns.LAST_TIME_USED, currentTimeMillis);
   7970                             mActiveDb.get().update(Tables.DATA_USAGE_STAT, values,
   7971                                     DataUsageStatColumns._ID + " =?",
   7972                                     new String[] { cursor.getString(0) });
   7973                         }
   7974                     } else {
   7975                         values.clear();
   7976                         values.put(DataUsageStatColumns.DATA_ID, dataId);
   7977                         values.put(DataUsageStatColumns.USAGE_TYPE_INT, typeInt);
   7978                         values.put(DataUsageStatColumns.TIMES_USED, 1);
   7979                         values.put(DataUsageStatColumns.LAST_TIME_USED, currentTimeMillis);
   7980                         mActiveDb.get().insert(Tables.DATA_USAGE_STAT, null, values);
   7981                     }
   7982                     mActiveDb.get().setTransactionSuccessful();
   7983                 } finally {
   7984                     cursor.close();
   7985                 }
   7986             } finally {
   7987                 mActiveDb.get().endTransaction();
   7988             }
   7989         }
   7990 
   7991         return dataIds.size();
   7992     }
   7993 
   7994     /**
   7995      * Returns a sort order String for promoting data rows (email addresses, phone numbers, etc.)
   7996      * associated with a primary account. The primary account should be supplied from applications
   7997      * with {@link ContactsContract#PRIMARY_ACCOUNT_NAME} and
   7998      * {@link ContactsContract#PRIMARY_ACCOUNT_TYPE}. Null will be returned when the primary
   7999      * account isn't available.
   8000      */
   8001     private String getAccountPromotionSortOrder(Uri uri) {
   8002         final String primaryAccountName =
   8003                 uri.getQueryParameter(ContactsContract.PRIMARY_ACCOUNT_NAME);
   8004         final String primaryAccountType =
   8005                 uri.getQueryParameter(ContactsContract.PRIMARY_ACCOUNT_TYPE);
   8006 
   8007         // Data rows associated with primary account should be promoted.
   8008         if (!TextUtils.isEmpty(primaryAccountName)) {
   8009             StringBuilder sb = new StringBuilder();
   8010             sb.append("(CASE WHEN " + RawContacts.ACCOUNT_NAME + "=");
   8011             DatabaseUtils.appendEscapedSQLString(sb, primaryAccountName);
   8012             if (!TextUtils.isEmpty(primaryAccountType)) {
   8013                 sb.append(" AND " + RawContacts.ACCOUNT_TYPE + "=");
   8014                 DatabaseUtils.appendEscapedSQLString(sb, primaryAccountType);
   8015             }
   8016             sb.append(" THEN 0 ELSE 1 END)");
   8017             return sb.toString();
   8018         } else {
   8019             return null;
   8020         }
   8021     }
   8022 
   8023     /**
   8024      * Checks the URI for a deferred snippeting request
   8025      * @return a boolean indicating if a deferred snippeting request is in the RI
   8026      */
   8027     private boolean deferredSnippetingRequested(Uri uri) {
   8028         String deferredSnippeting =
   8029             getQueryParameter(uri, SearchSnippetColumns.DEFERRED_SNIPPETING_KEY);
   8030         return !TextUtils.isEmpty(deferredSnippeting) &&  deferredSnippeting.equals("1");
   8031     }
   8032 
   8033     /**
   8034      * Checks if query is a single word or not.
   8035      * @return a boolean indicating if the query is one word or not
   8036      */
   8037     private boolean isSingleWordQuery(String query) {
   8038         return query.split(QUERY_TOKENIZER_REGEX).length == 1;
   8039     }
   8040 
   8041     /**
   8042      * Checks the projection for a SNIPPET column indicating that a snippet is needed
   8043      * @return a boolean indicating if a snippet is needed or not.
   8044      */
   8045     private boolean snippetNeeded(String [] projection) {
   8046         return mDbHelper.get().isInProjection(projection, SearchSnippetColumns.SNIPPET);
   8047     }
   8048 }
   8049