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