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