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