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