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