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