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