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