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