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