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