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