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