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