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