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