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