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