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