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 com.android.providers.contacts.sqlite.DatabaseAnalyzer;
20 import com.android.providers.contacts.sqlite.SqlChecker;
21 import com.android.providers.contacts.sqlite.SqlChecker.InvalidSqlException;
22 import com.android.providers.contacts.util.PropertyUtils;
23 
24 import android.content.ContentResolver;
25 import android.content.ContentValues;
26 import android.content.Context;
27 import android.content.Intent;
28 import android.content.pm.ApplicationInfo;
29 import android.content.pm.PackageManager;
30 import android.content.pm.PackageManager.NameNotFoundException;
31 import android.content.pm.UserInfo;
32 import android.content.res.Resources;
33 import android.database.CharArrayBuffer;
34 import android.database.Cursor;
35 import android.database.DatabaseUtils;
36 import android.database.SQLException;
37 import android.database.sqlite.SQLiteConstraintException;
38 import android.database.sqlite.SQLiteDatabase;
39 import android.database.sqlite.SQLiteDoneException;
40 import android.database.sqlite.SQLiteException;
41 import android.database.sqlite.SQLiteOpenHelper;
42 import android.database.sqlite.SQLiteQueryBuilder;
43 import android.database.sqlite.SQLiteStatement;
44 import android.net.Uri;
45 import android.os.Binder;
46 import android.os.Bundle;
47 import android.os.SystemClock;
48 import android.os.UserManager;
49 import android.provider.BaseColumns;
50 import android.provider.ContactsContract;
51 import android.provider.ContactsContract.AggregationExceptions;
52 import android.provider.ContactsContract.CommonDataKinds.Email;
53 import android.provider.ContactsContract.CommonDataKinds.Event;
54 import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
55 import android.provider.ContactsContract.CommonDataKinds.Identity;
56 import android.provider.ContactsContract.CommonDataKinds.Im;
57 import android.provider.ContactsContract.CommonDataKinds.Nickname;
58 import android.provider.ContactsContract.CommonDataKinds.Note;
59 import android.provider.ContactsContract.CommonDataKinds.Organization;
60 import android.provider.ContactsContract.CommonDataKinds.Phone;
61 import android.provider.ContactsContract.CommonDataKinds.Relation;
62 import android.provider.ContactsContract.CommonDataKinds.SipAddress;
63 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
64 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
65 import android.provider.ContactsContract.CommonDataKinds.Website;
66 import android.provider.ContactsContract.Contacts;
67 import android.provider.ContactsContract.Contacts.Photo;
68 import android.provider.ContactsContract.Data;
69 import android.provider.ContactsContract.Directory;
70 import android.provider.ContactsContract.DisplayNameSources;
71 import android.provider.ContactsContract.DisplayPhoto;
72 import android.provider.ContactsContract.FullNameStyle;
73 import android.provider.ContactsContract.Groups;
74 import android.provider.ContactsContract.MetadataSync;
75 import android.provider.ContactsContract.MetadataSyncState;
76 import android.provider.ContactsContract.PhoneticNameStyle;
77 import android.provider.ContactsContract.PhotoFiles;
78 import android.provider.ContactsContract.PinnedPositions;
79 import android.provider.ContactsContract.ProviderStatus;
80 import android.provider.ContactsContract.RawContacts;
81 import android.provider.ContactsContract.Settings;
82 import android.provider.ContactsContract.StatusUpdates;
83 import android.provider.ContactsContract.StreamItemPhotos;
84 import android.provider.ContactsContract.StreamItems;
85 import android.telephony.PhoneNumberUtils;
86 import android.telephony.SubscriptionInfo;
87 import android.telephony.SubscriptionManager;
88 import android.text.TextUtils;
89 import android.text.util.Rfc822Token;
90 import android.text.util.Rfc822Tokenizer;
91 import android.util.ArrayMap;
92 import android.util.ArraySet;
93 import android.util.Base64;
94 import android.util.Log;
95 import android.util.Slog;
96 
97 import com.android.common.content.SyncStateContentProviderHelper;
98 import com.android.internal.annotations.VisibleForTesting;
99 import com.android.providers.contacts.aggregation.util.CommonNicknameCache;
100 import com.android.providers.contacts.database.ContactsTableUtil;
101 import com.android.providers.contacts.database.DeletedContactsTableUtil;
102 import com.android.providers.contacts.database.MoreDatabaseUtils;
103 import com.android.providers.contacts.util.NeededForTesting;
104 
105 import libcore.icu.ICU;
106 
107 import java.security.MessageDigest;
108 import java.security.NoSuchAlgorithmException;
109 import java.util.ArrayList;
110 import java.util.Locale;
111 import java.util.Set;
112 
113 /**
114  * Database helper for contacts. Designed as a singleton to make sure that all
115  * {@link android.content.ContentProvider} users get the same reference.
116  * Provides handy methods for maintaining package and mime-type lookup tables.
117  */
118 public class ContactsDatabaseHelper extends SQLiteOpenHelper {
119 
120     /**
121      * Contacts DB version ranges:
122      * <pre>
123      *   0-98    Cupcake/Donut
124      *   100-199 Eclair
125      *   200-299 Eclair-MR1
126      *   300-349 Froyo
127      *   350-399 Gingerbread
128      *   400-499 Honeycomb
129      *   500-549 Honeycomb-MR1
130      *   550-599 Honeycomb-MR2
131      *   600-699 Ice Cream Sandwich
132      *   700-799 Jelly Bean
133      *   800-899 Kitkat
134      *   900-999 Lollipop
135      *   1000-1099 M
136      *   1100-1199 N
137      *   1200-1299 O
138      * </pre>
139      */
140     static final int DATABASE_VERSION = 1202;
141     private static final int MINIMUM_SUPPORTED_VERSION = 700;
142 
143     @VisibleForTesting
144     static final boolean DISALLOW_SUB_QUERIES = false;
145 
146     public interface Tables {
147         public static final String CONTACTS = "contacts";
148         public static final String DELETED_CONTACTS = "deleted_contacts";
149         public static final String RAW_CONTACTS = "raw_contacts";
150         public static final String STREAM_ITEMS = "stream_items";
151         public static final String STREAM_ITEM_PHOTOS = "stream_item_photos";
152         public static final String PHOTO_FILES = "photo_files";
153         public static final String PACKAGES = "packages";
154         public static final String MIMETYPES = "mimetypes";
155         public static final String PHONE_LOOKUP = "phone_lookup";
156         public static final String NAME_LOOKUP = "name_lookup";
157         public static final String AGGREGATION_EXCEPTIONS = "agg_exceptions";
158         public static final String SETTINGS = "settings";
159         public static final String DATA = "data";
160         public static final String GROUPS = "groups";
161         public static final String PRESENCE = "presence";
162         public static final String AGGREGATED_PRESENCE = "agg_presence";
163         public static final String NICKNAME_LOOKUP = "nickname_lookup";
164         public static final String STATUS_UPDATES = "status_updates";
165         public static final String ACCOUNTS = "accounts";
166         public static final String VISIBLE_CONTACTS = "visible_contacts";
167         public static final String DIRECTORIES = "directories";
168         public static final String DEFAULT_DIRECTORY = "default_directory";
169         public static final String SEARCH_INDEX = "search_index";
170         public static final String METADATA_SYNC = "metadata_sync";
171         public static final String METADATA_SYNC_STATE = "metadata_sync_state";
172         public static final String PRE_AUTHORIZED_URIS = "pre_authorized_uris";
173 
174         // This list of tables contains auto-incremented sequences.
175         public static final String[] SEQUENCE_TABLES = new String[] {
176                 CONTACTS,
177                 RAW_CONTACTS,
178                 STREAM_ITEMS,
179                 STREAM_ITEM_PHOTOS,
180                 PHOTO_FILES,
181                 DATA,
182                 GROUPS,
183                 DIRECTORIES};
184 
185         /**
186          * For {@link android.provider.ContactsContract.DataUsageFeedback}. The table structure
187          * itself is not exposed outside.
188          */
189         public static final String DATA_USAGE_STAT = "data_usage_stat";
190 
191         public static final String DATA_JOIN_MIMETYPES = "data "
192                 + "JOIN mimetypes ON (data.mimetype_id = mimetypes._id)";
193 
194         public static final String DATA_JOIN_RAW_CONTACTS = "data "
195                 + "JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id)";
196 
197         // NOTE: If you want to refer to account name/type/data_set, AccountsColumns.CONCRETE_XXX
198         // MUST be used, as upgraded raw_contacts may have the account info columns too.
199         public static final String DATA_JOIN_MIMETYPE_RAW_CONTACTS = "data "
200                 + "JOIN mimetypes ON (data.mimetype_id = mimetypes._id) "
201                 + "JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id)"
202                 + " JOIN " + Tables.ACCOUNTS + " ON ("
203                     + RawContactsColumns.CONCRETE_ACCOUNT_ID + "=" + AccountsColumns.CONCRETE_ID
204                     + ")";
205 
206         // NOTE: This requires late binding of GroupMembership MIME-type
207         // TODO Consolidate settings and accounts
208         public static final String RAW_CONTACTS_JOIN_SETTINGS_DATA_GROUPS = Tables.RAW_CONTACTS
209                 + " JOIN " + Tables.ACCOUNTS + " ON ("
210                 +   RawContactsColumns.CONCRETE_ACCOUNT_ID + "=" + AccountsColumns.CONCRETE_ID
211                     + ")"
212                 + "LEFT OUTER JOIN " + Tables.SETTINGS + " ON ("
213                     + AccountsColumns.CONCRETE_ACCOUNT_NAME + "="
214                         + SettingsColumns.CONCRETE_ACCOUNT_NAME + " AND "
215                     + AccountsColumns.CONCRETE_ACCOUNT_TYPE + "="
216                         + SettingsColumns.CONCRETE_ACCOUNT_TYPE + " AND "
217                     + "((" + AccountsColumns.CONCRETE_DATA_SET + " IS NULL AND "
218                             + SettingsColumns.CONCRETE_DATA_SET + " IS NULL) OR ("
219                         + AccountsColumns.CONCRETE_DATA_SET + "="
220                             + SettingsColumns.CONCRETE_DATA_SET + "))) "
221                 + "LEFT OUTER JOIN data ON (data.mimetype_id=? AND "
222                     + "data.raw_contact_id = raw_contacts._id) "
223                 + "LEFT OUTER JOIN groups ON (groups._id = data." + GroupMembership.GROUP_ROW_ID
224                 + ")";
225 
226         // NOTE: This requires late binding of GroupMembership MIME-type
227         // TODO Add missing DATA_SET join -- or just consolidate settings and accounts
228         public static final String SETTINGS_JOIN_RAW_CONTACTS_DATA_MIMETYPES_CONTACTS = "settings "
229                 + "LEFT OUTER JOIN raw_contacts ON ("
230                     + RawContactsColumns.CONCRETE_ACCOUNT_ID + "=(SELECT "
231                         + AccountsColumns.CONCRETE_ID
232                         + " FROM " + Tables.ACCOUNTS
233                         + " WHERE "
234                             + "(" + AccountsColumns.CONCRETE_ACCOUNT_NAME
235                                 + "=" + SettingsColumns.CONCRETE_ACCOUNT_NAME + ") AND "
236                             + "(" + AccountsColumns.CONCRETE_ACCOUNT_TYPE
237                                 + "=" + SettingsColumns.CONCRETE_ACCOUNT_TYPE + ")))"
238                 + "LEFT OUTER JOIN data ON (data.mimetype_id=? AND "
239                     + "data.raw_contact_id = raw_contacts._id) "
240                 + "LEFT OUTER JOIN contacts ON (raw_contacts.contact_id = contacts._id)";
241 
242         public static final String CONTACTS_JOIN_RAW_CONTACTS_DATA_FILTERED_BY_GROUPMEMBERSHIP =
243                 Tables.CONTACTS
244                     + " INNER JOIN " + Tables.RAW_CONTACTS
245                         + " ON (" + RawContactsColumns.CONCRETE_CONTACT_ID + "="
246                             + ContactsColumns.CONCRETE_ID
247                         + ")"
248                     + " INNER JOIN " + Tables.DATA
249                         + " ON (" + DataColumns.CONCRETE_DATA1 + "=" + GroupsColumns.CONCRETE_ID
250                         + " AND "
251                         + DataColumns.CONCRETE_RAW_CONTACT_ID + "=" + RawContactsColumns.CONCRETE_ID
252                         + " AND "
253                         + DataColumns.CONCRETE_MIMETYPE_ID + "="
254                             + "(SELECT " + MimetypesColumns._ID
255                             + " FROM " + Tables.MIMETYPES
256                             + " WHERE "
257                             + MimetypesColumns.CONCRETE_MIMETYPE + "="
258                                 + "'" + GroupMembership.CONTENT_ITEM_TYPE + "'"
259                             + ")"
260                         + ")";
261 
262         // NOTE: If you want to refer to account name/type/data_set, AccountsColumns.CONCRETE_XXX
263         // MUST be used, as upgraded raw_contacts may have the account info columns too.
264         public static final String DATA_JOIN_PACKAGES_MIMETYPES_RAW_CONTACTS_GROUPS = "data "
265                 + "JOIN mimetypes ON (data.mimetype_id = mimetypes._id) "
266                 + "JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id) "
267                 + " JOIN " + Tables.ACCOUNTS + " ON ("
268                     + RawContactsColumns.CONCRETE_ACCOUNT_ID + "=" + AccountsColumns.CONCRETE_ID
269                     + ")"
270                 + "LEFT OUTER JOIN packages ON (data.package_id = packages._id) "
271                 + "LEFT OUTER JOIN groups "
272                 + "  ON (mimetypes.mimetype='" + GroupMembership.CONTENT_ITEM_TYPE + "' "
273                 + "      AND groups._id = data." + GroupMembership.GROUP_ROW_ID + ") ";
274 
275         public static final String ACTIVITIES_JOIN_MIMETYPES = "activities "
276                 + "LEFT OUTER JOIN mimetypes ON (activities.mimetype_id = mimetypes._id)";
277 
278         public static final String ACTIVITIES_JOIN_PACKAGES_MIMETYPES_RAW_CONTACTS_CONTACTS =
279                 "activities "
280                 + "LEFT OUTER JOIN packages ON (activities.package_id = packages._id) "
281                 + "LEFT OUTER JOIN mimetypes ON (activities.mimetype_id = mimetypes._id) "
282                 + "LEFT OUTER JOIN raw_contacts ON (activities.author_contact_id = " +
283                         "raw_contacts._id) "
284                 + "LEFT OUTER JOIN contacts ON (raw_contacts.contact_id = contacts._id)";
285 
286         public static final String NAME_LOOKUP_JOIN_RAW_CONTACTS = "name_lookup "
287                 + "INNER JOIN view_raw_contacts ON (name_lookup.raw_contact_id = "
288                 + "view_raw_contacts._id)";
289 
290         public static final String RAW_CONTACTS_JOIN_ACCOUNTS = Tables.RAW_CONTACTS
291                 + " JOIN " + Tables.ACCOUNTS + " ON ("
292                 + AccountsColumns.CONCRETE_ID + "=" + RawContactsColumns.CONCRETE_ACCOUNT_ID
293                 + ")";
294 
295         public static final String RAW_CONTACTS_JOIN_METADATA_SYNC = Tables.RAW_CONTACTS
296                 + " JOIN " + Tables.METADATA_SYNC + " ON ("
297                 + RawContactsColumns.CONCRETE_BACKUP_ID + "="
298                 + MetadataSyncColumns.CONCRETE_BACKUP_ID
299                 + " AND "
300                 + RawContactsColumns.CONCRETE_ACCOUNT_ID + "="
301                 + MetadataSyncColumns.CONCRETE_ACCOUNT_ID
302                 + ")";
303     }
304 
305     public interface Joins {
306         /**
307          * Join string intended to be used with the GROUPS table/view.  The main table must be named
308          * as "groups".
309          *
310          * Adds the "group_member_count column" to the query, which will be null if a group has
311          * no members.  Use ifnull(group_member_count, 0) if 0 is needed instead.
312          */
313         public static final String GROUP_MEMBER_COUNT =
314                 " LEFT OUTER JOIN (SELECT "
315                         + "data.data1 AS member_count_group_id, "
316                         + "COUNT(data.raw_contact_id) AS group_member_count "
317                     + "FROM data "
318                     + "WHERE "
319                         + "data.mimetype_id = (SELECT _id FROM mimetypes WHERE "
320                             + "mimetypes.mimetype = '" + GroupMembership.CONTENT_ITEM_TYPE + "')"
321                     + "GROUP BY member_count_group_id) AS member_count_table" // End of inner query
322                 + " ON (groups._id = member_count_table.member_count_group_id)";
323     }
324 
325     public interface Views {
326         public static final String DATA = "view_data";
327         public static final String RAW_CONTACTS = "view_raw_contacts";
328         public static final String CONTACTS = "view_contacts";
329         public static final String ENTITIES = "view_entities";
330         public static final String RAW_ENTITIES = "view_raw_entities";
331         public static final String GROUPS = "view_groups";
332 
333         /** The data_usage_stat table joined with other tables. */
334         public static final String DATA_USAGE_STAT = "view_data_usage_stat";
335 
336         /** The data_usage_stat table with the low-res columns. */
337         public static final String DATA_USAGE_LR = "view_data_usage";
338         public static final String STREAM_ITEMS = "view_stream_items";
339         public static final String METADATA_SYNC = "view_metadata_sync";
340         public static final String METADATA_SYNC_STATE = "view_metadata_sync_state";
341     }
342 
343     public interface Projections {
344         String[] ID = new String[] {BaseColumns._ID};
345         String[] LITERAL_ONE = new String[] {"1"};
346     }
347 
348     /**
349      * Property names for {@link ContactsDatabaseHelper#getProperty} and
350      * {@link ContactsDatabaseHelper#setProperty}.
351      */
352     public interface DbProperties {
353         String DIRECTORY_SCAN_COMPLETE = "directoryScanComplete";
354         String AGGREGATION_ALGORITHM = "aggregation_v2";
355         String KNOWN_ACCOUNTS = "known_accounts";
356         String ICU_VERSION = "icu_version";
357         String LOCALE = "locale";
358         String DATABASE_TIME_CREATED = "database_time_created";
359         String KNOWN_DIRECTORY_PACKAGES = "knownDirectoryPackages";
360     }
361 
362     public interface Clauses {
363         final String HAVING_NO_GROUPS = "COUNT(" + DataColumns.CONCRETE_GROUP_ID + ") == 0";
364 
365         final String GROUP_BY_ACCOUNT_CONTACT_ID = SettingsColumns.CONCRETE_ACCOUNT_NAME + ","
366                 + SettingsColumns.CONCRETE_ACCOUNT_TYPE + "," + RawContacts.CONTACT_ID;
367 
368         String LOCAL_ACCOUNT_ID =
369                 "(SELECT " + AccountsColumns._ID +
370                 " FROM " + Tables.ACCOUNTS +
371                 " WHERE " +
372                     AccountsColumns.ACCOUNT_NAME + " IS NULL AND " +
373                     AccountsColumns.ACCOUNT_TYPE + " IS NULL AND " +
374                     AccountsColumns.DATA_SET + " IS NULL)";
375 
376         final String RAW_CONTACT_IS_LOCAL = RawContactsColumns.CONCRETE_ACCOUNT_ID
377                 + "=" + LOCAL_ACCOUNT_ID;
378 
379         final String ZERO_GROUP_MEMBERSHIPS = "COUNT(" + GroupsColumns.CONCRETE_ID + ")=0";
380 
381         final String OUTER_RAW_CONTACTS = "outer_raw_contacts";
382         final String OUTER_RAW_CONTACTS_ID = OUTER_RAW_CONTACTS + "." + RawContacts._ID;
383 
384         final String CONTACT_IS_VISIBLE =
385                 "SELECT " +
386                     "MAX((SELECT (CASE WHEN " +
387                         "(CASE" +
388                             " WHEN " + RAW_CONTACT_IS_LOCAL +
389                             " THEN 1 " +
390                             " WHEN " + ZERO_GROUP_MEMBERSHIPS +
391                             " THEN " + Settings.UNGROUPED_VISIBLE +
392                             " ELSE MAX(" + Groups.GROUP_VISIBLE + ")" +
393                          "END)=1 THEN 1 ELSE 0 END)" +
394                 " FROM " + Tables.RAW_CONTACTS_JOIN_SETTINGS_DATA_GROUPS +
395                 " WHERE " + RawContactsColumns.CONCRETE_ID + "=" + OUTER_RAW_CONTACTS_ID + "))" +
396                 " FROM " + Tables.RAW_CONTACTS + " AS " + OUTER_RAW_CONTACTS +
397                 " WHERE " + RawContacts.CONTACT_ID + "=" + ContactsColumns.CONCRETE_ID +
398                 " GROUP BY " + RawContacts.CONTACT_ID;
399 
400         final String GROUP_HAS_ACCOUNT_AND_SOURCE_ID = Groups.SOURCE_ID + "=? AND "
401                 + GroupsColumns.ACCOUNT_ID + "=?";
402 
403         public static final String CONTACT_VISIBLE =
404             "EXISTS (SELECT _id FROM " + Tables.VISIBLE_CONTACTS
405                 + " WHERE " + Tables.CONTACTS +"." + Contacts._ID
406                         + "=" + Tables.VISIBLE_CONTACTS +"." + Contacts._ID + ")";
407 
408         public static final String CONTACT_IN_DEFAULT_DIRECTORY =
409                 "EXISTS (SELECT _id FROM " + Tables.DEFAULT_DIRECTORY
410                         + " WHERE " + Tables.CONTACTS +"." + Contacts._ID
411                         + "=" + Tables.DEFAULT_DIRECTORY +"." + Contacts._ID + ")";
412     }
413 
414     public interface ContactsColumns {
415         public static final String LAST_STATUS_UPDATE_ID = "status_update_id";
416 
417         public static final String CONCRETE_ID = Tables.CONTACTS + "." + BaseColumns._ID;
418 
419         public static final String CONCRETE_PHOTO_FILE_ID = Tables.CONTACTS + "."
420                 + Contacts.PHOTO_FILE_ID;
421 
422         public static final String CONCRETE_RAW_TIMES_CONTACTED = Tables.CONTACTS + "."
423                 + Contacts.RAW_TIMES_CONTACTED;
424         public static final String CONCRETE_RAW_LAST_TIME_CONTACTED = Tables.CONTACTS + "."
425                 + Contacts.RAW_LAST_TIME_CONTACTED;
426 
427         public static final String CONCRETE_STARRED = Tables.CONTACTS + "." + Contacts.STARRED;
428         public static final String CONCRETE_PINNED = Tables.CONTACTS + "." + Contacts.PINNED;
429         public static final String CONCRETE_CUSTOM_RINGTONE = Tables.CONTACTS + "."
430                 + Contacts.CUSTOM_RINGTONE;
431         public static final String CONCRETE_SEND_TO_VOICEMAIL = Tables.CONTACTS + "."
432                 + Contacts.SEND_TO_VOICEMAIL;
433         public static final String CONCRETE_LOOKUP_KEY = Tables.CONTACTS + "."
434                 + Contacts.LOOKUP_KEY;
435         public static final String CONCRETE_CONTACT_LAST_UPDATED_TIMESTAMP = Tables.CONTACTS + "."
436                 + Contacts.CONTACT_LAST_UPDATED_TIMESTAMP;
437         public static final String PHONEBOOK_LABEL_PRIMARY = "phonebook_label";
438         public static final String PHONEBOOK_BUCKET_PRIMARY = "phonebook_bucket";
439         public static final String PHONEBOOK_LABEL_ALTERNATIVE = "phonebook_label_alt";
440         public static final String PHONEBOOK_BUCKET_ALTERNATIVE = "phonebook_bucket_alt";
441     }
442 
443     public interface RawContactsColumns {
444         public static final String CONCRETE_ID =
445                 Tables.RAW_CONTACTS + "." + BaseColumns._ID;
446 
447         public static final String ACCOUNT_ID = "account_id";
448         public static final String CONCRETE_ACCOUNT_ID = Tables.RAW_CONTACTS + "." + ACCOUNT_ID;
449         public static final String CONCRETE_SOURCE_ID =
450                 Tables.RAW_CONTACTS + "." + RawContacts.SOURCE_ID;
451         public static final String CONCRETE_BACKUP_ID =
452                 Tables.RAW_CONTACTS + "." + RawContacts.BACKUP_ID;
453         public static final String CONCRETE_VERSION =
454                 Tables.RAW_CONTACTS + "." + RawContacts.VERSION;
455         public static final String CONCRETE_DIRTY =
456                 Tables.RAW_CONTACTS + "." + RawContacts.DIRTY;
457         public static final String CONCRETE_DELETED =
458                 Tables.RAW_CONTACTS + "." + RawContacts.DELETED;
459         public static final String CONCRETE_SYNC1 =
460                 Tables.RAW_CONTACTS + "." + RawContacts.SYNC1;
461         public static final String CONCRETE_SYNC2 =
462                 Tables.RAW_CONTACTS + "." + RawContacts.SYNC2;
463         public static final String CONCRETE_SYNC3 =
464                 Tables.RAW_CONTACTS + "." + RawContacts.SYNC3;
465         public static final String CONCRETE_SYNC4 =
466                 Tables.RAW_CONTACTS + "." + RawContacts.SYNC4;
467         public static final String CONCRETE_CUSTOM_RINGTONE =
468                 Tables.RAW_CONTACTS + "." + RawContacts.CUSTOM_RINGTONE;
469         public static final String CONCRETE_SEND_TO_VOICEMAIL =
470                 Tables.RAW_CONTACTS + "." + RawContacts.SEND_TO_VOICEMAIL;
471         public static final String CONCRETE_RAW_LAST_TIME_CONTACTED =
472                 Tables.RAW_CONTACTS + "." + RawContacts.RAW_LAST_TIME_CONTACTED;
473         public static final String CONCRETE_RAW_TIMES_CONTACTED =
474                 Tables.RAW_CONTACTS + "." + RawContacts.RAW_TIMES_CONTACTED;
475         public static final String CONCRETE_STARRED =
476                 Tables.RAW_CONTACTS + "." + RawContacts.STARRED;
477         public static final String CONCRETE_PINNED =
478                 Tables.RAW_CONTACTS + "." + RawContacts.PINNED;
479 
480         public static final String CONCRETE_METADATA_DIRTY =
481                 Tables.RAW_CONTACTS + "." + RawContacts.METADATA_DIRTY;
482         public static final String DISPLAY_NAME = RawContacts.DISPLAY_NAME_PRIMARY;
483         public static final String DISPLAY_NAME_SOURCE = RawContacts.DISPLAY_NAME_SOURCE;
484         public static final String AGGREGATION_NEEDED = "aggregation_needed";
485 
486         public static final String CONCRETE_DISPLAY_NAME =
487                 Tables.RAW_CONTACTS + "." + DISPLAY_NAME;
488         public static final String CONCRETE_CONTACT_ID =
489                 Tables.RAW_CONTACTS + "." + RawContacts.CONTACT_ID;
490         public static final String PHONEBOOK_LABEL_PRIMARY =
491             ContactsColumns.PHONEBOOK_LABEL_PRIMARY;
492         public static final String PHONEBOOK_BUCKET_PRIMARY =
493             ContactsColumns.PHONEBOOK_BUCKET_PRIMARY;
494         public static final String PHONEBOOK_LABEL_ALTERNATIVE =
495             ContactsColumns.PHONEBOOK_LABEL_ALTERNATIVE;
496         public static final String PHONEBOOK_BUCKET_ALTERNATIVE =
497             ContactsColumns.PHONEBOOK_BUCKET_ALTERNATIVE;
498 
499         /**
500          * This column is no longer used, but we keep it in the table so an upgraded database
501          * will look the same as a new database. This reduces the chance of OEMs adding a second
502          * column with the same name.
503          */
504         public static final String NAME_VERIFIED_OBSOLETE = "name_verified";
505     }
506 
507     public interface ViewRawContactsColumns {
508         String CONCRETE_ACCOUNT_NAME = Views.RAW_CONTACTS + "." + RawContacts.ACCOUNT_NAME;
509         String CONCRETE_ACCOUNT_TYPE = Views.RAW_CONTACTS + "." + RawContacts.ACCOUNT_TYPE;
510         String CONCRETE_DATA_SET = Views.RAW_CONTACTS + "." + RawContacts.DATA_SET;
511     }
512 
513     public interface DataColumns {
514         public static final String PACKAGE_ID = "package_id";
515         public static final String MIMETYPE_ID = "mimetype_id";
516 
517         public static final String CONCRETE_ID = Tables.DATA + "." + BaseColumns._ID;
518         public static final String CONCRETE_MIMETYPE_ID = Tables.DATA + "." + MIMETYPE_ID;
519         public static final String CONCRETE_RAW_CONTACT_ID = Tables.DATA + "."
520                 + Data.RAW_CONTACT_ID;
521         public static final String CONCRETE_GROUP_ID = Tables.DATA + "."
522                 + GroupMembership.GROUP_ROW_ID;
523 
524         public static final String CONCRETE_DATA1 = Tables.DATA + "." + Data.DATA1;
525         public static final String CONCRETE_DATA2 = Tables.DATA + "." + Data.DATA2;
526         public static final String CONCRETE_DATA3 = Tables.DATA + "." + Data.DATA3;
527         public static final String CONCRETE_DATA4 = Tables.DATA + "." + Data.DATA4;
528         public static final String CONCRETE_DATA5 = Tables.DATA + "." + Data.DATA5;
529         public static final String CONCRETE_DATA6 = Tables.DATA + "." + Data.DATA6;
530         public static final String CONCRETE_DATA7 = Tables.DATA + "." + Data.DATA7;
531         public static final String CONCRETE_DATA8 = Tables.DATA + "." + Data.DATA8;
532         public static final String CONCRETE_DATA9 = Tables.DATA + "." + Data.DATA9;
533         public static final String CONCRETE_DATA10 = Tables.DATA + "." + Data.DATA10;
534         public static final String CONCRETE_DATA11 = Tables.DATA + "." + Data.DATA11;
535         public static final String CONCRETE_DATA12 = Tables.DATA + "." + Data.DATA12;
536         public static final String CONCRETE_DATA13 = Tables.DATA + "." + Data.DATA13;
537         public static final String CONCRETE_DATA14 = Tables.DATA + "." + Data.DATA14;
538         public static final String CONCRETE_DATA15 = Tables.DATA + "." + Data.DATA15;
539         public static final String CONCRETE_IS_PRIMARY = Tables.DATA + "." + Data.IS_PRIMARY;
540         public static final String CONCRETE_PACKAGE_ID = Tables.DATA + "." + PACKAGE_ID;
541     }
542 
543     // Used only for legacy API support.
544     public interface ExtensionsColumns {
545         public static final String NAME = Data.DATA1;
546         public static final String VALUE = Data.DATA2;
547     }
548 
549     public interface GroupMembershipColumns {
550         public static final String RAW_CONTACT_ID = Data.RAW_CONTACT_ID;
551         public static final String GROUP_ROW_ID = GroupMembership.GROUP_ROW_ID;
552     }
553 
554     public interface GroupsColumns {
555         public static final String PACKAGE_ID = "package_id";
556         public static final String CONCRETE_PACKAGE_ID = Tables.GROUPS + "." + PACKAGE_ID;
557 
558         public static final String CONCRETE_ID = Tables.GROUPS + "." + BaseColumns._ID;
559         public static final String CONCRETE_SOURCE_ID = Tables.GROUPS + "." + Groups.SOURCE_ID;
560 
561         public static final String ACCOUNT_ID = "account_id";
562         public static final String CONCRETE_ACCOUNT_ID = Tables.GROUPS + "." + ACCOUNT_ID;
563     }
564 
565     public interface ViewGroupsColumns {
566         String CONCRETE_ACCOUNT_NAME = Views.GROUPS + "." + Groups.ACCOUNT_NAME;
567         String CONCRETE_ACCOUNT_TYPE = Views.GROUPS + "." + Groups.ACCOUNT_TYPE;
568         String CONCRETE_DATA_SET = Views.GROUPS + "." + Groups.DATA_SET;
569     }
570 
571     public interface ActivitiesColumns {
572         public static final String PACKAGE_ID = "package_id";
573         public static final String MIMETYPE_ID = "mimetype_id";
574     }
575 
576     public interface PhoneLookupColumns {
577         public static final String _ID = BaseColumns._ID;
578         public static final String DATA_ID = "data_id";
579         public static final String RAW_CONTACT_ID = "raw_contact_id";
580         public static final String NORMALIZED_NUMBER = "normalized_number";
581         public static final String MIN_MATCH = "min_match";
582     }
583 
584     public interface NameLookupColumns {
585         public static final String RAW_CONTACT_ID = "raw_contact_id";
586         public static final String DATA_ID = "data_id";
587         public static final String NORMALIZED_NAME = "normalized_name";
588         public static final String NAME_TYPE = "name_type";
589     }
590 
591     public interface PackagesColumns {
592         public static final String _ID = BaseColumns._ID;
593         public static final String PACKAGE = "package";
594 
595         public static final String CONCRETE_ID = Tables.PACKAGES + "." + _ID;
596     }
597 
598     public interface MimetypesColumns {
599         public static final String _ID = BaseColumns._ID;
600         public static final String MIMETYPE = "mimetype";
601 
602         public static final String CONCRETE_ID = Tables.MIMETYPES + "." + BaseColumns._ID;
603         public static final String CONCRETE_MIMETYPE = Tables.MIMETYPES + "." + MIMETYPE;
604     }
605 
606     public interface AggregationExceptionColumns {
607         public static final String _ID = BaseColumns._ID;
608     }
609 
610     public interface NicknameLookupColumns {
611         public static final String NAME = "name";
612         public static final String CLUSTER = "cluster";
613     }
614 
615     public interface SettingsColumns {
616         public static final String CONCRETE_ACCOUNT_NAME = Tables.SETTINGS + "."
617                 + Settings.ACCOUNT_NAME;
618         public static final String CONCRETE_ACCOUNT_TYPE = Tables.SETTINGS + "."
619                 + Settings.ACCOUNT_TYPE;
620         public static final String CONCRETE_DATA_SET = Tables.SETTINGS + "."
621                 + Settings.DATA_SET;
622     }
623 
624     public interface PresenceColumns {
625         String RAW_CONTACT_ID = "presence_raw_contact_id";
626         String CONTACT_ID = "presence_contact_id";
627     }
628 
629     public interface AggregatedPresenceColumns {
630         String CONTACT_ID = "presence_contact_id";
631         String CONCRETE_CONTACT_ID = Tables.AGGREGATED_PRESENCE + "." + CONTACT_ID;
632     }
633 
634     public interface StatusUpdatesColumns {
635         String DATA_ID = "status_update_data_id";
636 
637         String CONCRETE_DATA_ID = Tables.STATUS_UPDATES + "." + DATA_ID;
638 
639         String CONCRETE_PRESENCE = Tables.STATUS_UPDATES + "." + StatusUpdates.PRESENCE;
640         String CONCRETE_STATUS = Tables.STATUS_UPDATES + "." + StatusUpdates.STATUS;
641         String CONCRETE_STATUS_TIMESTAMP = Tables.STATUS_UPDATES + "."
642                 + StatusUpdates.STATUS_TIMESTAMP;
643         String CONCRETE_STATUS_RES_PACKAGE = Tables.STATUS_UPDATES + "."
644                 + StatusUpdates.STATUS_RES_PACKAGE;
645         String CONCRETE_STATUS_LABEL = Tables.STATUS_UPDATES + "." + StatusUpdates.STATUS_LABEL;
646         String CONCRETE_STATUS_ICON = Tables.STATUS_UPDATES + "." + StatusUpdates.STATUS_ICON;
647     }
648 
649     public interface ContactsStatusUpdatesColumns {
650         String ALIAS = "contacts_" + Tables.STATUS_UPDATES;
651 
652         String CONCRETE_DATA_ID = ALIAS + "." + StatusUpdatesColumns.DATA_ID;
653 
654         String CONCRETE_PRESENCE = ALIAS + "." + StatusUpdates.PRESENCE;
655         String CONCRETE_STATUS = ALIAS + "." + StatusUpdates.STATUS;
656         String CONCRETE_STATUS_TIMESTAMP = ALIAS + "." + StatusUpdates.STATUS_TIMESTAMP;
657         String CONCRETE_STATUS_RES_PACKAGE = ALIAS + "." + StatusUpdates.STATUS_RES_PACKAGE;
658         String CONCRETE_STATUS_LABEL = ALIAS + "." + StatusUpdates.STATUS_LABEL;
659         String CONCRETE_STATUS_ICON = ALIAS + "." + StatusUpdates.STATUS_ICON;
660     }
661 
662     public interface StreamItemsColumns {
663         final String CONCRETE_ID = Tables.STREAM_ITEMS + "." + BaseColumns._ID;
664         final String CONCRETE_RAW_CONTACT_ID =
665                 Tables.STREAM_ITEMS + "." + StreamItems.RAW_CONTACT_ID;
666         final String CONCRETE_PACKAGE = Tables.STREAM_ITEMS + "." + StreamItems.RES_PACKAGE;
667         final String CONCRETE_ICON = Tables.STREAM_ITEMS + "." + StreamItems.RES_ICON;
668         final String CONCRETE_LABEL = Tables.STREAM_ITEMS + "." + StreamItems.RES_LABEL;
669         final String CONCRETE_TEXT = Tables.STREAM_ITEMS + "." + StreamItems.TEXT;
670         final String CONCRETE_TIMESTAMP = Tables.STREAM_ITEMS + "." + StreamItems.TIMESTAMP;
671         final String CONCRETE_COMMENTS = Tables.STREAM_ITEMS + "." + StreamItems.COMMENTS;
672         final String CONCRETE_SYNC1 = Tables.STREAM_ITEMS + "." + StreamItems.SYNC1;
673         final String CONCRETE_SYNC2 = Tables.STREAM_ITEMS + "." + StreamItems.SYNC2;
674         final String CONCRETE_SYNC3 = Tables.STREAM_ITEMS + "." + StreamItems.SYNC3;
675         final String CONCRETE_SYNC4 = Tables.STREAM_ITEMS + "." + StreamItems.SYNC4;
676     }
677 
678     public interface StreamItemPhotosColumns {
679         final String CONCRETE_ID = Tables.STREAM_ITEM_PHOTOS + "." + BaseColumns._ID;
680         final String CONCRETE_STREAM_ITEM_ID = Tables.STREAM_ITEM_PHOTOS + "."
681                 + StreamItemPhotos.STREAM_ITEM_ID;
682         final String CONCRETE_SORT_INDEX =
683                 Tables.STREAM_ITEM_PHOTOS + "." + StreamItemPhotos.SORT_INDEX;
684         final String CONCRETE_PHOTO_FILE_ID = Tables.STREAM_ITEM_PHOTOS + "."
685                 + StreamItemPhotos.PHOTO_FILE_ID;
686         final String CONCRETE_SYNC1 = Tables.STREAM_ITEM_PHOTOS + "." + StreamItemPhotos.SYNC1;
687         final String CONCRETE_SYNC2 = Tables.STREAM_ITEM_PHOTOS + "." + StreamItemPhotos.SYNC2;
688         final String CONCRETE_SYNC3 = Tables.STREAM_ITEM_PHOTOS + "." + StreamItemPhotos.SYNC3;
689         final String CONCRETE_SYNC4 = Tables.STREAM_ITEM_PHOTOS + "." + StreamItemPhotos.SYNC4;
690     }
691 
692     public interface PhotoFilesColumns {
693         String CONCRETE_ID = Tables.PHOTO_FILES + "." + BaseColumns._ID;
694         String CONCRETE_HEIGHT = Tables.PHOTO_FILES + "." + PhotoFiles.HEIGHT;
695         String CONCRETE_WIDTH = Tables.PHOTO_FILES + "." + PhotoFiles.WIDTH;
696         String CONCRETE_FILESIZE = Tables.PHOTO_FILES + "." + PhotoFiles.FILESIZE;
697     }
698 
699     public interface AccountsColumns extends BaseColumns {
700         String CONCRETE_ID = Tables.ACCOUNTS + "." + BaseColumns._ID;
701 
702         String ACCOUNT_NAME = RawContacts.ACCOUNT_NAME;
703         String ACCOUNT_TYPE = RawContacts.ACCOUNT_TYPE;
704         String DATA_SET = RawContacts.DATA_SET;
705 
706         String CONCRETE_ACCOUNT_NAME = Tables.ACCOUNTS + "." + ACCOUNT_NAME;
707         String CONCRETE_ACCOUNT_TYPE = Tables.ACCOUNTS + "." + ACCOUNT_TYPE;
708         String CONCRETE_DATA_SET = Tables.ACCOUNTS + "." + DATA_SET;
709     }
710 
711     public interface DirectoryColumns {
712         public static final String TYPE_RESOURCE_NAME = "typeResourceName";
713     }
714 
715     public interface SearchIndexColumns {
716         public static final String CONTACT_ID = "contact_id";
717         public static final String CONTENT = "content";
718         public static final String NAME = "name";
719         public static final String TOKENS = "tokens";
720     }
721 
722     public interface PreAuthorizedUris {
723         public static final String _ID = BaseColumns._ID;
724         public static final String URI = "uri";
725         public static final String EXPIRATION = "expiration";
726     }
727 
728     /**
729      * Private table for calculating per-contact-method ranking.
730      */
731     public interface DataUsageStatColumns {
732         /** type: INTEGER (long) */
733         public static final String _ID = "stat_id";
734         public static final String CONCRETE_ID = Tables.DATA_USAGE_STAT + "." + _ID;
735 
736         /** type: INTEGER (long) */
737         public static final String DATA_ID = "data_id";
738         public static final String CONCRETE_DATA_ID = Tables.DATA_USAGE_STAT + "." + DATA_ID;
739 
740         /** type: INTEGER (long) */
741         public static final String RAW_LAST_TIME_USED = Data.RAW_LAST_TIME_USED;
742         public static final String LR_LAST_TIME_USED = Data.LR_LAST_TIME_USED;
743 
744         /** type: INTEGER */
745         public static final String RAW_TIMES_USED = Data.RAW_TIMES_USED;
746         public static final String LR_TIMES_USED = Data.LR_TIMES_USED;
747 
748         public static final String CONCRETE_RAW_LAST_TIME_USED =
749                 Tables.DATA_USAGE_STAT + "." + RAW_LAST_TIME_USED;
750 
751         public static final String CONCRETE_RAW_TIMES_USED =
752                 Tables.DATA_USAGE_STAT + "." + RAW_TIMES_USED;
753 
754         public static final String CONCRETE_LR_LAST_TIME_USED =
755                 Tables.DATA_USAGE_STAT + "." + LR_LAST_TIME_USED;
756 
757         public static final String CONCRETE_LR_TIMES_USED =
758                 Tables.DATA_USAGE_STAT + "." + LR_TIMES_USED;
759 
760         /** type: INTEGER */
761         public static final String USAGE_TYPE_INT = "usage_type";
762         public static final String CONCRETE_USAGE_TYPE =
763                 Tables.DATA_USAGE_STAT + "." + USAGE_TYPE_INT;
764 
765         /**
766          * Integer values for USAGE_TYPE.
767          *
768          * @see android.provider.ContactsContract.DataUsageFeedback#USAGE_TYPE
769          */
770         public static final int USAGE_TYPE_INT_CALL = 0;
771         public static final int USAGE_TYPE_INT_LONG_TEXT = 1;
772         public static final int USAGE_TYPE_INT_SHORT_TEXT = 2;
773     }
774 
775     public interface MetadataSyncColumns {
776         static final String CONCRETE_ID = Tables.METADATA_SYNC + "._id";
777         static final String ACCOUNT_ID = "account_id";
778         static final String CONCRETE_BACKUP_ID = Tables.METADATA_SYNC + "." +
779                 MetadataSync.RAW_CONTACT_BACKUP_ID;
780         static final String CONCRETE_ACCOUNT_ID = Tables.METADATA_SYNC + "." + ACCOUNT_ID;
781         static final String CONCRETE_DELETED = Tables.METADATA_SYNC + "." +
782                 MetadataSync.DELETED;
783     }
784 
785     public interface MetadataSyncStateColumns {
786         static final String CONCRETE_ID = Tables.METADATA_SYNC_STATE + "._id";
787         static final String ACCOUNT_ID = "account_id";
788         static final String CONCRETE_ACCOUNT_ID = Tables.METADATA_SYNC_STATE + "." + ACCOUNT_ID;
789     }
790 
791     private  interface EmailQuery {
792         public static final String TABLE = Tables.DATA;
793 
794         public static final String SELECTION =
795                 DataColumns.MIMETYPE_ID + "=? AND " + Data.DATA1 + " NOT NULL";
796 
797         public static final String COLUMNS[] = {
798                 Email._ID,
799                 Email.RAW_CONTACT_ID,
800                 Email.ADDRESS};
801 
802         public static final int ID = 0;
803         public static final int RAW_CONTACT_ID = 1;
804         public static final int ADDRESS = 2;
805     }
806 
807     private interface StructuredNameQuery {
808         public static final String TABLE = Tables.DATA;
809 
810         public static final String SELECTION =
811                 DataColumns.MIMETYPE_ID + "=? AND " + Data.DATA1 + " NOT NULL";
812 
813         public static final String COLUMNS[] = {
814                 StructuredName._ID,
815                 StructuredName.RAW_CONTACT_ID,
816                 StructuredName.DISPLAY_NAME,
817         };
818 
819         public static final int ID = 0;
820         public static final int RAW_CONTACT_ID = 1;
821         public static final int DISPLAY_NAME = 2;
822     }
823 
824     private interface NicknameQuery {
825         public static final String TABLE = Tables.DATA;
826 
827         public static final String SELECTION =
828                 DataColumns.MIMETYPE_ID + "=? AND " + Data.DATA1 + " NOT NULL";
829 
830         public static final String COLUMNS[] = {
831                 Nickname._ID,
832                 Nickname.RAW_CONTACT_ID,
833                 Nickname.NAME};
834 
835         public static final int ID = 0;
836         public static final int RAW_CONTACT_ID = 1;
837         public static final int NAME = 2;
838     }
839 
840     private interface RawContactNameQuery {
841         public static final String RAW_SQL =
842                 "SELECT "
843                         + DataColumns.MIMETYPE_ID + ","
844                         + Data.IS_PRIMARY + ","
845                         + Data.DATA1 + ","
846                         + Data.DATA2 + ","
847                         + Data.DATA3 + ","
848                         + Data.DATA4 + ","
849                         + Data.DATA5 + ","
850                         + Data.DATA6 + ","
851                         + Data.DATA7 + ","
852                         + Data.DATA8 + ","
853                         + Data.DATA9 + ","
854                         + Data.DATA10 + ","
855                         + Data.DATA11 +
856                 " FROM " + Tables.DATA +
857                 " WHERE " + Data.RAW_CONTACT_ID + "=?" +
858                         " AND (" + Data.DATA1 + " NOT NULL OR " +
859                                 Data.DATA8 + " NOT NULL OR " +
860                                 Data.DATA9 + " NOT NULL OR " +
861                                 Data.DATA10 + " NOT NULL OR " +  // Phonetic name not empty
862                                 Organization.TITLE + " NOT NULL)";
863 
864         public static final int MIMETYPE = 0;
865         public static final int IS_PRIMARY = 1;
866         public static final int DATA1 = 2;
867         public static final int GIVEN_NAME = 3;                         // data2
868         public static final int FAMILY_NAME = 4;                        // data3
869         public static final int PREFIX = 5;                             // data4
870         public static final int TITLE = 5;                              // data4
871         public static final int MIDDLE_NAME = 6;                        // data5
872         public static final int SUFFIX = 7;                             // data6
873         public static final int PHONETIC_GIVEN_NAME = 8;                // data7
874         public static final int PHONETIC_MIDDLE_NAME = 9;               // data8
875         public static final int ORGANIZATION_PHONETIC_NAME = 9;         // data8
876         public static final int PHONETIC_FAMILY_NAME = 10;              // data9
877         public static final int FULL_NAME_STYLE = 11;                   // data10
878         public static final int ORGANIZATION_PHONETIC_NAME_STYLE = 11;  // data10
879         public static final int PHONETIC_NAME_STYLE = 12;               // data11
880     }
881 
882     public final static class NameLookupType {
883         public static final int NAME_EXACT = 0;
884         public static final int NAME_VARIANT = 1;
885         public static final int NAME_COLLATION_KEY = 2;
886         public static final int NICKNAME = 3;
887         public static final int EMAIL_BASED_NICKNAME = 4;
888 
889         // The highest name-lookup type plus one.
890         public static final int TYPE_COUNT = 5;
891 
isBasedOnStructuredName(int nameLookupType)892         public static boolean isBasedOnStructuredName(int nameLookupType) {
893             return nameLookupType == NameLookupType.NAME_EXACT
894                     || nameLookupType == NameLookupType.NAME_VARIANT
895                     || nameLookupType == NameLookupType.NAME_COLLATION_KEY;
896         }
897     }
898 
899     private class StructuredNameLookupBuilder extends NameLookupBuilder {
900         // NOTE(gilad): Is in intentional that we don't use the declaration on L960?
901         private final SQLiteStatement mNameLookupInsert;
902         private final CommonNicknameCache mCommonNicknameCache;
903 
StructuredNameLookupBuilder(NameSplitter splitter, CommonNicknameCache commonNicknameCache, SQLiteStatement nameLookupInsert)904         public StructuredNameLookupBuilder(NameSplitter splitter,
905                 CommonNicknameCache commonNicknameCache, SQLiteStatement nameLookupInsert) {
906 
907             super(splitter);
908             this.mCommonNicknameCache = commonNicknameCache;
909             this.mNameLookupInsert = nameLookupInsert;
910         }
911 
912         @Override
insertNameLookup( long rawContactId, long dataId, int lookupType, String name)913         protected void insertNameLookup(
914                 long rawContactId, long dataId, int lookupType, String name) {
915 
916             if (!TextUtils.isEmpty(name)) {
917                 ContactsDatabaseHelper.this.insertNormalizedNameLookup(
918                         mNameLookupInsert, rawContactId, dataId, lookupType, name);
919             }
920         }
921 
922         @Override
getCommonNicknameClusters(String normalizedName)923         protected String[] getCommonNicknameClusters(String normalizedName) {
924             return mCommonNicknameCache.getCommonNicknameClusters(normalizedName);
925         }
926     }
927 
928     /** Placeholder for the methods to build the "low-res" SQL expressions. */
929     @VisibleForTesting
930     interface LowRes {
931         /** To be replaced with a real column name.  Only used within this interface. */
932         String TEMPLATE_PLACEHOLDER = "XX";
933 
934         /**
935          * To be replaced with a constant in the expression.
936          * Only used within this interface.
937          */
938         String CONSTANT_PLACEHOLDER = "YY";
939 
940         /** Only used within this interface. */
941         int TIMES_USED_GRANULARITY = 10;
942 
943         /** Only used within this interface. */
944         int LAST_TIME_USED_GRANULARITY = 24 * 60 * 60;
945 
946         /**
947          * Template to build the "low-res times used/contacted".  Only used within this interface.
948          * The outermost cast is needed to tell SQLite that the result is of the integer type.
949          */
950         String TEMPLATE_TIMES_USED =
951                 ("cast(ifnull((case when (XX) <= 0 then 0"
952                 + " when (XX) < (YY) then (XX)"
953                 + " else (cast((XX) as int) / (YY)) * (YY) end), 0) as int)")
954                 .replaceAll(CONSTANT_PLACEHOLDER, String.valueOf(TIMES_USED_GRANULARITY));
955 
956         /**
957          * Template to build the "low-res last time used/contacted".
958          * Only used within this interface.
959          * The outermost cast is needed to tell SQLite that the result is of the integer type.
960          */
961         String TEMPLATE_LAST_TIME_USED =
962                 ("cast((cast((XX) as int) / (YY)) * (YY) as int)")
963                 .replaceAll(CONSTANT_PLACEHOLDER, String.valueOf(LAST_TIME_USED_GRANULARITY));
964 
965         /**
966          * Build the SQL expression for the "low-res times used/contacted" expression from the
967          * give column name.
968          */
getTimesUsedExpression(String column)969         static String getTimesUsedExpression(String column) {
970             return TEMPLATE_TIMES_USED.replaceAll(TEMPLATE_PLACEHOLDER, column);
971         }
972 
973         /**
974          * Build the SQL expression for the "low-res last time used/contacted" expression from the
975          * give column name.
976          */
getLastTimeUsedExpression(String column)977         static String getLastTimeUsedExpression(String column) {
978             return TEMPLATE_LAST_TIME_USED.replaceAll(TEMPLATE_PLACEHOLDER, column);
979         }
980     }
981 
982     private static final String TAG = "ContactsDatabaseHelper";
983 
984     private static final String DATABASE_NAME = "contacts2.db";
985 
986     private static ContactsDatabaseHelper sSingleton = null;
987 
988     /** In-memory map of commonly found MIME-types to their ids in the MIMETYPES table */
989     @VisibleForTesting
990     final ArrayMap<String, Long> mCommonMimeTypeIdsCache = new ArrayMap<>();
991 
992     @VisibleForTesting
993     static final String[] COMMON_MIME_TYPES = {
994             Email.CONTENT_ITEM_TYPE,
995             Im.CONTENT_ITEM_TYPE,
996             Nickname.CONTENT_ITEM_TYPE,
997             Organization.CONTENT_ITEM_TYPE,
998             Phone.CONTENT_ITEM_TYPE,
999             SipAddress.CONTENT_ITEM_TYPE,
1000             StructuredName.CONTENT_ITEM_TYPE,
1001             StructuredPostal.CONTENT_ITEM_TYPE,
1002             Identity.CONTENT_ITEM_TYPE,
1003             android.provider.ContactsContract.CommonDataKinds.Photo.CONTENT_ITEM_TYPE,
1004             GroupMembership.CONTENT_ITEM_TYPE,
1005             Note.CONTENT_ITEM_TYPE,
1006             Event.CONTENT_ITEM_TYPE,
1007             Website.CONTENT_ITEM_TYPE,
1008             Relation.CONTENT_ITEM_TYPE,
1009             "vnd.com.google.cursor.item/contact_misc"
1010     };
1011 
1012     private final Context mContext;
1013     private final boolean mDatabaseOptimizationEnabled;
1014     private final boolean mIsTestInstance;
1015     private final SyncStateContentProviderHelper mSyncState;
1016     private final CountryMonitor mCountryMonitor;
1017 
1018     /**
1019      * Time when the DB was created.  It's persisted in {@link DbProperties#DATABASE_TIME_CREATED},
1020      * but loaded into memory so it can be accessed even when the DB is busy.
1021      */
1022     private long mDatabaseCreationTime;
1023 
1024     private MessageDigest mMessageDigest;
1025     {
1026         try {
1027             mMessageDigest = MessageDigest.getInstance("SHA-1");
1028         } catch (NoSuchAlgorithmException e) {
1029             throw new RuntimeException("No such algorithm.", e);
1030         }
1031     }
1032 
1033     private boolean mUseStrictPhoneNumberComparison;
1034 
1035     private String[] mSelectionArgs1 = new String[1];
1036     private NameSplitter.Name mName = new NameSplitter.Name();
1037     private CharArrayBuffer mCharArrayBuffer = new CharArrayBuffer(128);
1038     private NameSplitter mNameSplitter;
1039 
getInstance(Context context)1040     public static synchronized ContactsDatabaseHelper getInstance(Context context) {
1041         if (sSingleton == null) {
1042             sSingleton = new ContactsDatabaseHelper(context, DATABASE_NAME, true,
1043                     /* isTestInstance=*/ false);
1044         }
1045         return sSingleton;
1046     }
1047 
1048     /**
1049      * Returns a new instance for unit tests.
1050      */
1051     @NeededForTesting
getNewInstanceForTest(Context context, String filename)1052     public static ContactsDatabaseHelper getNewInstanceForTest(Context context, String filename) {
1053         return new ContactsDatabaseHelper(context, filename, false, /* isTestInstance=*/ true);
1054     }
1055 
ContactsDatabaseHelper( Context context, String databaseName, boolean optimizationEnabled, boolean isTestInstance)1056     protected ContactsDatabaseHelper(
1057             Context context, String databaseName, boolean optimizationEnabled,
1058             boolean isTestInstance) {
1059         super(context, databaseName, null, DATABASE_VERSION, MINIMUM_SUPPORTED_VERSION, null);
1060         boolean enableWal = android.provider.Settings.Global.getInt(context.getContentResolver(),
1061                 android.provider.Settings.Global.CONTACTS_DATABASE_WAL_ENABLED, 1) == 1;
1062         setWriteAheadLoggingEnabled(enableWal);
1063         mDatabaseOptimizationEnabled = optimizationEnabled;
1064         mIsTestInstance = isTestInstance;
1065         Resources resources = context.getResources();
1066         mContext = context;
1067         mSyncState = new SyncStateContentProviderHelper();
1068         mCountryMonitor = new CountryMonitor(context);
1069         mUseStrictPhoneNumberComparison = resources.getBoolean(
1070                 com.android.internal.R.bool.config_use_strict_phone_number_comparation);
1071     }
1072 
getDatabase(boolean writable)1073     public SQLiteDatabase getDatabase(boolean writable) {
1074         return writable ? getWritableDatabase() : getReadableDatabase();
1075     }
1076 
1077     /**
1078      * Populate ids of known mimetypes into a map for easy access
1079      *
1080      * @param db target database
1081      */
prepopulateCommonMimeTypes(SQLiteDatabase db)1082     private void prepopulateCommonMimeTypes(SQLiteDatabase db) {
1083         mCommonMimeTypeIdsCache.clear();
1084         for(String commonMimeType: COMMON_MIME_TYPES) {
1085             mCommonMimeTypeIdsCache.put(commonMimeType, insertMimeType(db, commonMimeType));
1086         }
1087     }
1088 
1089     @Override
onBeforeDelete(SQLiteDatabase db)1090     public void onBeforeDelete(SQLiteDatabase db) {
1091         Log.w(TAG, "Database version " + db.getVersion() + " for " + DATABASE_NAME
1092                 + " is no longer supported. Data will be lost on upgrading to " + DATABASE_VERSION);
1093     }
1094 
1095     @Override
onOpen(SQLiteDatabase db)1096     public void onOpen(SQLiteDatabase db) {
1097         Log.d(TAG, "WAL enabled for " + getDatabaseName() + ": " + db.isWriteAheadLoggingEnabled());
1098         prepopulateCommonMimeTypes(db);
1099         mSyncState.onDatabaseOpened(db);
1100         // Deleting any state from the presence tables to mimic their behavior from the time they
1101         // were in-memory tables
1102         db.execSQL("DELETE FROM " + Tables.PRESENCE + ";");
1103         db.execSQL("DELETE FROM " + Tables.AGGREGATED_PRESENCE + ";");
1104 
1105         loadDatabaseCreationTime(db);
1106     }
1107 
setDatabaseCreationTime(SQLiteDatabase db)1108     protected void setDatabaseCreationTime(SQLiteDatabase db) {
1109         // Note we don't do this in the profile DB helper.
1110         mDatabaseCreationTime = System.currentTimeMillis();
1111         PropertyUtils.setProperty(db, DbProperties.DATABASE_TIME_CREATED, String.valueOf(
1112                 mDatabaseCreationTime));
1113     }
1114 
loadDatabaseCreationTime(SQLiteDatabase db)1115     protected void loadDatabaseCreationTime(SQLiteDatabase db) {
1116         // Note we don't do this in the profile DB helper.
1117 
1118         mDatabaseCreationTime = 0;
1119         final String timestamp = PropertyUtils.getProperty(db,
1120                 DbProperties.DATABASE_TIME_CREATED, "");
1121         if (!TextUtils.isEmpty(timestamp)) {
1122             try {
1123                 mDatabaseCreationTime = Long.parseLong(timestamp);
1124             } catch (NumberFormatException e) {
1125                 Log.w(TAG, "Failed to parse timestamp: " + timestamp);
1126             }
1127         }
1128         if (AbstractContactsProvider.VERBOSE_LOGGING) {
1129             Log.v(TAG, "Open: creation time=" + mDatabaseCreationTime);
1130         }
1131         if (mDatabaseCreationTime == 0) {
1132             Log.w(TAG, "Unable to load creating time; resetting.");
1133             // Hmm, failed to load the timestamp.  Just set the current time then.
1134             mDatabaseCreationTime = System.currentTimeMillis();
1135             PropertyUtils.setProperty(db,
1136                     DbProperties.DATABASE_TIME_CREATED, Long.toString(mDatabaseCreationTime));
1137         }
1138     }
1139 
createPresenceTables(SQLiteDatabase db)1140     private void createPresenceTables(SQLiteDatabase db) {
1141         db.execSQL("CREATE TABLE IF NOT EXISTS " + Tables.PRESENCE + " ("+
1142                 StatusUpdates.DATA_ID + " INTEGER PRIMARY KEY REFERENCES data(_id)," +
1143                 StatusUpdates.PROTOCOL + " INTEGER NOT NULL," +
1144                 StatusUpdates.CUSTOM_PROTOCOL + " TEXT," +
1145                 StatusUpdates.IM_HANDLE + " TEXT," +
1146                 StatusUpdates.IM_ACCOUNT + " TEXT," +
1147                 PresenceColumns.CONTACT_ID + " INTEGER REFERENCES contacts(_id)," +
1148                 PresenceColumns.RAW_CONTACT_ID + " INTEGER REFERENCES raw_contacts(_id)," +
1149                 StatusUpdates.PRESENCE + " INTEGER," +
1150                 StatusUpdates.CHAT_CAPABILITY + " INTEGER NOT NULL DEFAULT 0," +
1151                 "UNIQUE(" + StatusUpdates.PROTOCOL + ", " + StatusUpdates.CUSTOM_PROTOCOL
1152                     + ", " + StatusUpdates.IM_HANDLE + ", " + StatusUpdates.IM_ACCOUNT + ")" +
1153         ");");
1154 
1155         db.execSQL("CREATE INDEX IF NOT EXISTS presenceIndex" + " ON "
1156                 + Tables.PRESENCE + " (" + PresenceColumns.RAW_CONTACT_ID + ");");
1157         db.execSQL("CREATE INDEX IF NOT EXISTS presenceIndex2" + " ON "
1158                 + Tables.PRESENCE + " (" + PresenceColumns.CONTACT_ID + ");");
1159 
1160         db.execSQL("CREATE TABLE IF NOT EXISTS "
1161                 + Tables.AGGREGATED_PRESENCE + " ("+
1162                 AggregatedPresenceColumns.CONTACT_ID
1163                         + " INTEGER PRIMARY KEY REFERENCES contacts(_id)," +
1164                 StatusUpdates.PRESENCE + " INTEGER," +
1165                 StatusUpdates.CHAT_CAPABILITY + " INTEGER NOT NULL DEFAULT 0" +
1166         ");");
1167 
1168         db.execSQL("CREATE TRIGGER IF NOT EXISTS " + Tables.PRESENCE + "_deleted"
1169                 + " BEFORE DELETE ON " + Tables.PRESENCE
1170                 + " BEGIN "
1171                 + "   DELETE FROM " + Tables.AGGREGATED_PRESENCE
1172                 + "     WHERE " + AggregatedPresenceColumns.CONTACT_ID + " = " +
1173                         "(SELECT " + PresenceColumns.CONTACT_ID +
1174                         " FROM " + Tables.PRESENCE +
1175                         " WHERE " + PresenceColumns.RAW_CONTACT_ID
1176                                 + "=OLD." + PresenceColumns.RAW_CONTACT_ID +
1177                         " AND NOT EXISTS" +
1178                                 "(SELECT " + PresenceColumns.RAW_CONTACT_ID +
1179                                 " FROM " + Tables.PRESENCE +
1180                                 " WHERE " + PresenceColumns.CONTACT_ID
1181                                         + "=OLD." + PresenceColumns.CONTACT_ID +
1182                                 " AND " + PresenceColumns.RAW_CONTACT_ID
1183                                         + "!=OLD." + PresenceColumns.RAW_CONTACT_ID + "));"
1184                 + " END");
1185 
1186         final String replaceAggregatePresenceSql =
1187                 "INSERT OR REPLACE INTO " + Tables.AGGREGATED_PRESENCE + "("
1188                         + AggregatedPresenceColumns.CONTACT_ID + ", "
1189                         + StatusUpdates.PRESENCE + ", "
1190                         + StatusUpdates.CHAT_CAPABILITY + ")"
1191                 + " SELECT "
1192                         + PresenceColumns.CONTACT_ID + ","
1193                         + StatusUpdates.PRESENCE + ","
1194                         + StatusUpdates.CHAT_CAPABILITY
1195                 + " FROM " + Tables.PRESENCE
1196                 + " WHERE "
1197                     + " (ifnull(" + StatusUpdates.PRESENCE + ",0)  * 10 "
1198                             + "+ ifnull(" + StatusUpdates.CHAT_CAPABILITY + ", 0))"
1199                     + " = (SELECT "
1200                         + "MAX (ifnull(" + StatusUpdates.PRESENCE + ",0)  * 10 "
1201                                 + "+ ifnull(" + StatusUpdates.CHAT_CAPABILITY + ", 0))"
1202                         + " FROM " + Tables.PRESENCE
1203                         + " WHERE " + PresenceColumns.CONTACT_ID
1204                             + "=NEW." + PresenceColumns.CONTACT_ID
1205                     + ")"
1206                 + " AND " + PresenceColumns.CONTACT_ID + "=NEW." + PresenceColumns.CONTACT_ID + ";";
1207 
1208         db.execSQL("CREATE TRIGGER IF NOT EXISTS " + Tables.PRESENCE + "_inserted"
1209                 + " AFTER INSERT ON " + Tables.PRESENCE
1210                 + " BEGIN "
1211                 + replaceAggregatePresenceSql
1212                 + " END");
1213 
1214         db.execSQL("CREATE TRIGGER IF NOT EXISTS " + Tables.PRESENCE + "_updated"
1215                 + " AFTER UPDATE ON " + Tables.PRESENCE
1216                 + " BEGIN "
1217                 + replaceAggregatePresenceSql
1218                 + " END");
1219     }
1220 
1221     @Override
onCreate(SQLiteDatabase db)1222     public void onCreate(SQLiteDatabase db) {
1223         Log.i(TAG, "Bootstrapping database " + DATABASE_NAME + " version: " + DATABASE_VERSION);
1224 
1225         mSyncState.createDatabase(db);
1226 
1227         // Create the properties table first so the create time is available as soon as possible.
1228         // The create time is needed by BOOT_COMPLETE to send broadcasts.
1229         PropertyUtils.createPropertiesTable(db);
1230 
1231         setDatabaseCreationTime(db);
1232 
1233         db.execSQL("CREATE TABLE " + Tables.ACCOUNTS + " (" +
1234                 AccountsColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
1235                 AccountsColumns.ACCOUNT_NAME + " TEXT, " +
1236                 AccountsColumns.ACCOUNT_TYPE + " TEXT, " +
1237                 AccountsColumns.DATA_SET + " TEXT" +
1238         ");");
1239 
1240         // Note, there are two sets of the usage stat columns: LR_* and RAW_*.
1241         // RAW_* contain the real values, which clients can't access.  The column names start
1242         // with a special prefix, which clients are prohibited from using in queries (including
1243         // "where" of deletes/updates.)
1244         // The LR_* columns have the original, public names.  The views have the LR columns too,
1245         // which contain the "low-res" numbers.  The tables, though, do *not* have to have these
1246         // columns, because we won't use them anyway.  However, because old versions of the tables
1247         // had those columns, and SQLite doesn't allow removing existing columns, meaning upgraded
1248         // tables will have these LR_* columns anyway.  So, in order to make a new database look
1249         // the same as an upgraded database, we create the LR columns in a new database too.
1250         // Otherwise, we would easily end up with writing SQLs that will run fine in a new DB
1251         // but not in an upgraded database, and because all unit tests will run with a new database,
1252         // we can't easily catch these sort of issues.
1253 
1254         // One row per group of contacts corresponding to the same person
1255         db.execSQL("CREATE TABLE " + Tables.CONTACTS + " (" +
1256                 BaseColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
1257                 Contacts.NAME_RAW_CONTACT_ID + " INTEGER REFERENCES raw_contacts(_id)," +
1258                 Contacts.PHOTO_ID + " INTEGER REFERENCES data(_id)," +
1259                 Contacts.PHOTO_FILE_ID + " INTEGER REFERENCES photo_files(_id)," +
1260                 Contacts.CUSTOM_RINGTONE + " TEXT," +
1261                 Contacts.SEND_TO_VOICEMAIL + " INTEGER NOT NULL DEFAULT 0," +
1262 
1263                 Contacts.RAW_TIMES_CONTACTED + " INTEGER NOT NULL DEFAULT 0," +
1264                 Contacts.RAW_LAST_TIME_CONTACTED + " INTEGER," +
1265 
1266                 Contacts.LR_TIMES_CONTACTED + " INTEGER NOT NULL DEFAULT 0," +
1267                 Contacts.LR_LAST_TIME_CONTACTED + " INTEGER," +
1268 
1269                 Contacts.STARRED + " INTEGER NOT NULL DEFAULT 0," +
1270                 Contacts.PINNED + " INTEGER NOT NULL DEFAULT " + PinnedPositions.UNPINNED + "," +
1271                 Contacts.HAS_PHONE_NUMBER + " INTEGER NOT NULL DEFAULT 0," +
1272                 Contacts.LOOKUP_KEY + " TEXT," +
1273                 ContactsColumns.LAST_STATUS_UPDATE_ID + " INTEGER REFERENCES data(_id)," +
1274                 Contacts.CONTACT_LAST_UPDATED_TIMESTAMP + " INTEGER" +
1275         ");");
1276 
1277         ContactsTableUtil.createIndexes(db);
1278 
1279         // deleted_contacts table
1280         DeletedContactsTableUtil.create(db);
1281 
1282         // Raw_contacts table
1283         db.execSQL("CREATE TABLE " + Tables.RAW_CONTACTS + " (" +
1284                 RawContacts._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
1285                 RawContactsColumns.ACCOUNT_ID + " INTEGER REFERENCES " +
1286                     Tables.ACCOUNTS + "(" + AccountsColumns._ID + ")," +
1287                 RawContacts.SOURCE_ID + " TEXT," +
1288                 RawContacts.BACKUP_ID + " TEXT," +
1289                 RawContacts.RAW_CONTACT_IS_READ_ONLY + " INTEGER NOT NULL DEFAULT 0," +
1290                 RawContacts.VERSION + " INTEGER NOT NULL DEFAULT 1," +
1291                 RawContacts.DIRTY + " INTEGER NOT NULL DEFAULT 0," +
1292                 RawContacts.DELETED + " INTEGER NOT NULL DEFAULT 0," +
1293                 RawContacts.METADATA_DIRTY + " INTEGER NOT NULL DEFAULT 0," +
1294                 RawContacts.CONTACT_ID + " INTEGER REFERENCES contacts(_id)," +
1295                 RawContacts.AGGREGATION_MODE + " INTEGER NOT NULL DEFAULT " +
1296                         RawContacts.AGGREGATION_MODE_DEFAULT + "," +
1297                 RawContactsColumns.AGGREGATION_NEEDED + " INTEGER NOT NULL DEFAULT 1," +
1298                 RawContacts.CUSTOM_RINGTONE + " TEXT," +
1299                 RawContacts.SEND_TO_VOICEMAIL + " INTEGER NOT NULL DEFAULT 0," +
1300 
1301                 RawContacts.RAW_TIMES_CONTACTED + " INTEGER NOT NULL DEFAULT 0," +
1302                 RawContacts.RAW_LAST_TIME_CONTACTED + " INTEGER," +
1303 
1304                 RawContacts.LR_TIMES_CONTACTED + " INTEGER NOT NULL DEFAULT 0," +
1305                 RawContacts.LR_LAST_TIME_CONTACTED + " INTEGER," +
1306 
1307                 RawContacts.STARRED + " INTEGER NOT NULL DEFAULT 0," +
1308                 RawContacts.PINNED + " INTEGER NOT NULL DEFAULT "  + PinnedPositions.UNPINNED +
1309                     "," + RawContacts.DISPLAY_NAME_PRIMARY + " TEXT," +
1310                 RawContacts.DISPLAY_NAME_ALTERNATIVE + " TEXT," +
1311                 RawContacts.DISPLAY_NAME_SOURCE + " INTEGER NOT NULL DEFAULT " +
1312                         DisplayNameSources.UNDEFINED + "," +
1313                 RawContacts.PHONETIC_NAME + " TEXT," +
1314                 // TODO: PHONETIC_NAME_STYLE should be INTEGER. There is a
1315                 // mismatch between how the column is created here (TEXT) and
1316                 // how it is created in upgradeToVersion205 (INTEGER).
1317                 RawContacts.PHONETIC_NAME_STYLE + " TEXT," +
1318                 RawContacts.SORT_KEY_PRIMARY + " TEXT COLLATE " +
1319                         ContactsProvider2.PHONEBOOK_COLLATOR_NAME + "," +
1320                 RawContactsColumns.PHONEBOOK_LABEL_PRIMARY + " TEXT," +
1321                 RawContactsColumns.PHONEBOOK_BUCKET_PRIMARY + " INTEGER," +
1322                 RawContacts.SORT_KEY_ALTERNATIVE + " TEXT COLLATE " +
1323                         ContactsProvider2.PHONEBOOK_COLLATOR_NAME + "," +
1324                 RawContactsColumns.PHONEBOOK_LABEL_ALTERNATIVE + " TEXT," +
1325                 RawContactsColumns.PHONEBOOK_BUCKET_ALTERNATIVE + " INTEGER," +
1326                 RawContactsColumns.NAME_VERIFIED_OBSOLETE + " INTEGER NOT NULL DEFAULT 0," +
1327                 RawContacts.SYNC1 + " TEXT, " +
1328                 RawContacts.SYNC2 + " TEXT, " +
1329                 RawContacts.SYNC3 + " TEXT, " +
1330                 RawContacts.SYNC4 + " TEXT " +
1331         ");");
1332 
1333         db.execSQL("CREATE INDEX raw_contacts_contact_id_index ON " + Tables.RAW_CONTACTS + " (" +
1334                 RawContacts.CONTACT_ID +
1335         ");");
1336 
1337         db.execSQL("CREATE INDEX raw_contacts_source_id_account_id_index ON " +
1338                 Tables.RAW_CONTACTS + " (" +
1339                 RawContacts.SOURCE_ID + ", " +
1340                 RawContactsColumns.ACCOUNT_ID +
1341         ");");
1342 
1343         db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS raw_contacts_backup_id_account_id_index ON " +
1344                 Tables.RAW_CONTACTS + " (" +
1345                 RawContacts.BACKUP_ID + ", " +
1346                 RawContactsColumns.ACCOUNT_ID +
1347         ");");
1348 
1349         db.execSQL("CREATE TABLE " + Tables.STREAM_ITEMS + " (" +
1350                 StreamItems._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
1351                 StreamItems.RAW_CONTACT_ID + " INTEGER NOT NULL, " +
1352                 StreamItems.RES_PACKAGE + " TEXT, " +
1353                 StreamItems.RES_ICON + " TEXT, " +
1354                 StreamItems.RES_LABEL + " TEXT, " +
1355                 StreamItems.TEXT + " TEXT, " +
1356                 StreamItems.TIMESTAMP + " INTEGER NOT NULL, " +
1357                 StreamItems.COMMENTS + " TEXT, " +
1358                 StreamItems.SYNC1 + " TEXT, " +
1359                 StreamItems.SYNC2 + " TEXT, " +
1360                 StreamItems.SYNC3 + " TEXT, " +
1361                 StreamItems.SYNC4 + " TEXT, " +
1362                 "FOREIGN KEY(" + StreamItems.RAW_CONTACT_ID + ") REFERENCES " +
1363                         Tables.RAW_CONTACTS + "(" + RawContacts._ID + "));");
1364 
1365         db.execSQL("CREATE TABLE " + Tables.STREAM_ITEM_PHOTOS + " (" +
1366                 StreamItemPhotos._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
1367                 StreamItemPhotos.STREAM_ITEM_ID + " INTEGER NOT NULL, " +
1368                 StreamItemPhotos.SORT_INDEX + " INTEGER, " +
1369                 StreamItemPhotos.PHOTO_FILE_ID + " INTEGER NOT NULL, " +
1370                 StreamItemPhotos.SYNC1 + " TEXT, " +
1371                 StreamItemPhotos.SYNC2 + " TEXT, " +
1372                 StreamItemPhotos.SYNC3 + " TEXT, " +
1373                 StreamItemPhotos.SYNC4 + " TEXT, " +
1374                 "FOREIGN KEY(" + StreamItemPhotos.STREAM_ITEM_ID + ") REFERENCES " +
1375                         Tables.STREAM_ITEMS + "(" + StreamItems._ID + "));");
1376 
1377         db.execSQL("CREATE TABLE " + Tables.PHOTO_FILES + " (" +
1378                 PhotoFiles._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
1379                 PhotoFiles.HEIGHT + " INTEGER NOT NULL, " +
1380                 PhotoFiles.WIDTH + " INTEGER NOT NULL, " +
1381                 PhotoFiles.FILESIZE + " INTEGER NOT NULL);");
1382 
1383         // TODO readd the index and investigate a controlled use of it
1384 //        db.execSQL("CREATE INDEX raw_contacts_agg_index ON " + Tables.RAW_CONTACTS + " (" +
1385 //                RawContactsColumns.AGGREGATION_NEEDED +
1386 //        ");");
1387 
1388         // Package name mapping table
1389         db.execSQL("CREATE TABLE " + Tables.PACKAGES + " (" +
1390                 PackagesColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
1391                 PackagesColumns.PACKAGE + " TEXT NOT NULL" +
1392         ");");
1393 
1394         // Mimetype mapping table
1395         db.execSQL("CREATE TABLE " + Tables.MIMETYPES + " (" +
1396                 MimetypesColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
1397                 MimetypesColumns.MIMETYPE + " TEXT NOT NULL" +
1398         ");");
1399 
1400         // Mimetype table requires an index on mime type
1401         db.execSQL("CREATE UNIQUE INDEX mime_type ON " + Tables.MIMETYPES + " (" +
1402                 MimetypesColumns.MIMETYPE +
1403         ");");
1404 
1405         // Public generic data table
1406         db.execSQL("CREATE TABLE " + Tables.DATA + " (" +
1407                 Data._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
1408                 DataColumns.PACKAGE_ID + " INTEGER REFERENCES package(_id)," +
1409                 DataColumns.MIMETYPE_ID + " INTEGER REFERENCES mimetype(_id) NOT NULL," +
1410                 Data.RAW_CONTACT_ID + " INTEGER REFERENCES raw_contacts(_id) NOT NULL," +
1411                 Data.HASH_ID + " TEXT," +
1412                 Data.IS_READ_ONLY + " INTEGER NOT NULL DEFAULT 0," +
1413                 Data.IS_PRIMARY + " INTEGER NOT NULL DEFAULT 0," +
1414                 Data.IS_SUPER_PRIMARY + " INTEGER NOT NULL DEFAULT 0," +
1415                 Data.DATA_VERSION + " INTEGER NOT NULL DEFAULT 0," +
1416                 Data.DATA1 + " TEXT," +
1417                 Data.DATA2 + " TEXT," +
1418                 Data.DATA3 + " TEXT," +
1419                 Data.DATA4 + " TEXT," +
1420                 Data.DATA5 + " TEXT," +
1421                 Data.DATA6 + " TEXT," +
1422                 Data.DATA7 + " TEXT," +
1423                 Data.DATA8 + " TEXT," +
1424                 Data.DATA9 + " TEXT," +
1425                 Data.DATA10 + " TEXT," +
1426                 Data.DATA11 + " TEXT," +
1427                 Data.DATA12 + " TEXT," +
1428                 Data.DATA13 + " TEXT," +
1429                 Data.DATA14 + " TEXT," +
1430                 Data.DATA15 + " TEXT," +
1431                 Data.SYNC1 + " TEXT, " +
1432                 Data.SYNC2 + " TEXT, " +
1433                 Data.SYNC3 + " TEXT, " +
1434                 Data.SYNC4 + " TEXT, " +
1435                 Data.CARRIER_PRESENCE + " INTEGER NOT NULL DEFAULT 0 " +
1436         ");");
1437 
1438         db.execSQL("CREATE INDEX data_raw_contact_id ON " + Tables.DATA + " (" +
1439                 Data.RAW_CONTACT_ID +
1440         ");");
1441 
1442         /**
1443          * For email lookup and similar queries.
1444          */
1445         db.execSQL("CREATE INDEX data_mimetype_data1_index ON " + Tables.DATA + " (" +
1446                 DataColumns.MIMETYPE_ID + "," +
1447                 Data.DATA1 +
1448         ");");
1449 
1450         /**
1451          * For contact backup restore queries.
1452          */
1453         db.execSQL("CREATE INDEX IF NOT EXISTS data_hash_id_index ON " + Tables.DATA + " (" +
1454                 Data.HASH_ID +
1455         ");");
1456 
1457 
1458         // Private phone numbers table used for lookup
1459         db.execSQL("CREATE TABLE " + Tables.PHONE_LOOKUP + " (" +
1460                 PhoneLookupColumns.DATA_ID
1461                         + " INTEGER REFERENCES data(_id) NOT NULL," +
1462                 PhoneLookupColumns.RAW_CONTACT_ID
1463                         + " INTEGER REFERENCES raw_contacts(_id) NOT NULL," +
1464                 PhoneLookupColumns.NORMALIZED_NUMBER + " TEXT NOT NULL," +
1465                 PhoneLookupColumns.MIN_MATCH + " TEXT NOT NULL" +
1466         ");");
1467 
1468         db.execSQL("CREATE INDEX phone_lookup_index ON " + Tables.PHONE_LOOKUP + " (" +
1469                 PhoneLookupColumns.NORMALIZED_NUMBER + "," +
1470                 PhoneLookupColumns.RAW_CONTACT_ID + "," +
1471                 PhoneLookupColumns.DATA_ID +
1472         ");");
1473 
1474         db.execSQL("CREATE INDEX phone_lookup_min_match_index ON " + Tables.PHONE_LOOKUP + " (" +
1475                 PhoneLookupColumns.MIN_MATCH + "," +
1476                 PhoneLookupColumns.RAW_CONTACT_ID + "," +
1477                 PhoneLookupColumns.DATA_ID +
1478         ");");
1479 
1480         db.execSQL("CREATE INDEX phone_lookup_data_id_min_match_index ON " + Tables.PHONE_LOOKUP +
1481                 " (" + PhoneLookupColumns.DATA_ID + ", " + PhoneLookupColumns.MIN_MATCH + ");");
1482 
1483         // Private name/nickname table used for lookup.
1484         db.execSQL("CREATE TABLE " + Tables.NAME_LOOKUP + " (" +
1485                 NameLookupColumns.DATA_ID
1486                         + " INTEGER REFERENCES data(_id) NOT NULL," +
1487                 NameLookupColumns.RAW_CONTACT_ID
1488                         + " INTEGER REFERENCES raw_contacts(_id) NOT NULL," +
1489                 NameLookupColumns.NORMALIZED_NAME + " TEXT NOT NULL," +
1490                 NameLookupColumns.NAME_TYPE + " INTEGER NOT NULL," +
1491                 "PRIMARY KEY ("
1492                         + NameLookupColumns.DATA_ID + ", "
1493                         + NameLookupColumns.NORMALIZED_NAME + ", "
1494                         + NameLookupColumns.NAME_TYPE + ")" +
1495         ");");
1496 
1497         db.execSQL("CREATE INDEX name_lookup_raw_contact_id_index ON " + Tables.NAME_LOOKUP + " (" +
1498                 NameLookupColumns.RAW_CONTACT_ID +
1499         ");");
1500 
1501         db.execSQL("CREATE TABLE " + Tables.NICKNAME_LOOKUP + " (" +
1502                 NicknameLookupColumns.NAME + " TEXT," +
1503                 NicknameLookupColumns.CLUSTER + " TEXT" +
1504         ");");
1505 
1506         db.execSQL("CREATE UNIQUE INDEX nickname_lookup_index ON " + Tables.NICKNAME_LOOKUP + " (" +
1507                 NicknameLookupColumns.NAME + ", " +
1508                 NicknameLookupColumns.CLUSTER +
1509         ");");
1510 
1511         // Groups table.
1512         db.execSQL("CREATE TABLE " + Tables.GROUPS + " (" +
1513                 Groups._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
1514                 GroupsColumns.PACKAGE_ID + " INTEGER REFERENCES package(_id)," +
1515                 GroupsColumns.ACCOUNT_ID + " INTEGER REFERENCES " +
1516                     Tables.ACCOUNTS + "(" + AccountsColumns._ID + ")," +
1517                 Groups.SOURCE_ID + " TEXT," +
1518                 Groups.VERSION + " INTEGER NOT NULL DEFAULT 1," +
1519                 Groups.DIRTY + " INTEGER NOT NULL DEFAULT 0," +
1520                 Groups.TITLE + " TEXT," +
1521                 Groups.TITLE_RES + " INTEGER," +
1522                 Groups.NOTES + " TEXT," +
1523                 Groups.SYSTEM_ID + " TEXT," +
1524                 Groups.DELETED + " INTEGER NOT NULL DEFAULT 0," +
1525                 Groups.GROUP_VISIBLE + " INTEGER NOT NULL DEFAULT 0," +
1526                 Groups.SHOULD_SYNC + " INTEGER NOT NULL DEFAULT 1," +
1527                 Groups.AUTO_ADD + " INTEGER NOT NULL DEFAULT 0," +
1528                 Groups.FAVORITES + " INTEGER NOT NULL DEFAULT 0," +
1529                 Groups.GROUP_IS_READ_ONLY + " INTEGER NOT NULL DEFAULT 0," +
1530                 Groups.SYNC1 + " TEXT, " +
1531                 Groups.SYNC2 + " TEXT, " +
1532                 Groups.SYNC3 + " TEXT, " +
1533                 Groups.SYNC4 + " TEXT " +
1534         ");");
1535 
1536         db.execSQL("CREATE INDEX groups_source_id_account_id_index ON " + Tables.GROUPS + " (" +
1537                 Groups.SOURCE_ID + ", " +
1538                 GroupsColumns.ACCOUNT_ID +
1539         ");");
1540 
1541         db.execSQL("CREATE TABLE IF NOT EXISTS " + Tables.AGGREGATION_EXCEPTIONS + " (" +
1542                 AggregationExceptionColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
1543                 AggregationExceptions.TYPE + " INTEGER NOT NULL, " +
1544                 AggregationExceptions.RAW_CONTACT_ID1
1545                         + " INTEGER REFERENCES raw_contacts(_id), " +
1546                 AggregationExceptions.RAW_CONTACT_ID2
1547                         + " INTEGER REFERENCES raw_contacts(_id)" +
1548         ");");
1549 
1550         db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS aggregation_exception_index1 ON " +
1551                 Tables.AGGREGATION_EXCEPTIONS + " (" +
1552                 AggregationExceptions.RAW_CONTACT_ID1 + ", " +
1553                 AggregationExceptions.RAW_CONTACT_ID2 +
1554         ");");
1555 
1556         db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS aggregation_exception_index2 ON " +
1557                 Tables.AGGREGATION_EXCEPTIONS + " (" +
1558                 AggregationExceptions.RAW_CONTACT_ID2 + ", " +
1559                 AggregationExceptions.RAW_CONTACT_ID1 +
1560         ");");
1561 
1562         db.execSQL("CREATE TABLE IF NOT EXISTS " + Tables.SETTINGS + " (" +
1563                 Settings.ACCOUNT_NAME + " STRING NOT NULL," +
1564                 Settings.ACCOUNT_TYPE + " STRING NOT NULL," +
1565                 Settings.DATA_SET + " STRING," +
1566                 Settings.UNGROUPED_VISIBLE + " INTEGER NOT NULL DEFAULT 0," +
1567                 Settings.SHOULD_SYNC + " INTEGER NOT NULL DEFAULT 1" +
1568         ");");
1569 
1570         db.execSQL("CREATE TABLE " + Tables.VISIBLE_CONTACTS + " (" +
1571                 Contacts._ID + " INTEGER PRIMARY KEY" +
1572         ");");
1573 
1574         db.execSQL("CREATE TABLE " + Tables.DEFAULT_DIRECTORY + " (" +
1575                 Contacts._ID + " INTEGER PRIMARY KEY" +
1576         ");");
1577 
1578         db.execSQL("CREATE TABLE " + Tables.STATUS_UPDATES + " (" +
1579                 StatusUpdatesColumns.DATA_ID + " INTEGER PRIMARY KEY REFERENCES data(_id)," +
1580                 StatusUpdates.STATUS + " TEXT," +
1581                 StatusUpdates.STATUS_TIMESTAMP + " INTEGER," +
1582                 StatusUpdates.STATUS_RES_PACKAGE + " TEXT, " +
1583                 StatusUpdates.STATUS_LABEL + " INTEGER, " +
1584                 StatusUpdates.STATUS_ICON + " INTEGER" +
1585         ");");
1586 
1587         createDirectoriesTable(db);
1588         createSearchIndexTable(db, false /* we build stats table later */);
1589 
1590         db.execSQL("CREATE TABLE " + Tables.DATA_USAGE_STAT + "(" +
1591                 DataUsageStatColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
1592                 DataUsageStatColumns.DATA_ID + " INTEGER NOT NULL, " +
1593                 DataUsageStatColumns.USAGE_TYPE_INT + " INTEGER NOT NULL DEFAULT 0, " +
1594 
1595                 DataUsageStatColumns.RAW_TIMES_USED + " INTEGER NOT NULL DEFAULT 0, " +
1596                 DataUsageStatColumns.RAW_LAST_TIME_USED + " INTEGER NOT NULL DEFAULT 0, " +
1597 
1598                 DataUsageStatColumns.LR_TIMES_USED + " INTEGER NOT NULL DEFAULT 0, " +
1599                 DataUsageStatColumns.LR_LAST_TIME_USED + " INTEGER NOT NULL DEFAULT 0, " +
1600 
1601                 "FOREIGN KEY(" + DataUsageStatColumns.DATA_ID + ") REFERENCES "
1602                         + Tables.DATA + "(" + Data._ID + ")" +
1603         ");");
1604         db.execSQL("CREATE UNIQUE INDEX data_usage_stat_index ON " +
1605                 Tables.DATA_USAGE_STAT + " (" +
1606                 DataUsageStatColumns.DATA_ID + ", " +
1607                 DataUsageStatColumns.USAGE_TYPE_INT +
1608         ");");
1609 
1610         db.execSQL("CREATE TABLE IF NOT EXISTS "
1611                 + Tables.METADATA_SYNC + " (" +
1612                 MetadataSync._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
1613                 MetadataSync.RAW_CONTACT_BACKUP_ID + " TEXT NOT NULL," +
1614                 MetadataSyncColumns.ACCOUNT_ID + " INTEGER NOT NULL," +
1615                 MetadataSync.DATA + " TEXT," +
1616                 MetadataSync.DELETED + " INTEGER NOT NULL DEFAULT 0);");
1617 
1618         db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS metadata_sync_index ON " +
1619                 Tables.METADATA_SYNC + " (" +
1620                 MetadataSync.RAW_CONTACT_BACKUP_ID + ", " +
1621                 MetadataSyncColumns.ACCOUNT_ID +");");
1622 
1623         db.execSQL("CREATE TABLE " + Tables.PRE_AUTHORIZED_URIS + " ("+
1624                 PreAuthorizedUris._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
1625                 PreAuthorizedUris.URI + " STRING NOT NULL, " +
1626                 PreAuthorizedUris.EXPIRATION + " INTEGER NOT NULL DEFAULT 0);");
1627 
1628         db.execSQL("CREATE TABLE IF NOT EXISTS "
1629                 + Tables.METADATA_SYNC_STATE + " (" +
1630                 MetadataSyncState._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
1631                 MetadataSyncStateColumns.ACCOUNT_ID + " INTEGER NOT NULL," +
1632                 MetadataSyncState.STATE + " BLOB);");
1633 
1634         db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS metadata_sync_state_index ON " +
1635                 Tables.METADATA_SYNC_STATE + " (" +
1636                 MetadataSyncColumns.ACCOUNT_ID +");");
1637 
1638         // When adding new tables, be sure to also add size-estimates in updateSqliteStats
1639         createContactsViews(db);
1640         createGroupsView(db);
1641         createContactsTriggers(db);
1642         createContactsIndexes(db, false /* we build stats table later */);
1643         createPresenceTables(db);
1644 
1645         loadNicknameLookupTable(db);
1646 
1647         // Set sequence starts.
1648         initializeAutoIncrementSequences(db);
1649 
1650         // Add the legacy API support views, etc.
1651         LegacyApiSupport.createDatabase(db);
1652 
1653         if (mDatabaseOptimizationEnabled) {
1654             // This will create a sqlite_stat1 table that is used for query optimization
1655             db.execSQL("ANALYZE;");
1656 
1657             updateSqliteStats(db);
1658         }
1659 
1660         postOnCreate();
1661     }
1662 
postOnCreate()1663     protected void postOnCreate() {
1664         // Only do this for the main DB, but not for the profile DB.
1665 
1666         notifyProviderStatusChange(mContext);
1667 
1668         // Trigger all sync adapters.
1669         ContentResolver.requestSync(null /* all accounts */,
1670                 ContactsContract.AUTHORITY, new Bundle());
1671 
1672         // Send the broadcast.
1673         final Intent dbCreatedIntent = new Intent(
1674                 ContactsContract.Intents.CONTACTS_DATABASE_CREATED);
1675         dbCreatedIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
1676         mContext.sendBroadcast(dbCreatedIntent, android.Manifest.permission.READ_CONTACTS);
1677     }
1678 
initializeAutoIncrementSequences(SQLiteDatabase db)1679     protected void initializeAutoIncrementSequences(SQLiteDatabase db) {
1680         // Default implementation does nothing.
1681     }
1682 
createDirectoriesTable(SQLiteDatabase db)1683     private void createDirectoriesTable(SQLiteDatabase db) {
1684         db.execSQL("CREATE TABLE " + Tables.DIRECTORIES + "(" +
1685                 Directory._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
1686                 Directory.PACKAGE_NAME + " TEXT NOT NULL," +
1687                 Directory.DIRECTORY_AUTHORITY + " TEXT NOT NULL," +
1688                 Directory.TYPE_RESOURCE_ID + " INTEGER," +
1689                 DirectoryColumns.TYPE_RESOURCE_NAME + " TEXT," +
1690                 Directory.ACCOUNT_TYPE + " TEXT," +
1691                 Directory.ACCOUNT_NAME + " TEXT," +
1692                 Directory.DISPLAY_NAME + " TEXT, " +
1693                 Directory.EXPORT_SUPPORT + " INTEGER NOT NULL" +
1694                         " DEFAULT " + Directory.EXPORT_SUPPORT_NONE + "," +
1695                 Directory.SHORTCUT_SUPPORT + " INTEGER NOT NULL" +
1696                         " DEFAULT " + Directory.SHORTCUT_SUPPORT_NONE + "," +
1697                 Directory.PHOTO_SUPPORT + " INTEGER NOT NULL" +
1698                         " DEFAULT " + Directory.PHOTO_SUPPORT_NONE +
1699         ");");
1700 
1701         // Trigger a full scan of directories in the system
1702         PropertyUtils.setProperty(db, DbProperties.DIRECTORY_SCAN_COMPLETE, "0");
1703     }
1704 
createSearchIndexTable(SQLiteDatabase db, boolean rebuildSqliteStats)1705     public void createSearchIndexTable(SQLiteDatabase db, boolean rebuildSqliteStats) {
1706         db.beginTransactionNonExclusive();
1707         try {
1708             db.execSQL("DROP TABLE IF EXISTS " + Tables.SEARCH_INDEX);
1709             db.execSQL("CREATE VIRTUAL TABLE " + Tables.SEARCH_INDEX
1710                     + " USING FTS4 ("
1711                     + SearchIndexColumns.CONTACT_ID + " INTEGER REFERENCES contacts(_id) NOT NULL,"
1712                     + SearchIndexColumns.CONTENT + " TEXT, "
1713                     + SearchIndexColumns.NAME + " TEXT, "
1714                     + SearchIndexColumns.TOKENS + " TEXT"
1715                     + ")");
1716             if (rebuildSqliteStats) {
1717                 updateSqliteStats(db);
1718             }
1719             db.setTransactionSuccessful();
1720         } finally {
1721             db.endTransaction();
1722         }
1723     }
1724 
createContactsTriggers(SQLiteDatabase db)1725     private void createContactsTriggers(SQLiteDatabase db) {
1726 
1727         // Automatically delete Data rows when a raw contact is deleted.
1728         db.execSQL("DROP TRIGGER IF EXISTS " + Tables.RAW_CONTACTS + "_deleted;");
1729         db.execSQL("CREATE TRIGGER " + Tables.RAW_CONTACTS + "_deleted "
1730                 + "   BEFORE DELETE ON " + Tables.RAW_CONTACTS
1731                 + " BEGIN "
1732                 + "   DELETE FROM " + Tables.DATA
1733                 + "     WHERE " + Data.RAW_CONTACT_ID
1734                                 + "=OLD." + RawContacts._ID + ";"
1735                 + "   DELETE FROM " + Tables.AGGREGATION_EXCEPTIONS
1736                 + "     WHERE " + AggregationExceptions.RAW_CONTACT_ID1
1737                                 + "=OLD." + RawContacts._ID
1738                 + "        OR " + AggregationExceptions.RAW_CONTACT_ID2
1739                                 + "=OLD." + RawContacts._ID + ";"
1740                 + "   DELETE FROM " + Tables.VISIBLE_CONTACTS
1741                 + "     WHERE " + Contacts._ID + "=OLD." + RawContacts.CONTACT_ID
1742                 + "       AND (SELECT COUNT(*) FROM " + Tables.RAW_CONTACTS
1743                 + "            WHERE " + RawContacts.CONTACT_ID + "=OLD." + RawContacts.CONTACT_ID
1744                 + "           )=1;"
1745                 + "   DELETE FROM " + Tables.DEFAULT_DIRECTORY
1746                 + "     WHERE " + Contacts._ID + "=OLD." + RawContacts.CONTACT_ID
1747                 + "       AND (SELECT COUNT(*) FROM " + Tables.RAW_CONTACTS
1748                 + "            WHERE " + RawContacts.CONTACT_ID + "=OLD." + RawContacts.CONTACT_ID
1749                 + "           )=1;"
1750                 + "   DELETE FROM " + Tables.CONTACTS
1751                 + "     WHERE " + Contacts._ID + "=OLD." + RawContacts.CONTACT_ID
1752                 + "       AND (SELECT COUNT(*) FROM " + Tables.RAW_CONTACTS
1753                 + "            WHERE " + RawContacts.CONTACT_ID + "=OLD." + RawContacts.CONTACT_ID
1754                 + "           )=1;"
1755                 + " END");
1756 
1757 
1758         db.execSQL("DROP TRIGGER IF EXISTS contacts_times_contacted;");
1759         db.execSQL("DROP TRIGGER IF EXISTS raw_contacts_times_contacted;");
1760 
1761         // Triggers that update {@link RawContacts#VERSION} when the contact is marked for deletion
1762         // or any time a data row is inserted, updated or deleted.
1763         db.execSQL("DROP TRIGGER IF EXISTS " + Tables.RAW_CONTACTS + "_marked_deleted;");
1764         db.execSQL("CREATE TRIGGER " + Tables.RAW_CONTACTS + "_marked_deleted "
1765                 + "   AFTER UPDATE ON " + Tables.RAW_CONTACTS
1766                 + " BEGIN "
1767                 + "   UPDATE " + Tables.RAW_CONTACTS
1768                 + "     SET "
1769                 +         RawContacts.VERSION + "=OLD." + RawContacts.VERSION + "+1 "
1770                 + "     WHERE " + RawContacts._ID + "=OLD." + RawContacts._ID
1771                 + "       AND NEW." + RawContacts.DELETED + "!= OLD." + RawContacts.DELETED + ";"
1772                 + " END");
1773 
1774         db.execSQL("DROP TRIGGER IF EXISTS " + Tables.DATA + "_updated;");
1775         db.execSQL("CREATE TRIGGER " + Tables.DATA + "_updated AFTER UPDATE ON " + Tables.DATA
1776                 + " BEGIN "
1777                 + "   UPDATE " + Tables.DATA
1778                 + "     SET " + Data.DATA_VERSION + "=OLD." + Data.DATA_VERSION + "+1 "
1779                 + "     WHERE " + Data._ID + "=OLD." + Data._ID + ";"
1780                 + "   UPDATE " + Tables.RAW_CONTACTS
1781                 + "     SET " + RawContacts.VERSION + "=" + RawContacts.VERSION + "+1 "
1782                 + "     WHERE " + RawContacts._ID + "=OLD." + Data.RAW_CONTACT_ID + ";"
1783                 + " END");
1784 
1785         db.execSQL("DROP TRIGGER IF EXISTS " + Tables.DATA + "_deleted;");
1786         db.execSQL("CREATE TRIGGER " + Tables.DATA + "_deleted BEFORE DELETE ON " + Tables.DATA
1787                 + " BEGIN "
1788                 + "   UPDATE " + Tables.RAW_CONTACTS
1789                 + "     SET " + RawContacts.VERSION + "=" + RawContacts.VERSION + "+1 "
1790                 + "     WHERE " + RawContacts._ID + "=OLD." + Data.RAW_CONTACT_ID + ";"
1791                 + "   DELETE FROM " + Tables.PHONE_LOOKUP
1792                 + "     WHERE " + PhoneLookupColumns.DATA_ID + "=OLD." + Data._ID + ";"
1793                 + "   DELETE FROM " + Tables.STATUS_UPDATES
1794                 + "     WHERE " + StatusUpdatesColumns.DATA_ID + "=OLD." + Data._ID + ";"
1795                 + "   DELETE FROM " + Tables.NAME_LOOKUP
1796                 + "     WHERE " + NameLookupColumns.DATA_ID + "=OLD." + Data._ID + ";"
1797                 + " END");
1798 
1799 
1800         db.execSQL("DROP TRIGGER IF EXISTS " + Tables.GROUPS + "_updated1;");
1801         db.execSQL("CREATE TRIGGER " + Tables.GROUPS + "_updated1 "
1802                 + "   AFTER UPDATE ON " + Tables.GROUPS
1803                 + " BEGIN "
1804                 + "   UPDATE " + Tables.GROUPS
1805                 + "     SET "
1806                 +         Groups.VERSION + "=OLD." + Groups.VERSION + "+1"
1807                 + "     WHERE " + Groups._ID + "=OLD." + Groups._ID + ";"
1808                 + " END");
1809 
1810         // Update DEFAULT_FILTER table per AUTO_ADD column update, see upgradeToVersion411.
1811         final String insertContactsWithoutAccount = (
1812                 " INSERT OR IGNORE INTO " + Tables.DEFAULT_DIRECTORY +
1813                 "     SELECT " + RawContacts.CONTACT_ID +
1814                 "     FROM " + Tables.RAW_CONTACTS +
1815                 "     WHERE " + RawContactsColumns.CONCRETE_ACCOUNT_ID +
1816                             "=" + Clauses.LOCAL_ACCOUNT_ID + ";");
1817 
1818         final String insertContactsWithAccountNoDefaultGroup = (
1819                 " INSERT OR IGNORE INTO " + Tables.DEFAULT_DIRECTORY +
1820                 "     SELECT " + RawContacts.CONTACT_ID +
1821                 "         FROM " + Tables.RAW_CONTACTS +
1822                 "     WHERE NOT EXISTS" +
1823                 "         (SELECT " + Groups._ID +
1824                 "             FROM " + Tables.GROUPS +
1825                 "             WHERE " + RawContactsColumns.CONCRETE_ACCOUNT_ID + " = " +
1826                                     GroupsColumns.CONCRETE_ACCOUNT_ID +
1827                 "             AND " + Groups.AUTO_ADD + " != 0" + ");");
1828 
1829         final String insertContactsWithAccountDefaultGroup = (
1830                 " INSERT OR IGNORE INTO " + Tables.DEFAULT_DIRECTORY +
1831                 "     SELECT " + RawContacts.CONTACT_ID +
1832                 "         FROM " + Tables.RAW_CONTACTS +
1833                 "     JOIN " + Tables.DATA +
1834                 "           ON (" + RawContactsColumns.CONCRETE_ID + "=" +
1835                         Data.RAW_CONTACT_ID + ")" +
1836                 "     WHERE " + DataColumns.MIMETYPE_ID + "=" +
1837                     "(SELECT " + MimetypesColumns._ID + " FROM " + Tables.MIMETYPES +
1838                         " WHERE " + MimetypesColumns.MIMETYPE +
1839                             "='" + GroupMembership.CONTENT_ITEM_TYPE + "')" +
1840                 "     AND EXISTS" +
1841                 "         (SELECT " + Groups._ID +
1842                 "             FROM " + Tables.GROUPS +
1843                 "                 WHERE " + RawContactsColumns.CONCRETE_ACCOUNT_ID + " = " +
1844                                         GroupsColumns.CONCRETE_ACCOUNT_ID +
1845                 "                 AND " + Groups.AUTO_ADD + " != 0" + ");");
1846 
1847         db.execSQL("DROP TRIGGER IF EXISTS " + Tables.GROUPS + "_auto_add_updated1;");
1848         db.execSQL("CREATE TRIGGER " + Tables.GROUPS + "_auto_add_updated1 "
1849                 + "   AFTER UPDATE OF " + Groups.AUTO_ADD + " ON " + Tables.GROUPS
1850                 + " BEGIN "
1851                 + "   DELETE FROM " + Tables.DEFAULT_DIRECTORY + ";"
1852                     + insertContactsWithoutAccount
1853                     + insertContactsWithAccountNoDefaultGroup
1854                     + insertContactsWithAccountDefaultGroup
1855                 + " END");
1856     }
1857 
createContactsIndexes(SQLiteDatabase db, boolean rebuildSqliteStats)1858     private void createContactsIndexes(SQLiteDatabase db, boolean rebuildSqliteStats) {
1859         db.execSQL("DROP INDEX IF EXISTS name_lookup_index");
1860         db.execSQL("CREATE INDEX name_lookup_index ON " + Tables.NAME_LOOKUP + " (" +
1861                 NameLookupColumns.NORMALIZED_NAME + "," +
1862                 NameLookupColumns.NAME_TYPE + ", " +
1863                 NameLookupColumns.RAW_CONTACT_ID + ", " +
1864                 NameLookupColumns.DATA_ID +
1865         ");");
1866 
1867         db.execSQL("DROP INDEX IF EXISTS raw_contact_sort_key1_index");
1868         db.execSQL("CREATE INDEX raw_contact_sort_key1_index ON " + Tables.RAW_CONTACTS + " (" +
1869                 RawContacts.SORT_KEY_PRIMARY +
1870         ");");
1871 
1872         db.execSQL("DROP INDEX IF EXISTS raw_contact_sort_key2_index");
1873         db.execSQL("CREATE INDEX raw_contact_sort_key2_index ON " + Tables.RAW_CONTACTS + " (" +
1874                 RawContacts.SORT_KEY_ALTERNATIVE +
1875         ");");
1876 
1877         if (rebuildSqliteStats) {
1878             updateSqliteStats(db);
1879         }
1880     }
1881 
createContactsViews(SQLiteDatabase db)1882     private void createContactsViews(SQLiteDatabase db) {
1883         db.execSQL("DROP VIEW IF EXISTS " + Views.CONTACTS + ";");
1884         db.execSQL("DROP VIEW IF EXISTS " + Views.DATA + ";");
1885         db.execSQL("DROP VIEW IF EXISTS " + Views.RAW_CONTACTS + ";");
1886         db.execSQL("DROP VIEW IF EXISTS " + Views.RAW_ENTITIES + ";");
1887         db.execSQL("DROP VIEW IF EXISTS " + Views.ENTITIES + ";");
1888         db.execSQL("DROP VIEW IF EXISTS " + Views.DATA_USAGE_STAT + ";");
1889         db.execSQL("DROP VIEW IF EXISTS " + Views.DATA_USAGE_LR + ";");
1890         db.execSQL("DROP VIEW IF EXISTS " + Views.STREAM_ITEMS + ";");
1891         db.execSQL("DROP VIEW IF EXISTS " + Views.METADATA_SYNC_STATE + ";");
1892         db.execSQL("DROP VIEW IF EXISTS " + Views.METADATA_SYNC + ";");
1893 
1894         String dataColumns =
1895                 Data.IS_PRIMARY + ", "
1896                 + Data.IS_SUPER_PRIMARY + ", "
1897                 + Data.DATA_VERSION + ", "
1898                 + DataColumns.CONCRETE_PACKAGE_ID + ","
1899                 + PackagesColumns.PACKAGE + " AS " + Data.RES_PACKAGE + ","
1900                 + DataColumns.CONCRETE_MIMETYPE_ID + ","
1901                 + MimetypesColumns.MIMETYPE + " AS " + Data.MIMETYPE + ", "
1902                 + Data.IS_READ_ONLY + ", "
1903                 + Data.DATA1 + ", "
1904                 + Data.DATA2 + ", "
1905                 + Data.DATA3 + ", "
1906                 + Data.DATA4 + ", "
1907                 + Data.DATA5 + ", "
1908                 + Data.DATA6 + ", "
1909                 + Data.DATA7 + ", "
1910                 + Data.DATA8 + ", "
1911                 + Data.DATA9 + ", "
1912                 + Data.DATA10 + ", "
1913                 + Data.DATA11 + ", "
1914                 + Data.DATA12 + ", "
1915                 + Data.DATA13 + ", "
1916                 + Data.DATA14 + ", "
1917                 + Data.DATA15 + ", "
1918                 + Data.CARRIER_PRESENCE + ", "
1919                 + Data.SYNC1 + ", "
1920                 + Data.SYNC2 + ", "
1921                 + Data.SYNC3 + ", "
1922                 + Data.SYNC4;
1923 
1924         String syncColumns =
1925                 RawContactsColumns.CONCRETE_ACCOUNT_ID + ","
1926                 + AccountsColumns.CONCRETE_ACCOUNT_NAME + " AS " + RawContacts.ACCOUNT_NAME + ","
1927                 + AccountsColumns.CONCRETE_ACCOUNT_TYPE + " AS " + RawContacts.ACCOUNT_TYPE + ","
1928                 + AccountsColumns.CONCRETE_DATA_SET + " AS " + RawContacts.DATA_SET + ","
1929                 + "(CASE WHEN " + AccountsColumns.CONCRETE_DATA_SET + " IS NULL THEN "
1930                             + AccountsColumns.CONCRETE_ACCOUNT_TYPE
1931                         + " ELSE " + AccountsColumns.CONCRETE_ACCOUNT_TYPE + "||'/'||"
1932                             + AccountsColumns.CONCRETE_DATA_SET + " END) AS "
1933                                 + RawContacts.ACCOUNT_TYPE_AND_DATA_SET + ","
1934                 + RawContactsColumns.CONCRETE_SOURCE_ID + " AS " + RawContacts.SOURCE_ID + ","
1935                 + RawContactsColumns.CONCRETE_BACKUP_ID + " AS " + RawContacts.BACKUP_ID + ","
1936                 + RawContactsColumns.CONCRETE_VERSION + " AS " + RawContacts.VERSION + ","
1937                 + RawContactsColumns.CONCRETE_DIRTY + " AS " + RawContacts.DIRTY + ","
1938                 + RawContactsColumns.CONCRETE_SYNC1 + " AS " + RawContacts.SYNC1 + ","
1939                 + RawContactsColumns.CONCRETE_SYNC2 + " AS " + RawContacts.SYNC2 + ","
1940                 + RawContactsColumns.CONCRETE_SYNC3 + " AS " + RawContacts.SYNC3 + ","
1941                 + RawContactsColumns.CONCRETE_SYNC4 + " AS " + RawContacts.SYNC4;
1942 
1943         String baseContactColumns =
1944                 Contacts.HAS_PHONE_NUMBER + ", "
1945                 + Contacts.NAME_RAW_CONTACT_ID + ", "
1946                 + Contacts.LOOKUP_KEY + ", "
1947                 + Contacts.PHOTO_ID + ", "
1948                 + Contacts.PHOTO_FILE_ID + ", "
1949                 + "CAST(" + Clauses.CONTACT_VISIBLE + " AS INTEGER) AS "
1950                         + Contacts.IN_VISIBLE_GROUP + ", "
1951                 + "CAST(" + Clauses.CONTACT_IN_DEFAULT_DIRECTORY + " AS INTEGER) AS "
1952                         + Contacts.IN_DEFAULT_DIRECTORY + ", "
1953                 + ContactsColumns.LAST_STATUS_UPDATE_ID + ", "
1954                 + ContactsColumns.CONCRETE_CONTACT_LAST_UPDATED_TIMESTAMP;
1955 
1956         String contactOptionColumns =
1957                 ContactsColumns.CONCRETE_CUSTOM_RINGTONE
1958                         + " AS " + Contacts.CUSTOM_RINGTONE + ","
1959                 + ContactsColumns.CONCRETE_SEND_TO_VOICEMAIL
1960                         + " AS " + Contacts.SEND_TO_VOICEMAIL + ","
1961 
1962                 + ContactsColumns.CONCRETE_RAW_LAST_TIME_CONTACTED
1963                         + " AS " + Contacts.RAW_LAST_TIME_CONTACTED + ","
1964                 + ContactsColumns.CONCRETE_RAW_TIMES_CONTACTED
1965                         + " AS " + Contacts.RAW_TIMES_CONTACTED + ","
1966 
1967                 + LowRes.getLastTimeUsedExpression(ContactsColumns.CONCRETE_RAW_LAST_TIME_CONTACTED)
1968                         + " AS " + Contacts.LR_LAST_TIME_CONTACTED + ","
1969                 + LowRes.getTimesUsedExpression(ContactsColumns.CONCRETE_RAW_TIMES_CONTACTED)
1970                         + " AS " + Contacts.LR_TIMES_CONTACTED + ","
1971 
1972                 + ContactsColumns.CONCRETE_STARRED
1973                         + " AS " + Contacts.STARRED + ","
1974                 + ContactsColumns.CONCRETE_PINNED
1975                         + " AS " + Contacts.PINNED;
1976 
1977         String contactNameColumns =
1978                 "name_raw_contact." + RawContacts.DISPLAY_NAME_SOURCE
1979                         + " AS " + Contacts.DISPLAY_NAME_SOURCE + ", "
1980                 + "name_raw_contact." + RawContacts.DISPLAY_NAME_PRIMARY
1981                         + " AS " + Contacts.DISPLAY_NAME_PRIMARY + ", "
1982                 + "name_raw_contact." + RawContacts.DISPLAY_NAME_ALTERNATIVE
1983                         + " AS " + Contacts.DISPLAY_NAME_ALTERNATIVE + ", "
1984                 + "name_raw_contact." + RawContacts.PHONETIC_NAME
1985                         + " AS " + Contacts.PHONETIC_NAME + ", "
1986                 + "name_raw_contact." + RawContacts.PHONETIC_NAME_STYLE
1987                         + " AS " + Contacts.PHONETIC_NAME_STYLE + ", "
1988                 + "name_raw_contact." + RawContacts.SORT_KEY_PRIMARY
1989                         + " AS " + Contacts.SORT_KEY_PRIMARY + ", "
1990                 + "name_raw_contact." + RawContactsColumns.PHONEBOOK_LABEL_PRIMARY
1991                         + " AS " + ContactsColumns.PHONEBOOK_LABEL_PRIMARY + ", "
1992                 + "name_raw_contact." + RawContactsColumns.PHONEBOOK_BUCKET_PRIMARY
1993                         + " AS " + ContactsColumns.PHONEBOOK_BUCKET_PRIMARY + ", "
1994                 + "name_raw_contact." + RawContacts.SORT_KEY_ALTERNATIVE
1995                         + " AS " + Contacts.SORT_KEY_ALTERNATIVE + ", "
1996                 + "name_raw_contact." + RawContactsColumns.PHONEBOOK_LABEL_ALTERNATIVE
1997                         + " AS " + ContactsColumns.PHONEBOOK_LABEL_ALTERNATIVE + ", "
1998                 + "name_raw_contact." + RawContactsColumns.PHONEBOOK_BUCKET_ALTERNATIVE
1999                         + " AS " + ContactsColumns.PHONEBOOK_BUCKET_ALTERNATIVE;
2000 
2001         String dataSelect = "SELECT "
2002                 + DataColumns.CONCRETE_ID + " AS " + Data._ID + ","
2003                 + Data.HASH_ID + ", "
2004                 + Data.RAW_CONTACT_ID + ", "
2005                 + RawContactsColumns.CONCRETE_CONTACT_ID + " AS " + RawContacts.CONTACT_ID + ", "
2006                 + syncColumns + ", "
2007                 + dataColumns + ", "
2008                 + contactOptionColumns + ", "
2009                 + contactNameColumns + ", "
2010                 + baseContactColumns + ", "
2011                 + buildDisplayPhotoUriAlias(RawContactsColumns.CONCRETE_CONTACT_ID,
2012                         Contacts.PHOTO_URI) + ", "
2013                 + buildThumbnailPhotoUriAlias(RawContactsColumns.CONCRETE_CONTACT_ID,
2014                         Contacts.PHOTO_THUMBNAIL_URI) + ", "
2015                 + dbForProfile() + " AS " + RawContacts.RAW_CONTACT_IS_USER_PROFILE + ", "
2016                 + Tables.GROUPS + "." + Groups.SOURCE_ID + " AS " + GroupMembership.GROUP_SOURCE_ID
2017                 + " FROM " + Tables.DATA
2018                 + " JOIN " + Tables.MIMETYPES + " ON ("
2019                 +   DataColumns.CONCRETE_MIMETYPE_ID + "=" + MimetypesColumns.CONCRETE_ID + ")"
2020                 + " JOIN " + Tables.RAW_CONTACTS + " ON ("
2021                 +   DataColumns.CONCRETE_RAW_CONTACT_ID + "=" + RawContactsColumns.CONCRETE_ID + ")"
2022                 + " JOIN " + Tables.ACCOUNTS + " ON ("
2023                 +   RawContactsColumns.CONCRETE_ACCOUNT_ID + "=" + AccountsColumns.CONCRETE_ID
2024                     + ")"
2025                 + " JOIN " + Tables.CONTACTS + " ON ("
2026                 +   RawContactsColumns.CONCRETE_CONTACT_ID + "=" + ContactsColumns.CONCRETE_ID + ")"
2027                 + " JOIN " + Tables.RAW_CONTACTS + " AS name_raw_contact ON("
2028                 +   Contacts.NAME_RAW_CONTACT_ID + "=name_raw_contact." + RawContacts._ID + ")"
2029                 + " LEFT OUTER JOIN " + Tables.PACKAGES + " ON ("
2030                 +   DataColumns.CONCRETE_PACKAGE_ID + "=" + PackagesColumns.CONCRETE_ID + ")"
2031                 + " LEFT OUTER JOIN " + Tables.GROUPS + " ON ("
2032                 +   MimetypesColumns.CONCRETE_MIMETYPE + "='" + GroupMembership.CONTENT_ITEM_TYPE
2033                 +   "' AND " + GroupsColumns.CONCRETE_ID + "="
2034                         + Tables.DATA + "." + GroupMembership.GROUP_ROW_ID + ")";
2035 
2036         db.execSQL("CREATE VIEW " + Views.DATA + " AS " + dataSelect);
2037 
2038         String rawContactOptionColumns =
2039                 RawContacts.CUSTOM_RINGTONE + ","
2040                 + RawContacts.SEND_TO_VOICEMAIL + ","
2041                 + RawContacts.RAW_LAST_TIME_CONTACTED + ","
2042                 + LowRes.getLastTimeUsedExpression(RawContacts.RAW_LAST_TIME_CONTACTED)
2043                         + " AS " + RawContacts.LR_LAST_TIME_CONTACTED + ","
2044                 + RawContacts.RAW_TIMES_CONTACTED + ","
2045                 + LowRes.getTimesUsedExpression(RawContacts.RAW_TIMES_CONTACTED)
2046                         + " AS " + RawContacts.LR_TIMES_CONTACTED + ","
2047                 + RawContacts.STARRED + ","
2048                 + RawContacts.PINNED;
2049 
2050         String rawContactsSelect = "SELECT "
2051                 + RawContactsColumns.CONCRETE_ID + " AS " + RawContacts._ID + ","
2052                 + RawContacts.CONTACT_ID + ", "
2053                 + RawContacts.AGGREGATION_MODE + ", "
2054                 + RawContacts.RAW_CONTACT_IS_READ_ONLY + ", "
2055                 + RawContacts.DELETED + ", "
2056                 + RawContactsColumns.CONCRETE_METADATA_DIRTY + ", "
2057                 + RawContacts.DISPLAY_NAME_SOURCE  + ", "
2058                 + RawContacts.DISPLAY_NAME_PRIMARY  + ", "
2059                 + RawContacts.DISPLAY_NAME_ALTERNATIVE  + ", "
2060                 + RawContacts.PHONETIC_NAME  + ", "
2061                 + RawContacts.PHONETIC_NAME_STYLE  + ", "
2062                 + RawContacts.SORT_KEY_PRIMARY  + ", "
2063                 + RawContactsColumns.PHONEBOOK_LABEL_PRIMARY  + ", "
2064                 + RawContactsColumns.PHONEBOOK_BUCKET_PRIMARY  + ", "
2065                 + RawContacts.SORT_KEY_ALTERNATIVE + ", "
2066                 + RawContactsColumns.PHONEBOOK_LABEL_ALTERNATIVE  + ", "
2067                 + RawContactsColumns.PHONEBOOK_BUCKET_ALTERNATIVE  + ", "
2068                 + dbForProfile() + " AS " + RawContacts.RAW_CONTACT_IS_USER_PROFILE + ", "
2069                 + rawContactOptionColumns + ", "
2070                 + syncColumns
2071                 + " FROM " + Tables.RAW_CONTACTS
2072                 + " JOIN " + Tables.ACCOUNTS + " ON ("
2073                 +   RawContactsColumns.CONCRETE_ACCOUNT_ID + "=" + AccountsColumns.CONCRETE_ID
2074                     + ")";
2075 
2076         db.execSQL("CREATE VIEW " + Views.RAW_CONTACTS + " AS " + rawContactsSelect);
2077 
2078         String contactsColumns =
2079                 ContactsColumns.CONCRETE_CUSTOM_RINGTONE
2080                         + " AS " + Contacts.CUSTOM_RINGTONE + ", "
2081                 + contactNameColumns + ", "
2082                 + baseContactColumns + ", "
2083 
2084                 + ContactsColumns.CONCRETE_RAW_LAST_TIME_CONTACTED
2085                         + " AS " + Contacts.RAW_LAST_TIME_CONTACTED + ", "
2086                 + LowRes.getLastTimeUsedExpression(ContactsColumns.CONCRETE_RAW_LAST_TIME_CONTACTED)
2087                         + " AS " + Contacts.LR_LAST_TIME_CONTACTED + ", "
2088 
2089                 + ContactsColumns.CONCRETE_SEND_TO_VOICEMAIL
2090                         + " AS " + Contacts.SEND_TO_VOICEMAIL + ", "
2091                 + ContactsColumns.CONCRETE_STARRED
2092                         + " AS " + Contacts.STARRED + ", "
2093                 + ContactsColumns.CONCRETE_PINNED
2094                 + " AS " + Contacts.PINNED + ", "
2095 
2096                 + ContactsColumns.CONCRETE_RAW_TIMES_CONTACTED
2097                         + " AS " + Contacts.RAW_TIMES_CONTACTED + ", "
2098                 + LowRes.getTimesUsedExpression(ContactsColumns.CONCRETE_RAW_TIMES_CONTACTED)
2099                         + " AS " + Contacts.LR_TIMES_CONTACTED;
2100 
2101         String contactsSelect = "SELECT "
2102                 + ContactsColumns.CONCRETE_ID + " AS " + Contacts._ID + ","
2103                 + contactsColumns + ", "
2104                 + buildDisplayPhotoUriAlias(ContactsColumns.CONCRETE_ID, Contacts.PHOTO_URI) + ", "
2105                 + buildThumbnailPhotoUriAlias(ContactsColumns.CONCRETE_ID,
2106                         Contacts.PHOTO_THUMBNAIL_URI) + ", "
2107                 + dbForProfile() + " AS " + Contacts.IS_USER_PROFILE
2108                 + " FROM " + Tables.CONTACTS
2109                 + " JOIN " + Tables.RAW_CONTACTS + " AS name_raw_contact ON("
2110                 +   Contacts.NAME_RAW_CONTACT_ID + "=name_raw_contact." + RawContacts._ID + ")";
2111 
2112         db.execSQL("CREATE VIEW " + Views.CONTACTS + " AS " + contactsSelect);
2113 
2114         String rawEntitiesSelect = "SELECT "
2115                 + RawContacts.CONTACT_ID + ", "
2116                 + RawContactsColumns.CONCRETE_DELETED + " AS " + RawContacts.DELETED + ","
2117                 + RawContactsColumns.CONCRETE_METADATA_DIRTY + ", "
2118                 + dataColumns + ", "
2119                 + syncColumns + ", "
2120                 + Data.SYNC1 + ", "
2121                 + Data.SYNC2 + ", "
2122                 + Data.SYNC3 + ", "
2123                 + Data.SYNC4 + ", "
2124                 + RawContactsColumns.CONCRETE_ID + " AS " + RawContacts._ID + ", "
2125                 + DataColumns.CONCRETE_ID + " AS " + RawContacts.Entity.DATA_ID + ","
2126                 + RawContactsColumns.CONCRETE_STARRED + " AS " + RawContacts.STARRED + ","
2127                 + dbForProfile() + " AS " + RawContacts.RAW_CONTACT_IS_USER_PROFILE + ","
2128                 + Tables.GROUPS + "." + Groups.SOURCE_ID + " AS " + GroupMembership.GROUP_SOURCE_ID
2129                 + " FROM " + Tables.RAW_CONTACTS
2130                 + " JOIN " + Tables.ACCOUNTS + " ON ("
2131                 +   RawContactsColumns.CONCRETE_ACCOUNT_ID + "=" + AccountsColumns.CONCRETE_ID
2132                     + ")"
2133                 + " LEFT OUTER JOIN " + Tables.DATA + " ON ("
2134                 +   DataColumns.CONCRETE_RAW_CONTACT_ID + "=" + RawContactsColumns.CONCRETE_ID + ")"
2135                 + " LEFT OUTER JOIN " + Tables.PACKAGES + " ON ("
2136                 +   DataColumns.CONCRETE_PACKAGE_ID + "=" + PackagesColumns.CONCRETE_ID + ")"
2137                 + " LEFT OUTER JOIN " + Tables.MIMETYPES + " ON ("
2138                 +   DataColumns.CONCRETE_MIMETYPE_ID + "=" + MimetypesColumns.CONCRETE_ID + ")"
2139                 + " LEFT OUTER JOIN " + Tables.GROUPS + " ON ("
2140                 +   MimetypesColumns.CONCRETE_MIMETYPE + "='" + GroupMembership.CONTENT_ITEM_TYPE
2141                 +   "' AND " + GroupsColumns.CONCRETE_ID + "="
2142                 + Tables.DATA + "." + GroupMembership.GROUP_ROW_ID + ")";
2143 
2144         db.execSQL("CREATE VIEW " + Views.RAW_ENTITIES + " AS "
2145                 + rawEntitiesSelect);
2146 
2147         String entitiesSelect = "SELECT "
2148                 + RawContactsColumns.CONCRETE_CONTACT_ID + " AS " + Contacts._ID + ", "
2149                 + RawContactsColumns.CONCRETE_CONTACT_ID + " AS " + RawContacts.CONTACT_ID + ", "
2150                 + RawContactsColumns.CONCRETE_DELETED + " AS " + RawContacts.DELETED + ","
2151                 + RawContactsColumns.CONCRETE_METADATA_DIRTY + ", "
2152                 + dataColumns + ", "
2153                 + syncColumns + ", "
2154                 + contactsColumns + ", "
2155                 + buildDisplayPhotoUriAlias(RawContactsColumns.CONCRETE_CONTACT_ID,
2156                         Contacts.PHOTO_URI) + ", "
2157                 + buildThumbnailPhotoUriAlias(RawContactsColumns.CONCRETE_CONTACT_ID,
2158                         Contacts.PHOTO_THUMBNAIL_URI) + ", "
2159                 + dbForProfile() + " AS " + Contacts.IS_USER_PROFILE + ", "
2160                 + Data.SYNC1 + ", "
2161                 + Data.SYNC2 + ", "
2162                 + Data.SYNC3 + ", "
2163                 + Data.SYNC4 + ", "
2164                 + RawContactsColumns.CONCRETE_ID + " AS " + Contacts.Entity.RAW_CONTACT_ID + ", "
2165                 + DataColumns.CONCRETE_ID + " AS " + Contacts.Entity.DATA_ID + ","
2166                 + Tables.GROUPS + "." + Groups.SOURCE_ID + " AS " + GroupMembership.GROUP_SOURCE_ID
2167                 + " FROM " + Tables.RAW_CONTACTS
2168                 + " JOIN " + Tables.ACCOUNTS + " ON ("
2169                 +   RawContactsColumns.CONCRETE_ACCOUNT_ID + "=" + AccountsColumns.CONCRETE_ID
2170                     + ")"
2171                 + " JOIN " + Tables.CONTACTS + " ON ("
2172                 +   RawContactsColumns.CONCRETE_CONTACT_ID + "=" + ContactsColumns.CONCRETE_ID + ")"
2173                 + " JOIN " + Tables.RAW_CONTACTS + " AS name_raw_contact ON("
2174                 +   Contacts.NAME_RAW_CONTACT_ID + "=name_raw_contact." + RawContacts._ID + ")"
2175                 + " LEFT OUTER JOIN " + Tables.DATA + " ON ("
2176                 +   DataColumns.CONCRETE_RAW_CONTACT_ID + "=" + RawContactsColumns.CONCRETE_ID + ")"
2177                 + " LEFT OUTER JOIN " + Tables.PACKAGES + " ON ("
2178                 +   DataColumns.CONCRETE_PACKAGE_ID + "=" + PackagesColumns.CONCRETE_ID + ")"
2179                 + " LEFT OUTER JOIN " + Tables.MIMETYPES + " ON ("
2180                 +   DataColumns.CONCRETE_MIMETYPE_ID + "=" + MimetypesColumns.CONCRETE_ID + ")"
2181                 + " LEFT OUTER JOIN " + Tables.GROUPS + " ON ("
2182                 +   MimetypesColumns.CONCRETE_MIMETYPE + "='" + GroupMembership.CONTENT_ITEM_TYPE
2183                 +   "' AND " + GroupsColumns.CONCRETE_ID + "="
2184                 + Tables.DATA + "." + GroupMembership.GROUP_ROW_ID + ")";
2185 
2186         db.execSQL("CREATE VIEW " + Views.ENTITIES + " AS "
2187                 + entitiesSelect);
2188 
2189         // Data usage view, with the low res columns, with no joins.
2190         final String dataUsageViewSelect = "SELECT "
2191                 + DataUsageStatColumns._ID + ", "
2192                 + DataUsageStatColumns.DATA_ID + ", "
2193                 + DataUsageStatColumns.USAGE_TYPE_INT + ", "
2194                 + DataUsageStatColumns.RAW_TIMES_USED + ", "
2195                 + DataUsageStatColumns.RAW_LAST_TIME_USED + ","
2196                 + LowRes.getTimesUsedExpression(DataUsageStatColumns.RAW_TIMES_USED)
2197                     + " AS " + DataUsageStatColumns.LR_TIMES_USED + ","
2198                 + LowRes.getLastTimeUsedExpression(DataUsageStatColumns.RAW_LAST_TIME_USED)
2199                     + " AS " + DataUsageStatColumns.LR_LAST_TIME_USED
2200                 + " FROM " + Tables.DATA_USAGE_STAT;
2201 
2202         // When the data_usage_stat table is needed with the low-res columns, use this, which is
2203         // faster than the DATA_USAGE_STAT view since it doesn't involve joins.
2204         db.execSQL("CREATE VIEW " + Views.DATA_USAGE_LR + " AS " + dataUsageViewSelect);
2205 
2206         String dataUsageStatSelect = "SELECT "
2207                 + DataUsageStatColumns.CONCRETE_ID + " AS " + DataUsageStatColumns._ID + ", "
2208                 + DataUsageStatColumns.DATA_ID + ", "
2209                 + RawContactsColumns.CONCRETE_CONTACT_ID + " AS " + RawContacts.CONTACT_ID + ", "
2210                 + MimetypesColumns.CONCRETE_MIMETYPE + " AS " + Data.MIMETYPE + ", "
2211                 + DataUsageStatColumns.USAGE_TYPE_INT + ", "
2212                 + DataUsageStatColumns.RAW_TIMES_USED + ", "
2213                 + DataUsageStatColumns.RAW_LAST_TIME_USED + ", "
2214                 + DataUsageStatColumns.LR_TIMES_USED + ", "
2215                 + DataUsageStatColumns.LR_LAST_TIME_USED
2216                 + " FROM " + Views.DATA_USAGE_LR + " AS " + Tables.DATA_USAGE_STAT
2217                 + " JOIN " + Tables.DATA + " ON ("
2218                 +   DataColumns.CONCRETE_ID + "=" + DataUsageStatColumns.CONCRETE_DATA_ID + ")"
2219                 + " JOIN " + Tables.RAW_CONTACTS + " ON ("
2220                 +   RawContactsColumns.CONCRETE_ID + "=" + DataColumns.CONCRETE_RAW_CONTACT_ID
2221                     + " )"
2222                 + " JOIN " + Tables.MIMETYPES + " ON ("
2223                 +   MimetypesColumns.CONCRETE_ID + "=" + DataColumns.CONCRETE_MIMETYPE_ID + ")";
2224 
2225         db.execSQL("CREATE VIEW " + Views.DATA_USAGE_STAT + " AS " + dataUsageStatSelect);
2226 
2227         String streamItemSelect = "SELECT " +
2228                 StreamItemsColumns.CONCRETE_ID + ", " +
2229                 ContactsColumns.CONCRETE_ID + " AS " + StreamItems.CONTACT_ID + ", " +
2230                 ContactsColumns.CONCRETE_LOOKUP_KEY +
2231                         " AS " + StreamItems.CONTACT_LOOKUP_KEY + ", " +
2232                 AccountsColumns.CONCRETE_ACCOUNT_NAME + ", " +
2233                 AccountsColumns.CONCRETE_ACCOUNT_TYPE + ", " +
2234                 AccountsColumns.CONCRETE_DATA_SET + ", " +
2235                 StreamItemsColumns.CONCRETE_RAW_CONTACT_ID +
2236                         " as " + StreamItems.RAW_CONTACT_ID + ", " +
2237                 RawContactsColumns.CONCRETE_SOURCE_ID +
2238                         " as " + StreamItems.RAW_CONTACT_SOURCE_ID + ", " +
2239                 StreamItemsColumns.CONCRETE_PACKAGE + ", " +
2240                 StreamItemsColumns.CONCRETE_ICON + ", " +
2241                 StreamItemsColumns.CONCRETE_LABEL + ", " +
2242                 StreamItemsColumns.CONCRETE_TEXT + ", " +
2243                 StreamItemsColumns.CONCRETE_TIMESTAMP + ", " +
2244                 StreamItemsColumns.CONCRETE_COMMENTS + ", " +
2245                 StreamItemsColumns.CONCRETE_SYNC1 + ", " +
2246                 StreamItemsColumns.CONCRETE_SYNC2 + ", " +
2247                 StreamItemsColumns.CONCRETE_SYNC3 + ", " +
2248                 StreamItemsColumns.CONCRETE_SYNC4 +
2249                 " FROM " + Tables.STREAM_ITEMS
2250                 + " JOIN " + Tables.RAW_CONTACTS + " ON ("
2251                 + StreamItemsColumns.CONCRETE_RAW_CONTACT_ID + "=" + RawContactsColumns.CONCRETE_ID
2252                     + ")"
2253                 + " JOIN " + Tables.ACCOUNTS + " ON ("
2254                 +   RawContactsColumns.CONCRETE_ACCOUNT_ID + "=" + AccountsColumns.CONCRETE_ID
2255                     + ")"
2256                 + " JOIN " + Tables.CONTACTS + " ON ("
2257                 + RawContactsColumns.CONCRETE_CONTACT_ID + "=" + ContactsColumns.CONCRETE_ID + ")";
2258 
2259         db.execSQL("CREATE VIEW " + Views.STREAM_ITEMS + " AS " + streamItemSelect);
2260 
2261         String metadataSyncSelect = "SELECT " +
2262                 MetadataSyncColumns.CONCRETE_ID + ", " +
2263                 MetadataSync.RAW_CONTACT_BACKUP_ID + ", " +
2264                 AccountsColumns.ACCOUNT_NAME + ", " +
2265                 AccountsColumns.ACCOUNT_TYPE + ", " +
2266                 AccountsColumns.DATA_SET + ", " +
2267                 MetadataSync.DATA + ", " +
2268                 MetadataSync.DELETED +
2269                 " FROM " + Tables.METADATA_SYNC
2270                 + " JOIN " + Tables.ACCOUNTS + " ON ("
2271                 +   MetadataSyncColumns.CONCRETE_ACCOUNT_ID + "=" + AccountsColumns.CONCRETE_ID
2272                 + ")";
2273 
2274         db.execSQL("CREATE VIEW " + Views.METADATA_SYNC + " AS " + metadataSyncSelect);
2275 
2276         String metadataSyncStateSelect = "SELECT " +
2277                 MetadataSyncStateColumns.CONCRETE_ID + ", " +
2278                 AccountsColumns.ACCOUNT_NAME + ", " +
2279                 AccountsColumns.ACCOUNT_TYPE + ", " +
2280                 AccountsColumns.DATA_SET + ", " +
2281                 MetadataSyncState.STATE +
2282                 " FROM " + Tables.METADATA_SYNC_STATE
2283                 + " JOIN " + Tables.ACCOUNTS + " ON ("
2284                 +   MetadataSyncStateColumns.CONCRETE_ACCOUNT_ID + "=" + AccountsColumns.CONCRETE_ID
2285                 + ")";
2286 
2287         db.execSQL("CREATE VIEW " + Views.METADATA_SYNC_STATE + " AS " + metadataSyncStateSelect);
2288     }
2289 
buildDisplayPhotoUriAlias(String contactIdColumn, String alias)2290     private static String buildDisplayPhotoUriAlias(String contactIdColumn, String alias) {
2291         return "(CASE WHEN " + Contacts.PHOTO_FILE_ID + " IS NULL THEN (CASE WHEN "
2292                 + Contacts.PHOTO_ID + " IS NULL"
2293                 + " OR " + Contacts.PHOTO_ID + "=0"
2294                 + " THEN NULL"
2295                 + " ELSE '" + Contacts.CONTENT_URI + "/'||"
2296                         + contactIdColumn + "|| '/" + Photo.CONTENT_DIRECTORY + "'"
2297                 + " END) ELSE '" + DisplayPhoto.CONTENT_URI + "/'||"
2298                         + Contacts.PHOTO_FILE_ID + " END)"
2299                 + " AS " + alias;
2300     }
2301 
buildThumbnailPhotoUriAlias(String contactIdColumn, String alias)2302     private static String buildThumbnailPhotoUriAlias(String contactIdColumn, String alias) {
2303         return "(CASE WHEN "
2304                 + Contacts.PHOTO_ID + " IS NULL"
2305                 + " OR " + Contacts.PHOTO_ID + "=0"
2306                 + " THEN NULL"
2307                 + " ELSE '" + Contacts.CONTENT_URI + "/'||"
2308                         + contactIdColumn + "|| '/" + Photo.CONTENT_DIRECTORY + "'"
2309                 + " END)"
2310                 + " AS " + alias;
2311     }
2312 
2313     /**
2314      * Returns the value to be returned when querying the column indicating that the contact
2315      * or raw contact belongs to the user's personal profile.  Overridden in the profile
2316      * DB helper subclass.
2317      */
dbForProfile()2318     protected int dbForProfile() {
2319         return 0;
2320     }
2321 
createGroupsView(SQLiteDatabase db)2322     private void createGroupsView(SQLiteDatabase db) {
2323         db.execSQL("DROP VIEW IF EXISTS " + Views.GROUPS + ";");
2324 
2325         String groupsColumns =
2326                 GroupsColumns.CONCRETE_ACCOUNT_ID + " AS " + GroupsColumns.ACCOUNT_ID + ","
2327                 + AccountsColumns.CONCRETE_ACCOUNT_NAME + " AS " + Groups.ACCOUNT_NAME + ","
2328                 + AccountsColumns.CONCRETE_ACCOUNT_TYPE + " AS " + Groups.ACCOUNT_TYPE + ","
2329                 + AccountsColumns.CONCRETE_DATA_SET + " AS " + Groups.DATA_SET + ","
2330                 + "(CASE WHEN " + AccountsColumns.CONCRETE_DATA_SET
2331                     + " IS NULL THEN " + AccountsColumns.CONCRETE_ACCOUNT_TYPE
2332                     + " ELSE " + AccountsColumns.CONCRETE_ACCOUNT_TYPE
2333                         + "||'/'||" + AccountsColumns.CONCRETE_DATA_SET + " END) AS "
2334                             + Groups.ACCOUNT_TYPE_AND_DATA_SET + ","
2335                 + Groups.SOURCE_ID + ","
2336                 + Groups.VERSION + ","
2337                 + Groups.DIRTY + ","
2338                 + Groups.TITLE + ","
2339                 + Groups.TITLE_RES + ","
2340                 + Groups.NOTES + ","
2341                 + Groups.SYSTEM_ID + ","
2342                 + Groups.DELETED + ","
2343                 + Groups.GROUP_VISIBLE + ","
2344                 + Groups.SHOULD_SYNC + ","
2345                 + Groups.AUTO_ADD + ","
2346                 + Groups.FAVORITES + ","
2347                 + Groups.GROUP_IS_READ_ONLY + ","
2348                 + Groups.SYNC1 + ","
2349                 + Groups.SYNC2 + ","
2350                 + Groups.SYNC3 + ","
2351                 + Groups.SYNC4 + ","
2352                 + PackagesColumns.PACKAGE + " AS " + Groups.RES_PACKAGE;
2353 
2354         String groupsSelect = "SELECT "
2355                 + GroupsColumns.CONCRETE_ID + " AS " + Groups._ID + ","
2356                 + groupsColumns
2357                 + " FROM " + Tables.GROUPS
2358                 + " JOIN " + Tables.ACCOUNTS + " ON ("
2359                     + GroupsColumns.CONCRETE_ACCOUNT_ID + "=" + AccountsColumns.CONCRETE_ID + ")"
2360                 + " LEFT OUTER JOIN " + Tables.PACKAGES + " ON ("
2361                     + GroupsColumns.CONCRETE_PACKAGE_ID + "=" + PackagesColumns.CONCRETE_ID + ")";
2362 
2363         db.execSQL("CREATE VIEW " + Views.GROUPS + " AS " + groupsSelect);
2364     }
2365 
2366     @Override
onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion)2367     public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
2368         Log.i(TAG, "ContactsProvider cannot proceed because downgrading your database is not " +
2369                 "supported. To continue, please either re-upgrade to your previous Android " +
2370                 "version, or clear all application data in Contacts Storage (this will result " +
2371                 "in the loss of all local contacts that are not synced). To avoid data loss, " +
2372                 "your contacts database will not be wiped automatically.");
2373         super.onDowngrade(db, oldVersion, newVersion);
2374     }
2375 
2376     @Override
onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)2377     public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
2378         Log.i(TAG,
2379                 "Upgrading " + DATABASE_NAME + " from version " + oldVersion + " to " + newVersion);
2380 
2381         prepopulateCommonMimeTypes(db);
2382 
2383         boolean upgradeViewsAndTriggers = false;
2384         boolean upgradeNameLookup = false;
2385         boolean upgradeLegacyApiSupport = false;
2386         boolean upgradeSearchIndex = false;
2387         boolean rescanDirectories = false;
2388         boolean rebuildSqliteStats = false;
2389         boolean upgradeLocaleSpecificData = false;
2390 
2391         if (oldVersion < 701) {
2392             upgradeToVersion701(db);
2393             oldVersion = 701;
2394         }
2395 
2396         if (oldVersion < 702) {
2397             upgradeToVersion702(db);
2398             oldVersion = 702;
2399         }
2400 
2401         if (oldVersion < 703) {
2402             // Now names like "L'Image" will be searchable.
2403             upgradeSearchIndex = true;
2404             oldVersion = 703;
2405         }
2406 
2407         if (oldVersion < 704) {
2408             db.execSQL("DROP TABLE IF EXISTS activities;");
2409             oldVersion = 704;
2410         }
2411 
2412         if (oldVersion < 705) {
2413             // Before this version, we didn't rebuild the search index on locale changes, so
2414             // if the locale has changed after sync, the index contains gets stale.
2415             // To correct the issue we have to rebuild the index here.
2416             upgradeSearchIndex = true;
2417             oldVersion = 705;
2418         }
2419 
2420         if (oldVersion < 706) {
2421             // Prior to this version, we didn't rebuild the stats table after drop operations,
2422             // which resulted in losing some of the rows from the stats table.
2423             rebuildSqliteStats = true;
2424             oldVersion = 706;
2425         }
2426 
2427         if (oldVersion < 707) {
2428             upgradeToVersion707(db);
2429             upgradeViewsAndTriggers = true;
2430             oldVersion = 707;
2431         }
2432 
2433         if (oldVersion < 708) {
2434             // Sort keys, phonebook labels and buckets, and search keys have
2435             // changed so force a rebuild.
2436             upgradeLocaleSpecificData = true;
2437             oldVersion = 708;
2438         }
2439         if (oldVersion < 709) {
2440             // Added secondary locale phonebook labels; changed Japanese
2441             // and Chinese sort keys.
2442             upgradeLocaleSpecificData = true;
2443             oldVersion = 709;
2444         }
2445 
2446         if (oldVersion < 710) {
2447             upgradeToVersion710(db);
2448             upgradeViewsAndTriggers = true;
2449             oldVersion = 710;
2450         }
2451 
2452         if (oldVersion < 800) {
2453             upgradeToVersion800(db);
2454             oldVersion = 800;
2455         }
2456 
2457         if (oldVersion < 801) {
2458             PropertyUtils.setProperty(db, DbProperties.DATABASE_TIME_CREATED, String.valueOf(
2459                     System.currentTimeMillis()));
2460             oldVersion = 801;
2461         }
2462 
2463         if (oldVersion < 802) {
2464             upgradeToVersion802(db);
2465             upgradeViewsAndTriggers = true;
2466             oldVersion = 802;
2467         }
2468 
2469         if (oldVersion < 803) {
2470             // Rebuild the search index so that names, organizations and nicknames are
2471             // now indexed as names.
2472             upgradeSearchIndex = true;
2473             oldVersion = 803;
2474         }
2475 
2476         if (oldVersion < 804) {
2477             // Reserved.
2478             oldVersion = 804;
2479         }
2480 
2481         if (oldVersion < 900) {
2482             upgradeViewsAndTriggers = true;
2483             oldVersion = 900;
2484         }
2485 
2486         if (oldVersion < 901) {
2487             // Rebuild the search index to fix any search index that was previously in a
2488             // broken state due to b/11059351
2489             upgradeSearchIndex = true;
2490             oldVersion = 901;
2491         }
2492 
2493         if (oldVersion < 902) {
2494             upgradeToVersion902(db);
2495             oldVersion = 902;
2496         }
2497 
2498         if (oldVersion < 903) {
2499             upgradeToVersion903(db);
2500             oldVersion = 903;
2501         }
2502 
2503         if (oldVersion < 904) {
2504             upgradeToVersion904(db);
2505             oldVersion = 904;
2506         }
2507 
2508         if (oldVersion < 905) {
2509             upgradeToVersion905(db);
2510             oldVersion = 905;
2511         }
2512 
2513         if (oldVersion < 906) {
2514             upgradeToVersion906(db);
2515             oldVersion = 906;
2516         }
2517 
2518         if (oldVersion < 907) {
2519             // Rebuild NAME_LOOKUP.
2520             upgradeNameLookup = true;
2521             oldVersion = 907;
2522         }
2523 
2524         if (oldVersion < 908) {
2525             upgradeToVersion908(db);
2526             oldVersion = 908;
2527         }
2528 
2529         if (oldVersion < 909) {
2530             upgradeToVersion909(db);
2531             oldVersion = 909;
2532         }
2533 
2534         if (oldVersion < 910) {
2535             upgradeToVersion910(db);
2536             oldVersion = 910;
2537         }
2538         if (oldVersion < 1000) {
2539             upgradeToVersion1000(db);
2540             upgradeViewsAndTriggers = true;
2541             oldVersion = 1000;
2542         }
2543 
2544         if (oldVersion < 1002) {
2545             rebuildSqliteStats = true;
2546             upgradeToVersion1002(db);
2547             oldVersion = 1002;
2548         }
2549 
2550         if (oldVersion < 1003) {
2551             upgradeToVersion1003(db);
2552             oldVersion = 1003;
2553         }
2554 
2555         if (oldVersion < 1004) {
2556             upgradeToVersion1004(db);
2557             oldVersion = 1004;
2558         }
2559 
2560         if (oldVersion < 1005) {
2561             upgradeToVersion1005(db);
2562             oldVersion = 1005;
2563         }
2564 
2565         if (oldVersion < 1006) {
2566             upgradeViewsAndTriggers = true;
2567             oldVersion = 1006;
2568         }
2569 
2570         if (oldVersion < 1007) {
2571             upgradeToVersion1007(db);
2572             oldVersion = 1007;
2573         }
2574 
2575         if (oldVersion < 1009) {
2576             upgradeToVersion1009(db);
2577             oldVersion = 1009;
2578         }
2579 
2580         if (oldVersion < 1100) {
2581             upgradeToVersion1100(db);
2582             upgradeViewsAndTriggers = true;
2583             oldVersion = 1100;
2584         }
2585 
2586         if (oldVersion < 1101) {
2587             upgradeToVersion1101(db);
2588             oldVersion = 1101;
2589         }
2590 
2591         if (oldVersion < 1102) {
2592             // Version 1009 was added *after* 1100/1101.  For master devices
2593             // that have already been updated to 1101, we do it again.
2594             upgradeToVersion1009(db);
2595             oldVersion = 1102;
2596         }
2597 
2598         if (oldVersion < 1103) {
2599             upgradeViewsAndTriggers = true;
2600             oldVersion = 1103;
2601         }
2602 
2603         if (oldVersion < 1104) {
2604             upgradeToVersion1104(db);
2605             upgradeViewsAndTriggers = true;
2606             oldVersion = 1104;
2607         }
2608 
2609         if (oldVersion < 1105) {
2610             upgradeToVersion1105(db);
2611             upgradeViewsAndTriggers = true;
2612             oldVersion = 1105;
2613         }
2614 
2615         if (oldVersion < 1106) {
2616             upgradeToVersion1106(db);
2617             oldVersion = 1106;
2618         }
2619 
2620         if (oldVersion < 1107) {
2621             upgradeToVersion1107(db);
2622             oldVersion = 1107;
2623         }
2624 
2625         if (oldVersion < 1108) {
2626             upgradeToVersion1108(db);
2627             oldVersion = 1108;
2628         }
2629 
2630         if (isUpgradeRequired(oldVersion, newVersion, 1109)) {
2631             upgradeToVersion1109(db);
2632             oldVersion = 1109;
2633         }
2634 
2635         if (isUpgradeRequired(oldVersion, newVersion, 1110)) {
2636             upgradeToVersion1110(db);
2637             oldVersion = 1110;
2638         }
2639 
2640         if (isUpgradeRequired(oldVersion, newVersion, 1111)) {
2641             upgradeToVersion1111(db);
2642             oldVersion = 1111;
2643         }
2644 
2645         if (isUpgradeRequired(oldVersion, newVersion, 1200)) {
2646             createPresenceTables(db);
2647             oldVersion = 1200;
2648         }
2649 
2650         if (isUpgradeRequired(oldVersion, newVersion, 1201)) {
2651             upgradeToVersion1201(db);
2652             upgradeViewsAndTriggers = true;
2653             oldVersion = 1201;
2654         }
2655 
2656         if (isUpgradeRequired(oldVersion, newVersion, 1202)) {
2657             upgradeViewsAndTriggers = true;
2658             oldVersion = 1202;
2659         }
2660 
2661         // We extracted "calls" and "voicemail_status" at this point, but we can't remove them here
2662         // yet, until CallLogDatabaseHelper moves the data.
2663 
2664         if (upgradeViewsAndTriggers) {
2665             createContactsViews(db);
2666             createGroupsView(db);
2667             createContactsTriggers(db);
2668             createContactsIndexes(db, false /* we build stats table later */);
2669             upgradeLegacyApiSupport = true;
2670             rebuildSqliteStats = true;
2671         }
2672 
2673         if (upgradeLegacyApiSupport) {
2674             LegacyApiSupport.createViews(db);
2675         }
2676 
2677         if (upgradeLocaleSpecificData) {
2678             upgradeLocaleData(db, false /* we build stats table later */);
2679             // Name lookups are rebuilt as part of the full locale rebuild
2680             upgradeNameLookup = false;
2681             upgradeSearchIndex = true;
2682             rebuildSqliteStats = true;
2683         }
2684 
2685         if (upgradeNameLookup) {
2686             rebuildNameLookup(db, false /* we build stats table later */);
2687             rebuildSqliteStats = true;
2688         }
2689 
2690         if (upgradeSearchIndex) {
2691             rebuildSearchIndex(db, false /* we build stats table later */);
2692             rebuildSqliteStats = true;
2693         }
2694 
2695         if (rescanDirectories) {
2696             // Force the next ContactDirectoryManager.scanAllPackages() to rescan all packages.
2697             // (It's called from the BACKGROUND_TASK_UPDATE_ACCOUNTS background task.)
2698             PropertyUtils.setProperty(db, DbProperties.DIRECTORY_SCAN_COMPLETE, "0");
2699         }
2700 
2701         if (rebuildSqliteStats) {
2702             updateSqliteStats(db);
2703         }
2704 
2705         if (oldVersion != newVersion) {
2706             throw new IllegalStateException(
2707                     "error upgrading the database to version " + newVersion);
2708         }
2709     }
2710 
isUpgradeRequired(int oldVersion, int newVersion, int version)2711     private static boolean isUpgradeRequired(int oldVersion, int newVersion, int version) {
2712         return oldVersion < version && newVersion >= version;
2713     }
2714 
rebuildNameLookup(SQLiteDatabase db, boolean rebuildSqliteStats)2715     private void rebuildNameLookup(SQLiteDatabase db, boolean rebuildSqliteStats) {
2716         db.execSQL("DROP INDEX IF EXISTS name_lookup_index");
2717         insertNameLookup(db);
2718         createContactsIndexes(db, rebuildSqliteStats);
2719     }
2720 
rebuildSearchIndex()2721     protected void rebuildSearchIndex() {
2722         rebuildSearchIndex(getWritableDatabase(), true);
2723     }
2724 
rebuildSearchIndex(SQLiteDatabase db, boolean rebuildSqliteStats)2725     private void rebuildSearchIndex(SQLiteDatabase db, boolean rebuildSqliteStats) {
2726         createSearchIndexTable(db, rebuildSqliteStats);
2727         PropertyUtils.setProperty(db, SearchIndexManager.PROPERTY_SEARCH_INDEX_VERSION, "0");
2728     }
2729 
2730     /**
2731      * Checks whether the current ICU code version matches that used to build
2732      * the locale specific data in the ContactsDB.
2733      */
needsToUpdateLocaleData(LocaleSet locales)2734     public boolean needsToUpdateLocaleData(LocaleSet locales) {
2735         final String dbLocale = getProperty(DbProperties.LOCALE, "");
2736         if (!dbLocale.equals(locales.toString())) {
2737             return true;
2738         }
2739         final String curICUVersion = ICU.getIcuVersion();
2740         final String dbICUVersion = getProperty(DbProperties.ICU_VERSION,
2741                 "(unknown)");
2742         if (!curICUVersion.equals(dbICUVersion)) {
2743             Log.i(TAG, "ICU version has changed. Current version is "
2744                     + curICUVersion + "; DB was built with " + dbICUVersion);
2745             return true;
2746         }
2747         return false;
2748     }
2749 
upgradeLocaleData(SQLiteDatabase db, boolean rebuildSqliteStats)2750     private void upgradeLocaleData(SQLiteDatabase db, boolean rebuildSqliteStats) {
2751         final LocaleSet locales = LocaleSet.newDefault();
2752         Log.i(TAG, "Upgrading locale data for " + locales
2753                 + " (ICU v" + ICU.getIcuVersion() + ")");
2754         final long start = SystemClock.elapsedRealtime();
2755         rebuildLocaleData(db, locales, rebuildSqliteStats);
2756         Log.i(TAG, "Locale update completed in " + (SystemClock.elapsedRealtime() - start) + "ms");
2757     }
2758 
rebuildLocaleData(SQLiteDatabase db, LocaleSet locales, boolean rebuildSqliteStats)2759     private void rebuildLocaleData(SQLiteDatabase db, LocaleSet locales, boolean rebuildSqliteStats) {
2760         db.execSQL("DROP INDEX raw_contact_sort_key1_index");
2761         db.execSQL("DROP INDEX raw_contact_sort_key2_index");
2762         db.execSQL("DROP INDEX IF EXISTS name_lookup_index");
2763 
2764         loadNicknameLookupTable(db);
2765         insertNameLookup(db);
2766         rebuildSortKeys(db);
2767         createContactsIndexes(db, rebuildSqliteStats);
2768 
2769         FastScrollingIndexCache.getInstance(mContext).invalidate();
2770         // Update the ICU version used to generate the locale derived data
2771         // so we can tell when we need to rebuild with new ICU versions.
2772         PropertyUtils.setProperty(db, DbProperties.ICU_VERSION, ICU.getIcuVersion());
2773         PropertyUtils.setProperty(db, DbProperties.LOCALE, locales.toString());
2774     }
2775 
2776     /**
2777      * Regenerates all locale-sensitive data if needed:
2778      * nickname_lookup, name_lookup and sort keys. Invalidates the fast
2779      * scrolling index cache.
2780      */
setLocale(LocaleSet locales)2781     public void setLocale(LocaleSet locales) {
2782         if (!needsToUpdateLocaleData(locales)) {
2783             return;
2784         }
2785         Log.i(TAG, "Switching to locale " + locales
2786                 + " (ICU v" + ICU.getIcuVersion() + ")");
2787 
2788         final long start = SystemClock.elapsedRealtime();
2789         SQLiteDatabase db = getWritableDatabase();
2790         db.setLocale(locales.getPrimaryLocale());
2791         db.beginTransaction();
2792         try {
2793             rebuildLocaleData(db, locales, true);
2794             db.setTransactionSuccessful();
2795         } finally {
2796             db.endTransaction();
2797         }
2798 
2799         Log.i(TAG, "Locale change completed in " + (SystemClock.elapsedRealtime() - start) + "ms");
2800     }
2801 
2802     /**
2803      * Regenerates sort keys for all contacts.
2804      */
rebuildSortKeys(SQLiteDatabase db)2805     private void rebuildSortKeys(SQLiteDatabase db) {
2806         Cursor cursor = db.query(Tables.RAW_CONTACTS, new String[] {RawContacts._ID},
2807                 null, null, null, null, null);
2808         try {
2809             while (cursor.moveToNext()) {
2810                 long rawContactId = cursor.getLong(0);
2811                 updateRawContactDisplayName(db, rawContactId);
2812             }
2813         } finally {
2814             cursor.close();
2815         }
2816     }
2817 
insertNameLookup(SQLiteDatabase db)2818     private void insertNameLookup(SQLiteDatabase db) {
2819         db.execSQL("DELETE FROM " + Tables.NAME_LOOKUP);
2820 
2821         final SQLiteStatement nameLookupInsert = db.compileStatement(
2822                 "INSERT OR IGNORE INTO " + Tables.NAME_LOOKUP + "("
2823                         + NameLookupColumns.RAW_CONTACT_ID + ","
2824                         + NameLookupColumns.DATA_ID + ","
2825                         + NameLookupColumns.NAME_TYPE + ","
2826                         + NameLookupColumns.NORMALIZED_NAME +
2827                 ") VALUES (?,?,?,?)");
2828 
2829         try {
2830             insertStructuredNameLookup(db, nameLookupInsert);
2831             insertEmailLookup(db, nameLookupInsert);
2832             insertNicknameLookup(db, nameLookupInsert);
2833         } finally {
2834             nameLookupInsert.close();
2835         }
2836     }
2837 
2838     /**
2839      * Inserts name lookup rows for all structured names in the database.
2840      */
insertStructuredNameLookup(SQLiteDatabase db, SQLiteStatement nameLookupInsert)2841     private void insertStructuredNameLookup(SQLiteDatabase db, SQLiteStatement nameLookupInsert) {
2842         NameSplitter nameSplitter = createNameSplitter();
2843         NameLookupBuilder nameLookupBuilder = new StructuredNameLookupBuilder(nameSplitter,
2844                 new CommonNicknameCache(db), nameLookupInsert);
2845         final long mimeTypeId = lookupMimeTypeId(db, StructuredName.CONTENT_ITEM_TYPE);
2846         Cursor cursor = db.query(StructuredNameQuery.TABLE, StructuredNameQuery.COLUMNS,
2847                 StructuredNameQuery.SELECTION, new String[] {String.valueOf(mimeTypeId)},
2848                 null, null, null);
2849         try {
2850             while (cursor.moveToNext()) {
2851                 long dataId = cursor.getLong(StructuredNameQuery.ID);
2852                 long rawContactId = cursor.getLong(StructuredNameQuery.RAW_CONTACT_ID);
2853                 String name = cursor.getString(StructuredNameQuery.DISPLAY_NAME);
2854                 int fullNameStyle = nameSplitter.guessFullNameStyle(name);
2855                 fullNameStyle = nameSplitter.getAdjustedFullNameStyle(fullNameStyle);
2856                 nameLookupBuilder.insertNameLookup(rawContactId, dataId, name, fullNameStyle);
2857             }
2858         } finally {
2859             cursor.close();
2860         }
2861     }
2862 
2863     /**
2864      * Inserts name lookup rows for all email addresses in the database.
2865      */
insertEmailLookup(SQLiteDatabase db, SQLiteStatement nameLookupInsert)2866     private void insertEmailLookup(SQLiteDatabase db, SQLiteStatement nameLookupInsert) {
2867         final long mimeTypeId = lookupMimeTypeId(db, Email.CONTENT_ITEM_TYPE);
2868         Cursor cursor = db.query(EmailQuery.TABLE, EmailQuery.COLUMNS,
2869                 EmailQuery.SELECTION, new String[] {String.valueOf(mimeTypeId)},
2870                 null, null, null);
2871         try {
2872             while (cursor.moveToNext()) {
2873                 long dataId = cursor.getLong(EmailQuery.ID);
2874                 long rawContactId = cursor.getLong(EmailQuery.RAW_CONTACT_ID);
2875                 String address = cursor.getString(EmailQuery.ADDRESS);
2876                 address = extractHandleFromEmailAddress(address);
2877                 insertNameLookup(nameLookupInsert, rawContactId, dataId,
2878                         NameLookupType.EMAIL_BASED_NICKNAME, address);
2879             }
2880         } finally {
2881             cursor.close();
2882         }
2883     }
2884 
2885     /**
2886      * Inserts name lookup rows for all nicknames in the database.
2887      */
insertNicknameLookup(SQLiteDatabase db, SQLiteStatement nameLookupInsert)2888     private void insertNicknameLookup(SQLiteDatabase db, SQLiteStatement nameLookupInsert) {
2889         final long mimeTypeId = lookupMimeTypeId(db, Nickname.CONTENT_ITEM_TYPE);
2890         Cursor cursor = db.query(NicknameQuery.TABLE, NicknameQuery.COLUMNS,
2891                 NicknameQuery.SELECTION, new String[]{String.valueOf(mimeTypeId)},
2892                 null, null, null);
2893         try {
2894             while (cursor.moveToNext()) {
2895                 long dataId = cursor.getLong(NicknameQuery.ID);
2896                 long rawContactId = cursor.getLong(NicknameQuery.RAW_CONTACT_ID);
2897                 String nickname = cursor.getString(NicknameQuery.NAME);
2898                 insertNameLookup(nameLookupInsert, rawContactId, dataId,
2899                         NameLookupType.NICKNAME, nickname);
2900             }
2901         } finally {
2902             cursor.close();
2903         }
2904     }
2905 
2906     /**
2907      * Inserts a record in the {@link Tables#NAME_LOOKUP} table.
2908      */
insertNameLookup(SQLiteStatement stmt, long rawContactId, long dataId, int lookupType, String name)2909     public void insertNameLookup(SQLiteStatement stmt, long rawContactId, long dataId,
2910             int lookupType, String name) {
2911         if (TextUtils.isEmpty(name)) {
2912             return;
2913         }
2914 
2915         String normalized = NameNormalizer.normalize(name);
2916         if (TextUtils.isEmpty(normalized)) {
2917             return;
2918         }
2919 
2920         insertNormalizedNameLookup(stmt, rawContactId, dataId, lookupType, normalized);
2921     }
2922 
insertNormalizedNameLookup(SQLiteStatement stmt, long rawContactId, long dataId, int lookupType, String normalizedName)2923     private void insertNormalizedNameLookup(SQLiteStatement stmt, long rawContactId, long dataId,
2924             int lookupType, String normalizedName) {
2925         stmt.bindLong(1, rawContactId);
2926         stmt.bindLong(2, dataId);
2927         stmt.bindLong(3, lookupType);
2928         stmt.bindString(4, normalizedName);
2929         stmt.executeInsert();
2930     }
2931 
upgradeToVersion701(SQLiteDatabase db)2932     private void upgradeToVersion701(SQLiteDatabase db) {
2933         db.execSQL("UPDATE raw_contacts SET last_time_contacted =" +
2934                 " max(ifnull(last_time_contacted, 0), " +
2935                 " ifnull((SELECT max(last_time_used) " +
2936                     " FROM data JOIN data_usage_stat ON (data._id = data_usage_stat.data_id)" +
2937                     " WHERE data.raw_contact_id = raw_contacts._id), 0))");
2938         // Replace 0 with null.  This isn't really necessary, but we do this anyway for consistency.
2939         db.execSQL("UPDATE raw_contacts SET last_time_contacted = null" +
2940                 " where last_time_contacted = 0");
2941     }
2942 
2943     /**
2944      * Pre-HC devices don't have correct "NORMALIZED_NUMBERS".  Clear them up.
2945      */
upgradeToVersion702(SQLiteDatabase db)2946     private void upgradeToVersion702(SQLiteDatabase db) {
2947         // All the "correct" Phone.NORMALIZED_NUMBERS should begin with "+".  The upgraded data
2948         // don't.  Find all Phone.NORMALIZED_NUMBERS that don't begin with "+".
2949         final int count;
2950         final long[] dataIds;
2951         final long[] rawContactIds;
2952         final String[] phoneNumbers;
2953         final StringBuilder sbDataIds;
2954         final Cursor c = db.rawQuery(
2955                 "SELECT _id, raw_contact_id, data1 FROM data " +
2956                 " WHERE mimetype_id=" +
2957                     "(SELECT _id FROM mimetypes" +
2958                     " WHERE mimetype='vnd.android.cursor.item/phone_v2')" +
2959                 " AND data4 not like '+%'", // "Not like" will exclude nulls too.
2960                 null);
2961         try {
2962             count = c.getCount();
2963             if (count == 0) {
2964                 return;
2965             }
2966             dataIds = new long[count];
2967             rawContactIds = new long[count];
2968             phoneNumbers = new String[count];
2969             sbDataIds = new StringBuilder();
2970 
2971             c.moveToPosition(-1);
2972             while (c.moveToNext()) {
2973                 final int i = c.getPosition();
2974                 dataIds[i] = c.getLong(0);
2975                 rawContactIds[i] = c.getLong(1);
2976                 phoneNumbers[i] = c.getString(2);
2977 
2978                 if (sbDataIds.length() > 0) {
2979                     sbDataIds.append(",");
2980                 }
2981                 sbDataIds.append(dataIds[i]);
2982             }
2983         } finally {
2984             c.close();
2985         }
2986 
2987         final String dataIdList = sbDataIds.toString();
2988 
2989         // Then, update the Data and PhoneLookup tables.
2990 
2991         // First, just null out all Phone.NORMALIZED_NUMBERS for those.
2992         db.execSQL("UPDATE data SET data4 = null" +
2993                 " WHERE _id IN (" + dataIdList + ")");
2994 
2995         // Then, re-create phone_lookup for them.
2996         db.execSQL("DELETE FROM phone_lookup" +
2997                 " WHERE data_id IN (" + dataIdList + ")");
2998 
2999         for (int i = 0; i < count; i++) {
3000             // Mimic how DataRowHandlerForPhoneNumber.insert() works when it can't normalize
3001             // numbers.
3002             final String phoneNumber = phoneNumbers[i];
3003             if (TextUtils.isEmpty(phoneNumber)) continue;
3004 
3005             final String normalized = PhoneNumberUtils.normalizeNumber(phoneNumber);
3006             if (!TextUtils.isEmpty(normalized)) {
3007                 db.execSQL("INSERT INTO phone_lookup" +
3008                         "(data_id, raw_contact_id, normalized_number, min_match)" +
3009                         " VALUES(?,?,?,?)",
3010                         new String[] {
3011                             String.valueOf(dataIds[i]),
3012                             String.valueOf(rawContactIds[i]),
3013                             normalized,
3014                             PhoneNumberUtils.toCallerIDMinMatch(normalized)});
3015             }
3016         }
3017     }
3018 
upgradeToVersion707(SQLiteDatabase db)3019     private void upgradeToVersion707(SQLiteDatabase db) {
3020         db.execSQL("ALTER TABLE raw_contacts ADD phonebook_label TEXT;");
3021         db.execSQL("ALTER TABLE raw_contacts ADD phonebook_bucket INTEGER;");
3022         db.execSQL("ALTER TABLE raw_contacts ADD phonebook_label_alt TEXT;");
3023         db.execSQL("ALTER TABLE raw_contacts ADD phonebook_bucket_alt INTEGER;");
3024     }
3025 
upgradeToVersion710(SQLiteDatabase db)3026     private void upgradeToVersion710(SQLiteDatabase db) {
3027 
3028         // Adding timestamp to contacts table.
3029         db.execSQL("ALTER TABLE contacts"
3030                 + " ADD contact_last_updated_timestamp INTEGER;");
3031 
3032         db.execSQL("UPDATE contacts"
3033                 + " SET contact_last_updated_timestamp"
3034                 + " = " + System.currentTimeMillis());
3035 
3036         db.execSQL("CREATE INDEX contacts_contact_last_updated_timestamp_index "
3037                 + "ON contacts(contact_last_updated_timestamp)");
3038 
3039         // New deleted contacts table.
3040         db.execSQL("CREATE TABLE deleted_contacts (" +
3041                 "contact_id INTEGER PRIMARY KEY," +
3042                 "contact_deleted_timestamp INTEGER NOT NULL default 0"
3043                 + ");");
3044 
3045         db.execSQL("CREATE INDEX deleted_contacts_contact_deleted_timestamp_index "
3046                 + "ON deleted_contacts(contact_deleted_timestamp)");
3047     }
3048 
upgradeToVersion800(SQLiteDatabase db)3049     private void upgradeToVersion800(SQLiteDatabase db) {
3050         // Default Calls.PRESENTATION_ALLOWED=1
3051         db.execSQL("ALTER TABLE calls ADD presentation INTEGER NOT NULL DEFAULT 1;");
3052 
3053         // Re-map CallerInfo.{..}_NUMBER strings to Calls.PRESENTATION_{..} ints
3054         //  PRIVATE_NUMBER="-2" -> PRESENTATION_RESTRICTED=2
3055         //  UNKNOWN_NUMBER="-1" -> PRESENTATION_UNKNOWN   =3
3056         // PAYPHONE_NUMBER="-3" -> PRESENTATION_PAYPHONE  =4
3057         db.execSQL("UPDATE calls SET presentation=2, number='' WHERE number='-2';");
3058         db.execSQL("UPDATE calls SET presentation=3, number='' WHERE number='-1';");
3059         db.execSQL("UPDATE calls SET presentation=4, number='' WHERE number='-3';");
3060     }
3061 
upgradeToVersion802(SQLiteDatabase db)3062     private void upgradeToVersion802(SQLiteDatabase db) {
3063         db.execSQL("ALTER TABLE contacts ADD pinned INTEGER NOT NULL DEFAULT " +
3064                 ContactsContract.PinnedPositions.UNPINNED + ";");
3065         db.execSQL("ALTER TABLE raw_contacts ADD pinned INTEGER NOT NULL DEFAULT  " +
3066                 ContactsContract.PinnedPositions.UNPINNED + ";");
3067     }
3068 
upgradeToVersion902(SQLiteDatabase db)3069     private void upgradeToVersion902(SQLiteDatabase db) {
3070         // adding account identifier to call log table
3071         db.execSQL("ALTER TABLE calls ADD subscription_component_name TEXT;");
3072         db.execSQL("ALTER TABLE calls ADD subscription_id TEXT;");
3073     }
3074 
3075     /**
3076      * Searches for any calls in the call log with no normalized phone number and attempts to add
3077      * one if the number can be normalized.
3078      *
3079      * @param db The database.
3080      */
upgradeToVersion903(SQLiteDatabase db)3081     private void upgradeToVersion903(SQLiteDatabase db) {
3082         // Find the calls in the call log with no normalized phone number.
3083         final Cursor c = db.rawQuery(
3084                 "SELECT _id, number, countryiso FROM calls " +
3085                         " WHERE (normalized_number is null OR normalized_number = '') " +
3086                         " AND countryiso != '' AND countryiso is not null " +
3087                         " AND number != '' AND number is not null;",
3088                 null
3089         );
3090 
3091         try {
3092             if (c.getCount() == 0) {
3093                 return;
3094             }
3095 
3096             db.beginTransaction();
3097             try {
3098                 c.moveToPosition(-1);
3099                 while (c.moveToNext()) {
3100                     final long callId = c.getLong(0);
3101                     final String unNormalizedNumber = c.getString(1);
3102                     final String countryIso = c.getString(2);
3103 
3104                     // Attempt to get normalized number.
3105                     String normalizedNumber = PhoneNumberUtils
3106                             .formatNumberToE164(unNormalizedNumber, countryIso);
3107 
3108                     if (!TextUtils.isEmpty(normalizedNumber)) {
3109                         db.execSQL("UPDATE calls set normalized_number = ? " +
3110                                         "where _id = ?;",
3111                                 new String[]{
3112                                         normalizedNumber,
3113                                         String.valueOf(callId),
3114                                 }
3115                         );
3116                     }
3117                 }
3118 
3119                 db.setTransactionSuccessful();
3120             } finally {
3121                 db.endTransaction();
3122             }
3123         } finally {
3124             c.close();
3125         }
3126     }
3127 
3128     /**
3129      * Updates the calls table in the database to include the call_duration and features columns.
3130      * @param db The database to update.
3131      */
upgradeToVersion904(SQLiteDatabase db)3132     private void upgradeToVersion904(SQLiteDatabase db) {
3133         db.execSQL("ALTER TABLE calls ADD features INTEGER NOT NULL DEFAULT 0;");
3134         db.execSQL("ALTER TABLE calls ADD data_usage INTEGER;");
3135     }
3136 
3137     /**
3138      * Adds the voicemail transcription to the Table.Calls
3139      */
upgradeToVersion905(SQLiteDatabase db)3140     private void upgradeToVersion905(SQLiteDatabase db) {
3141         db.execSQL("ALTER TABLE calls ADD transcription TEXT;");
3142     }
3143 
3144     /**
3145      * Upgrades the database with the new value for {@link PinnedPositions#UNPINNED}. In this
3146      * database version upgrade, the value is changed from 2147483647 (Integer.MAX_VALUE) to 0.
3147      *
3148      * The first pinned contact now starts from position 1.
3149      */
3150     @VisibleForTesting
upgradeToVersion906(SQLiteDatabase db)3151     public void upgradeToVersion906(SQLiteDatabase db) {
3152         db.execSQL("UPDATE contacts SET pinned = pinned + 1"
3153                 + " WHERE pinned >= 0 AND pinned < 2147483647;");
3154         db.execSQL("UPDATE raw_contacts SET pinned = pinned + 1"
3155                 + " WHERE pinned >= 0 AND pinned < 2147483647;");
3156 
3157         db.execSQL("UPDATE contacts SET pinned = 0"
3158                 + " WHERE pinned = 2147483647;");
3159         db.execSQL("UPDATE raw_contacts SET pinned = 0"
3160                 + " WHERE pinned = 2147483647;");
3161     }
3162 
upgradeToVersion908(SQLiteDatabase db)3163     private void upgradeToVersion908(SQLiteDatabase db) {
3164         db.execSQL("UPDATE contacts SET pinned = 0 WHERE pinned = 2147483647;");
3165         db.execSQL("UPDATE raw_contacts SET pinned = 0 WHERE pinned = 2147483647;");
3166     }
3167 
upgradeToVersion909(SQLiteDatabase db)3168     private void upgradeToVersion909(SQLiteDatabase db) {
3169         try {
3170             db.execSQL("ALTER TABLE calls ADD sub_id INTEGER DEFAULT -1;");
3171         } catch (SQLiteException e) {
3172             // The column already exists--copy over data
3173             db.execSQL("UPDATE calls SET subscription_component_name='com.android.phone/"
3174                     + "com.android.services.telephony.TelephonyConnectionService';");
3175             db.execSQL("UPDATE calls SET subscription_id=sub_id;");
3176         }
3177     }
3178 
3179     /**
3180      * Delete any remaining rows in the calls table if the user is a profile of another user.
3181      * b/17096027
3182      */
upgradeToVersion910(SQLiteDatabase db)3183     private void upgradeToVersion910(SQLiteDatabase db) {
3184         final UserManager userManager = (UserManager) mContext.getSystemService(
3185                 Context.USER_SERVICE);
3186         final UserInfo user = userManager.getUserInfo(userManager.getUserHandle());
3187         if (user.isManagedProfile()) {
3188             db.execSQL("DELETE FROM calls;");
3189         }
3190     }
3191 
3192     /**
3193      * Add backup_id column to raw_contacts table and hash_id column to data table.
3194      */
upgradeToVersion1000(SQLiteDatabase db)3195     private void upgradeToVersion1000(SQLiteDatabase db) {
3196         db.execSQL("ALTER TABLE raw_contacts ADD backup_id TEXT;");
3197         db.execSQL("ALTER TABLE data ADD hash_id TEXT;");
3198         db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS raw_contacts_backup_id_account_id_index ON " +
3199                 "raw_contacts (backup_id, account_id);");
3200         db.execSQL("CREATE INDEX IF NOT EXISTS data_hash_id_index ON data (hash_id);");
3201     }
3202 
3203     @VisibleForTesting
upgradeToVersion1002(SQLiteDatabase db)3204     public void upgradeToVersion1002(SQLiteDatabase db) {
3205         db.execSQL("DROP TABLE IF EXISTS pre_authorized_uris;");
3206         db.execSQL("CREATE TABLE pre_authorized_uris ("+
3207                 "_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
3208                 "uri STRING NOT NULL, " +
3209                 "expiration INTEGER NOT NULL DEFAULT 0);");
3210     }
3211 
upgradeToVersion1003(SQLiteDatabase db)3212     public void upgradeToVersion1003(SQLiteDatabase db) {
3213         db.execSQL("ALTER TABLE calls ADD phone_account_address TEXT;");
3214 
3215         // After version 1003, we are using the ICC ID as the phone-account ID. This code updates
3216         // any existing telephony connection-service calllog entries to the ICC ID from the
3217         // previously used subscription ID.
3218         // TODO: This is inconsistent, depending on the initialization state of SubscriptionManager.
3219         //       Sometimes it returns zero subscriptions. May want to move this upgrade to run after
3220         //       ON_BOOT_COMPLETE instead of PRE_BOOT_COMPLETE.
3221         SubscriptionManager sm = SubscriptionManager.from(mContext);
3222         if (sm != null) {
3223             Log.i(TAG, "count: " + sm.getAllSubscriptionInfoCount());
3224             for (SubscriptionInfo info : sm.getAllSubscriptionInfoList()) {
3225                 String iccId = info.getIccId();
3226                 int subId = info.getSubscriptionId();
3227                 if (!TextUtils.isEmpty(iccId) &&
3228                         subId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
3229                     StringBuilder sb = new StringBuilder();
3230                     sb.append("UPDATE calls SET subscription_id=");
3231                     DatabaseUtils.appendEscapedSQLString(sb, iccId);
3232                     sb.append(" WHERE subscription_id=");
3233                     sb.append(subId);
3234                     sb.append(" AND subscription_component_name='com.android.phone/"
3235                             + "com.android.services.telephony.TelephonyConnectionService';");
3236 
3237                     db.execSQL(sb.toString());
3238                 }
3239             }
3240         }
3241     }
3242 
3243     /**
3244      * Add a "hidden" column for call log entries we want to hide after an upgrade until the user
3245      * adds the right phone account to the device.
3246      */
upgradeToVersion1004(SQLiteDatabase db)3247     public void upgradeToVersion1004(SQLiteDatabase db) {
3248         db.execSQL("ALTER TABLE calls ADD phone_account_hidden INTEGER NOT NULL DEFAULT 0;");
3249     }
3250 
upgradeToVersion1005(SQLiteDatabase db)3251     public void upgradeToVersion1005(SQLiteDatabase db) {
3252         db.execSQL("ALTER TABLE calls ADD photo_uri TEXT;");
3253     }
3254 
3255     /**
3256      * The try/catch pattern exists because some devices have the upgrade and some do not. This is
3257      * because the below updates were merged into version 1005 after some devices had already
3258      * upgraded to version 1005 and hence did not receive the below upgrades.
3259      */
upgradeToVersion1007(SQLiteDatabase db)3260     public void upgradeToVersion1007(SQLiteDatabase db) {
3261         try {
3262             // Add multi-sim fields
3263             db.execSQL("ALTER TABLE voicemail_status ADD phone_account_component_name TEXT;");
3264             db.execSQL("ALTER TABLE voicemail_status ADD phone_account_id TEXT;");
3265 
3266             // For use by the sync adapter
3267             db.execSQL("ALTER TABLE calls ADD dirty INTEGER NOT NULL DEFAULT 0;");
3268             db.execSQL("ALTER TABLE calls ADD deleted INTEGER NOT NULL DEFAULT 0;");
3269         } catch (SQLiteException e) {
3270             // These columns already exist. Do nothing.
3271             // Log verbose because this should be the majority case.
3272             Log.v(TAG, "Version 1007: Columns already exist, skipping upgrade steps.");
3273         }
3274   }
3275 
3276 
upgradeToVersion1009(SQLiteDatabase db)3277     public void upgradeToVersion1009(SQLiteDatabase db) {
3278         try {
3279             db.execSQL("ALTER TABLE data ADD carrier_presence INTEGER NOT NULL DEFAULT 0");
3280         } catch (SQLiteException ignore) {
3281         }
3282     }
3283 
upgradeToVersion1100(SQLiteDatabase db)3284     private void upgradeToVersion1100(SQLiteDatabase db) {
3285         db.execSQL("ALTER TABLE raw_contacts ADD metadata_dirty INTEGER NOT NULL DEFAULT 0;");
3286     }
3287 
3288     // Data.hash_id column is used for metadata backup, and this upgrade is to generate
3289     // hash_id column. Usually data1 and data2 are two main columns to store data info.
3290     // But for photo, we don't use data1 and data2, instead, use data15 to store photo blob.
3291     // So this upgrade generates hash_id from (data1 + data2) or (data15) using sha-1.
upgradeToVersion1101(SQLiteDatabase db)3292     public void upgradeToVersion1101(SQLiteDatabase db) {
3293         final SQLiteStatement update = db.compileStatement(
3294                 "UPDATE " + Tables.DATA +
3295                 " SET " + Data.HASH_ID + "=?" +
3296                 " WHERE " + Data._ID + "=?"
3297         );
3298         final Cursor c = db.query(Tables.DATA,
3299                 new String[] {Data._ID, Data.DATA1, Data.DATA2, Data.DATA15},
3300                 null, null, null, null, Data._ID);
3301         try {
3302             while (c.moveToNext()) {
3303                 final long dataId = c.getLong(0);
3304                 final String data1 = c.getString(1);
3305                 final String data2 = c.getString(2);
3306                 final byte[] data15 = c.getBlob(3);
3307                 final String hashId = legacyGenerateHashId(data1, data2, data15);
3308                 if (!TextUtils.isEmpty(hashId)) {
3309                     update.bindString(1, hashId);
3310                     update.bindLong(2, dataId);
3311                     update.execute();
3312                 }
3313             }
3314         } finally {
3315             c.close();
3316         }
3317     }
3318 
3319     /**
3320      * Add new metadata_sync table to cache the meta data on raw contacts level from server before
3321      * they are merged into other CP2 tables. The data column is the blob column containing all
3322      * the backed up metadata for this raw_contact. This table should only be used by metadata
3323      * sync adapter.
3324      */
upgradeToVersion1104(SQLiteDatabase db)3325     public void upgradeToVersion1104(SQLiteDatabase db) {
3326         db.execSQL("DROP TABLE IF EXISTS metadata_sync;");
3327         db.execSQL("CREATE TABLE metadata_sync (" +
3328                 "_id INTEGER PRIMARY KEY AUTOINCREMENT, raw_contact_backup_id TEXT NOT NULL, " +
3329                 "account_id INTEGER NOT NULL, data TEXT, deleted INTEGER NOT NULL DEFAULT 0);");
3330         db.execSQL("CREATE UNIQUE INDEX metadata_sync_index ON metadata_sync (" +
3331                 "raw_contact_backup_id, account_id);");
3332     }
3333 
3334     /**
3335      * Add new metadata_sync_state table to store the metadata sync state for a set of accounts.
3336      */
upgradeToVersion1105(SQLiteDatabase db)3337     public void upgradeToVersion1105(SQLiteDatabase db) {
3338         db.execSQL("DROP TABLE IF EXISTS metadata_sync_state;");
3339         db.execSQL("CREATE TABLE metadata_sync_state (" +
3340                 "_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
3341                 "account_id INTEGER NOT NULL, state BLOB);");
3342         db.execSQL("CREATE UNIQUE INDEX metadata_sync_state_index ON metadata_sync_state (" +
3343                 "account_id);");
3344     }
3345 
upgradeToVersion1106(SQLiteDatabase db)3346     public void upgradeToVersion1106(SQLiteDatabase db) {
3347         db.execSQL("ALTER TABLE calls ADD post_dial_digits TEXT NOT NULL DEFAULT ''");
3348     }
3349 
upgradeToVersion1107(SQLiteDatabase db)3350     public void upgradeToVersion1107(SQLiteDatabase db) {
3351         try {
3352             db.execSQL("ALTER TABLE calls ADD post_dial_digits TEXT NOT NULL DEFAULT ''");
3353         } catch (SQLiteException ignore) {
3354             // This is for devices which got initialized without a post_dial_digits
3355             // column from version 1106. The exception indicates that the column is
3356             // already present, so nothing needs to be done.
3357         }
3358     }
3359 
upgradeToVersion1108(SQLiteDatabase db)3360     public void upgradeToVersion1108(SQLiteDatabase db) {
3361         db.execSQL(
3362                 "ALTER TABLE calls ADD add_for_all_users INTEGER NOT NULL DEFAULT 1");
3363     }
3364 
upgradeToVersion1109(SQLiteDatabase db)3365     public void upgradeToVersion1109(SQLiteDatabase db) {
3366         db.execSQL("ALTER TABLE voicemail_status ADD quota_occupied INTEGER DEFAULT -1;");
3367         db.execSQL("ALTER TABLE voicemail_status ADD quota_total INTEGER DEFAULT -1;");
3368         db.execSQL("ALTER TABLE calls ADD last_modified INTEGER DEFAULT 0;");
3369     }
3370 
3371     /**
3372      * Update hash_id for photo data. Generates the same value for all photo mimetype data, since
3373      * there's only one photo for each raw_contact.
3374      */
upgradeToVersion1110(SQLiteDatabase db)3375     public void upgradeToVersion1110(SQLiteDatabase db) {
3376         final long mimeTypeId = lookupMimeTypeId(db,
3377                 ContactsContract.CommonDataKinds.Photo.CONTENT_ITEM_TYPE);
3378         final ContentValues values = new ContentValues();
3379         values.put(Data.HASH_ID, getPhotoHashId());
3380         db.update(Tables.DATA, values, DataColumns.MIMETYPE_ID + " = " + mimeTypeId, null);
3381     }
3382 
getPhotoHashId()3383     public String getPhotoHashId() {
3384         return generateHashId(ContactsContract.CommonDataKinds.Photo.CONTENT_ITEM_TYPE, null);
3385     }
3386 
3387     @VisibleForTesting
upgradeToVersion1111(SQLiteDatabase db)3388     public void upgradeToVersion1111(SQLiteDatabase db) {
3389         // Re-order contacts with no display name to the phone number bucket and give
3390         // them the phone number label. See b/21736630.
3391         final ContactLocaleUtils localeUtils = ContactLocaleUtils.getInstance();
3392         final int index = localeUtils.getNumberBucketIndex();
3393         final String label = localeUtils.getBucketLabel(index);
3394         // Note, sort_key = null is equivalent to display_name = null
3395         db.execSQL("UPDATE raw_contacts SET phonebook_bucket = " + index +
3396                 ", phonebook_label='" + label + "' WHERE sort_key IS NULL AND phonebook_bucket=0;");
3397         db.execSQL("UPDATE raw_contacts SET phonebook_bucket_alt = " + index +
3398                 ", phonebook_label_alt='" + label +
3399                 "' WHERE sort_key_alt IS NULL AND phonebook_bucket_alt=0;");
3400 
3401         FastScrollingIndexCache.getInstance(mContext).invalidate();
3402     }
3403 
upgradeToVersion1201(SQLiteDatabase db)3404     private void upgradeToVersion1201(SQLiteDatabase db) {
3405         db.execSQL("ALTER TABLE contacts ADD x_times_contacted INTEGER NOT NULL DEFAULT 0");
3406         db.execSQL("ALTER TABLE contacts ADD x_last_time_contacted INTEGER");
3407 
3408         db.execSQL("ALTER TABLE raw_contacts ADD x_times_contacted INTEGER NOT NULL DEFAULT 0");
3409         db.execSQL("ALTER TABLE raw_contacts ADD x_last_time_contacted INTEGER");
3410 
3411         db.execSQL("ALTER TABLE data_usage_stat ADD x_times_used INTEGER NOT NULL DEFAULT 0");
3412         db.execSQL("ALTER TABLE data_usage_stat ADD x_last_time_used INTEGER NOT NULL DEFAULT 0");
3413 
3414         db.execSQL("UPDATE contacts SET "
3415                 + "x_times_contacted = ifnull(times_contacted,0),"
3416                 + "x_last_time_contacted = ifnull(last_time_contacted,0),"
3417                 + "times_contacted = 0,"
3418                 + "last_time_contacted = 0");
3419 
3420         db.execSQL("UPDATE raw_contacts SET "
3421                 + "x_times_contacted = ifnull(times_contacted,0),"
3422                 + "x_last_time_contacted = ifnull(last_time_contacted,0),"
3423                 + "times_contacted = 0,"
3424                 + "last_time_contacted = 0");
3425 
3426         db.execSQL("UPDATE data_usage_stat SET "
3427                 + "x_times_used = ifnull(times_used,0),"
3428                 + "x_last_time_used = ifnull(last_time_used,0),"
3429                 + "times_used = 0,"
3430                 + "last_time_used = 0");
3431     }
3432 
3433     /**
3434      * This method is only used in upgradeToVersion1101 method, and should not be used in other
3435      * places now. Because data15 is not used to generate hash_id for photo, and the new generating
3436      * method for photos should be getPhotoHashId().
3437      *
3438      * Generate hash_id from data1, data2 and data15 columns.
3439      * If one of data1 and data2 is not null, using data1 and data2 to get hash_id,
3440      * otherwise, using data15 to generate.
3441      */
legacyGenerateHashId(String data1, String data2, byte[] data15)3442     public String legacyGenerateHashId(String data1, String data2, byte[] data15) {
3443         final StringBuilder sb = new StringBuilder();
3444         byte[] hashInput = null;
3445         if (!TextUtils.isEmpty(data1) || !TextUtils.isEmpty(data2)) {
3446             sb.append(data1);
3447             sb.append(data2);
3448             hashInput = sb.toString().getBytes();
3449         } else if (data15 != null) {
3450             hashInput = data15;
3451         }
3452         if (hashInput != null) {
3453             final String hashId = generateHashIdForData(hashInput);
3454             return hashId;
3455         } else {
3456             return null;
3457         }
3458     }
3459 
generateHashId(String data1, String data2)3460     public String generateHashId(String data1, String data2) {
3461         final StringBuilder sb = new StringBuilder();
3462         byte[] hashInput = null;
3463         if (!TextUtils.isEmpty(data1) || !TextUtils.isEmpty(data2)) {
3464             sb.append(data1);
3465             sb.append(data2);
3466             hashInput = sb.toString().getBytes();
3467         }
3468         if (hashInput != null) {
3469             final String hashId = generateHashIdForData(hashInput);
3470             return hashId;
3471         } else {
3472             return null;
3473         }
3474     }
3475 
3476     // Use SHA-1 hash method to generate hash string for the input.
3477     @VisibleForTesting
generateHashIdForData(byte[] input)3478     String generateHashIdForData(byte[] input) {
3479         synchronized (mMessageDigest) {
3480             final byte[] hashResult = mMessageDigest.digest(input);
3481             return Base64.encodeToString(hashResult, Base64.DEFAULT);
3482         }
3483     }
3484 
extractHandleFromEmailAddress(String email)3485     public String extractHandleFromEmailAddress(String email) {
3486         Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(email);
3487         if (tokens.length == 0) {
3488             return null;
3489         }
3490 
3491         String address = tokens[0].getAddress();
3492         int index = address.indexOf('@');
3493         if (index != -1) {
3494             return address.substring(0, index);
3495         }
3496         return null;
3497     }
3498 
extractAddressFromEmailAddress(String email)3499     public String extractAddressFromEmailAddress(String email) {
3500         Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(email);
3501         if (tokens.length == 0) {
3502             return null;
3503         }
3504         return tokens[0].getAddress().trim();
3505     }
3506 
3507     /**
3508      * Inserts a new mimetype into the table Tables.MIMETYPES and returns its id. Use
3509      * {@link #lookupMimeTypeId(SQLiteDatabase, String)} to lookup id of a mimetype that is
3510      * guaranteed to be in the database
3511      *
3512      * @param db the SQLiteDatabase object returned by {@link #getWritableDatabase()}
3513      * @param mimeType The mimetype to insert
3514      * @return the id of the newly inserted row
3515      */
insertMimeType(SQLiteDatabase db, String mimeType)3516     private long insertMimeType(SQLiteDatabase db, String mimeType) {
3517         final String insert = "INSERT INTO " + Tables.MIMETYPES + "("
3518                 + MimetypesColumns.MIMETYPE +
3519                 ") VALUES (?)";
3520         long id = insertWithOneArgAndReturnId(db, insert, mimeType);
3521         if (id >= 0) {
3522             return id;
3523         }
3524         return lookupMimeTypeId(db, mimeType);
3525     }
3526 
3527     /**
3528      * Looks up Tables.MIMETYPES for the mime type and returns its id. Returns -1 if the mime type
3529      * is not found. Use {@link #insertMimeType(SQLiteDatabase, String)} when it is doubtful whether
3530      * the mimetype already exists in the table or not.
3531      *
3532      * @param db
3533      * @param mimeType
3534      * @return the id of the row containing the mime type or -1 if the mime type was not found.
3535      */
lookupMimeTypeId(SQLiteDatabase db, String mimeType)3536     private long lookupMimeTypeId(SQLiteDatabase db, String mimeType) {
3537         Long id = mCommonMimeTypeIdsCache.get(mimeType);
3538         if (id != null) {
3539             return id;
3540         }
3541         final String query = "SELECT " +
3542                 MimetypesColumns._ID + " FROM " + Tables.MIMETYPES + " WHERE "
3543                 + MimetypesColumns.MIMETYPE +
3544                 "=?";
3545         id = queryIdWithOneArg(db, query, mimeType);
3546         if (id < 0) {
3547             Log.e(TAG, "Mimetype " + mimeType + " not found in the MIMETYPES table");
3548         }
3549         return id;
3550     }
3551 
bindString(SQLiteStatement stmt, int index, String value)3552     private static void bindString(SQLiteStatement stmt, int index, String value) {
3553         if (value == null) {
3554             stmt.bindNull(index);
3555         } else {
3556             stmt.bindString(index, value);
3557         }
3558     }
3559 
bindLong(SQLiteStatement stmt, int index, Number value)3560     private void bindLong(SQLiteStatement stmt, int index, Number value) {
3561         if (value == null) {
3562             stmt.bindNull(index);
3563         } else {
3564             stmt.bindLong(index, value.longValue());
3565         }
3566     }
3567 
3568     /**
3569      * Adds index stats into the SQLite database to force it to always use the lookup indexes.
3570      *
3571      * Note if you drop a table or an index, the corresponding row will be removed from this table.
3572      * Make sure to call this method after such operations.
3573      */
updateSqliteStats(SQLiteDatabase db)3574     private void updateSqliteStats(SQLiteDatabase db) {
3575         if (!mDatabaseOptimizationEnabled) {
3576             return;  // We don't use sqlite_stat1 during tests.
3577         }
3578 
3579         // Specific stats strings are based on an actual large database after running ANALYZE
3580         // Important here are relative sizes. Raw-Contacts is slightly bigger than Contacts
3581         // Warning: Missing tables in here will make SQLite assume to contain 1000000 rows,
3582         // which can lead to catastrophic query plans for small tables
3583 
3584         // What these numbers mean is described in this file.
3585         // http://www.sqlite.org/cgi/src/finfo?name=src/analyze.c
3586 
3587         // Excerpt:
3588         /*
3589         ** Format of sqlite_stat1:
3590         **
3591         ** There is normally one row per index, with the index identified by the
3592         ** name in the idx column.  The tbl column is the name of the table to
3593         ** which the index belongs.  In each such row, the stat column will be
3594         ** a string consisting of a list of integers.  The first integer in this
3595         ** list is the number of rows in the index and in the table.  The second
3596         ** integer is the average number of rows in the index that have the same
3597         ** value in the first column of the index.  The third integer is the average
3598         ** number of rows in the index that have the same value for the first two
3599         ** columns.  The N-th integer (for N>1) is the average number of rows in
3600         ** the index which have the same value for the first N-1 columns.  For
3601         ** a K-column index, there will be K+1 integers in the stat column.  If
3602         ** the index is unique, then the last integer will be 1.
3603         **
3604         ** The list of integers in the stat column can optionally be followed
3605         ** by the keyword "unordered".  The "unordered" keyword, if it is present,
3606         ** must be separated from the last integer by a single space.  If the
3607         ** "unordered" keyword is present, then the query planner assumes that
3608         ** the index is unordered and will not use the index for a range query.
3609         **
3610         ** If the sqlite_stat1.idx column is NULL, then the sqlite_stat1.stat
3611         ** column contains a single integer which is the (estimated) number of
3612         ** rows in the table identified by sqlite_stat1.tbl.
3613         */
3614 
3615         try {
3616             db.execSQL("DELETE FROM sqlite_stat1");
3617             updateIndexStats(db, Tables.CONTACTS,
3618                     "contacts_has_phone_index", "9000 500");
3619             updateIndexStats(db, Tables.CONTACTS,
3620                     "contacts_name_raw_contact_id_index", "9000 1");
3621             updateIndexStats(db, Tables.CONTACTS, MoreDatabaseUtils.buildIndexName(Tables.CONTACTS,
3622                     Contacts.CONTACT_LAST_UPDATED_TIMESTAMP), "9000 10");
3623 
3624             updateIndexStats(db, Tables.RAW_CONTACTS,
3625                     "raw_contacts_contact_id_index", "10000 2");
3626             updateIndexStats(db, Tables.RAW_CONTACTS,
3627                     "raw_contact_sort_key2_index", "10000 2");
3628             updateIndexStats(db, Tables.RAW_CONTACTS,
3629                     "raw_contact_sort_key1_index", "10000 2");
3630             updateIndexStats(db, Tables.RAW_CONTACTS,
3631                     "raw_contacts_source_id_account_id_index", "10000 1 1");
3632 
3633             updateIndexStats(db, Tables.NAME_LOOKUP,
3634                     "name_lookup_raw_contact_id_index", "35000 4");
3635             updateIndexStats(db, Tables.NAME_LOOKUP,
3636                     "name_lookup_index", "35000 2 2 2 1");
3637             updateIndexStats(db, Tables.NAME_LOOKUP,
3638                     "sqlite_autoindex_name_lookup_1", "35000 3 2 1");
3639 
3640             updateIndexStats(db, Tables.PHONE_LOOKUP,
3641                     "phone_lookup_index", "3500 3 2 1");
3642             updateIndexStats(db, Tables.PHONE_LOOKUP,
3643                     "phone_lookup_min_match_index", "3500 3 2 2");
3644             updateIndexStats(db, Tables.PHONE_LOOKUP,
3645                     "phone_lookup_data_id_min_match_index", "3500 2 2");
3646 
3647             updateIndexStats(db, Tables.DATA,
3648                     "data_mimetype_data1_index", "60000 5000 2");
3649             updateIndexStats(db, Tables.DATA,
3650                     "data_raw_contact_id", "60000 10");
3651 
3652             updateIndexStats(db, Tables.GROUPS,
3653                     "groups_source_id_account_id_index", "50 2 2 1 1");
3654 
3655             updateIndexStats(db, Tables.NICKNAME_LOOKUP,
3656                     "nickname_lookup_index", "500 2 1");
3657 
3658             updateIndexStats(db, Tables.STATUS_UPDATES,
3659                     null, "100");
3660 
3661             updateIndexStats(db, Tables.STREAM_ITEMS,
3662                     null, "500");
3663             updateIndexStats(db, Tables.STREAM_ITEM_PHOTOS,
3664                     null, "50");
3665 
3666             updateIndexStats(db, Tables.ACCOUNTS,
3667                     null, "3");
3668 
3669             updateIndexStats(db, Tables.PRE_AUTHORIZED_URIS,
3670                     null, "1");
3671 
3672             updateIndexStats(db, Tables.VISIBLE_CONTACTS,
3673                     null, "2000");
3674 
3675             updateIndexStats(db, Tables.PHOTO_FILES,
3676                     null, "50");
3677 
3678             updateIndexStats(db, Tables.DEFAULT_DIRECTORY,
3679                     null, "1500");
3680 
3681             updateIndexStats(db, Tables.MIMETYPES,
3682                     "mime_type", "18 1");
3683 
3684             updateIndexStats(db, Tables.DATA_USAGE_STAT,
3685                     "data_usage_stat_index", "20 2 1");
3686 
3687             updateIndexStats(db, Tables.METADATA_SYNC,
3688                     "metadata_sync_index", "10000 1 1");
3689 
3690             // Tiny tables
3691             updateIndexStats(db, Tables.AGGREGATION_EXCEPTIONS,
3692                     null, "10");
3693             updateIndexStats(db, Tables.SETTINGS,
3694                     null, "10");
3695             updateIndexStats(db, Tables.PACKAGES,
3696                     null, "0");
3697             updateIndexStats(db, Tables.DIRECTORIES,
3698                     null, "3");
3699             updateIndexStats(db, LegacyApiSupport.LegacyTables.SETTINGS,
3700                     null, "0");
3701             updateIndexStats(db, "android_metadata",
3702                     null, "1");
3703             updateIndexStats(db, "_sync_state",
3704                     "sqlite_autoindex__sync_state_1", "2 1 1");
3705             updateIndexStats(db, "_sync_state_metadata",
3706                     null, "1");
3707             updateIndexStats(db, "properties",
3708                     "sqlite_autoindex_properties_1", "4 1");
3709 
3710             updateIndexStats(db, Tables.METADATA_SYNC_STATE,
3711                     "metadata_sync_state_index", "2 1 1");
3712 
3713             // Search index
3714             updateIndexStats(db, "search_index_docsize",
3715                     null, "9000");
3716             updateIndexStats(db, "search_index_content",
3717                     null, "9000");
3718             updateIndexStats(db, "search_index_stat",
3719                     null, "1");
3720             updateIndexStats(db, "search_index_segments",
3721                     null, "450");
3722             updateIndexStats(db, "search_index_segdir",
3723                     "sqlite_autoindex_search_index_segdir_1", "9 5 1");
3724 
3725             updateIndexStats(db, Tables.PRESENCE, "presenceIndex", "1 1");
3726             updateIndexStats(db, Tables.PRESENCE, "presenceIndex2", "1 1");
3727             updateIndexStats(db, Tables.AGGREGATED_PRESENCE, null, "1");
3728 
3729             // Force SQLite to reload sqlite_stat1.
3730             db.execSQL("ANALYZE sqlite_master;");
3731         } catch (SQLException e) {
3732             Log.e(TAG, "Could not update index stats", e);
3733         }
3734     }
3735 
3736     /**
3737      * Stores statistics for a given index.
3738      *
3739      * @param stats has the following structure: the first index is the expected size of
3740      * the table.  The following integer(s) are the expected number of records selected with the
3741      * index.  There should be one integer per indexed column.
3742      */
updateIndexStats(SQLiteDatabase db, String table, String index, String stats)3743     private void updateIndexStats(SQLiteDatabase db, String table, String index, String stats) {
3744         if (index == null) {
3745             db.execSQL("DELETE FROM sqlite_stat1 WHERE tbl=? AND idx IS NULL",
3746                     new String[] {table});
3747         } else {
3748             db.execSQL("DELETE FROM sqlite_stat1 WHERE tbl=? AND idx=?",
3749                     new String[] {table, index});
3750         }
3751         db.execSQL("INSERT INTO sqlite_stat1 (tbl,idx,stat) VALUES (?,?,?)",
3752                 new String[] {table, index, stats});
3753     }
3754 
3755     /**
3756      * Wipes all data except mime type and package lookup tables.
3757      */
wipeData()3758     public void wipeData() {
3759         SQLiteDatabase db = getWritableDatabase();
3760 
3761         db.execSQL("DELETE FROM " + Tables.ACCOUNTS + ";");
3762         db.execSQL("DELETE FROM " + Tables.CONTACTS + ";");
3763         db.execSQL("DELETE FROM " + Tables.RAW_CONTACTS + ";");
3764         db.execSQL("DELETE FROM " + Tables.STREAM_ITEMS + ";");
3765         db.execSQL("DELETE FROM " + Tables.STREAM_ITEM_PHOTOS + ";");
3766         db.execSQL("DELETE FROM " + Tables.PHOTO_FILES + ";");
3767         db.execSQL("DELETE FROM " + Tables.DATA + ";");
3768         db.execSQL("DELETE FROM " + Tables.PHONE_LOOKUP + ";");
3769         db.execSQL("DELETE FROM " + Tables.NAME_LOOKUP + ";");
3770         db.execSQL("DELETE FROM " + Tables.GROUPS + ";");
3771         db.execSQL("DELETE FROM " + Tables.AGGREGATION_EXCEPTIONS + ";");
3772         db.execSQL("DELETE FROM " + Tables.SETTINGS + ";");
3773         db.execSQL("DELETE FROM " + Tables.DIRECTORIES + ";");
3774         db.execSQL("DELETE FROM " + Tables.SEARCH_INDEX + ";");
3775         db.execSQL("DELETE FROM " + Tables.DELETED_CONTACTS + ";");
3776         db.execSQL("DELETE FROM " + Tables.MIMETYPES + ";");
3777         db.execSQL("DELETE FROM " + Tables.PACKAGES + ";");
3778         db.execSQL("DELETE FROM " + Tables.PRESENCE + ";");
3779         db.execSQL("DELETE FROM " + Tables.AGGREGATED_PRESENCE + ";");
3780 
3781         prepopulateCommonMimeTypes(db);
3782         // Note: we are not removing reference data from Tables.NICKNAME_LOOKUP
3783     }
3784 
createNameSplitter()3785     public NameSplitter createNameSplitter() {
3786         return createNameSplitter(Locale.getDefault());
3787     }
3788 
createNameSplitter(Locale locale)3789     public NameSplitter createNameSplitter(Locale locale) {
3790         mNameSplitter = new NameSplitter(
3791                 mContext.getString(com.android.internal.R.string.common_name_prefixes),
3792                 mContext.getString(com.android.internal.R.string.common_last_name_prefixes),
3793                 mContext.getString(com.android.internal.R.string.common_name_suffixes),
3794                 mContext.getString(com.android.internal.R.string.common_name_conjunctions),
3795                 locale);
3796         return mNameSplitter;
3797     }
3798 
3799     /**
3800      * Return the {@link ApplicationInfo#uid} for the given package name.
3801      */
getUidForPackageName(PackageManager pm, String packageName)3802     public static int getUidForPackageName(PackageManager pm, String packageName) {
3803         try {
3804             ApplicationInfo clientInfo = pm.getApplicationInfo(packageName, 0 /* no flags */);
3805             return clientInfo.uid;
3806         } catch (NameNotFoundException e) {
3807             throw new RuntimeException(e);
3808         }
3809     }
3810 
3811     @VisibleForTesting
queryIdWithOneArg(SQLiteDatabase db, String sql, String sqlArgument)3812     static long queryIdWithOneArg(SQLiteDatabase db, String sql, String sqlArgument) {
3813         final SQLiteStatement query = db.compileStatement(sql);
3814         try {
3815             bindString(query, 1, sqlArgument);
3816             try {
3817                 return query.simpleQueryForLong();
3818             } catch (SQLiteDoneException notFound) {
3819                 return -1;
3820             }
3821         } finally {
3822             query.close();
3823         }
3824     }
3825 
3826     @VisibleForTesting
insertWithOneArgAndReturnId(SQLiteDatabase db, String sql, String sqlArgument)3827     static long insertWithOneArgAndReturnId(SQLiteDatabase db, String sql, String sqlArgument) {
3828         final SQLiteStatement insert = db.compileStatement(sql);
3829         try {
3830             bindString(insert, 1, sqlArgument);
3831             try {
3832                 return insert.executeInsert();
3833             } catch (SQLiteConstraintException conflict) {
3834                 return -1;
3835             }
3836         } finally {
3837             insert.close();
3838         }
3839     }
3840 
3841     /**
3842      * Convert a package name into an integer, using {@link Tables#PACKAGES} for
3843      * lookups and possible allocation of new IDs as needed.
3844      */
getPackageId(String packageName)3845     public long getPackageId(String packageName) {
3846         final String query =
3847                 "SELECT " + PackagesColumns._ID +
3848                 " FROM " + Tables.PACKAGES +
3849                 " WHERE " + PackagesColumns.PACKAGE + "=?";
3850 
3851         final String insert =
3852                 "INSERT INTO " + Tables.PACKAGES + "("
3853                         + PackagesColumns.PACKAGE +
3854                 ") VALUES (?)";
3855 
3856         SQLiteDatabase db = getWritableDatabase();
3857         long id = queryIdWithOneArg(db, query, packageName);
3858         if (id >= 0) {
3859             return id;
3860         }
3861         id = insertWithOneArgAndReturnId(db, insert, packageName);
3862         if (id >= 0) {
3863             return id;
3864         }
3865         // just in case there was a race while doing insert above
3866         return queryIdWithOneArg(db, query, packageName);
3867     }
3868 
3869     /**
3870      * Convert a mimetype into an integer, using {@link Tables#MIMETYPES} for
3871      * lookups and possible allocation of new IDs as needed.
3872      */
getMimeTypeId(String mimeType)3873     public long getMimeTypeId(String mimeType) {
3874         SQLiteDatabase db = getWritableDatabase();
3875         long id = lookupMimeTypeId(db, mimeType);
3876         if (id < 0) {
3877             return insertMimeType(db, mimeType);
3878         }
3879         return id;
3880     }
3881 
getMimeTypeIdForStructuredName()3882     public long getMimeTypeIdForStructuredName() {
3883         return lookupMimeTypeId(getWritableDatabase(), StructuredName.CONTENT_ITEM_TYPE);
3884     }
3885 
getMimeTypeIdForStructuredPostal()3886     public long getMimeTypeIdForStructuredPostal() {
3887         return lookupMimeTypeId(getWritableDatabase(), StructuredPostal.CONTENT_ITEM_TYPE);
3888     }
3889 
getMimeTypeIdForOrganization()3890     public long getMimeTypeIdForOrganization() {
3891         return lookupMimeTypeId(getWritableDatabase(), Organization.CONTENT_ITEM_TYPE);
3892     }
3893 
getMimeTypeIdForIm()3894     public long getMimeTypeIdForIm() {
3895         return lookupMimeTypeId(getWritableDatabase(), Im.CONTENT_ITEM_TYPE);
3896     }
3897 
getMimeTypeIdForEmail()3898     public long getMimeTypeIdForEmail() {
3899         return lookupMimeTypeId(getWritableDatabase(), Email.CONTENT_ITEM_TYPE);
3900     }
3901 
getMimeTypeIdForPhone()3902     public long getMimeTypeIdForPhone() {
3903         return lookupMimeTypeId(getWritableDatabase(), Phone.CONTENT_ITEM_TYPE);
3904     }
3905 
getMimeTypeIdForSip()3906     public long getMimeTypeIdForSip() {
3907         return lookupMimeTypeId(getWritableDatabase(), SipAddress.CONTENT_ITEM_TYPE);
3908     }
3909 
3910     /**
3911      * Returns a {@link ContactsContract.DisplayNameSources} value based on {@param mimeTypeId}.
3912      * This does not return {@link ContactsContract.DisplayNameSources#STRUCTURED_PHONETIC_NAME}.
3913      * The calling client needs to inspect the structured name itself to distinguish between
3914      * {@link ContactsContract.DisplayNameSources#STRUCTURED_NAME} and
3915      * {@code STRUCTURED_PHONETIC_NAME}.
3916      */
getDisplayNameSourceForMimeTypeId(int mimeTypeId)3917     private int getDisplayNameSourceForMimeTypeId(int mimeTypeId) {
3918         if (mimeTypeId == mCommonMimeTypeIdsCache.get(StructuredName.CONTENT_ITEM_TYPE)) {
3919             return DisplayNameSources.STRUCTURED_NAME;
3920         }
3921         if (mimeTypeId == mCommonMimeTypeIdsCache.get(Email.CONTENT_ITEM_TYPE)) {
3922             return DisplayNameSources.EMAIL;
3923         }
3924         if (mimeTypeId == mCommonMimeTypeIdsCache.get(Phone.CONTENT_ITEM_TYPE)) {
3925             return DisplayNameSources.PHONE;
3926         }
3927         if (mimeTypeId == mCommonMimeTypeIdsCache.get(Organization.CONTENT_ITEM_TYPE)) {
3928             return DisplayNameSources.ORGANIZATION;
3929         }
3930         if (mimeTypeId == mCommonMimeTypeIdsCache.get(Nickname.CONTENT_ITEM_TYPE)) {
3931             return DisplayNameSources.NICKNAME;
3932         }
3933         return DisplayNameSources.UNDEFINED;
3934     }
3935 
3936     /**
3937      * Find the mimetype for the given {@link Data#_ID}.
3938      */
getDataMimeType(long dataId)3939     public String getDataMimeType(long dataId) {
3940         final SQLiteStatement dataMimetypeQuery = getWritableDatabase().compileStatement(
3941                     "SELECT " + MimetypesColumns.MIMETYPE +
3942                     " FROM " + Tables.DATA_JOIN_MIMETYPES +
3943                     " WHERE " + Tables.DATA + "." + Data._ID + "=?");
3944         try {
3945             // Try database query to find mimetype
3946             dataMimetypeQuery.bindLong(1, dataId);
3947             return dataMimetypeQuery.simpleQueryForString();
3948         } catch (SQLiteDoneException e) {
3949             // No valid mapping found, so return null
3950             return null;
3951         }
3952     }
3953 
3954     /**
3955      * Gets all accounts in the accounts table.
3956      */
getAllAccountsWithDataSets()3957     public Set<AccountWithDataSet> getAllAccountsWithDataSets() {
3958         final ArraySet<AccountWithDataSet> result = new ArraySet<>();
3959         Cursor c = getReadableDatabase().rawQuery(
3960                 "SELECT DISTINCT " +  AccountsColumns._ID + "," + AccountsColumns.ACCOUNT_NAME +
3961                 "," + AccountsColumns.ACCOUNT_TYPE + "," + AccountsColumns.DATA_SET +
3962                 " FROM " + Tables.ACCOUNTS, null);
3963         try {
3964             while (c.moveToNext()) {
3965                 result.add(AccountWithDataSet.get(c.getString(1), c.getString(2), c.getString(3)));
3966             }
3967         } finally {
3968             c.close();
3969         }
3970         return result;
3971     }
3972 
3973     /**
3974      * @return ID of the specified account, or null if the account doesn't exist.
3975      */
getAccountIdOrNull(AccountWithDataSet accountWithDataSet)3976     public Long getAccountIdOrNull(AccountWithDataSet accountWithDataSet) {
3977         if (accountWithDataSet == null) {
3978             accountWithDataSet = AccountWithDataSet.LOCAL;
3979         }
3980         final SQLiteStatement select = getWritableDatabase().compileStatement(
3981                 "SELECT " + AccountsColumns._ID +
3982                         " FROM " + Tables.ACCOUNTS +
3983                         " WHERE " +
3984                         "((?1 IS NULL AND " + AccountsColumns.ACCOUNT_NAME + " IS NULL) OR " +
3985                         "(" + AccountsColumns.ACCOUNT_NAME + "=?1)) AND " +
3986                         "((?2 IS NULL AND " + AccountsColumns.ACCOUNT_TYPE + " IS NULL) OR " +
3987                         "(" + AccountsColumns.ACCOUNT_TYPE + "=?2)) AND " +
3988                         "((?3 IS NULL AND " + AccountsColumns.DATA_SET + " IS NULL) OR " +
3989                         "(" + AccountsColumns.DATA_SET + "=?3))");
3990         try {
3991             DatabaseUtils.bindObjectToProgram(select, 1, accountWithDataSet.getAccountName());
3992             DatabaseUtils.bindObjectToProgram(select, 2, accountWithDataSet.getAccountType());
3993             DatabaseUtils.bindObjectToProgram(select, 3, accountWithDataSet.getDataSet());
3994             try {
3995                 return select.simpleQueryForLong();
3996             } catch (SQLiteDoneException notFound) {
3997                 return null;
3998             }
3999         } finally {
4000             select.close();
4001         }
4002     }
4003 
4004     /**
4005      * @return ID of the specified account.  This method will create a record in the accounts table
4006      *     if the account doesn't exist in the accounts table.
4007      *
4008      * This must be used in a transaction, so there's no need for synchronization.
4009      */
getOrCreateAccountIdInTransaction(AccountWithDataSet accountWithDataSet)4010     public long getOrCreateAccountIdInTransaction(AccountWithDataSet accountWithDataSet) {
4011         if (accountWithDataSet == null) {
4012             accountWithDataSet = AccountWithDataSet.LOCAL;
4013         }
4014         Long id = getAccountIdOrNull(accountWithDataSet);
4015         if (id != null) {
4016             return id;
4017         }
4018         final SQLiteStatement insert = getWritableDatabase().compileStatement(
4019                 "INSERT INTO " + Tables.ACCOUNTS +
4020                 " (" + AccountsColumns.ACCOUNT_NAME + ", " +
4021                 AccountsColumns.ACCOUNT_TYPE + ", " +
4022                 AccountsColumns.DATA_SET + ") VALUES (?, ?, ?)");
4023         try {
4024             DatabaseUtils.bindObjectToProgram(insert, 1, accountWithDataSet.getAccountName());
4025             DatabaseUtils.bindObjectToProgram(insert, 2, accountWithDataSet.getAccountType());
4026             DatabaseUtils.bindObjectToProgram(insert, 3, accountWithDataSet.getDataSet());
4027             id = insert.executeInsert();
4028         } finally {
4029             insert.close();
4030         }
4031 
4032         return id;
4033     }
4034 
4035     /**
4036      * Update {@link Contacts#IN_VISIBLE_GROUP} for all contacts.
4037      */
updateAllVisible()4038     public void updateAllVisible() {
4039         updateCustomContactVisibility(getWritableDatabase(), -1);
4040     }
4041 
4042     /**
4043      * Updates contact visibility and return true iff the visibility was actually changed.
4044      */
updateContactVisibleOnlyIfChanged(TransactionContext txContext, long contactId)4045     public boolean updateContactVisibleOnlyIfChanged(TransactionContext txContext, long contactId) {
4046         return updateContactVisible(txContext, contactId, true);
4047     }
4048 
4049     /**
4050      * Update {@link Contacts#IN_VISIBLE_GROUP} and
4051      * {@link Tables#DEFAULT_DIRECTORY} for a specific contact.
4052      */
updateContactVisible(TransactionContext txContext, long contactId)4053     public void updateContactVisible(TransactionContext txContext, long contactId) {
4054         updateContactVisible(txContext, contactId, false);
4055     }
4056 
updateContactVisible( TransactionContext txContext, long contactId, boolean onlyIfChanged)4057     public boolean updateContactVisible(
4058             TransactionContext txContext, long contactId, boolean onlyIfChanged) {
4059         SQLiteDatabase db = getWritableDatabase();
4060         updateCustomContactVisibility(db, contactId);
4061 
4062         String contactIdAsString = String.valueOf(contactId);
4063         long mimetype = getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE);
4064 
4065         // The contact will be included in the default directory if contains a raw contact that is
4066         // in any group or in an account that does not have any AUTO_ADD groups.
4067         boolean newVisibility = DatabaseUtils.longForQuery(db,
4068                 "SELECT EXISTS (" +
4069                     "SELECT " + RawContacts.CONTACT_ID +
4070                     " FROM " + Tables.RAW_CONTACTS +
4071                     " JOIN " + Tables.DATA +
4072                     "   ON (" + RawContactsColumns.CONCRETE_ID + "="
4073                             + Data.RAW_CONTACT_ID + ")" +
4074                     " WHERE " + RawContacts.CONTACT_ID + "=?1" +
4075                     "   AND " + DataColumns.MIMETYPE_ID + "=?2" +
4076                 ") OR EXISTS (" +
4077                     "SELECT " + RawContacts._ID +
4078                     " FROM " + Tables.RAW_CONTACTS +
4079                     " WHERE " + RawContacts.CONTACT_ID + "=?1" +
4080                     "   AND NOT EXISTS" +
4081                         " (SELECT " + Groups._ID +
4082                         "  FROM " + Tables.GROUPS +
4083                         "  WHERE " + RawContactsColumns.CONCRETE_ACCOUNT_ID + " = "
4084                                 + GroupsColumns.CONCRETE_ACCOUNT_ID +
4085                         "  AND " + Groups.AUTO_ADD + " != 0" +
4086                         ")" +
4087                 ") OR EXISTS (" +
4088                     "SELECT " + RawContacts._ID +
4089                     " FROM " + Tables.RAW_CONTACTS +
4090                     " WHERE " + RawContacts.CONTACT_ID + "=?1" +
4091                     "   AND " + RawContactsColumns.CONCRETE_ACCOUNT_ID + "=" +
4092                         Clauses.LOCAL_ACCOUNT_ID +
4093                 ")",
4094                 new String[] {
4095                     contactIdAsString,
4096                     String.valueOf(mimetype)
4097                 }) != 0;
4098 
4099         if (onlyIfChanged) {
4100             boolean oldVisibility = isContactInDefaultDirectory(db, contactId);
4101             if (oldVisibility == newVisibility) {
4102                 return false;
4103             }
4104         }
4105 
4106         if (newVisibility) {
4107             db.execSQL("INSERT OR IGNORE INTO " + Tables.DEFAULT_DIRECTORY + " VALUES(?)",
4108                     new String[] {contactIdAsString});
4109             txContext.invalidateSearchIndexForContact(contactId);
4110         } else {
4111             db.execSQL("DELETE FROM " + Tables.DEFAULT_DIRECTORY +
4112                         " WHERE " + Contacts._ID + "=?",
4113                     new String[] {contactIdAsString});
4114             db.execSQL("DELETE FROM " + Tables.SEARCH_INDEX +
4115                         " WHERE " + SearchIndexColumns.CONTACT_ID + "=CAST(? AS int)",
4116                     new String[] {contactIdAsString});
4117         }
4118         return true;
4119     }
4120 
isContactInDefaultDirectory(SQLiteDatabase db, long contactId)4121     public boolean isContactInDefaultDirectory(SQLiteDatabase db, long contactId) {
4122         final SQLiteStatement contactInDefaultDirectoryQuery = db.compileStatement(
4123                     "SELECT EXISTS (" +
4124                             "SELECT 1 FROM " + Tables.DEFAULT_DIRECTORY +
4125                             " WHERE " + Contacts._ID + "=?)");
4126         contactInDefaultDirectoryQuery.bindLong(1, contactId);
4127         return contactInDefaultDirectoryQuery.simpleQueryForLong() != 0;
4128     }
4129 
4130     /**
4131      * Update the visible_contacts table according to the current visibility of contacts, which
4132      * is defined by {@link Clauses#CONTACT_IS_VISIBLE}.
4133      *
4134      * If {@code optionalContactId} is non-negative, it'll update only for the specified contact.
4135      */
updateCustomContactVisibility(SQLiteDatabase db, long optionalContactId)4136     private void updateCustomContactVisibility(SQLiteDatabase db, long optionalContactId) {
4137         final long groupMembershipMimetypeId = getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE);
4138         String[] selectionArgs = new String[] {String.valueOf(groupMembershipMimetypeId)};
4139 
4140         final String contactIdSelect = (optionalContactId < 0) ? "" :
4141                 (Contacts._ID + "=" + optionalContactId + " AND ");
4142 
4143         // First delete what needs to be deleted, then insert what needs to be added.
4144         // Since flash writes are very expensive, this approach is much better than
4145         // delete-all-insert-all.
4146         db.execSQL(
4147                 "DELETE FROM " + Tables.VISIBLE_CONTACTS +
4148                 " WHERE " + Contacts._ID + " IN" +
4149                     "(SELECT " + Contacts._ID +
4150                     " FROM " + Tables.CONTACTS +
4151                     " WHERE " + contactIdSelect + "(" + Clauses.CONTACT_IS_VISIBLE + ")=0) ",
4152                 selectionArgs);
4153 
4154         db.execSQL(
4155                 "INSERT INTO " + Tables.VISIBLE_CONTACTS +
4156                 " SELECT " + Contacts._ID +
4157                 " FROM " + Tables.CONTACTS +
4158                 " WHERE " +
4159                     contactIdSelect +
4160                     Contacts._ID + " NOT IN " + Tables.VISIBLE_CONTACTS +
4161                     " AND (" + Clauses.CONTACT_IS_VISIBLE + ")=1 ",
4162                 selectionArgs);
4163     }
4164 
4165     /**
4166      * Returns contact ID for the given contact or zero if it is NULL.
4167      */
getContactId(long rawContactId)4168     public long getContactId(long rawContactId) {
4169         final SQLiteStatement contactIdQuery = getWritableDatabase().compileStatement(
4170                     "SELECT " + RawContacts.CONTACT_ID +
4171                     " FROM " + Tables.RAW_CONTACTS +
4172                     " WHERE " + RawContacts._ID + "=?");
4173         try {
4174             contactIdQuery.bindLong(1, rawContactId);
4175             return contactIdQuery.simpleQueryForLong();
4176         } catch (SQLiteDoneException e) {
4177             return 0;  // No valid mapping found.
4178         }
4179     }
4180 
getAggregationMode(long rawContactId)4181     public int getAggregationMode(long rawContactId) {
4182         final SQLiteStatement aggregationModeQuery = getWritableDatabase().compileStatement(
4183                     "SELECT " + RawContacts.AGGREGATION_MODE +
4184                     " FROM " + Tables.RAW_CONTACTS +
4185                     " WHERE " + RawContacts._ID + "=?");
4186         try {
4187             aggregationModeQuery.bindLong(1, rawContactId);
4188             return (int) aggregationModeQuery.simpleQueryForLong();
4189         } catch (SQLiteDoneException e) {
4190             return RawContacts.AGGREGATION_MODE_DISABLED;  // No valid row found.
4191         }
4192     }
4193 
buildPhoneLookupAndContactQuery( SQLiteQueryBuilder qb, String normalizedNumber, String numberE164)4194     public void buildPhoneLookupAndContactQuery(
4195             SQLiteQueryBuilder qb, String normalizedNumber, String numberE164) {
4196 
4197         String minMatch = PhoneNumberUtils.toCallerIDMinMatch(normalizedNumber);
4198         StringBuilder sb = new StringBuilder();
4199         appendPhoneLookupTables(sb, minMatch, true);
4200         qb.setTables(sb.toString());
4201 
4202         sb = new StringBuilder();
4203         appendPhoneLookupSelection(sb, normalizedNumber, numberE164);
4204         qb.appendWhere(sb.toString());
4205     }
4206 
4207     /**
4208      * Phone lookup method that uses the custom SQLite function phone_number_compare_loose
4209      * that serves as a fallback in case the regular lookup does not return any results.
4210      * @param qb The query builder.
4211      * @param number The phone number to search for.
4212      */
buildFallbackPhoneLookupAndContactQuery(SQLiteQueryBuilder qb, String number)4213     public void buildFallbackPhoneLookupAndContactQuery(SQLiteQueryBuilder qb, String number) {
4214         final String minMatch = PhoneNumberUtils.toCallerIDMinMatch(number);
4215         final StringBuilder sb = new StringBuilder();
4216         // Append lookup tables.
4217         sb.append(Tables.RAW_CONTACTS);
4218         sb.append(" JOIN " + Views.CONTACTS + " as contacts_view"
4219                 + " ON (contacts_view._id = " + Tables.RAW_CONTACTS
4220                 + "." + RawContacts.CONTACT_ID + ")" +
4221                 " JOIN (SELECT " + PhoneLookupColumns.DATA_ID + "," +
4222                 PhoneLookupColumns.NORMALIZED_NUMBER + " FROM "+ Tables.PHONE_LOOKUP + " "
4223                 + "WHERE (" + Tables.PHONE_LOOKUP + "." + PhoneLookupColumns.MIN_MATCH + " = '");
4224         sb.append(minMatch);
4225         sb.append("')) AS lookup " +
4226                 "ON lookup." + PhoneLookupColumns.DATA_ID + "=" + Tables.DATA + "." + Data._ID
4227                 + " JOIN " + Tables.DATA + " "
4228                 + "ON " + Tables.DATA + "." + Data.RAW_CONTACT_ID + "=" + Tables.RAW_CONTACTS + "."
4229                 + RawContacts._ID);
4230 
4231         qb.setTables(sb.toString());
4232 
4233         sb.setLength(0);
4234         sb.append("PHONE_NUMBERS_EQUAL(" + Tables.DATA + "." + Phone.NUMBER + ", ");
4235         DatabaseUtils.appendEscapedSQLString(sb, number);
4236         sb.append(mUseStrictPhoneNumberComparison ? ", 1)" : ", 0)");
4237         qb.appendWhere(sb.toString());
4238     }
4239 
4240     /**
4241      * Adds query for selecting the contact with the given {@code sipAddress} to the given
4242      * {@link StringBuilder}.
4243      *
4244      * @return the query arguments to be passed in with the query
4245      */
buildSipContactQuery(StringBuilder sb, String sipAddress)4246     public String[] buildSipContactQuery(StringBuilder sb, String sipAddress) {
4247         sb.append("upper(");
4248         sb.append(Data.DATA1);
4249         sb.append(")=upper(?) AND ");
4250         sb.append(DataColumns.MIMETYPE_ID);
4251         sb.append("=");
4252         sb.append(Long.toString(getMimeTypeIdForSip()));
4253         // Return the arguments to be passed to the query.
4254         return new String[] {sipAddress};
4255     }
4256 
buildPhoneLookupAsNestedQuery(String number)4257     public String buildPhoneLookupAsNestedQuery(String number) {
4258         StringBuilder sb = new StringBuilder();
4259         final String minMatch = PhoneNumberUtils.toCallerIDMinMatch(number);
4260         sb.append("(SELECT DISTINCT raw_contact_id" + " FROM ");
4261         appendPhoneLookupTables(sb, minMatch, false);
4262         sb.append(" WHERE ");
4263         appendPhoneLookupSelection(sb, number, null);
4264         sb.append(")");
4265         return sb.toString();
4266     }
4267 
appendPhoneLookupTables( StringBuilder sb, final String minMatch, boolean joinContacts)4268     private void appendPhoneLookupTables(
4269             StringBuilder sb, final String minMatch, boolean joinContacts) {
4270 
4271         sb.append(Tables.RAW_CONTACTS);
4272         if (joinContacts) {
4273             sb.append(" JOIN " + Views.CONTACTS + " contacts_view"
4274                     + " ON (contacts_view._id = raw_contacts.contact_id)");
4275         }
4276         sb.append(", (SELECT data_id, normalized_number, length(normalized_number) as len "
4277                 + " FROM phone_lookup " + " WHERE (" + Tables.PHONE_LOOKUP + "."
4278                 + PhoneLookupColumns.MIN_MATCH + " = '");
4279         sb.append(minMatch);
4280         sb.append("')) AS lookup, " + Tables.DATA);
4281     }
4282 
appendPhoneLookupSelection(StringBuilder sb, String number, String numberE164)4283     private void appendPhoneLookupSelection(StringBuilder sb, String number, String numberE164) {
4284         sb.append("lookup.data_id=data._id AND data.raw_contact_id=raw_contacts._id");
4285         boolean hasNumberE164 = !TextUtils.isEmpty(numberE164);
4286         boolean hasNumber = !TextUtils.isEmpty(number);
4287         if (hasNumberE164 || hasNumber) {
4288             sb.append(" AND ( ");
4289             if (hasNumberE164) {
4290                 sb.append(" lookup.normalized_number = ");
4291                 DatabaseUtils.appendEscapedSQLString(sb, numberE164);
4292             }
4293             if (hasNumberE164 && hasNumber) {
4294                 sb.append(" OR ");
4295             }
4296             if (hasNumber) {
4297                 // Skip the suffix match entirely if we are using strict number comparison.
4298                 if (!mUseStrictPhoneNumberComparison) {
4299                     int numberLen = number.length();
4300                     sb.append(" lookup.len <= ");
4301                     sb.append(numberLen);
4302                     sb.append(" AND substr(");
4303                     DatabaseUtils.appendEscapedSQLString(sb, number);
4304                     sb.append(',');
4305                     sb.append(numberLen);
4306                     sb.append(" - lookup.len + 1) = lookup.normalized_number");
4307 
4308                     // Some countries (e.g. Brazil) can have incoming calls which contain only
4309                     // the local number (no country calling code and no area code).  This case
4310                     // is handled below, see b/5197612.
4311                     // This also handles a Gingerbread -> ICS upgrade issue; see b/5638376.
4312                     sb.append(" OR (");
4313                     sb.append(" lookup.len > ");
4314                     sb.append(numberLen);
4315                     sb.append(" AND substr(lookup.normalized_number,");
4316                     sb.append("lookup.len + 1 - ");
4317                     sb.append(numberLen);
4318                     sb.append(") = ");
4319                     DatabaseUtils.appendEscapedSQLString(sb, number);
4320                     sb.append(")");
4321                 } else {
4322                     sb.append("0");
4323                 }
4324             }
4325             sb.append(')');
4326         }
4327     }
4328 
getUseStrictPhoneNumberComparisonParameter()4329     public String getUseStrictPhoneNumberComparisonParameter() {
4330         return mUseStrictPhoneNumberComparison ? "1" : "0";
4331     }
4332 
4333     /**
4334      * Loads common nickname mappings into the database.
4335      */
loadNicknameLookupTable(SQLiteDatabase db)4336     private void loadNicknameLookupTable(SQLiteDatabase db) {
4337         db.execSQL("DELETE FROM " + Tables.NICKNAME_LOOKUP);
4338 
4339         String[] strings = mContext.getResources().getStringArray(
4340                 com.android.internal.R.array.common_nicknames);
4341         if (strings == null || strings.length == 0) {
4342             return;
4343         }
4344 
4345         final SQLiteStatement nicknameLookupInsert = db.compileStatement("INSERT INTO "
4346                 + Tables.NICKNAME_LOOKUP + "(" + NicknameLookupColumns.NAME + ","
4347                 + NicknameLookupColumns.CLUSTER + ") VALUES (?,?)");
4348 
4349         try {
4350             for (int clusterId = 0; clusterId < strings.length; clusterId++) {
4351                 String[] names = strings[clusterId].split(",");
4352                 for (String name : names) {
4353                     String normalizedName = NameNormalizer.normalize(name);
4354                     try {
4355                         nicknameLookupInsert.bindString(1, normalizedName);
4356                         nicknameLookupInsert.bindString(2, String.valueOf(clusterId));
4357                         nicknameLookupInsert.executeInsert();
4358                     } catch (SQLiteException e) {
4359                         // Print the exception and keep going (this is not a fatal error).
4360                         Log.e(TAG, "Cannot insert nickname: " + name, e);
4361                     }
4362                 }
4363             }
4364         } finally {
4365             nicknameLookupInsert.close();
4366         }
4367     }
4368 
copyStringValue( ContentValues toValues, String toKey, ContentValues fromValues, String fromKey)4369     public static void copyStringValue(
4370             ContentValues toValues, String toKey, ContentValues fromValues, String fromKey) {
4371 
4372         if (fromValues.containsKey(fromKey)) {
4373             toValues.put(toKey, fromValues.getAsString(fromKey));
4374         }
4375     }
4376 
copyLongValue( ContentValues toValues, String toKey, ContentValues fromValues, String fromKey)4377     public static void copyLongValue(
4378             ContentValues toValues, String toKey, ContentValues fromValues, String fromKey) {
4379 
4380         if (fromValues.containsKey(fromKey)) {
4381             long longValue;
4382             Object value = fromValues.get(fromKey);
4383             if (value instanceof Boolean) {
4384                 longValue = (Boolean) value ? 1 : 0;
4385             } else if (value instanceof String) {
4386                 longValue = Long.parseLong((String)value);
4387             } else {
4388                 longValue = ((Number)value).longValue();
4389             }
4390             toValues.put(toKey, longValue);
4391         }
4392     }
4393 
getSyncState()4394     public SyncStateContentProviderHelper getSyncState() {
4395         return mSyncState;
4396     }
4397 
4398     /**
4399      * Returns the value from the {@link PropertyUtils.Tables#PROPERTIES} table.
4400      */
getProperty(String key, String defaultValue)4401     public String getProperty(String key, String defaultValue) {
4402         return PropertyUtils.getProperty(getReadableDatabase(), key, defaultValue);
4403     }
4404 
4405     /**
4406      * Stores a key-value pair in the {@link PropertyUtils.Tables#PROPERTIES} table.
4407      */
setProperty(String key, String value)4408     public void setProperty(String key, String value) {
4409         PropertyUtils.setProperty(getWritableDatabase(), key, value);
4410     }
4411 
forceDirectoryRescan()4412     public void forceDirectoryRescan() {
4413         setProperty(DbProperties.DIRECTORY_SCAN_COMPLETE, "0");
4414     }
4415 
4416     /**
4417      * Test if the given column appears in the given projection.
4418      */
isInProjection(String[] projection, String column)4419     public static boolean isInProjection(String[] projection, String column) {
4420         if (projection == null) {
4421             return true;  // Null means "all columns".  We can't really tell if it's in there.
4422         }
4423         for (String test : projection) {
4424             if (column.equals(test)) {
4425                 return true;
4426             }
4427         }
4428         return false;
4429     }
4430 
4431     /**
4432      * Tests if any of the columns appear in the given projection.
4433      */
isInProjection(String[] projection, String... columns)4434     public static boolean isInProjection(String[] projection, String... columns) {
4435         if (projection == null) {
4436             return true;
4437         }
4438 
4439         // Optimized for a single-column test
4440         if (columns.length == 1) {
4441             return isInProjection(projection, columns[0]);
4442         }
4443         for (String test : projection) {
4444             for (String column : columns) {
4445                 if (column.equals(test)) {
4446                     return true;
4447                 }
4448             }
4449         }
4450         return false;
4451     }
4452 
4453     /**
4454      * Returns a detailed exception message for the supplied URI.  It includes the calling
4455      * user and calling package(s).
4456      */
exceptionMessage(Uri uri)4457     public String exceptionMessage(Uri uri) {
4458         return exceptionMessage(null, uri);
4459     }
4460 
4461     /**
4462      * Returns a detailed exception message for the supplied URI.  It includes the calling
4463      * user and calling package(s).
4464      */
exceptionMessage(String message, Uri uri)4465     public String exceptionMessage(String message, Uri uri) {
4466         StringBuilder sb = new StringBuilder();
4467         if (message != null) {
4468             sb.append(message).append("; ");
4469         }
4470         sb.append("URI: ").append(uri);
4471         final PackageManager pm = mContext.getPackageManager();
4472         int callingUid = Binder.getCallingUid();
4473         sb.append(", calling user: ");
4474         String userName = pm.getNameForUid(callingUid);
4475         sb.append(userName == null ? callingUid : userName);
4476 
4477         final String[] callerPackages = pm.getPackagesForUid(callingUid);
4478         if (callerPackages != null && callerPackages.length > 0) {
4479             if (callerPackages.length == 1) {
4480                 sb.append(", calling package:");
4481                 sb.append(callerPackages[0]);
4482             } else {
4483                 sb.append(", calling package is one of: [");
4484                 for (int i = 0; i < callerPackages.length; i++) {
4485                     if (i != 0) {
4486                         sb.append(", ");
4487                     }
4488                     sb.append(callerPackages[i]);
4489                 }
4490                 sb.append("]");
4491             }
4492         }
4493         return sb.toString();
4494     }
4495 
deleteStatusUpdate(long dataId)4496     public void deleteStatusUpdate(long dataId) {
4497         final SQLiteStatement statusUpdateDelete = getWritableDatabase().compileStatement(
4498                     "DELETE FROM " + Tables.STATUS_UPDATES +
4499                     " WHERE " + StatusUpdatesColumns.DATA_ID + "=?");
4500         statusUpdateDelete.bindLong(1, dataId);
4501         statusUpdateDelete.execute();
4502     }
4503 
replaceStatusUpdate(Long dataId, long timestamp, String status, String resPackage, Integer iconResource, Integer labelResource)4504     public void replaceStatusUpdate(Long dataId, long timestamp, String status, String resPackage,
4505             Integer iconResource, Integer labelResource) {
4506         final SQLiteStatement statusUpdateReplace = getWritableDatabase().compileStatement(
4507                     "INSERT OR REPLACE INTO " + Tables.STATUS_UPDATES + "("
4508                             + StatusUpdatesColumns.DATA_ID + ", "
4509                             + StatusUpdates.STATUS_TIMESTAMP + ","
4510                             + StatusUpdates.STATUS + ","
4511                             + StatusUpdates.STATUS_RES_PACKAGE + ","
4512                             + StatusUpdates.STATUS_ICON + ","
4513                             + StatusUpdates.STATUS_LABEL + ")" +
4514                     " VALUES (?,?,?,?,?,?)");
4515         statusUpdateReplace.bindLong(1, dataId);
4516         statusUpdateReplace.bindLong(2, timestamp);
4517         bindString(statusUpdateReplace, 3, status);
4518         bindString(statusUpdateReplace, 4, resPackage);
4519         bindLong(statusUpdateReplace, 5, iconResource);
4520         bindLong(statusUpdateReplace, 6, labelResource);
4521         statusUpdateReplace.execute();
4522     }
4523 
insertStatusUpdate(Long dataId, String status, String resPackage, Integer iconResource, Integer labelResource)4524     public void insertStatusUpdate(Long dataId, String status, String resPackage,
4525             Integer iconResource, Integer labelResource) {
4526         final SQLiteStatement statusUpdateInsert = getWritableDatabase().compileStatement(
4527                     "INSERT INTO " + Tables.STATUS_UPDATES + "("
4528                             + StatusUpdatesColumns.DATA_ID + ", "
4529                             + StatusUpdates.STATUS + ","
4530                             + StatusUpdates.STATUS_RES_PACKAGE + ","
4531                             + StatusUpdates.STATUS_ICON + ","
4532                             + StatusUpdates.STATUS_LABEL + ")" +
4533                     " VALUES (?,?,?,?,?)");
4534         try {
4535             statusUpdateInsert.bindLong(1, dataId);
4536             bindString(statusUpdateInsert, 2, status);
4537             bindString(statusUpdateInsert, 3, resPackage);
4538             bindLong(statusUpdateInsert, 4, iconResource);
4539             bindLong(statusUpdateInsert, 5, labelResource);
4540             statusUpdateInsert.executeInsert();
4541         } catch (SQLiteConstraintException e) {
4542             // The row already exists - update it
4543             final SQLiteStatement statusUpdateAutoTimestamp = getWritableDatabase()
4544                     .compileStatement(
4545                         "UPDATE " + Tables.STATUS_UPDATES +
4546                         " SET " + StatusUpdates.STATUS_TIMESTAMP + "=?,"
4547                                 + StatusUpdates.STATUS + "=?" +
4548                         " WHERE " + StatusUpdatesColumns.DATA_ID + "=?"
4549                                 + " AND " + StatusUpdates.STATUS + "!=?");
4550 
4551             long timestamp = System.currentTimeMillis();
4552             statusUpdateAutoTimestamp.bindLong(1, timestamp);
4553             bindString(statusUpdateAutoTimestamp, 2, status);
4554             statusUpdateAutoTimestamp.bindLong(3, dataId);
4555             bindString(statusUpdateAutoTimestamp, 4, status);
4556             statusUpdateAutoTimestamp.execute();
4557 
4558             final SQLiteStatement statusAttributionUpdate = getWritableDatabase().compileStatement(
4559                         "UPDATE " + Tables.STATUS_UPDATES +
4560                         " SET " + StatusUpdates.STATUS_RES_PACKAGE + "=?,"
4561                                 + StatusUpdates.STATUS_ICON + "=?,"
4562                                 + StatusUpdates.STATUS_LABEL + "=?" +
4563                         " WHERE " + StatusUpdatesColumns.DATA_ID + "=?");
4564             bindString(statusAttributionUpdate, 1, resPackage);
4565             bindLong(statusAttributionUpdate, 2, iconResource);
4566             bindLong(statusAttributionUpdate, 3, labelResource);
4567             statusAttributionUpdate.bindLong(4, dataId);
4568             statusAttributionUpdate.execute();
4569         }
4570     }
4571 
4572     /**
4573      * Updates a raw contact display name based on data rows, e.g. structured name,
4574      * organization, email etc.
4575      */
updateRawContactDisplayName(SQLiteDatabase db, long rawContactId)4576     public void updateRawContactDisplayName(SQLiteDatabase db, long rawContactId) {
4577         if (mNameSplitter == null) {
4578             createNameSplitter();
4579         }
4580 
4581         int bestDisplayNameSource = DisplayNameSources.UNDEFINED;
4582         NameSplitter.Name bestName = null;
4583         String bestDisplayName = null;
4584         String bestPhoneticName = null;
4585         int bestPhoneticNameStyle = PhoneticNameStyle.UNDEFINED;
4586 
4587         mSelectionArgs1[0] = String.valueOf(rawContactId);
4588         Cursor c = db.rawQuery(RawContactNameQuery.RAW_SQL, mSelectionArgs1);
4589         try {
4590             while (c.moveToNext()) {
4591                 int mimeType = c.getInt(RawContactNameQuery.MIMETYPE);
4592                 int source = getDisplayNameSourceForMimeTypeId(mimeType);
4593 
4594                 if (source == DisplayNameSources.STRUCTURED_NAME) {
4595                     final String given = c.getString(RawContactNameQuery.GIVEN_NAME);
4596                     final String middle = c.getString(RawContactNameQuery.MIDDLE_NAME);
4597                     final String family = c.getString(RawContactNameQuery.FAMILY_NAME);
4598                     final String suffix = c.getString(RawContactNameQuery.SUFFIX);
4599                     final String prefix = c.getString(RawContactNameQuery.PREFIX);
4600                     if (TextUtils.isEmpty(given) && TextUtils.isEmpty(middle)
4601                             && TextUtils.isEmpty(family) && TextUtils.isEmpty(suffix)
4602                             && TextUtils.isEmpty(prefix)) {
4603                         // Every non-phonetic name component is empty. Therefore, lets lower the
4604                         // source score to STRUCTURED_PHONETIC_NAME.
4605                         source = DisplayNameSources.STRUCTURED_PHONETIC_NAME;
4606                     }
4607                 }
4608 
4609                 if (source < bestDisplayNameSource || source == DisplayNameSources.UNDEFINED) {
4610                     continue;
4611                 }
4612 
4613                 if (source == bestDisplayNameSource
4614                         && c.getInt(RawContactNameQuery.IS_PRIMARY) == 0) {
4615                     continue;
4616                 }
4617 
4618                 if (mimeType == getMimeTypeIdForStructuredName()) {
4619                     NameSplitter.Name name;
4620                     if (bestName != null) {
4621                         name = new NameSplitter.Name();
4622                     } else {
4623                         name = mName;
4624                         name.clear();
4625                     }
4626                     name.prefix = c.getString(RawContactNameQuery.PREFIX);
4627                     name.givenNames = c.getString(RawContactNameQuery.GIVEN_NAME);
4628                     name.middleName = c.getString(RawContactNameQuery.MIDDLE_NAME);
4629                     name.familyName = c.getString(RawContactNameQuery.FAMILY_NAME);
4630                     name.suffix = c.getString(RawContactNameQuery.SUFFIX);
4631                     name.fullNameStyle = c.isNull(RawContactNameQuery.FULL_NAME_STYLE)
4632                             ? FullNameStyle.UNDEFINED
4633                             : c.getInt(RawContactNameQuery.FULL_NAME_STYLE);
4634                     name.phoneticFamilyName = c.getString(RawContactNameQuery.PHONETIC_FAMILY_NAME);
4635                     name.phoneticMiddleName = c.getString(RawContactNameQuery.PHONETIC_MIDDLE_NAME);
4636                     name.phoneticGivenName = c.getString(RawContactNameQuery.PHONETIC_GIVEN_NAME);
4637                     name.phoneticNameStyle = c.isNull(RawContactNameQuery.PHONETIC_NAME_STYLE)
4638                             ? PhoneticNameStyle.UNDEFINED
4639                             : c.getInt(RawContactNameQuery.PHONETIC_NAME_STYLE);
4640                     if (!name.isEmpty()) {
4641                         bestDisplayNameSource = source;
4642                         bestName = name;
4643                     }
4644                 } else if (mimeType == getMimeTypeIdForOrganization()) {
4645                     mCharArrayBuffer.sizeCopied = 0;
4646                     c.copyStringToBuffer(RawContactNameQuery.DATA1, mCharArrayBuffer);
4647                     if (mCharArrayBuffer.sizeCopied != 0) {
4648                         bestDisplayNameSource = source;
4649                         bestDisplayName = new String(mCharArrayBuffer.data, 0,
4650                                 mCharArrayBuffer.sizeCopied);
4651                         bestPhoneticName = c.getString(
4652                                 RawContactNameQuery.ORGANIZATION_PHONETIC_NAME);
4653                         bestPhoneticNameStyle =
4654                                 c.isNull(RawContactNameQuery.ORGANIZATION_PHONETIC_NAME_STYLE)
4655                                    ? PhoneticNameStyle.UNDEFINED
4656                                    : c.getInt(RawContactNameQuery.ORGANIZATION_PHONETIC_NAME_STYLE);
4657                     } else {
4658                         c.copyStringToBuffer(RawContactNameQuery.TITLE, mCharArrayBuffer);
4659                         if (mCharArrayBuffer.sizeCopied != 0) {
4660                             bestDisplayNameSource = source;
4661                             bestDisplayName = new String(mCharArrayBuffer.data, 0,
4662                                     mCharArrayBuffer.sizeCopied);
4663                             bestPhoneticName = null;
4664                             bestPhoneticNameStyle = PhoneticNameStyle.UNDEFINED;
4665                         }
4666                     }
4667                 } else {
4668                     // Display name is at DATA1 in all other types.
4669                     // This is ensured in the constructor.
4670 
4671                     mCharArrayBuffer.sizeCopied = 0;
4672                     c.copyStringToBuffer(RawContactNameQuery.DATA1, mCharArrayBuffer);
4673                     if (mCharArrayBuffer.sizeCopied != 0) {
4674                         bestDisplayNameSource = source;
4675                         bestDisplayName = new String(mCharArrayBuffer.data, 0,
4676                                 mCharArrayBuffer.sizeCopied);
4677                         bestPhoneticName = null;
4678                         bestPhoneticNameStyle = PhoneticNameStyle.UNDEFINED;
4679                     }
4680                 }
4681             }
4682 
4683         } finally {
4684             c.close();
4685         }
4686 
4687         String displayNamePrimary;
4688         String displayNameAlternative;
4689         String sortNamePrimary;
4690         String sortNameAlternative;
4691         String sortKeyPrimary = null;
4692         String sortKeyAlternative = null;
4693         int displayNameStyle = FullNameStyle.UNDEFINED;
4694 
4695         if (bestDisplayNameSource == DisplayNameSources.STRUCTURED_NAME
4696                 || bestDisplayNameSource == DisplayNameSources.STRUCTURED_PHONETIC_NAME) {
4697             displayNameStyle = bestName.fullNameStyle;
4698             if (displayNameStyle == FullNameStyle.CJK
4699                     || displayNameStyle == FullNameStyle.UNDEFINED) {
4700                 displayNameStyle = mNameSplitter.getAdjustedFullNameStyle(displayNameStyle);
4701                 bestName.fullNameStyle = displayNameStyle;
4702             }
4703 
4704             displayNamePrimary = mNameSplitter.join(bestName, true, true);
4705             displayNameAlternative = mNameSplitter.join(bestName, false, true);
4706 
4707             if (TextUtils.isEmpty(bestName.prefix)) {
4708                 sortNamePrimary = displayNamePrimary;
4709                 sortNameAlternative = displayNameAlternative;
4710             } else {
4711                 sortNamePrimary = mNameSplitter.join(bestName, true, false);
4712                 sortNameAlternative = mNameSplitter.join(bestName, false, false);
4713             }
4714 
4715             bestPhoneticName = mNameSplitter.joinPhoneticName(bestName);
4716             bestPhoneticNameStyle = bestName.phoneticNameStyle;
4717         } else {
4718             displayNamePrimary = displayNameAlternative = bestDisplayName;
4719             sortNamePrimary = sortNameAlternative = bestDisplayName;
4720         }
4721 
4722         if (bestPhoneticName != null) {
4723             if (displayNamePrimary == null) {
4724                 displayNamePrimary = bestPhoneticName;
4725             }
4726             if (displayNameAlternative == null) {
4727                 displayNameAlternative = bestPhoneticName;
4728             }
4729             // Phonetic names disregard name order so displayNamePrimary and displayNameAlternative
4730             // are the same.
4731             sortKeyPrimary = sortKeyAlternative = bestPhoneticName;
4732             if (bestPhoneticNameStyle == PhoneticNameStyle.UNDEFINED) {
4733                 bestPhoneticNameStyle = mNameSplitter.guessPhoneticNameStyle(bestPhoneticName);
4734             }
4735         } else {
4736             bestPhoneticNameStyle = PhoneticNameStyle.UNDEFINED;
4737             if (displayNameStyle == FullNameStyle.UNDEFINED) {
4738                 displayNameStyle = mNameSplitter.guessFullNameStyle(bestDisplayName);
4739                 if (displayNameStyle == FullNameStyle.UNDEFINED
4740                         || displayNameStyle == FullNameStyle.CJK) {
4741                     displayNameStyle = mNameSplitter.getAdjustedNameStyleBasedOnPhoneticNameStyle(
4742                             displayNameStyle, bestPhoneticNameStyle);
4743                 }
4744                 displayNameStyle = mNameSplitter.getAdjustedFullNameStyle(displayNameStyle);
4745             }
4746             if (displayNameStyle == FullNameStyle.CHINESE ||
4747                     displayNameStyle == FullNameStyle.CJK) {
4748                 sortKeyPrimary = sortKeyAlternative = sortNamePrimary;
4749             }
4750         }
4751 
4752         if (sortKeyPrimary == null) {
4753             sortKeyPrimary = sortNamePrimary;
4754             sortKeyAlternative = sortNameAlternative;
4755         }
4756 
4757         final ContactLocaleUtils localeUtils = ContactLocaleUtils.getInstance();
4758         int phonebookBucketPrimary = TextUtils.isEmpty(sortKeyPrimary)
4759                 ? localeUtils.getNumberBucketIndex()
4760                 : localeUtils.getBucketIndex(sortKeyPrimary);
4761         String phonebookLabelPrimary = localeUtils.getBucketLabel(phonebookBucketPrimary);
4762 
4763         int phonebookBucketAlternative = TextUtils.isEmpty(sortKeyAlternative)
4764                 ? localeUtils.getNumberBucketIndex()
4765                 : localeUtils.getBucketIndex(sortKeyAlternative);
4766         String phonebookLabelAlternative = localeUtils.getBucketLabel(phonebookBucketAlternative);
4767 
4768         final SQLiteStatement rawContactDisplayNameUpdate = db.compileStatement(
4769                     "UPDATE " + Tables.RAW_CONTACTS +
4770                     " SET " +
4771                             RawContacts.DISPLAY_NAME_SOURCE + "=?," +
4772                             RawContacts.DISPLAY_NAME_PRIMARY + "=?," +
4773                             RawContacts.DISPLAY_NAME_ALTERNATIVE + "=?," +
4774                             RawContacts.PHONETIC_NAME + "=?," +
4775                             RawContacts.PHONETIC_NAME_STYLE + "=?," +
4776                             RawContacts.SORT_KEY_PRIMARY + "=?," +
4777                             RawContactsColumns.PHONEBOOK_LABEL_PRIMARY + "=?," +
4778                             RawContactsColumns.PHONEBOOK_BUCKET_PRIMARY + "=?," +
4779                             RawContacts.SORT_KEY_ALTERNATIVE + "=?," +
4780                             RawContactsColumns.PHONEBOOK_LABEL_ALTERNATIVE + "=?," +
4781                             RawContactsColumns.PHONEBOOK_BUCKET_ALTERNATIVE + "=?" +
4782                     " WHERE " + RawContacts._ID + "=?");
4783 
4784         rawContactDisplayNameUpdate.bindLong(1, bestDisplayNameSource);
4785         bindString(rawContactDisplayNameUpdate, 2, displayNamePrimary);
4786         bindString(rawContactDisplayNameUpdate, 3, displayNameAlternative);
4787         bindString(rawContactDisplayNameUpdate, 4, bestPhoneticName);
4788         rawContactDisplayNameUpdate.bindLong(5, bestPhoneticNameStyle);
4789         bindString(rawContactDisplayNameUpdate, 6, sortKeyPrimary);
4790         bindString(rawContactDisplayNameUpdate, 7, phonebookLabelPrimary);
4791         rawContactDisplayNameUpdate.bindLong(8, phonebookBucketPrimary);
4792         bindString(rawContactDisplayNameUpdate, 9, sortKeyAlternative);
4793         bindString(rawContactDisplayNameUpdate, 10, phonebookLabelAlternative);
4794         rawContactDisplayNameUpdate.bindLong(11, phonebookBucketAlternative);
4795         rawContactDisplayNameUpdate.bindLong(12, rawContactId);
4796         rawContactDisplayNameUpdate.execute();
4797     }
4798 
4799     /**
4800      * Sets the given dataId record in the "data" table to primary, and resets all data records of
4801      * the same mimetype and under the same contact to not be primary.
4802      *
4803      * @param dataId the id of the data record to be set to primary. Pass -1 to clear the primary
4804      * flag of all data items of this raw contacts
4805      */
setIsPrimary(long rawContactId, long dataId, long mimeTypeId)4806     public void setIsPrimary(long rawContactId, long dataId, long mimeTypeId) {
4807         final SQLiteStatement setPrimaryStatement = getWritableDatabase().compileStatement(
4808                     "UPDATE " + Tables.DATA +
4809                     " SET " + Data.IS_PRIMARY + "=(_id=?)" +
4810                     " WHERE " + DataColumns.MIMETYPE_ID + "=?" +
4811                     "   AND " + Data.RAW_CONTACT_ID + "=?");
4812         setPrimaryStatement.bindLong(1, dataId);
4813         setPrimaryStatement.bindLong(2, mimeTypeId);
4814         setPrimaryStatement.bindLong(3, rawContactId);
4815         setPrimaryStatement.execute();
4816     }
4817 
4818     /**
4819      * Clears the super primary of all data items of the given raw contact. does not touch
4820      * other raw contacts of the same joined aggregate
4821      */
clearSuperPrimary(long rawContactId, long mimeTypeId)4822     public void clearSuperPrimary(long rawContactId, long mimeTypeId) {
4823         final SQLiteStatement clearSuperPrimaryStatement = getWritableDatabase().compileStatement(
4824                     "UPDATE " + Tables.DATA +
4825                     " SET " + Data.IS_SUPER_PRIMARY + "=0" +
4826                     " WHERE " + DataColumns.MIMETYPE_ID + "=?" +
4827                     "   AND " + Data.RAW_CONTACT_ID + "=?");
4828         clearSuperPrimaryStatement.bindLong(1, mimeTypeId);
4829         clearSuperPrimaryStatement.bindLong(2, rawContactId);
4830         clearSuperPrimaryStatement.execute();
4831     }
4832 
4833     /**
4834      * Sets the given dataId record in the "data" table to "super primary", and resets all data
4835      * records of the same mimetype and under the same aggregate to not be "super primary".
4836      *
4837      * @param dataId the id of the data record to be set to primary.
4838      */
setIsSuperPrimary(long rawContactId, long dataId, long mimeTypeId)4839     public void setIsSuperPrimary(long rawContactId, long dataId, long mimeTypeId) {
4840         final SQLiteStatement setSuperPrimaryStatement = getWritableDatabase().compileStatement(
4841                     "UPDATE " + Tables.DATA +
4842                     " SET " + Data.IS_SUPER_PRIMARY + "=(" + Data._ID + "=?)" +
4843                     " WHERE " + DataColumns.MIMETYPE_ID + "=?" +
4844                     "   AND " + Data.RAW_CONTACT_ID + " IN (" +
4845                             "SELECT " + RawContacts._ID +
4846                             " FROM " + Tables.RAW_CONTACTS +
4847                             " WHERE " + RawContacts.CONTACT_ID + " =(" +
4848                                     "SELECT " + RawContacts.CONTACT_ID +
4849                                     " FROM " + Tables.RAW_CONTACTS +
4850                                     " WHERE " + RawContacts._ID + "=?))");
4851         setSuperPrimaryStatement.bindLong(1, dataId);
4852         setSuperPrimaryStatement.bindLong(2, mimeTypeId);
4853         setSuperPrimaryStatement.bindLong(3, rawContactId);
4854         setSuperPrimaryStatement.execute();
4855     }
4856 
4857     /**
4858      * Inserts a record in the {@link Tables#NAME_LOOKUP} table.
4859      */
insertNameLookup(long rawContactId, long dataId, int lookupType, String name)4860     public void insertNameLookup(long rawContactId, long dataId, int lookupType, String name) {
4861         if (TextUtils.isEmpty(name)) {
4862             return;
4863         }
4864 
4865         final SQLiteStatement nameLookupInsert = getWritableDatabase().compileStatement(
4866                     "INSERT OR IGNORE INTO " + Tables.NAME_LOOKUP + "("
4867                             + NameLookupColumns.RAW_CONTACT_ID + ","
4868                             + NameLookupColumns.DATA_ID + ","
4869                             + NameLookupColumns.NAME_TYPE + ","
4870                             + NameLookupColumns.NORMALIZED_NAME
4871                     + ") VALUES (?,?,?,?)");
4872         nameLookupInsert.bindLong(1, rawContactId);
4873         nameLookupInsert.bindLong(2, dataId);
4874         nameLookupInsert.bindLong(3, lookupType);
4875         bindString(nameLookupInsert, 4, name);
4876         nameLookupInsert.executeInsert();
4877     }
4878 
4879     /**
4880      * Deletes all {@link Tables#NAME_LOOKUP} table rows associated with the specified data element.
4881      */
deleteNameLookup(long dataId)4882     public void deleteNameLookup(long dataId) {
4883         final SQLiteStatement nameLookupDelete = getWritableDatabase().compileStatement(
4884                     "DELETE FROM " + Tables.NAME_LOOKUP +
4885                     " WHERE " + NameLookupColumns.DATA_ID + "=?");
4886         nameLookupDelete.bindLong(1, dataId);
4887         nameLookupDelete.execute();
4888     }
4889 
insertNameLookupForEmail(long rawContactId, long dataId, String email)4890     public String insertNameLookupForEmail(long rawContactId, long dataId, String email) {
4891         if (TextUtils.isEmpty(email)) {
4892             return null;
4893         }
4894 
4895         String address = extractHandleFromEmailAddress(email);
4896         if (address == null) {
4897             return null;
4898         }
4899 
4900         insertNameLookup(rawContactId, dataId,
4901                 NameLookupType.EMAIL_BASED_NICKNAME, NameNormalizer.normalize(address));
4902         return address;
4903     }
4904 
4905     /**
4906      * Normalizes the nickname and inserts it in the name lookup table.
4907      */
insertNameLookupForNickname(long rawContactId, long dataId, String nickname)4908     public void insertNameLookupForNickname(long rawContactId, long dataId, String nickname) {
4909         if (!TextUtils.isEmpty(nickname)) {
4910             insertNameLookup(rawContactId, dataId,
4911                     NameLookupType.NICKNAME, NameNormalizer.normalize(nickname));
4912         }
4913     }
4914 
4915     /**
4916      * Performs a query and returns true if any Data item of the raw contact with the given
4917      * id and mimetype is marked as super-primary
4918      */
rawContactHasSuperPrimary(long rawContactId, long mimeTypeId)4919     public boolean rawContactHasSuperPrimary(long rawContactId, long mimeTypeId) {
4920         final Cursor existsCursor = getReadableDatabase().rawQuery(
4921                 "SELECT EXISTS(SELECT 1 FROM " + Tables.DATA +
4922                 " WHERE " + Data.RAW_CONTACT_ID + "=?" +
4923                 " AND " + DataColumns.MIMETYPE_ID + "=?" +
4924                 " AND " + Data.IS_SUPER_PRIMARY + "<>0)",
4925                 new String[] {String.valueOf(rawContactId), String.valueOf(mimeTypeId)});
4926         try {
4927             if (!existsCursor.moveToFirst()) throw new IllegalStateException();
4928             return existsCursor.getInt(0) != 0;
4929         } finally {
4930             existsCursor.close();
4931         }
4932     }
4933 
getCurrentCountryIso()4934     public String getCurrentCountryIso() {
4935         return mCountryMonitor.getCountryIso();
4936     }
4937 
4938     @NeededForTesting
setUseStrictPhoneNumberComparisonForTest(boolean useStrict)4939     /* package */ void setUseStrictPhoneNumberComparisonForTest(boolean useStrict) {
4940         mUseStrictPhoneNumberComparison = useStrict;
4941     }
4942 
4943     @NeededForTesting
getUseStrictPhoneNumberComparisonForTest()4944     /* package */ boolean getUseStrictPhoneNumberComparisonForTest() {
4945         return mUseStrictPhoneNumberComparison;
4946     }
4947 
4948     @NeededForTesting
querySearchIndexContentForTest(long contactId)4949     /* package */ String querySearchIndexContentForTest(long contactId) {
4950         return DatabaseUtils.stringForQuery(getReadableDatabase(),
4951                 "SELECT " + SearchIndexColumns.CONTENT +
4952                 " FROM " + Tables.SEARCH_INDEX +
4953                 " WHERE " + SearchIndexColumns.CONTACT_ID + "=CAST(? AS int)",
4954                 new String[] {String.valueOf(contactId)});
4955     }
4956 
4957     @NeededForTesting
querySearchIndexTokensForTest(long contactId)4958     /* package */ String querySearchIndexTokensForTest(long contactId) {
4959         return DatabaseUtils.stringForQuery(getReadableDatabase(),
4960                 "SELECT " + SearchIndexColumns.TOKENS +
4961                 " FROM " + Tables.SEARCH_INDEX +
4962                 " WHERE " + SearchIndexColumns.CONTACT_ID + "=CAST(? AS int)",
4963                 new String[] {String.valueOf(contactId)});
4964     }
4965 
upsertMetadataSync(String backupId, Long accountId, String data, Integer deleted)4966     public long upsertMetadataSync(String backupId, Long accountId, String data, Integer deleted) {
4967         final SQLiteStatement metadataSyncInsert = getWritableDatabase().compileStatement(
4968                     "INSERT OR REPLACE INTO " + Tables.METADATA_SYNC + "("
4969                             + MetadataSync.RAW_CONTACT_BACKUP_ID + ", "
4970                             + MetadataSyncColumns.ACCOUNT_ID + ", "
4971                             + MetadataSync.DATA + ","
4972                             + MetadataSync.DELETED + ")" +
4973                             " VALUES (?,?,?,?)");
4974         metadataSyncInsert.bindString(1, backupId);
4975         metadataSyncInsert.bindLong(2, accountId);
4976         data = (data == null) ? "" : data;
4977         metadataSyncInsert.bindString(3, data);
4978         metadataSyncInsert.bindLong(4, deleted);
4979         return metadataSyncInsert.executeInsert();
4980     }
4981 
notifyProviderStatusChange(Context context)4982     public static void notifyProviderStatusChange(Context context) {
4983         context.getContentResolver().notifyChange(ProviderStatus.CONTENT_URI,
4984                 /* observer= */ null, /* syncToNetwork= */ false);
4985     }
4986 
getDatabaseCreationTime()4987     public long getDatabaseCreationTime() {
4988         return mDatabaseCreationTime;
4989     }
4990 
4991     private SqlChecker mCachedSqlChecker;
4992 
getSqlChecker()4993     private SqlChecker getSqlChecker() {
4994         // No need for synchronization on mCachedSqlChecker, because worst-case we'll just
4995         // initialize it twice.
4996         if (mCachedSqlChecker != null) {
4997             return mCachedSqlChecker;
4998         }
4999         final ArrayList<String> invalidTokens = new ArrayList<>();
5000 
5001         if (DISALLOW_SUB_QUERIES) {
5002             // Disallow referring to tables and views.  However, we exempt tables whose names are
5003             // also used as column names of any tables.  (Right now it's only 'data'.)
5004             invalidTokens.addAll(
5005                     DatabaseAnalyzer.findTableViewsAllowingColumns(getReadableDatabase()));
5006 
5007             // Disallow token "select" to disallow subqueries.
5008             invalidTokens.add("select");
5009 
5010             // Allow the use of "default_directory" for now, as it used to be sort of commonly used...
5011             invalidTokens.remove(Tables.DEFAULT_DIRECTORY.toLowerCase());
5012         }
5013 
5014         mCachedSqlChecker = new SqlChecker(invalidTokens);
5015 
5016         return mCachedSqlChecker;
5017     }
5018 
5019     /**
5020      * Ensure (a piece of) SQL is valid and doesn't contain disallowed tokens.
5021      */
validateSql(String callerPackage, String sqlPiece)5022     public void validateSql(String callerPackage, String sqlPiece) {
5023         // TODO Replace the Runnable with a lambda -- which would crash right now due to an art bug?
5024         runSqlValidation(callerPackage, new Runnable() {
5025             @Override
5026             public void run() {
5027                 ContactsDatabaseHelper.this.getSqlChecker().ensureNoInvalidTokens(sqlPiece);
5028             }
5029         });
5030     }
5031 
5032     /**
5033      * Ensure all keys in {@code values} are valid. (i.e. they're all single token.)
5034      */
validateContentValues(String callerPackage, ContentValues values)5035     public void validateContentValues(String callerPackage, ContentValues values) {
5036         // TODO Replace the Runnable with a lambda -- which would crash right now due to an art bug?
5037         runSqlValidation(callerPackage, new Runnable() {
5038             @Override
5039             public void run() {
5040                 for (String key : values.keySet()) {
5041                     ContactsDatabaseHelper.this.getSqlChecker().ensureSingleTokenOnly(key);
5042                 }
5043             }
5044         });
5045    }
5046 
5047     /**
5048      * Ensure all column names in {@code projection} are valid. (i.e. they're all single token.)
5049      */
validateProjection(String callerPackage, String[] projection)5050     public void validateProjection(String callerPackage, String[] projection) {
5051         // TODO Replace the Runnable with a lambda -- which would crash right now due to an art bug?
5052         if (projection != null) {
5053             runSqlValidation(callerPackage, new Runnable() {
5054                 @Override
5055                 public void run() {
5056                     for (String column : projection) {
5057                         ContactsDatabaseHelper.this.getSqlChecker().ensureSingleTokenOnly(column);
5058                     }
5059                 }
5060             });
5061         }
5062     }
5063 
runSqlValidation(String callerPackage, Runnable r)5064     private void runSqlValidation(String callerPackage, Runnable r) {
5065         try {
5066             r.run();
5067         } catch (InvalidSqlException e) {
5068             reportInvalidSql(callerPackage, e);
5069         }
5070     }
5071 
reportInvalidSql(String callerPackage, InvalidSqlException e)5072     private void reportInvalidSql(String callerPackage, InvalidSqlException e) {
5073         Log.e(TAG, String.format("%s caller=%s", e.getMessage(), callerPackage));
5074         throw e;
5075     }
5076 
5077     /**
5078      * Calls WTF without crashing, so we can collect errors in the wild.  During unit tests, it'll
5079      * log only.
5080      */
logWtf(String message)5081     public void logWtf(String message) {
5082         if (mIsTestInstance) {
5083             Slog.w(TAG, "[Test mode, warning only] " + message);
5084         } else {
5085             Slog.wtfStack(TAG, message);
5086         }
5087     }
5088 }
5089