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