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