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