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