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