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