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