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