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