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