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