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