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