ContactsDatabaseHelper.java revision fada1f08e7ffc8012bf2175f61f3ef3270eba9ec
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.internal.content.SyncStateContentProviderHelper;
20
21import android.content.ContentResolver;
22import android.content.ContentValues;
23import android.content.Context;
24import android.content.pm.ApplicationInfo;
25import android.content.pm.PackageManager;
26import android.content.pm.PackageManager.NameNotFoundException;
27import android.content.res.Resources;
28import android.database.Cursor;
29import android.database.DatabaseUtils;
30import android.database.SQLException;
31import android.database.sqlite.SQLiteDatabase;
32import android.database.sqlite.SQLiteDoneException;
33import android.database.sqlite.SQLiteException;
34import android.database.sqlite.SQLiteOpenHelper;
35import android.database.sqlite.SQLiteQueryBuilder;
36import android.database.sqlite.SQLiteStatement;
37import android.os.Binder;
38import android.os.Bundle;
39import android.provider.BaseColumns;
40import android.provider.ContactsContract;
41import android.provider.CallLog.Calls;
42import android.provider.ContactsContract.AggregationExceptions;
43import android.provider.ContactsContract.Contacts;
44import android.provider.ContactsContract.Data;
45import android.provider.ContactsContract.Groups;
46import android.provider.ContactsContract.RawContacts;
47import android.provider.ContactsContract.Settings;
48import android.provider.ContactsContract.StatusUpdates;
49import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
50import android.provider.ContactsContract.CommonDataKinds.Phone;
51import android.provider.SocialContract.Activities;
52import android.telephony.PhoneNumberUtils;
53import android.text.TextUtils;
54import android.util.Log;
55
56import java.util.HashMap;
57
58/**
59 * Database helper for contacts. Designed as a singleton to make sure that all
60 * {@link android.content.ContentProvider} users get the same reference.
61 * Provides handy methods for maintaining package and mime-type lookup tables.
62 */
63/* package */ class ContactsDatabaseHelper extends SQLiteOpenHelper {
64    private static final String TAG = "ContactsDatabaseHelper";
65
66    private static final int DATABASE_VERSION = 203;
67
68    private static final String DATABASE_NAME = "contacts2.db";
69    private static final String DATABASE_PRESENCE = "presence_db";
70
71    /** size of the compiled-sql statement cache mainatained by {@link SQLiteDatabase} */
72    private static final int MAX_CACHE_SIZE_FOR_CONTACTS_DB = 250;
73
74    public interface Tables {
75        public static final String CONTACTS = "contacts";
76        public static final String RAW_CONTACTS = "raw_contacts";
77        public static final String PACKAGES = "packages";
78        public static final String MIMETYPES = "mimetypes";
79        public static final String PHONE_LOOKUP = "phone_lookup";
80        public static final String NAME_LOOKUP = "name_lookup";
81        public static final String AGGREGATION_EXCEPTIONS = "agg_exceptions";
82        public static final String SETTINGS = "settings";
83        public static final String DATA = "data";
84        public static final String GROUPS = "groups";
85        public static final String PRESENCE = "presence";
86        public static final String AGGREGATED_PRESENCE = "agg_presence";
87        public static final String NICKNAME_LOOKUP = "nickname_lookup";
88        public static final String CALLS = "calls";
89        public static final String CONTACT_ENTITIES = "contact_entities_view";
90        public static final String CONTACT_ENTITIES_RESTRICTED = "contact_entities_view_restricted";
91        public static final String STATUS_UPDATES = "status_updates";
92
93        public static final String DATA_JOIN_MIMETYPES = "data "
94                + "JOIN mimetypes ON (data.mimetype_id = mimetypes._id)";
95
96        public static final String DATA_JOIN_RAW_CONTACTS = "data "
97                + "JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id)";
98
99        public static final String DATA_JOIN_MIMETYPE_RAW_CONTACTS = "data "
100                + "JOIN mimetypes ON (data.mimetype_id = mimetypes._id) "
101                + "JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id)";
102
103        public static final String DATA_JOIN_RAW_CONTACTS_GROUPS = "data "
104                + "JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id)"
105                + "LEFT OUTER JOIN groups ON (groups._id = data." + GroupMembership.GROUP_ROW_ID
106                + ")";
107
108        public static final String DATA_JOIN_PACKAGES_MIMETYPES_RAW_CONTACTS = "data "
109                + "JOIN mimetypes ON (data.mimetype_id = mimetypes._id) "
110                + "JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id) "
111                + "LEFT OUTER JOIN packages ON (data.package_id = packages._id)";
112
113        public static final String DATA_JOIN_PACKAGES_MIMETYPES_RAW_CONTACTS_CONTACTS = "data "
114                + "JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id) "
115                + "JOIN mimetypes ON (data.mimetype_id = mimetypes._id) "
116                + "LEFT OUTER JOIN packages ON (data.package_id = packages._id) "
117                + "LEFT OUTER JOIN contacts ON (raw_contacts.contact_id = contacts._id)";
118
119        public static final String RAW_CONTACTS_JOIN_CONTACTS = "raw_contacts "
120                + "LEFT OUTER JOIN contacts ON (raw_contacts.contact_id = contacts._id)";
121
122        // NOTE: This requires late binding of GroupMembership MIME-type
123        public static final String RAW_CONTACTS_JOIN_SETTINGS_DATA_GROUPS = "raw_contacts "
124                + "LEFT OUTER JOIN settings ON ("
125                    + "raw_contacts.account_name = settings.account_name AND "
126                    + "raw_contacts.account_type = settings.account_type) "
127                + "LEFT OUTER JOIN data ON (data.mimetype_id=? AND "
128                    + "data.raw_contact_id = raw_contacts._id) "
129                + "LEFT OUTER JOIN groups ON (groups._id = data." + GroupMembership.GROUP_ROW_ID
130                + ")";
131
132        // NOTE: This requires late binding of GroupMembership MIME-type
133        public static final String SETTINGS_JOIN_RAW_CONTACTS_DATA_MIMETYPES_CONTACTS = "settings "
134                + "LEFT OUTER JOIN raw_contacts ON ("
135                    + "raw_contacts.account_name = settings.account_name AND "
136                    + "raw_contacts.account_type = settings.account_type) "
137                + "LEFT OUTER JOIN data ON (data.mimetype_id=? AND "
138                    + "data.raw_contact_id = raw_contacts._id) "
139                + "LEFT OUTER JOIN contacts ON (raw_contacts.contact_id = contacts._id)";
140
141        public static final String DATA_JOIN_MIMETYPES_RAW_CONTACTS_CONTACTS = "data "
142                + "JOIN mimetypes ON (data.mimetype_id = mimetypes._id) "
143                + "JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id) "
144                + "LEFT OUTER JOIN contacts ON (raw_contacts.contact_id = contacts._id)";
145
146        public static final String DATA_INNER_JOIN_MIMETYPES_RAW_CONTACTS_CONTACTS = "data "
147                + "JOIN mimetypes ON (data.mimetype_id = mimetypes._id) "
148                + "JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id) "
149                + "JOIN contacts ON (raw_contacts.contact_id = contacts._id)";
150
151        public static final String DATA_JOIN_PACKAGES_MIMETYPES_RAW_CONTACTS_CONTACTS_GROUPS =
152                "data "
153                + "JOIN mimetypes ON (data.mimetype_id = mimetypes._id) "
154                + "JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id) "
155                + "LEFT OUTER JOIN packages ON (data.package_id = packages._id) "
156                + "LEFT OUTER JOIN groups "
157                + "  ON (mimetypes.mimetype='" + GroupMembership.CONTENT_ITEM_TYPE + "' "
158                + "      AND groups._id = data." + GroupMembership.GROUP_ROW_ID + ") "
159                + "LEFT OUTER JOIN contacts ON (raw_contacts.contact_id = contacts._id)";
160
161        public static final String DATA_JOIN_PACKAGES_MIMETYPES_RAW_CONTACTS_GROUPS = "data "
162                + "JOIN mimetypes ON (data.mimetype_id = mimetypes._id) "
163                + "JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id) "
164                + "LEFT OUTER JOIN packages ON (data.package_id = packages._id) "
165                + "LEFT OUTER JOIN groups "
166                + "  ON (mimetypes.mimetype='" + GroupMembership.CONTENT_ITEM_TYPE + "' "
167                + "      AND groups._id = data." + GroupMembership.GROUP_ROW_ID + ") ";
168
169        public static final String GROUPS_JOIN_PACKAGES = "groups "
170                + "LEFT OUTER JOIN packages ON (groups.package_id = packages._id)";
171
172        public static final String GROUPS_JOIN_PACKAGES_DATA_RAW_CONTACTS_CONTACTS = "groups "
173                + "LEFT OUTER JOIN packages ON (groups.package_id = packages._id) "
174                + "LEFT OUTER JOIN data "
175                + "  ON (groups._id = data." + GroupMembership.GROUP_ROW_ID + ") "
176                + "LEFT OUTER JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id) "
177                + "LEFT OUTER JOIN contacts ON (raw_contacts.contact_id = contacts._id)";
178
179        public static final String ACTIVITIES = "activities";
180
181        public static final String ACTIVITIES_JOIN_MIMETYPES = "activities "
182                + "LEFT OUTER JOIN mimetypes ON (activities.mimetype_id = mimetypes._id)";
183
184        public static final String ACTIVITIES_JOIN_PACKAGES_MIMETYPES_RAW_CONTACTS_CONTACTS =
185                "activities "
186                + "LEFT OUTER JOIN packages ON (activities.package_id = packages._id) "
187                + "LEFT OUTER JOIN mimetypes ON (activities.mimetype_id = mimetypes._id) "
188                + "LEFT OUTER JOIN raw_contacts ON (activities.author_contact_id = " +
189                        "raw_contacts._id) "
190                + "LEFT OUTER JOIN contacts ON (raw_contacts.contact_id = contacts._id)";
191
192        public static final String NAME_LOOKUP_JOIN_RAW_CONTACTS = "name_lookup "
193                + "INNER JOIN raw_contacts ON (name_lookup.raw_contact_id = raw_contacts._id)";
194    }
195
196    public interface Views {
197        public static final String DATA_ALL = "view_data";
198        public static final String DATA_RESTRICTED = "view_data_restricted";
199
200        public static final String RAW_CONTACTS_ALL = "view_raw_contacts";
201        public static final String RAW_CONTACTS_RESTRICTED = "view_raw_contacts_restricted";
202
203        public static final String CONTACTS_ALL = "view_contacts";
204        public static final String CONTACTS_RESTRICTED = "view_contacts_restricted";
205
206        public static final String GROUPS_ALL = "view_groups";
207    }
208
209    public interface Clauses {
210        final String MIMETYPE_IS_GROUP_MEMBERSHIP = MimetypesColumns.CONCRETE_MIMETYPE + "='"
211                + GroupMembership.CONTENT_ITEM_TYPE + "'";
212
213        final String BELONGS_TO_GROUP = DataColumns.CONCRETE_GROUP_ID + "="
214                + GroupsColumns.CONCRETE_ID;
215
216        final String HAVING_NO_GROUPS = "COUNT(" + DataColumns.CONCRETE_GROUP_ID + ") == 0";
217
218        final String GROUP_BY_ACCOUNT_CONTACT_ID = SettingsColumns.CONCRETE_ACCOUNT_NAME + ","
219                + SettingsColumns.CONCRETE_ACCOUNT_TYPE + "," + RawContacts.CONTACT_ID;
220
221        final String RAW_CONTACT_IS_LOCAL = RawContactsColumns.CONCRETE_ACCOUNT_NAME
222                + " IS NULL AND " + RawContactsColumns.CONCRETE_ACCOUNT_TYPE + " IS NULL";
223
224        final String ZERO_GROUP_MEMBERSHIPS = "COUNT(" + GroupsColumns.CONCRETE_ID + ")=0";
225
226        final String OUTER_RAW_CONTACTS = "outer_raw_contacts";
227        final String OUTER_RAW_CONTACTS_ID = OUTER_RAW_CONTACTS + "." + RawContacts._ID;
228
229        final String CONTACT_IS_VISIBLE =
230                "SELECT " +
231                    "MAX((SELECT (CASE WHEN " +
232                        "(CASE" +
233                            " WHEN " + RAW_CONTACT_IS_LOCAL +
234                            " THEN 1 " +
235                            " WHEN " + ZERO_GROUP_MEMBERSHIPS +
236                            " THEN " + Settings.UNGROUPED_VISIBLE +
237                            " ELSE MAX(" + Groups.GROUP_VISIBLE + ")" +
238                         "END)=1 THEN 1 ELSE 0 END)" +
239                " FROM " + Tables.RAW_CONTACTS_JOIN_SETTINGS_DATA_GROUPS +
240                " WHERE " + RawContactsColumns.CONCRETE_ID + "=" + OUTER_RAW_CONTACTS_ID + "))" +
241                " FROM " + Tables.RAW_CONTACTS + " AS " + OUTER_RAW_CONTACTS +
242                " WHERE " + RawContacts.CONTACT_ID + "=" + ContactsColumns.CONCRETE_ID +
243                " GROUP BY " + RawContacts.CONTACT_ID;
244
245        final String GROUP_HAS_ACCOUNT_AND_SOURCE_ID = Groups.SOURCE_ID + "=? AND "
246                + Groups.ACCOUNT_NAME + "=? AND " + Groups.ACCOUNT_TYPE + "=?";
247    }
248
249    public interface ContactsColumns {
250        /**
251         * This flag is set for a contact if it has only one constituent raw contact and
252         * it is restricted.
253         */
254        public static final String SINGLE_IS_RESTRICTED = "single_is_restricted";
255
256        public static final String LAST_STATUS_UPDATE_ID = "status_update_id";
257
258        public static final String CONCRETE_ID = Tables.CONTACTS + "." + BaseColumns._ID;
259
260        public static final String CONCRETE_TIMES_CONTACTED = Tables.CONTACTS + "."
261                + Contacts.TIMES_CONTACTED;
262        public static final String CONCRETE_LAST_TIME_CONTACTED = Tables.CONTACTS + "."
263                + Contacts.LAST_TIME_CONTACTED;
264        public static final String CONCRETE_STARRED = Tables.CONTACTS + "." + Contacts.STARRED;
265        public static final String CONCRETE_CUSTOM_RINGTONE = Tables.CONTACTS + "."
266                + Contacts.CUSTOM_RINGTONE;
267        public static final String CONCRETE_SEND_TO_VOICEMAIL = Tables.CONTACTS + "."
268                + Contacts.SEND_TO_VOICEMAIL;
269    }
270
271    public interface RawContactsColumns {
272        public static final String CONCRETE_ID =
273                Tables.RAW_CONTACTS + "." + BaseColumns._ID;
274        public static final String CONCRETE_ACCOUNT_NAME =
275                Tables.RAW_CONTACTS + "." + RawContacts.ACCOUNT_NAME;
276        public static final String CONCRETE_ACCOUNT_TYPE =
277                Tables.RAW_CONTACTS + "." + RawContacts.ACCOUNT_TYPE;
278        public static final String CONCRETE_SOURCE_ID =
279                Tables.RAW_CONTACTS + "." + RawContacts.SOURCE_ID;
280        public static final String CONCRETE_VERSION =
281                Tables.RAW_CONTACTS + "." + RawContacts.VERSION;
282        public static final String CONCRETE_DIRTY =
283                Tables.RAW_CONTACTS + "." + RawContacts.DIRTY;
284        public static final String CONCRETE_DELETED =
285                Tables.RAW_CONTACTS + "." + RawContacts.DELETED;
286        public static final String CONCRETE_SYNC1 =
287                Tables.RAW_CONTACTS + "." + RawContacts.SYNC1;
288        public static final String CONCRETE_SYNC2 =
289                Tables.RAW_CONTACTS + "." + RawContacts.SYNC2;
290        public static final String CONCRETE_SYNC3 =
291                Tables.RAW_CONTACTS + "." + RawContacts.SYNC3;
292        public static final String CONCRETE_SYNC4 =
293                Tables.RAW_CONTACTS + "." + RawContacts.SYNC4;
294        public static final String CONCRETE_STARRED =
295                Tables.RAW_CONTACTS + "." + RawContacts.STARRED;
296        public static final String CONCRETE_IS_RESTRICTED =
297                Tables.RAW_CONTACTS + "." + RawContacts.IS_RESTRICTED;
298
299        public static final String DISPLAY_NAME = "display_name";
300        public static final String DISPLAY_NAME_SOURCE = "display_name_source";
301        public static final String AGGREGATION_NEEDED = "aggregation_needed";
302        public static final String CONTACT_IN_VISIBLE_GROUP = "contact_in_visible_group";
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    }
309
310    /**
311     * Types of data used to produce the display name for a contact. Listed in the order
312     * of increasing priority.
313     */
314    public interface DisplayNameSources {
315        int UNDEFINED = 0;
316        int EMAIL = 10;
317        int PHONE = 20;
318        int ORGANIZATION = 30;
319        int NICKNAME = 35;
320        int STRUCTURED_NAME = 40;
321    }
322
323    public interface DataColumns {
324        public static final String PACKAGE_ID = "package_id";
325        public static final String MIMETYPE_ID = "mimetype_id";
326
327        public static final String CONCRETE_ID = Tables.DATA + "." + BaseColumns._ID;
328        public static final String CONCRETE_MIMETYPE_ID = Tables.DATA + "." + MIMETYPE_ID;
329        public static final String CONCRETE_RAW_CONTACT_ID = Tables.DATA + "."
330                + Data.RAW_CONTACT_ID;
331        public static final String CONCRETE_GROUP_ID = Tables.DATA + "."
332                + GroupMembership.GROUP_ROW_ID;
333
334        public static final String CONCRETE_DATA1 = Tables.DATA + "." + Data.DATA1;
335        public static final String CONCRETE_DATA2 = Tables.DATA + "." + Data.DATA2;
336        public static final String CONCRETE_DATA3 = Tables.DATA + "." + Data.DATA3;
337        public static final String CONCRETE_DATA4 = Tables.DATA + "." + Data.DATA4;
338        public static final String CONCRETE_DATA5 = Tables.DATA + "." + Data.DATA5;
339        public static final String CONCRETE_DATA6 = Tables.DATA + "." + Data.DATA6;
340        public static final String CONCRETE_DATA7 = Tables.DATA + "." + Data.DATA7;
341        public static final String CONCRETE_DATA8 = Tables.DATA + "." + Data.DATA8;
342        public static final String CONCRETE_DATA9 = Tables.DATA + "." + Data.DATA9;
343        public static final String CONCRETE_DATA10 = Tables.DATA + "." + Data.DATA10;
344        public static final String CONCRETE_DATA11 = Tables.DATA + "." + Data.DATA11;
345        public static final String CONCRETE_DATA12 = Tables.DATA + "." + Data.DATA12;
346        public static final String CONCRETE_DATA13 = Tables.DATA + "." + Data.DATA13;
347        public static final String CONCRETE_DATA14 = Tables.DATA + "." + Data.DATA14;
348        public static final String CONCRETE_DATA15 = Tables.DATA + "." + Data.DATA15;
349        public static final String CONCRETE_IS_PRIMARY = Tables.DATA + "." + Data.IS_PRIMARY;
350        public static final String CONCRETE_PACKAGE_ID = Tables.DATA + "." + PACKAGE_ID;
351    }
352
353    // Used only for legacy API support
354    public interface ExtensionsColumns {
355        public static final String NAME = Data.DATA1;
356        public static final String VALUE = Data.DATA2;
357    }
358
359    public interface GroupMembershipColumns {
360        public static final String RAW_CONTACT_ID = Data.RAW_CONTACT_ID;
361        public static final String GROUP_ROW_ID = GroupMembership.GROUP_ROW_ID;
362    }
363
364    public interface PhoneColumns {
365        public static final String NORMALIZED_NUMBER = Data.DATA4;
366        public static final String CONCRETE_NORMALIZED_NUMBER = DataColumns.CONCRETE_DATA4;
367    }
368
369    public interface GroupsColumns {
370        public static final String PACKAGE_ID = "package_id";
371
372        public static final String CONCRETE_ID = Tables.GROUPS + "." + BaseColumns._ID;
373        public static final String CONCRETE_SOURCE_ID = Tables.GROUPS + "." + Groups.SOURCE_ID;
374        public static final String CONCRETE_ACCOUNT_NAME = Tables.GROUPS + "." + Groups.ACCOUNT_NAME;
375        public static final String CONCRETE_ACCOUNT_TYPE = Tables.GROUPS + "." + Groups.ACCOUNT_TYPE;
376    }
377
378    public interface ActivitiesColumns {
379        public static final String PACKAGE_ID = "package_id";
380        public static final String MIMETYPE_ID = "mimetype_id";
381    }
382
383    public interface PhoneLookupColumns {
384        public static final String _ID = BaseColumns._ID;
385        public static final String DATA_ID = "data_id";
386        public static final String RAW_CONTACT_ID = "raw_contact_id";
387        public static final String NORMALIZED_NUMBER = "normalized_number";
388        public static final String MIN_MATCH = "min_match";
389    }
390
391    public interface NameLookupColumns {
392        public static final String RAW_CONTACT_ID = "raw_contact_id";
393        public static final String DATA_ID = "data_id";
394        public static final String NORMALIZED_NAME = "normalized_name";
395        public static final String NAME_TYPE = "name_type";
396    }
397
398    public final static class NameLookupType {
399        public static final int NAME_EXACT = 0;
400        public static final int NAME_VARIANT = 1;
401        public static final int NAME_COLLATION_KEY = 2;
402        public static final int NICKNAME = 3;
403        public static final int EMAIL_BASED_NICKNAME = 4;
404        public static final int ORGANIZATION = 5;
405
406        // This is the highest name lookup type code plus one
407        public static final int TYPE_COUNT = 6;
408
409        public static boolean isBasedOnStructuredName(int nameLookupType) {
410            return nameLookupType == NameLookupType.NAME_EXACT
411                    || nameLookupType == NameLookupType.NAME_VARIANT
412                    || nameLookupType == NameLookupType.NAME_COLLATION_KEY;
413        }
414    }
415
416    public interface PackagesColumns {
417        public static final String _ID = BaseColumns._ID;
418        public static final String PACKAGE = "package";
419
420        public static final String CONCRETE_ID = Tables.PACKAGES + "." + _ID;
421    }
422
423    public interface MimetypesColumns {
424        public static final String _ID = BaseColumns._ID;
425        public static final String MIMETYPE = "mimetype";
426
427        public static final String CONCRETE_ID = Tables.MIMETYPES + "." + BaseColumns._ID;
428        public static final String CONCRETE_MIMETYPE = Tables.MIMETYPES + "." + MIMETYPE;
429    }
430
431    public interface AggregationExceptionColumns {
432        public static final String _ID = BaseColumns._ID;
433    }
434
435    public interface NicknameLookupColumns {
436        public static final String NAME = "name";
437        public static final String CLUSTER = "cluster";
438    }
439
440    public interface SettingsColumns {
441        public static final String CONCRETE_ACCOUNT_NAME = Tables.SETTINGS + "."
442                + Settings.ACCOUNT_NAME;
443        public static final String CONCRETE_ACCOUNT_TYPE = Tables.SETTINGS + "."
444                + Settings.ACCOUNT_TYPE;
445    }
446
447    public interface PresenceColumns {
448        String RAW_CONTACT_ID = "presence_raw_contact_id";
449        String CONTACT_ID = "presence_contact_id";
450    }
451
452    public interface AggregatedPresenceColumns {
453        String CONTACT_ID = "presence_contact_id";
454
455        String CONCRETE_CONTACT_ID = Tables.AGGREGATED_PRESENCE + "." + CONTACT_ID;
456    }
457
458    public interface StatusUpdatesColumns {
459        String DATA_ID = "status_update_data_id";
460
461        String CONCRETE_DATA_ID = Tables.STATUS_UPDATES + "." + DATA_ID;
462
463        String CONCRETE_PRESENCE = Tables.STATUS_UPDATES + "." + StatusUpdates.PRESENCE;
464        String CONCRETE_STATUS = Tables.STATUS_UPDATES + "." + StatusUpdates.STATUS;
465        String CONCRETE_STATUS_TIMESTAMP = Tables.STATUS_UPDATES + "."
466                + StatusUpdates.STATUS_TIMESTAMP;
467        String CONCRETE_STATUS_RES_PACKAGE = Tables.STATUS_UPDATES + "."
468                + StatusUpdates.STATUS_RES_PACKAGE;
469        String CONCRETE_STATUS_LABEL = Tables.STATUS_UPDATES + "." + StatusUpdates.STATUS_LABEL;
470        String CONCRETE_STATUS_ICON = Tables.STATUS_UPDATES + "." + StatusUpdates.STATUS_ICON;
471    }
472
473    public interface ContactsStatusUpdatesColumns {
474        String ALIAS = "contacts_" + Tables.STATUS_UPDATES;
475
476        String CONCRETE_DATA_ID = ALIAS + "." + StatusUpdatesColumns.DATA_ID;
477
478        String CONCRETE_PRESENCE = ALIAS + "." + StatusUpdates.PRESENCE;
479        String CONCRETE_STATUS = ALIAS + "." + StatusUpdates.STATUS;
480        String CONCRETE_STATUS_TIMESTAMP = ALIAS + "." + StatusUpdates.STATUS_TIMESTAMP;
481        String CONCRETE_STATUS_RES_PACKAGE = ALIAS + "." + StatusUpdates.STATUS_RES_PACKAGE;
482        String CONCRETE_STATUS_LABEL = ALIAS + "." + StatusUpdates.STATUS_LABEL;
483        String CONCRETE_STATUS_ICON = ALIAS + "." + StatusUpdates.STATUS_ICON;
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
492    /** Compiled statements for querying and inserting mappings */
493    private SQLiteStatement mMimetypeQuery;
494    private SQLiteStatement mPackageQuery;
495    private SQLiteStatement mContactIdQuery;
496    private SQLiteStatement mAggregationModeQuery;
497    private SQLiteStatement mMimetypeInsert;
498    private SQLiteStatement mPackageInsert;
499    private SQLiteStatement mDataMimetypeQuery;
500    private SQLiteStatement mActivitiesMimetypeQuery;
501
502    private final Context mContext;
503    private final SyncStateContentProviderHelper mSyncState;
504
505
506    /** Compiled statements for updating {@link Contacts#IN_VISIBLE_GROUP}. */
507    private SQLiteStatement mVisibleUpdate;
508    private SQLiteStatement mVisibleSpecificUpdate;
509    private SQLiteStatement mVisibleUpdateRawContacts;
510    private SQLiteStatement mVisibleSpecificUpdateRawContacts;
511
512    private boolean mReopenDatabase = false;
513
514    private static ContactsDatabaseHelper sSingleton = null;
515
516    private boolean mUseStrictPhoneNumberComparison;
517
518    /**
519     * List of package names with access to {@link RawContacts#IS_RESTRICTED} data.
520     */
521    private String[] mUnrestrictedPackages;
522
523    public static synchronized ContactsDatabaseHelper getInstance(Context context) {
524        if (sSingleton == null) {
525            sSingleton = new ContactsDatabaseHelper(context);
526        }
527        return sSingleton;
528    }
529
530    /**
531     * Private constructor, callers except unit tests should obtain an instance through
532     * {@link #getInstance(android.content.Context)} instead.
533     */
534    ContactsDatabaseHelper(Context context) {
535        super(context, DATABASE_NAME, null, DATABASE_VERSION);
536        if (false) Log.i(TAG, "Creating OpenHelper");
537        Resources resources = context.getResources();
538
539        mContext = context;
540        mSyncState = new SyncStateContentProviderHelper();
541        mUseStrictPhoneNumberComparison =
542                resources.getBoolean(
543                        com.android.internal.R.bool.config_use_strict_phone_number_comparation);
544        int resourceId = resources.getIdentifier("unrestricted_packages", "array",
545                context.getPackageName());
546        if (resourceId != 0) {
547            mUnrestrictedPackages = resources.getStringArray(resourceId);
548        } else {
549            mUnrestrictedPackages = new String[0];
550        }
551    }
552
553    @Override
554    public void onOpen(SQLiteDatabase db) {
555        mSyncState.onDatabaseOpened(db);
556
557        // Create compiled statements for package and mimetype lookups
558        mMimetypeQuery = db.compileStatement("SELECT " + MimetypesColumns._ID + " FROM "
559                + Tables.MIMETYPES + " WHERE " + MimetypesColumns.MIMETYPE + "=?");
560        mPackageQuery = db.compileStatement("SELECT " + PackagesColumns._ID + " FROM "
561                + Tables.PACKAGES + " WHERE " + PackagesColumns.PACKAGE + "=?");
562        mContactIdQuery = db.compileStatement("SELECT " + RawContacts.CONTACT_ID + " FROM "
563                + Tables.RAW_CONTACTS + " WHERE " + RawContacts._ID + "=?");
564        mAggregationModeQuery = db.compileStatement("SELECT " + RawContacts.AGGREGATION_MODE
565                + " FROM " + Tables.RAW_CONTACTS + " WHERE " + RawContacts._ID + "=?");
566        mMimetypeInsert = db.compileStatement("INSERT INTO " + Tables.MIMETYPES + "("
567                + MimetypesColumns.MIMETYPE + ") VALUES (?)");
568        mPackageInsert = db.compileStatement("INSERT INTO " + Tables.PACKAGES + "("
569                + PackagesColumns.PACKAGE + ") VALUES (?)");
570
571        mDataMimetypeQuery = db.compileStatement("SELECT " + MimetypesColumns.MIMETYPE + " FROM "
572                + Tables.DATA_JOIN_MIMETYPES + " WHERE " + Tables.DATA + "." + Data._ID + "=?");
573        mActivitiesMimetypeQuery = db.compileStatement("SELECT " + MimetypesColumns.MIMETYPE
574                + " FROM " + Tables.ACTIVITIES_JOIN_MIMETYPES + " WHERE " + Tables.ACTIVITIES + "."
575                + Activities._ID + "=?");
576
577        // Compile statements for updating visibility
578        final String visibleUpdate = "UPDATE " + Tables.CONTACTS + " SET "
579                + Contacts.IN_VISIBLE_GROUP + "=(" + Clauses.CONTACT_IS_VISIBLE + ")";
580
581        mVisibleUpdate = db.compileStatement(visibleUpdate);
582        mVisibleSpecificUpdate = db.compileStatement(visibleUpdate + " WHERE "
583                + ContactsColumns.CONCRETE_ID + "=?");
584
585        String visibleUpdateRawContacts =
586                "UPDATE " + Tables.RAW_CONTACTS +
587                " SET " + RawContactsColumns.CONTACT_IN_VISIBLE_GROUP + "=(" +
588                        "SELECT " + Contacts.IN_VISIBLE_GROUP +
589                        " FROM " + Tables.CONTACTS +
590                        " WHERE " + Contacts._ID + "=" + RawContacts.CONTACT_ID + ")" +
591                " WHERE " + RawContacts.DELETED + "=0";
592
593        mVisibleUpdateRawContacts = db.compileStatement(visibleUpdateRawContacts);
594        mVisibleSpecificUpdateRawContacts = db.compileStatement(visibleUpdateRawContacts +
595                    " AND " + RawContacts.CONTACT_ID + "=?");
596
597        db.execSQL("ATTACH DATABASE ':memory:' AS " + DATABASE_PRESENCE + ";");
598        db.execSQL("CREATE TABLE IF NOT EXISTS " + DATABASE_PRESENCE + "." + Tables.PRESENCE + " ("+
599                StatusUpdates.DATA_ID + " INTEGER PRIMARY KEY REFERENCES data(_id)," +
600                StatusUpdates.PROTOCOL + " INTEGER NOT NULL," +
601                StatusUpdates.CUSTOM_PROTOCOL + " TEXT," +
602                StatusUpdates.IM_HANDLE + " TEXT," +
603                StatusUpdates.IM_ACCOUNT + " TEXT," +
604                PresenceColumns.CONTACT_ID + " INTEGER REFERENCES contacts(_id)," +
605                PresenceColumns.RAW_CONTACT_ID + " INTEGER REFERENCES raw_contacts(_id)," +
606                StatusUpdates.PRESENCE + " INTEGER," +
607                "UNIQUE(" + StatusUpdates.PROTOCOL + ", " + StatusUpdates.CUSTOM_PROTOCOL
608                    + ", " + StatusUpdates.IM_HANDLE + ", " + StatusUpdates.IM_ACCOUNT + ")" +
609        ");");
610
611        db.execSQL("CREATE INDEX IF NOT EXISTS " + DATABASE_PRESENCE + ".presenceIndex" + " ON "
612                + Tables.PRESENCE + " (" + PresenceColumns.RAW_CONTACT_ID + ");");
613
614        db.execSQL("CREATE TABLE IF NOT EXISTS "
615                        + DATABASE_PRESENCE + "." + Tables.AGGREGATED_PRESENCE + " ("+
616                AggregatedPresenceColumns.CONTACT_ID
617                        + " INTEGER PRIMARY KEY REFERENCES contacts(_id)," +
618                StatusUpdates.PRESENCE_STATUS + " INTEGER" +
619        ");");
620
621
622        db.execSQL("CREATE TRIGGER " + DATABASE_PRESENCE + "." + Tables.PRESENCE + "_deleted"
623                + " BEFORE DELETE ON " + DATABASE_PRESENCE + "." + Tables.PRESENCE
624                + " BEGIN "
625                + "   DELETE FROM " + Tables.AGGREGATED_PRESENCE
626                + "     WHERE " + AggregatedPresenceColumns.CONTACT_ID + " = " +
627                        "(SELECT " + PresenceColumns.CONTACT_ID +
628                        " FROM " + Tables.PRESENCE +
629                        " WHERE " + PresenceColumns.RAW_CONTACT_ID
630                                + "=OLD." + PresenceColumns.RAW_CONTACT_ID +
631                        " AND NOT EXISTS" +
632                                "(SELECT " + PresenceColumns.RAW_CONTACT_ID +
633                                " FROM " + Tables.PRESENCE +
634                                " WHERE " + PresenceColumns.CONTACT_ID
635                                        + "=OLD." + PresenceColumns.CONTACT_ID +
636                                " AND " + PresenceColumns.RAW_CONTACT_ID
637                                        + "!=OLD." + PresenceColumns.RAW_CONTACT_ID + "));"
638                + " END");
639
640        String replaceAggregatePresenceSql =
641            "INSERT OR REPLACE INTO " + Tables.AGGREGATED_PRESENCE + "("
642                    + AggregatedPresenceColumns.CONTACT_ID + ", "
643                    + StatusUpdates.PRESENCE_STATUS + ")" +
644            " SELECT " + PresenceColumns.CONTACT_ID + ","
645                        + "MAX(" + StatusUpdates.PRESENCE_STATUS + ")" +
646                    " FROM " + Tables.PRESENCE +
647                    " WHERE " + PresenceColumns.CONTACT_ID
648                        + "=NEW." + PresenceColumns.CONTACT_ID + ";";
649
650        db.execSQL("CREATE TRIGGER " + DATABASE_PRESENCE + "." + Tables.PRESENCE + "_inserted"
651                + " AFTER INSERT ON " + DATABASE_PRESENCE + "." + Tables.PRESENCE
652                + " BEGIN "
653                + replaceAggregatePresenceSql
654                + " END");
655
656        db.execSQL("CREATE TRIGGER " + DATABASE_PRESENCE + "." + Tables.PRESENCE + "_updated"
657                + " AFTER UPDATE ON " + DATABASE_PRESENCE + "." + Tables.PRESENCE
658                + " BEGIN "
659                + replaceAggregatePresenceSql
660                + " END");
661    }
662
663    @Override
664    public void onCreate(SQLiteDatabase db) {
665        Log.i(TAG, "Bootstrapping database");
666
667        mSyncState.createDatabase(db);
668
669        // One row per group of contacts corresponding to the same person
670        db.execSQL("CREATE TABLE " + Tables.CONTACTS + " (" +
671                BaseColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
672                Contacts.NAME_RAW_CONTACT_ID + " INTEGER REFERENCES raw_contacts(_id)," +
673                Contacts.PHOTO_ID + " INTEGER REFERENCES data(_id)," +
674                Contacts.CUSTOM_RINGTONE + " TEXT," +
675                Contacts.SEND_TO_VOICEMAIL + " INTEGER NOT NULL DEFAULT 0," +
676                Contacts.TIMES_CONTACTED + " INTEGER NOT NULL DEFAULT 0," +
677                Contacts.LAST_TIME_CONTACTED + " INTEGER," +
678                Contacts.STARRED + " INTEGER NOT NULL DEFAULT 0," +
679                Contacts.IN_VISIBLE_GROUP + " INTEGER NOT NULL DEFAULT 1," +
680                Contacts.HAS_PHONE_NUMBER + " INTEGER NOT NULL DEFAULT 0," +
681                Contacts.LOOKUP_KEY + " TEXT," +
682                ContactsColumns.LAST_STATUS_UPDATE_ID + " INTEGER REFERENCES data(_id)," +
683                ContactsColumns.SINGLE_IS_RESTRICTED + " INTEGER NOT NULL DEFAULT 0" +
684        ");");
685
686        db.execSQL("CREATE INDEX contacts_visible_index ON " + Tables.CONTACTS + " (" +
687                Contacts.IN_VISIBLE_GROUP +
688        ");");
689
690        db.execSQL("CREATE INDEX contacts_has_phone_index ON " + Tables.CONTACTS + " (" +
691                Contacts.HAS_PHONE_NUMBER +
692        ");");
693
694        db.execSQL("CREATE INDEX contacts_restricted_index ON " + Tables.CONTACTS + " (" +
695                ContactsColumns.SINGLE_IS_RESTRICTED +
696        ");");
697
698        db.execSQL("CREATE INDEX contacts_name_raw_contact_id_index ON " + Tables.CONTACTS + " (" +
699                Contacts.NAME_RAW_CONTACT_ID +
700        ");");
701
702        // Contacts table
703        db.execSQL("CREATE TABLE " + Tables.RAW_CONTACTS + " (" +
704                RawContacts._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
705                RawContacts.IS_RESTRICTED + " INTEGER DEFAULT 0," +
706                RawContacts.ACCOUNT_NAME + " STRING DEFAULT NULL, " +
707                RawContacts.ACCOUNT_TYPE + " STRING DEFAULT NULL, " +
708                RawContacts.SOURCE_ID + " TEXT," +
709                RawContacts.VERSION + " INTEGER NOT NULL DEFAULT 1," +
710                RawContacts.DIRTY + " INTEGER NOT NULL DEFAULT 0," +
711                RawContacts.DELETED + " INTEGER NOT NULL DEFAULT 0," +
712                RawContacts.CONTACT_ID + " INTEGER REFERENCES contacts(_id)," +
713                RawContacts.AGGREGATION_MODE + " INTEGER NOT NULL DEFAULT " +
714                        RawContacts.AGGREGATION_MODE_DEFAULT + "," +
715                RawContactsColumns.AGGREGATION_NEEDED + " INTEGER NOT NULL DEFAULT 1," +
716                RawContacts.CUSTOM_RINGTONE + " TEXT," +
717                RawContacts.SEND_TO_VOICEMAIL + " INTEGER NOT NULL DEFAULT 0," +
718                RawContacts.TIMES_CONTACTED + " INTEGER NOT NULL DEFAULT 0," +
719                RawContacts.LAST_TIME_CONTACTED + " INTEGER," +
720                RawContacts.STARRED + " INTEGER NOT NULL DEFAULT 0," +
721                RawContactsColumns.DISPLAY_NAME + " TEXT," +
722                RawContactsColumns.DISPLAY_NAME_SOURCE + " INTEGER NOT NULL DEFAULT " +
723                        DisplayNameSources.UNDEFINED + "," +
724                RawContactsColumns.CONTACT_IN_VISIBLE_GROUP + " INTEGER NOT NULL DEFAULT 0," +
725                RawContacts.SYNC1 + " TEXT, " +
726                RawContacts.SYNC2 + " TEXT, " +
727                RawContacts.SYNC3 + " TEXT, " +
728                RawContacts.SYNC4 + " TEXT " +
729        ");");
730
731        db.execSQL("CREATE TRIGGER raw_contacts_times_contacted UPDATE OF " +
732                    RawContacts.LAST_TIME_CONTACTED + " ON " + Tables.RAW_CONTACTS + " " +
733                "BEGIN " +
734                    "UPDATE " + Tables.RAW_CONTACTS + " SET "
735                        + RawContacts.TIMES_CONTACTED + " = " + "" +
736                            "(new." + RawContacts.TIMES_CONTACTED + " + 1)"
737                        + " WHERE _id = new._id;" +
738                "END");
739
740        db.execSQL("CREATE INDEX raw_contacts_contact_id_index ON " + Tables.RAW_CONTACTS + " (" +
741                RawContacts.CONTACT_ID +
742        ");");
743
744        db.execSQL("CREATE INDEX raw_contacts_source_id_index ON " + Tables.RAW_CONTACTS + " (" +
745                RawContacts.SOURCE_ID + ", " +
746                RawContacts.ACCOUNT_TYPE + ", " +
747                RawContacts.ACCOUNT_NAME +
748        ");");
749
750        // TODO readd the index and investigate a controlled use of it
751//        db.execSQL("CREATE INDEX raw_contacts_agg_index ON " + Tables.RAW_CONTACTS + " (" +
752//                RawContactsColumns.AGGREGATION_NEEDED +
753//        ");");
754
755        db.execSQL("CREATE INDEX raw_contact_sort_key1_index ON " + Tables.RAW_CONTACTS + " (" +
756                RawContactsColumns.CONTACT_IN_VISIBLE_GROUP + "," +
757                RawContactsColumns.DISPLAY_NAME + " COLLATE LOCALIZED ASC" +
758        ");");
759
760        // Package name mapping table
761        db.execSQL("CREATE TABLE " + Tables.PACKAGES + " (" +
762                PackagesColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
763                PackagesColumns.PACKAGE + " TEXT NOT NULL" +
764        ");");
765
766        // Mimetype mapping table
767        db.execSQL("CREATE TABLE " + Tables.MIMETYPES + " (" +
768                MimetypesColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
769                MimetypesColumns.MIMETYPE + " TEXT NOT NULL" +
770        ");");
771
772        // Public generic data table
773        db.execSQL("CREATE TABLE " + Tables.DATA + " (" +
774                Data._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
775                DataColumns.PACKAGE_ID + " INTEGER REFERENCES package(_id)," +
776                DataColumns.MIMETYPE_ID + " INTEGER REFERENCES mimetype(_id) NOT NULL," +
777                Data.RAW_CONTACT_ID + " INTEGER REFERENCES raw_contacts(_id) NOT NULL," +
778                Data.IS_PRIMARY + " INTEGER NOT NULL DEFAULT 0," +
779                Data.IS_SUPER_PRIMARY + " INTEGER NOT NULL DEFAULT 0," +
780                Data.DATA_VERSION + " INTEGER NOT NULL DEFAULT 0," +
781                Data.DATA1 + " TEXT," +
782                Data.DATA2 + " TEXT," +
783                Data.DATA3 + " TEXT," +
784                Data.DATA4 + " TEXT," +
785                Data.DATA5 + " TEXT," +
786                Data.DATA6 + " TEXT," +
787                Data.DATA7 + " TEXT," +
788                Data.DATA8 + " TEXT," +
789                Data.DATA9 + " TEXT," +
790                Data.DATA10 + " TEXT," +
791                Data.DATA11 + " TEXT," +
792                Data.DATA12 + " TEXT," +
793                Data.DATA13 + " TEXT," +
794                Data.DATA14 + " TEXT," +
795                Data.DATA15 + " TEXT," +
796                Data.SYNC1 + " TEXT, " +
797                Data.SYNC2 + " TEXT, " +
798                Data.SYNC3 + " TEXT, " +
799                Data.SYNC4 + " TEXT " +
800        ");");
801
802        db.execSQL("CREATE INDEX data_raw_contact_id ON " + Tables.DATA + " (" +
803                Data.RAW_CONTACT_ID +
804        ");");
805
806        /**
807         * For email lookup and similar queries.
808         */
809        db.execSQL("CREATE INDEX data_mimetype_data1_index ON " + Tables.DATA + " (" +
810                DataColumns.MIMETYPE_ID + "," +
811                Data.DATA1 +
812        ");");
813
814        // Private phone numbers table used for lookup
815        db.execSQL("CREATE TABLE " + Tables.PHONE_LOOKUP + " (" +
816                PhoneLookupColumns.DATA_ID
817                        + " INTEGER PRIMARY KEY REFERENCES data(_id) NOT NULL," +
818                PhoneLookupColumns.RAW_CONTACT_ID
819                        + " INTEGER REFERENCES raw_contacts(_id) NOT NULL," +
820                PhoneLookupColumns.NORMALIZED_NUMBER + " TEXT NOT NULL," +
821                PhoneLookupColumns.MIN_MATCH + " TEXT NOT NULL" +
822        ");");
823
824        db.execSQL("CREATE INDEX phone_lookup_index ON " + Tables.PHONE_LOOKUP + " (" +
825                PhoneLookupColumns.NORMALIZED_NUMBER + "," +
826                PhoneLookupColumns.RAW_CONTACT_ID + "," +
827                PhoneLookupColumns.DATA_ID +
828        ");");
829
830        db.execSQL("CREATE INDEX phone_lookup_min_match_index ON " + Tables.PHONE_LOOKUP + " (" +
831                PhoneLookupColumns.MIN_MATCH + "," +
832                PhoneLookupColumns.RAW_CONTACT_ID + "," +
833                PhoneLookupColumns.DATA_ID +
834        ");");
835
836        // Private name/nickname table used for lookup
837        db.execSQL("CREATE TABLE " + Tables.NAME_LOOKUP + " (" +
838                NameLookupColumns.DATA_ID
839                        + " INTEGER REFERENCES data(_id) NOT NULL," +
840                NameLookupColumns.RAW_CONTACT_ID
841                        + " INTEGER REFERENCES raw_contacts(_id) NOT NULL," +
842                NameLookupColumns.NORMALIZED_NAME + " TEXT NOT NULL," +
843                NameLookupColumns.NAME_TYPE + " INTEGER NOT NULL," +
844                "PRIMARY KEY ("
845                        + NameLookupColumns.DATA_ID + ", "
846                        + NameLookupColumns.NORMALIZED_NAME + ", "
847                        + NameLookupColumns.NAME_TYPE + ")" +
848        ");");
849
850        db.execSQL("CREATE INDEX name_lookup_index ON " + Tables.NAME_LOOKUP + " (" +
851                NameLookupColumns.NORMALIZED_NAME + "," +
852                NameLookupColumns.NAME_TYPE + ", " +
853                NameLookupColumns.RAW_CONTACT_ID +
854        ");");
855
856        db.execSQL("CREATE INDEX name_lookup_raw_contact_id_index ON " + Tables.NAME_LOOKUP + " (" +
857                NameLookupColumns.RAW_CONTACT_ID +
858        ");");
859
860        db.execSQL("CREATE TABLE " + Tables.NICKNAME_LOOKUP + " (" +
861                NicknameLookupColumns.NAME + " TEXT," +
862                NicknameLookupColumns.CLUSTER + " TEXT" +
863        ");");
864
865        db.execSQL("CREATE UNIQUE INDEX nickname_lookup_index ON " + Tables.NICKNAME_LOOKUP + " (" +
866                NicknameLookupColumns.NAME + ", " +
867                NicknameLookupColumns.CLUSTER +
868        ");");
869
870        // Groups table
871        db.execSQL("CREATE TABLE " + Tables.GROUPS + " (" +
872                Groups._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
873                GroupsColumns.PACKAGE_ID + " INTEGER REFERENCES package(_id)," +
874                Groups.ACCOUNT_NAME + " STRING DEFAULT NULL, " +
875                Groups.ACCOUNT_TYPE + " STRING DEFAULT NULL, " +
876                Groups.SOURCE_ID + " TEXT," +
877                Groups.VERSION + " INTEGER NOT NULL DEFAULT 1," +
878                Groups.DIRTY + " INTEGER NOT NULL DEFAULT 0," +
879                Groups.TITLE + " TEXT," +
880                Groups.TITLE_RES + " INTEGER," +
881                Groups.NOTES + " TEXT," +
882                Groups.SYSTEM_ID + " TEXT," +
883                Groups.DELETED + " INTEGER NOT NULL DEFAULT 0," +
884                Groups.GROUP_VISIBLE + " INTEGER NOT NULL DEFAULT 0," +
885                Groups.SHOULD_SYNC + " INTEGER NOT NULL DEFAULT 1," +
886                Groups.SYNC1 + " TEXT, " +
887                Groups.SYNC2 + " TEXT, " +
888                Groups.SYNC3 + " TEXT, " +
889                Groups.SYNC4 + " TEXT " +
890        ");");
891
892        db.execSQL("CREATE INDEX groups_source_id_index ON " + Tables.GROUPS + " (" +
893                Groups.SOURCE_ID + ", " +
894                Groups.ACCOUNT_TYPE + ", " +
895                Groups.ACCOUNT_NAME +
896        ");");
897
898        db.execSQL("CREATE TABLE IF NOT EXISTS " + Tables.AGGREGATION_EXCEPTIONS + " (" +
899                AggregationExceptionColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
900                AggregationExceptions.TYPE + " INTEGER NOT NULL, " +
901                AggregationExceptions.RAW_CONTACT_ID1
902                        + " INTEGER REFERENCES raw_contacts(_id), " +
903                AggregationExceptions.RAW_CONTACT_ID2
904                        + " INTEGER REFERENCES raw_contacts(_id)" +
905        ");");
906
907        db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS aggregation_exception_index1 ON " +
908                Tables.AGGREGATION_EXCEPTIONS + " (" +
909                AggregationExceptions.RAW_CONTACT_ID1 + ", " +
910                AggregationExceptions.RAW_CONTACT_ID2 +
911        ");");
912
913        db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS aggregation_exception_index2 ON " +
914                Tables.AGGREGATION_EXCEPTIONS + " (" +
915                AggregationExceptions.RAW_CONTACT_ID2 + ", " +
916                AggregationExceptions.RAW_CONTACT_ID1 +
917        ");");
918
919        db.execSQL("CREATE TABLE IF NOT EXISTS " + Tables.SETTINGS + " (" +
920                Settings.ACCOUNT_NAME + " STRING NOT NULL," +
921                Settings.ACCOUNT_TYPE + " STRING NOT NULL," +
922                Settings.UNGROUPED_VISIBLE + " INTEGER NOT NULL DEFAULT 0," +
923                Settings.SHOULD_SYNC + " INTEGER NOT NULL DEFAULT 1, " +
924                "PRIMARY KEY (" + Settings.ACCOUNT_NAME + ", " +
925                    Settings.ACCOUNT_TYPE + ") ON CONFLICT REPLACE" +
926        ");");
927
928        // The table for recent calls is here so we can do table joins
929        // on people, phones, and calls all in one place.
930        db.execSQL("CREATE TABLE " + Tables.CALLS + " (" +
931                Calls._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
932                Calls.NUMBER + " TEXT," +
933                Calls.DATE + " INTEGER," +
934                Calls.DURATION + " INTEGER," +
935                Calls.TYPE + " INTEGER," +
936                Calls.NEW + " INTEGER," +
937                Calls.CACHED_NAME + " TEXT," +
938                Calls.CACHED_NUMBER_TYPE + " INTEGER," +
939                Calls.CACHED_NUMBER_LABEL + " TEXT" +
940        ");");
941
942        // Activities table
943        db.execSQL("CREATE TABLE " + Tables.ACTIVITIES + " (" +
944                Activities._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
945                ActivitiesColumns.PACKAGE_ID + " INTEGER REFERENCES package(_id)," +
946                ActivitiesColumns.MIMETYPE_ID + " INTEGER REFERENCES mimetype(_id) NOT NULL," +
947                Activities.RAW_ID + " TEXT," +
948                Activities.IN_REPLY_TO + " TEXT," +
949                Activities.AUTHOR_CONTACT_ID +  " INTEGER REFERENCES raw_contacts(_id)," +
950                Activities.TARGET_CONTACT_ID + " INTEGER REFERENCES raw_contacts(_id)," +
951                Activities.PUBLISHED + " INTEGER NOT NULL," +
952                Activities.THREAD_PUBLISHED + " INTEGER NOT NULL," +
953                Activities.TITLE + " TEXT NOT NULL," +
954                Activities.SUMMARY + " TEXT," +
955                Activities.LINK + " TEXT, " +
956                Activities.THUMBNAIL + " BLOB" +
957        ");");
958
959        db.execSQL("CREATE TABLE " + Tables.STATUS_UPDATES + " (" +
960                StatusUpdatesColumns.DATA_ID + " INTEGER PRIMARY KEY REFERENCES data(_id)," +
961                StatusUpdates.STATUS + " TEXT," +
962                StatusUpdates.STATUS_TIMESTAMP + " INTEGER," +
963                StatusUpdates.STATUS_RES_PACKAGE + " TEXT, " +
964                StatusUpdates.STATUS_LABEL + " INTEGER, " +
965                StatusUpdates.STATUS_ICON + " INTEGER" +
966        ");");
967
968        createContactsViews(db);
969        createGroupsView(db);
970        createContactEntitiesView(db);
971        createContactsTriggers(db);
972
973        loadNicknameLookupTable(db);
974
975        // Add the legacy API support views, etc
976        LegacyApiSupport.createDatabase(db);
977
978        // This will create a sqlite_stat1 table that is used for query optimization
979        db.execSQL("ANALYZE;");
980
981        updateSqliteStats(db);
982
983        // We need to close and reopen the database connection so that the stats are
984        // taken into account. Make a note of it and do the actual reopening in the
985        // getWritableDatabase method.
986        mReopenDatabase = true;
987
988        ContentResolver.requestSync(null /* all accounts */,
989                ContactsContract.AUTHORITY, new Bundle());
990    }
991
992    private void createContactsTriggers(SQLiteDatabase db) {
993
994        /*
995         * Automatically delete Data rows when a raw contact is deleted.
996         */
997        db.execSQL("DROP TRIGGER IF EXISTS " + Tables.RAW_CONTACTS + "_deleted;");
998        db.execSQL("CREATE TRIGGER " + Tables.RAW_CONTACTS + "_deleted "
999                + "   BEFORE DELETE ON " + Tables.RAW_CONTACTS
1000                + " BEGIN "
1001                + "   DELETE FROM " + Tables.DATA
1002                + "     WHERE " + Data.RAW_CONTACT_ID
1003                                + "=OLD." + RawContacts._ID + ";"
1004                + "   DELETE FROM " + Tables.AGGREGATION_EXCEPTIONS
1005                + "     WHERE " + AggregationExceptions.RAW_CONTACT_ID1
1006                                + "=OLD." + RawContacts._ID
1007                + "        OR " + AggregationExceptions.RAW_CONTACT_ID2
1008                                + "=OLD." + RawContacts._ID + ";"
1009                + "   DELETE FROM " + Tables.CONTACTS
1010                + "     WHERE " + Contacts._ID + "=OLD." + RawContacts.CONTACT_ID
1011                + "       AND (SELECT COUNT(*) FROM " + Tables.RAW_CONTACTS
1012                + "            WHERE " + RawContacts.CONTACT_ID + "=OLD." + RawContacts.CONTACT_ID
1013                + "           )=1;"
1014                + " END");
1015
1016
1017        db.execSQL("DROP TRIGGER IF EXISTS contacts_times_contacted;");
1018        db.execSQL("CREATE TRIGGER contacts_times_contacted UPDATE OF " +
1019                Contacts.LAST_TIME_CONTACTED + " ON " + Tables.CONTACTS + " " +
1020            "BEGIN " +
1021                "UPDATE " + Tables.CONTACTS + " SET "
1022                    + Contacts.TIMES_CONTACTED + " = " + "" +
1023                            "(new." + Contacts.TIMES_CONTACTED + " + 1)"
1024                    + " WHERE _id = new._id;" +
1025            "END");
1026
1027        /*
1028         * Triggers that update {@link RawContacts#VERSION} when the contact is
1029         * marked for deletion or any time a data row is inserted, updated or
1030         * deleted.
1031         */
1032        db.execSQL("DROP TRIGGER IF EXISTS " + Tables.RAW_CONTACTS + "_marked_deleted;");
1033        db.execSQL("CREATE TRIGGER " + Tables.RAW_CONTACTS + "_marked_deleted "
1034                + "   AFTER UPDATE ON " + Tables.RAW_CONTACTS
1035                + " BEGIN "
1036                + "   UPDATE " + Tables.RAW_CONTACTS
1037                + "     SET "
1038                +         RawContacts.VERSION + "=OLD." + RawContacts.VERSION + "+1 "
1039                + "     WHERE " + RawContacts._ID + "=OLD." + RawContacts._ID
1040                + "       AND NEW." + RawContacts.DELETED + "!= OLD." + RawContacts.DELETED + ";"
1041                + " END");
1042
1043        db.execSQL("DROP TRIGGER IF EXISTS " + Tables.DATA + "_updated;");
1044        db.execSQL("CREATE TRIGGER " + Tables.DATA + "_updated AFTER UPDATE ON " + Tables.DATA
1045                + " BEGIN "
1046                + "   UPDATE " + Tables.DATA
1047                + "     SET " + Data.DATA_VERSION + "=OLD." + Data.DATA_VERSION + "+1 "
1048                + "     WHERE " + Data._ID + "=OLD." + Data._ID + ";"
1049                + "   UPDATE " + Tables.RAW_CONTACTS
1050                + "     SET " + RawContacts.VERSION + "=" + RawContacts.VERSION + "+1 "
1051                + "     WHERE " + RawContacts._ID + "=OLD." + Data.RAW_CONTACT_ID + ";"
1052                + " END");
1053
1054        db.execSQL("DROP TRIGGER IF EXISTS " + Tables.DATA + "_deleted;");
1055        db.execSQL("CREATE TRIGGER " + Tables.DATA + "_deleted BEFORE DELETE ON " + Tables.DATA
1056                + " BEGIN "
1057                + "   UPDATE " + Tables.RAW_CONTACTS
1058                + "     SET " + RawContacts.VERSION + "=" + RawContacts.VERSION + "+1 "
1059                + "     WHERE " + RawContacts._ID + "=OLD." + Data.RAW_CONTACT_ID + ";"
1060                + "   DELETE FROM " + Tables.PHONE_LOOKUP
1061                + "     WHERE " + PhoneLookupColumns.DATA_ID + "=OLD." + Data._ID + ";"
1062                + "   DELETE FROM " + Tables.STATUS_UPDATES
1063                + "     WHERE " + StatusUpdatesColumns.DATA_ID + "=OLD." + Data._ID + ";"
1064                + "   DELETE FROM " + Tables.NAME_LOOKUP
1065                + "     WHERE " + NameLookupColumns.DATA_ID + "=OLD." + Data._ID + ";"
1066                + " END");
1067
1068
1069        db.execSQL("DROP TRIGGER IF EXISTS " + Tables.GROUPS + "_updated1;");
1070        db.execSQL("CREATE TRIGGER " + Tables.GROUPS + "_updated1 "
1071                + "   AFTER UPDATE ON " + Tables.GROUPS
1072                + " BEGIN "
1073                + "   UPDATE " + Tables.GROUPS
1074                + "     SET "
1075                +         Groups.VERSION + "=OLD." + Groups.VERSION + "+1"
1076                + "     WHERE " + Groups._ID + "=OLD." + Groups._ID + ";"
1077                + " END");
1078    }
1079
1080    private static void createContactsViews(SQLiteDatabase db) {
1081        db.execSQL("DROP VIEW IF EXISTS " + Views.CONTACTS_ALL + ";");
1082        db.execSQL("DROP VIEW IF EXISTS " + Views.CONTACTS_RESTRICTED + ";");
1083        db.execSQL("DROP VIEW IF EXISTS " + Views.DATA_ALL + ";");
1084        db.execSQL("DROP VIEW IF EXISTS " + Views.DATA_RESTRICTED + ";");
1085        db.execSQL("DROP VIEW IF EXISTS " + Views.RAW_CONTACTS_ALL + ";");
1086        db.execSQL("DROP VIEW IF EXISTS " + Views.RAW_CONTACTS_RESTRICTED + ";");
1087
1088        String dataColumns =
1089                Data.IS_PRIMARY + ", "
1090                + Data.IS_SUPER_PRIMARY + ", "
1091                + Data.DATA_VERSION + ", "
1092                + PackagesColumns.PACKAGE + " AS " + Data.RES_PACKAGE + ","
1093                + MimetypesColumns.MIMETYPE + " AS " + Data.MIMETYPE + ", "
1094                + Data.DATA1 + ", "
1095                + Data.DATA2 + ", "
1096                + Data.DATA3 + ", "
1097                + Data.DATA4 + ", "
1098                + Data.DATA5 + ", "
1099                + Data.DATA6 + ", "
1100                + Data.DATA7 + ", "
1101                + Data.DATA8 + ", "
1102                + Data.DATA9 + ", "
1103                + Data.DATA10 + ", "
1104                + Data.DATA11 + ", "
1105                + Data.DATA12 + ", "
1106                + Data.DATA13 + ", "
1107                + Data.DATA14 + ", "
1108                + Data.DATA15 + ", "
1109                + Data.SYNC1 + ", "
1110                + Data.SYNC2 + ", "
1111                + Data.SYNC3 + ", "
1112                + Data.SYNC4;
1113
1114        String syncColumns =
1115                RawContactsColumns.CONCRETE_ACCOUNT_NAME + " AS " + RawContacts.ACCOUNT_NAME + ","
1116                + RawContactsColumns.CONCRETE_ACCOUNT_TYPE + " AS " + RawContacts.ACCOUNT_TYPE + ","
1117                + RawContactsColumns.CONCRETE_SOURCE_ID + " AS " + RawContacts.SOURCE_ID + ","
1118                + RawContactsColumns.CONCRETE_VERSION + " AS " + RawContacts.VERSION + ","
1119                + RawContactsColumns.CONCRETE_DIRTY + " AS " + RawContacts.DIRTY + ","
1120                + RawContactsColumns.CONCRETE_SYNC1 + " AS " + RawContacts.SYNC1 + ","
1121                + RawContactsColumns.CONCRETE_SYNC2 + " AS " + RawContacts.SYNC2 + ","
1122                + RawContactsColumns.CONCRETE_SYNC3 + " AS " + RawContacts.SYNC3 + ","
1123                + RawContactsColumns.CONCRETE_SYNC4 + " AS " + RawContacts.SYNC4;
1124
1125        String contactOptionColumns =
1126                ContactsColumns.CONCRETE_CUSTOM_RINGTONE
1127                        + " AS " + RawContacts.CUSTOM_RINGTONE + ","
1128                + ContactsColumns.CONCRETE_SEND_TO_VOICEMAIL
1129                        + " AS " + RawContacts.SEND_TO_VOICEMAIL + ","
1130                + ContactsColumns.CONCRETE_LAST_TIME_CONTACTED
1131                        + " AS " + RawContacts.LAST_TIME_CONTACTED + ","
1132                + ContactsColumns.CONCRETE_TIMES_CONTACTED
1133                        + " AS " + RawContacts.TIMES_CONTACTED + ","
1134                + ContactsColumns.CONCRETE_STARRED
1135                        + " AS " + RawContacts.STARRED;
1136
1137        String dataSelect = "SELECT "
1138                + DataColumns.CONCRETE_ID + " AS " + Data._ID + ","
1139                + Data.RAW_CONTACT_ID + ", "
1140                + RawContactsColumns.CONCRETE_CONTACT_ID + " AS " + RawContacts.CONTACT_ID + ", "
1141                + syncColumns + ", "
1142                + dataColumns + ", "
1143                + contactOptionColumns + ", "
1144                + "name_raw_contact." + RawContactsColumns.DISPLAY_NAME
1145                        + " AS " + Contacts.DISPLAY_NAME + ", "
1146                + "name_raw_contact." + RawContactsColumns.CONTACT_IN_VISIBLE_GROUP
1147                        + " AS " + Contacts.IN_VISIBLE_GROUP + ", "
1148                + Contacts.LOOKUP_KEY + ", "
1149                + Contacts.PHOTO_ID + ", "
1150                + ContactsColumns.LAST_STATUS_UPDATE_ID + ", "
1151                + Tables.GROUPS + "." + Groups.SOURCE_ID + " AS " + GroupMembership.GROUP_SOURCE_ID
1152                + " FROM " + Tables.DATA
1153                + " JOIN " + Tables.MIMETYPES + " ON ("
1154                +   DataColumns.CONCRETE_MIMETYPE_ID + "=" + MimetypesColumns.CONCRETE_ID + ")"
1155                + " JOIN " + Tables.RAW_CONTACTS + " ON ("
1156                +   DataColumns.CONCRETE_RAW_CONTACT_ID + "=" + RawContactsColumns.CONCRETE_ID + ")"
1157                + " JOIN " + Tables.CONTACTS + " ON ("
1158                +   RawContactsColumns.CONCRETE_CONTACT_ID + "=" + ContactsColumns.CONCRETE_ID + ")"
1159                + " JOIN " + Tables.RAW_CONTACTS + " AS name_raw_contact ON("
1160                +   Contacts.NAME_RAW_CONTACT_ID + "=name_raw_contact." + RawContacts._ID + ")"
1161                + " LEFT OUTER JOIN " + Tables.PACKAGES + " ON ("
1162                +   DataColumns.CONCRETE_PACKAGE_ID + "=" + PackagesColumns.CONCRETE_ID + ")"
1163                + " LEFT OUTER JOIN " + Tables.GROUPS + " ON ("
1164                +   MimetypesColumns.CONCRETE_MIMETYPE + "='" + GroupMembership.CONTENT_ITEM_TYPE
1165                +   "' AND " + GroupsColumns.CONCRETE_ID + "="
1166                        + Tables.DATA + "." + GroupMembership.GROUP_ROW_ID + ")";
1167
1168        db.execSQL("CREATE VIEW " + Views.DATA_ALL + " AS " + dataSelect);
1169        db.execSQL("CREATE VIEW " + Views.DATA_RESTRICTED + " AS " + dataSelect + " WHERE "
1170                + RawContactsColumns.CONCRETE_IS_RESTRICTED + "=0");
1171
1172        String rawContactOptionColumns =
1173                RawContacts.CUSTOM_RINGTONE + ","
1174                + RawContacts.SEND_TO_VOICEMAIL + ","
1175                + RawContacts.LAST_TIME_CONTACTED + ","
1176                + RawContacts.TIMES_CONTACTED + ","
1177                + RawContacts.STARRED;
1178
1179        String rawContactsSelect = "SELECT "
1180                + RawContactsColumns.CONCRETE_ID + " AS " + RawContacts._ID + ","
1181                + RawContacts.CONTACT_ID + ", "
1182                + RawContacts.AGGREGATION_MODE + ", "
1183                + RawContacts.DELETED + ", "
1184                + rawContactOptionColumns + ", "
1185                + syncColumns
1186                + " FROM " + Tables.RAW_CONTACTS;
1187
1188        db.execSQL("CREATE VIEW " + Views.RAW_CONTACTS_ALL + " AS " + rawContactsSelect);
1189        db.execSQL("CREATE VIEW " + Views.RAW_CONTACTS_RESTRICTED + " AS " + rawContactsSelect
1190                + " WHERE " + RawContacts.IS_RESTRICTED + "=0");
1191
1192        String contactsColumns =
1193                ContactsColumns.CONCRETE_CUSTOM_RINGTONE
1194                        + " AS " + Contacts.CUSTOM_RINGTONE + ", "
1195                + "name_raw_contact." + RawContactsColumns.DISPLAY_NAME
1196                        + " AS " + Contacts.DISPLAY_NAME + ", "
1197                + "name_raw_contact." + RawContactsColumns.CONTACT_IN_VISIBLE_GROUP
1198                        + " AS " + Contacts.IN_VISIBLE_GROUP + ", "
1199                + Contacts.HAS_PHONE_NUMBER + ", "
1200                + Contacts.LOOKUP_KEY + ", "
1201                + Contacts.PHOTO_ID + ", "
1202                + ContactsColumns.CONCRETE_LAST_TIME_CONTACTED
1203                        + " AS " + Contacts.LAST_TIME_CONTACTED + ", "
1204                + ContactsColumns.CONCRETE_SEND_TO_VOICEMAIL
1205                        + " AS " + Contacts.SEND_TO_VOICEMAIL + ", "
1206                + ContactsColumns.CONCRETE_STARRED
1207                        + " AS " + Contacts.STARRED + ", "
1208                + ContactsColumns.CONCRETE_TIMES_CONTACTED
1209                        + " AS " + Contacts.TIMES_CONTACTED + ", "
1210                + ContactsColumns.LAST_STATUS_UPDATE_ID;
1211
1212        String contactsSelect = "SELECT "
1213                + ContactsColumns.CONCRETE_ID + " AS " + Contacts._ID + ","
1214                + contactsColumns
1215                + " FROM " + Tables.CONTACTS
1216                + " JOIN " + Tables.RAW_CONTACTS + " AS name_raw_contact ON("
1217                +   Contacts.NAME_RAW_CONTACT_ID + "=name_raw_contact." + RawContacts._ID + ")";
1218
1219        db.execSQL("CREATE VIEW " + Views.CONTACTS_ALL + " AS " + contactsSelect);
1220        db.execSQL("CREATE VIEW " + Views.CONTACTS_RESTRICTED + " AS " + contactsSelect
1221                + " WHERE " + ContactsColumns.SINGLE_IS_RESTRICTED + "=0");
1222    }
1223
1224    private static void createGroupsView(SQLiteDatabase db) {
1225        db.execSQL("DROP VIEW IF EXISTS " + Views.GROUPS_ALL + ";");
1226        String groupsColumns =
1227                Groups.ACCOUNT_NAME + ","
1228                + Groups.ACCOUNT_TYPE + ","
1229                + Groups.SOURCE_ID + ","
1230                + Groups.VERSION + ","
1231                + Groups.DIRTY + ","
1232                + Groups.TITLE + ","
1233                + Groups.TITLE_RES + ","
1234                + Groups.NOTES + ","
1235                + Groups.SYSTEM_ID + ","
1236                + Groups.DELETED + ","
1237                + Groups.GROUP_VISIBLE + ","
1238                + Groups.SHOULD_SYNC + ","
1239                + Groups.SYNC1 + ","
1240                + Groups.SYNC2 + ","
1241                + Groups.SYNC3 + ","
1242                + Groups.SYNC4 + ","
1243                + PackagesColumns.PACKAGE + " AS " + Groups.RES_PACKAGE;
1244
1245        String groupsSelect = "SELECT "
1246                + GroupsColumns.CONCRETE_ID + " AS " + Groups._ID + ","
1247                + groupsColumns
1248                + " FROM " + Tables.GROUPS_JOIN_PACKAGES;
1249
1250        db.execSQL("CREATE VIEW " + Views.GROUPS_ALL + " AS " + groupsSelect);
1251    }
1252
1253    private static void createContactEntitiesView(SQLiteDatabase db) {
1254        db.execSQL("DROP VIEW IF EXISTS " + Tables.CONTACT_ENTITIES + ";");
1255        db.execSQL("DROP VIEW IF EXISTS " + Tables.CONTACT_ENTITIES_RESTRICTED + ";");
1256
1257        String contactEntitiesSelect = "SELECT "
1258                + RawContactsColumns.CONCRETE_ACCOUNT_NAME + " AS " + RawContacts.ACCOUNT_NAME + ","
1259                + RawContactsColumns.CONCRETE_ACCOUNT_TYPE + " AS " + RawContacts.ACCOUNT_TYPE + ","
1260                + RawContactsColumns.CONCRETE_SOURCE_ID + " AS " + RawContacts.SOURCE_ID + ","
1261                + RawContactsColumns.CONCRETE_VERSION + " AS " + RawContacts.VERSION + ","
1262                + RawContactsColumns.CONCRETE_DIRTY + " AS " + RawContacts.DIRTY + ","
1263                + RawContactsColumns.CONCRETE_DELETED + " AS " + RawContacts.DELETED + ","
1264                + PackagesColumns.PACKAGE + " AS " + Data.RES_PACKAGE + ","
1265                + RawContacts.CONTACT_ID + ", "
1266                + RawContactsColumns.CONCRETE_SYNC1 + " AS " + RawContacts.SYNC1 + ", "
1267                + RawContactsColumns.CONCRETE_SYNC2 + " AS " + RawContacts.SYNC2 + ", "
1268                + RawContactsColumns.CONCRETE_SYNC3 + " AS " + RawContacts.SYNC3 + ", "
1269                + RawContactsColumns.CONCRETE_SYNC4 + " AS " + RawContacts.SYNC4 + ", "
1270                + Data.MIMETYPE + ", "
1271                + Data.DATA1 + ", "
1272                + Data.DATA2 + ", "
1273                + Data.DATA3 + ", "
1274                + Data.DATA4 + ", "
1275                + Data.DATA5 + ", "
1276                + Data.DATA6 + ", "
1277                + Data.DATA7 + ", "
1278                + Data.DATA8 + ", "
1279                + Data.DATA9 + ", "
1280                + Data.DATA10 + ", "
1281                + Data.DATA11 + ", "
1282                + Data.DATA12 + ", "
1283                + Data.DATA13 + ", "
1284                + Data.DATA14 + ", "
1285                + Data.DATA15 + ", "
1286                + Data.SYNC1 + ", "
1287                + Data.SYNC2 + ", "
1288                + Data.SYNC3 + ", "
1289                + Data.SYNC4 + ", "
1290                + RawContactsColumns.CONCRETE_ID + " AS " + RawContacts._ID + ", "
1291                + Data.IS_PRIMARY + ", "
1292                + Data.IS_SUPER_PRIMARY + ", "
1293                + Data.DATA_VERSION + ", "
1294                + DataColumns.CONCRETE_ID + " AS " + RawContacts.Entity.DATA_ID + ","
1295                + RawContactsColumns.CONCRETE_STARRED + " AS " + RawContacts.STARRED + ","
1296                + RawContactsColumns.CONCRETE_IS_RESTRICTED + " AS "
1297                        + RawContacts.IS_RESTRICTED + ","
1298                + Tables.GROUPS + "." + Groups.SOURCE_ID + " AS " + GroupMembership.GROUP_SOURCE_ID
1299                + " FROM " + Tables.RAW_CONTACTS
1300                + " LEFT OUTER JOIN " + Tables.DATA + " ON ("
1301                +   DataColumns.CONCRETE_RAW_CONTACT_ID + "=" + RawContactsColumns.CONCRETE_ID + ")"
1302                + " LEFT OUTER JOIN " + Tables.PACKAGES + " ON ("
1303                +   DataColumns.CONCRETE_PACKAGE_ID + "=" + PackagesColumns.CONCRETE_ID + ")"
1304                + " LEFT OUTER JOIN " + Tables.MIMETYPES + " ON ("
1305                +   DataColumns.CONCRETE_MIMETYPE_ID + "=" + MimetypesColumns.CONCRETE_ID + ")"
1306                + " LEFT OUTER JOIN " + Tables.GROUPS + " ON ("
1307                +   MimetypesColumns.CONCRETE_MIMETYPE + "='" + GroupMembership.CONTENT_ITEM_TYPE
1308                +   "' AND " + GroupsColumns.CONCRETE_ID + "="
1309                + Tables.DATA + "." + GroupMembership.GROUP_ROW_ID + ")";
1310
1311        db.execSQL("CREATE VIEW " + Tables.CONTACT_ENTITIES + " AS "
1312                + contactEntitiesSelect);
1313        db.execSQL("CREATE VIEW " + Tables.CONTACT_ENTITIES_RESTRICTED + " AS "
1314                + contactEntitiesSelect + " WHERE " + RawContacts.IS_RESTRICTED + "=0");
1315    }
1316
1317    @Override
1318    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
1319        if (oldVersion < 99) {
1320            Log.i(TAG, "Upgrading from version " + oldVersion + " to " + newVersion
1321                    + ", data will be lost!");
1322
1323            db.execSQL("DROP TABLE IF EXISTS " + Tables.CONTACTS + ";");
1324            db.execSQL("DROP TABLE IF EXISTS " + Tables.RAW_CONTACTS + ";");
1325            db.execSQL("DROP TABLE IF EXISTS " + Tables.PACKAGES + ";");
1326            db.execSQL("DROP TABLE IF EXISTS " + Tables.MIMETYPES + ";");
1327            db.execSQL("DROP TABLE IF EXISTS " + Tables.DATA + ";");
1328            db.execSQL("DROP TABLE IF EXISTS " + Tables.PHONE_LOOKUP + ";");
1329            db.execSQL("DROP TABLE IF EXISTS " + Tables.NAME_LOOKUP + ";");
1330            db.execSQL("DROP TABLE IF EXISTS " + Tables.NICKNAME_LOOKUP + ";");
1331            db.execSQL("DROP TABLE IF EXISTS " + Tables.GROUPS + ";");
1332            db.execSQL("DROP TABLE IF EXISTS " + Tables.ACTIVITIES + ";");
1333            db.execSQL("DROP TABLE IF EXISTS " + Tables.CALLS + ";");
1334            db.execSQL("DROP TABLE IF EXISTS " + Tables.SETTINGS + ";");
1335            db.execSQL("DROP TABLE IF EXISTS " + Tables.STATUS_UPDATES + ";");
1336
1337            // TODO: we should not be dropping agg_exceptions and contact_options. In case that
1338            // table's schema changes, we should try to preserve the data, because it was entered
1339            // by the user and has never been synched to the server.
1340            db.execSQL("DROP TABLE IF EXISTS " + Tables.AGGREGATION_EXCEPTIONS + ";");
1341
1342            onCreate(db);
1343            return;
1344        }
1345
1346        Log.i(TAG, "Upgrading from version " + oldVersion + " to " + newVersion);
1347
1348        if (oldVersion == 99) {
1349            createContactEntitiesView(db);
1350            oldVersion++;
1351        }
1352
1353        if (oldVersion == 100) {
1354            db.execSQL("CREATE INDEX IF NOT EXISTS mimetypes_mimetype_index ON "
1355                    + Tables.MIMETYPES + " ("
1356                            + MimetypesColumns.MIMETYPE + ","
1357                            + MimetypesColumns._ID + ");");
1358            updateIndexStats(db, Tables.MIMETYPES,
1359                    "mimetypes_mimetype_index", "50 1 1");
1360
1361            createContactsViews(db);
1362            oldVersion++;
1363        }
1364
1365        if (oldVersion == 101) {
1366            createContactsTriggers(db);
1367            oldVersion++;
1368        }
1369
1370        if (oldVersion == 102) {
1371            LegacyApiSupport.createViews(db);
1372            oldVersion++;
1373        }
1374
1375        if (oldVersion == 103) {
1376            createContactEntitiesView(db);
1377            oldVersion++;
1378        }
1379
1380        if (oldVersion == 104 || oldVersion == 201) {
1381            LegacyApiSupport.createViews(db);
1382            LegacyApiSupport.createSettingsTable(db);
1383            oldVersion++;
1384        }
1385
1386        if (oldVersion == 105) {
1387            addColumnPhoneNumberMinMatch(db);
1388            oldVersion = 202;
1389        }
1390
1391        if (oldVersion == 202) {
1392            addNameRawContactIdColumn(db);
1393            createContactsViews(db);
1394            oldVersion++;
1395        }
1396
1397        if (oldVersion != newVersion) {
1398            throw new IllegalStateException(
1399                    "error upgrading the database to version " + newVersion);
1400        }
1401    }
1402
1403    private void addColumnPhoneNumberMinMatch(SQLiteDatabase db) {
1404        db.execSQL(
1405                "ALTER TABLE " + Tables.PHONE_LOOKUP +
1406                " ADD " + PhoneLookupColumns.MIN_MATCH + " TEXT;");
1407
1408        db.execSQL("CREATE INDEX phone_lookup_min_match_index ON " + Tables.PHONE_LOOKUP + " (" +
1409                PhoneLookupColumns.MIN_MATCH + "," +
1410                PhoneLookupColumns.RAW_CONTACT_ID + "," +
1411                PhoneLookupColumns.DATA_ID +
1412        ");");
1413
1414        updateIndexStats(db, Tables.PHONE_LOOKUP,
1415                "phone_lookup_min_match_index", "10000 2 2 1");
1416
1417        SQLiteStatement update = db.compileStatement(
1418                "UPDATE " + Tables.PHONE_LOOKUP +
1419                " SET " + PhoneLookupColumns.MIN_MATCH + "=?" +
1420                " WHERE " + PhoneLookupColumns.DATA_ID + "=?");
1421
1422        // Populate the new column
1423        Cursor c = db.query(Tables.PHONE_LOOKUP + " JOIN " + Tables.DATA +
1424                " ON (" + PhoneLookupColumns.DATA_ID + "=" + DataColumns.CONCRETE_ID + ")",
1425                new String[]{Data._ID, Phone.NUMBER}, null, null, null, null, null);
1426        try {
1427            while (c.moveToNext()) {
1428                long dataId = c.getLong(0);
1429                String number = c.getString(1);
1430                if (!TextUtils.isEmpty(number)) {
1431                    update.bindString(1, PhoneNumberUtils.toCallerIDMinMatch(number));
1432                    update.bindLong(2, dataId);
1433                    update.execute();
1434                }
1435            }
1436        } finally {
1437            c.close();
1438        }
1439    }
1440
1441    private static void addNameRawContactIdColumn(SQLiteDatabase db) {
1442        db.execSQL(
1443                "ALTER TABLE " + Tables.CONTACTS +
1444                " ADD " + Contacts.NAME_RAW_CONTACT_ID + " INTEGER REFERENCES raw_contacts(_id)");
1445        db.execSQL(
1446                "ALTER TABLE " + Tables.RAW_CONTACTS +
1447                " ADD " + RawContactsColumns.CONTACT_IN_VISIBLE_GROUP
1448                        + " INTEGER NOT NULL DEFAULT 0");
1449
1450        // For each Contact, find the RawContact that contributed the display name
1451        db.execSQL(
1452                "UPDATE " + Tables.CONTACTS +
1453                " SET " + Contacts.NAME_RAW_CONTACT_ID + "=(" +
1454                        " SELECT " + RawContacts._ID +
1455                        " FROM " + Tables.RAW_CONTACTS +
1456                        " WHERE " + RawContacts.CONTACT_ID + "=" + ContactsColumns.CONCRETE_ID +
1457                        " AND " + RawContactsColumns.CONCRETE_DISPLAY_NAME + "=" +
1458                                Tables.CONTACTS + "." + Contacts.DISPLAY_NAME +
1459                        " ORDER BY " + RawContacts._ID +
1460                        " LIMIT 1)"
1461        );
1462
1463        db.execSQL("CREATE INDEX contacts_name_raw_contact_id_index ON " + Tables.CONTACTS + " (" +
1464                Contacts.NAME_RAW_CONTACT_ID +
1465        ");");
1466
1467        // If for some unknown reason we missed some names, let's make sure there are
1468        // no contacts without a name, picking a raw contact "at random".
1469        db.execSQL(
1470                "UPDATE " + Tables.CONTACTS +
1471                " SET " + Contacts.NAME_RAW_CONTACT_ID + "=(" +
1472                        " SELECT " + RawContacts._ID +
1473                        " FROM " + Tables.RAW_CONTACTS +
1474                        " WHERE " + RawContacts.CONTACT_ID + "=" + ContactsColumns.CONCRETE_ID +
1475                        " ORDER BY " + RawContacts._ID +
1476                        " LIMIT 1)" +
1477                " WHERE " + Contacts.NAME_RAW_CONTACT_ID + " IS NULL"
1478        );
1479
1480        // Wipe out DISPLAY_NAME on the Contacts table as it is no longer in use.
1481        db.execSQL(
1482                "UPDATE " + Tables.CONTACTS +
1483                " SET " + Contacts.DISPLAY_NAME + "=NULL"
1484        );
1485
1486        // Copy the IN_VISIBLE_GROUP flag down to all raw contacts to allow
1487        // indexing on (display_name, in_visible_group)
1488        db.execSQL(
1489                "UPDATE " + Tables.RAW_CONTACTS +
1490                " SET " + RawContactsColumns.CONTACT_IN_VISIBLE_GROUP + "=(" +
1491                        "SELECT " + Contacts.IN_VISIBLE_GROUP +
1492                        " FROM " + Tables.CONTACTS +
1493                        " WHERE " + Contacts._ID + "=" + RawContacts.CONTACT_ID + ")"
1494        );
1495
1496        db.execSQL("CREATE INDEX raw_contact_sort_key1_index ON " + Tables.RAW_CONTACTS + " (" +
1497                RawContactsColumns.CONTACT_IN_VISIBLE_GROUP + "," +
1498                RawContactsColumns.DISPLAY_NAME + " COLLATE LOCALIZED ASC" +
1499        ");");
1500
1501        db.execSQL("DROP INDEX contacts_visible_index");
1502        db.execSQL("CREATE INDEX contacts_visible_index ON " + Tables.CONTACTS + " (" +
1503                Contacts.IN_VISIBLE_GROUP +
1504        ");");
1505    }
1506
1507    /**
1508     * Adds index stats into the SQLite database to force it to always use the lookup indexes.
1509     */
1510    private void updateSqliteStats(SQLiteDatabase db) {
1511
1512        // Specific stats strings are based on an actual large database after running ANALYZE
1513        try {
1514            updateIndexStats(db, Tables.CONTACTS,
1515                    "contacts_restricted_index", "10000 9000");
1516            updateIndexStats(db, Tables.CONTACTS,
1517                    "contacts_has_phone_index", "10000 500");
1518            updateIndexStats(db, Tables.CONTACTS,
1519                    "contacts_visible_index", "10000 500 1");
1520
1521            updateIndexStats(db, Tables.RAW_CONTACTS,
1522                    "raw_contacts_source_id_index", "10000 1 1 1");
1523            updateIndexStats(db, Tables.RAW_CONTACTS,
1524                    "raw_contacts_contact_id_index", "10000 2");
1525
1526            updateIndexStats(db, Tables.NAME_LOOKUP,
1527                    "name_lookup_raw_contact_id_index", "10000 3");
1528            updateIndexStats(db, Tables.NAME_LOOKUP,
1529                    "name_lookup_index", "10000 3 2 2");
1530            updateIndexStats(db, Tables.NAME_LOOKUP,
1531                    "sqlite_autoindex_name_lookup_1", "10000 3 2 1");
1532
1533            updateIndexStats(db, Tables.PHONE_LOOKUP,
1534                    "phone_lookup_index", "10000 2 2 1");
1535            updateIndexStats(db, Tables.PHONE_LOOKUP,
1536                    "phone_lookup_min_match_index", "10000 2 2 1");
1537
1538            updateIndexStats(db, Tables.DATA,
1539                    "data_mimetype_data1_index", "60000 5000 2");
1540            updateIndexStats(db, Tables.DATA,
1541                    "data_raw_contact_id", "60000 10");
1542
1543            updateIndexStats(db, Tables.GROUPS,
1544                    "groups_source_id_index", "50 1 1 1");
1545
1546            updateIndexStats(db, Tables.NICKNAME_LOOKUP,
1547                    "sqlite_autoindex_name_lookup_1", "500 2 1");
1548
1549        } catch (SQLException e) {
1550            Log.e(TAG, "Could not update index stats", e);
1551        }
1552    }
1553
1554    /**
1555     * Stores statistics for a given index.
1556     *
1557     * @param stats has the following structure: the first index is the expected size of
1558     * the table.  The following integer(s) are the expected number of records selected with the
1559     * index.  There should be one integer per indexed column.
1560     */
1561    private void updateIndexStats(SQLiteDatabase db, String table, String index, String stats) {
1562        db.execSQL("DELETE FROM sqlite_stat1 WHERE tbl='" + table + "' AND idx='" + index + "';");
1563        db.execSQL("INSERT INTO sqlite_stat1 (tbl,idx,stat)"
1564                + " VALUES ('" + table + "','" + index + "','" + stats + "');");
1565    }
1566
1567    @Override
1568    public synchronized SQLiteDatabase getWritableDatabase() {
1569        SQLiteDatabase db = super.getWritableDatabase();
1570        if (mReopenDatabase) {
1571            mReopenDatabase = false;
1572            close();
1573            db = super.getWritableDatabase();
1574        }
1575        // let {@link SQLiteDatabase} cache my compiled-sql statements.
1576        db.setMaxSqlCacheSize(MAX_CACHE_SIZE_FOR_CONTACTS_DB);
1577        return db;
1578    }
1579
1580    /**
1581     * Wipes all data except mime type and package lookup tables.
1582     */
1583    public void wipeData() {
1584        SQLiteDatabase db = getWritableDatabase();
1585
1586        db.execSQL("DELETE FROM " + Tables.CONTACTS + ";");
1587        db.execSQL("DELETE FROM " + Tables.RAW_CONTACTS + ";");
1588        db.execSQL("DELETE FROM " + Tables.DATA + ";");
1589        db.execSQL("DELETE FROM " + Tables.PHONE_LOOKUP + ";");
1590        db.execSQL("DELETE FROM " + Tables.NAME_LOOKUP + ";");
1591        db.execSQL("DELETE FROM " + Tables.GROUPS + ";");
1592        db.execSQL("DELETE FROM " + Tables.AGGREGATION_EXCEPTIONS + ";");
1593        db.execSQL("DELETE FROM " + Tables.SETTINGS + ";");
1594        db.execSQL("DELETE FROM " + Tables.ACTIVITIES + ";");
1595        db.execSQL("DELETE FROM " + Tables.CALLS + ";");
1596
1597        // Note: we are not removing reference data from Tables.NICKNAME_LOOKUP
1598    }
1599
1600    /**
1601     * Return the {@link ApplicationInfo#uid} for the given package name.
1602     */
1603    public static int getUidForPackageName(PackageManager pm, String packageName) {
1604        try {
1605            ApplicationInfo clientInfo = pm.getApplicationInfo(packageName, 0 /* no flags */);
1606            return clientInfo.uid;
1607        } catch (NameNotFoundException e) {
1608            throw new RuntimeException(e);
1609        }
1610    }
1611
1612    /**
1613     * Perform an internal string-to-integer lookup using the compiled
1614     * {@link SQLiteStatement} provided, using the in-memory cache to speed up
1615     * lookups. If a mapping isn't found in cache or database, it will be
1616     * created. All new, uncached answers are added to the cache automatically.
1617     *
1618     * @param query Compiled statement used to query for the mapping.
1619     * @param insert Compiled statement used to insert a new mapping when no
1620     *            existing one is found in cache or from query.
1621     * @param value Value to find mapping for.
1622     * @param cache In-memory cache of previous answers.
1623     * @return An unique integer mapping for the given value.
1624     */
1625    private synchronized long getCachedId(SQLiteStatement query, SQLiteStatement insert,
1626            String value, HashMap<String, Long> cache) {
1627        // Try an in-memory cache lookup
1628        if (cache.containsKey(value)) {
1629            return cache.get(value);
1630        }
1631
1632        long id = -1;
1633        try {
1634            // Try searching database for mapping
1635            DatabaseUtils.bindObjectToProgram(query, 1, value);
1636            id = query.simpleQueryForLong();
1637        } catch (SQLiteDoneException e) {
1638            // Nothing found, so try inserting new mapping
1639            DatabaseUtils.bindObjectToProgram(insert, 1, value);
1640            id = insert.executeInsert();
1641        }
1642
1643        if (id != -1) {
1644            // Cache and return the new answer
1645            cache.put(value, id);
1646            return id;
1647        } else {
1648            // Otherwise throw if no mapping found or created
1649            throw new IllegalStateException("Couldn't find or create internal "
1650                    + "lookup table entry for value " + value);
1651        }
1652    }
1653
1654    /**
1655     * Convert a package name into an integer, using {@link Tables#PACKAGES} for
1656     * lookups and possible allocation of new IDs as needed.
1657     */
1658    public long getPackageId(String packageName) {
1659        // Make sure compiled statements are ready by opening database
1660        getReadableDatabase();
1661        return getCachedId(mPackageQuery, mPackageInsert, packageName, mPackageCache);
1662    }
1663
1664    /**
1665     * Convert a mimetype into an integer, using {@link Tables#MIMETYPES} for
1666     * lookups and possible allocation of new IDs as needed.
1667     */
1668    public long getMimeTypeId(String mimetype) {
1669        // Make sure compiled statements are ready by opening database
1670        getReadableDatabase();
1671        return getCachedId(mMimetypeQuery, mMimetypeInsert, mimetype, mMimetypeCache);
1672    }
1673
1674    /**
1675     * Find the mimetype for the given {@link Data#_ID}.
1676     */
1677    public String getDataMimeType(long dataId) {
1678        // Make sure compiled statements are ready by opening database
1679        getReadableDatabase();
1680        try {
1681            // Try database query to find mimetype
1682            DatabaseUtils.bindObjectToProgram(mDataMimetypeQuery, 1, dataId);
1683            String mimetype = mDataMimetypeQuery.simpleQueryForString();
1684            return mimetype;
1685        } catch (SQLiteDoneException e) {
1686            // No valid mapping found, so return null
1687            return null;
1688        }
1689    }
1690
1691    /**
1692     * Find the mime-type for the given {@link Activities#_ID}.
1693     */
1694    public String getActivityMimeType(long activityId) {
1695        // Make sure compiled statements are ready by opening database
1696        getReadableDatabase();
1697        try {
1698            // Try database query to find mimetype
1699            DatabaseUtils.bindObjectToProgram(mActivitiesMimetypeQuery, 1, activityId);
1700            String mimetype = mActivitiesMimetypeQuery.simpleQueryForString();
1701            return mimetype;
1702        } catch (SQLiteDoneException e) {
1703            // No valid mapping found, so return null
1704            return null;
1705        }
1706    }
1707
1708    /**
1709     * Update {@link Contacts#IN_VISIBLE_GROUP} for all contacts.
1710     */
1711    public void updateAllVisible() {
1712        final long groupMembershipMimetypeId = getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE);
1713        mVisibleUpdate.bindLong(1, groupMembershipMimetypeId);
1714        mVisibleUpdate.execute();
1715        mVisibleUpdateRawContacts.execute();
1716    }
1717
1718    /**
1719     * Update {@link Contacts#IN_VISIBLE_GROUP} for a specific contact.
1720     */
1721    public void updateContactVisible(long contactId) {
1722        final long groupMembershipMimetypeId = getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE);
1723        mVisibleSpecificUpdate.bindLong(1, groupMembershipMimetypeId);
1724        mVisibleSpecificUpdate.bindLong(2, contactId);
1725        mVisibleSpecificUpdate.execute();
1726
1727        mVisibleSpecificUpdateRawContacts.bindLong(1, contactId);
1728        mVisibleSpecificUpdateRawContacts.execute();
1729    }
1730
1731    /**
1732     * Returns contact ID for the given contact or zero if it is NULL.
1733     */
1734    public long getContactId(long rawContactId) {
1735        getReadableDatabase();
1736        try {
1737            DatabaseUtils.bindObjectToProgram(mContactIdQuery, 1, rawContactId);
1738            return mContactIdQuery.simpleQueryForLong();
1739        } catch (SQLiteDoneException e) {
1740            // No valid mapping found, so return 0
1741            return 0;
1742        }
1743    }
1744
1745    public int getAggregationMode(long rawContactId) {
1746        getReadableDatabase();
1747        try {
1748            DatabaseUtils.bindObjectToProgram(mAggregationModeQuery, 1, rawContactId);
1749            return (int)mAggregationModeQuery.simpleQueryForLong();
1750        } catch (SQLiteDoneException e) {
1751            // No valid row found, so return "disabled"
1752            return RawContacts.AGGREGATION_MODE_DISABLED;
1753        }
1754    }
1755
1756    public void buildPhoneLookupAndRawContactQuery(SQLiteQueryBuilder qb, String number) {
1757        String minMatch = PhoneNumberUtils.toCallerIDMinMatch(number);
1758        qb.setTables(Tables.DATA_JOIN_RAW_CONTACTS +
1759                " JOIN " + Tables.PHONE_LOOKUP
1760                + " ON(" + DataColumns.CONCRETE_ID + "=" + PhoneLookupColumns.DATA_ID + ")");
1761
1762        StringBuilder sb = new StringBuilder();
1763        sb.append(PhoneLookupColumns.MIN_MATCH + "='");
1764        sb.append(minMatch);
1765        sb.append("' AND PHONE_NUMBERS_EQUAL(data." + Phone.NUMBER + ", ");
1766        DatabaseUtils.appendEscapedSQLString(sb, number);
1767        sb.append(mUseStrictPhoneNumberComparison ? ", 1)" : ", 0)");
1768
1769        qb.appendWhere(sb.toString());
1770    }
1771
1772    public void buildPhoneLookupAndContactQuery(SQLiteQueryBuilder qb, String number) {
1773        String minMatch = PhoneNumberUtils.toCallerIDMinMatch(number);
1774        StringBuilder sb = new StringBuilder();
1775        appendPhoneLookupTables(sb, minMatch, true);
1776        qb.setTables(sb.toString());
1777
1778        sb = new StringBuilder();
1779        appendPhoneLookupSelection(sb, number);
1780        qb.appendWhere(sb.toString());
1781    }
1782
1783    public String buildPhoneLookupAsNestedQuery(String number) {
1784        StringBuilder sb = new StringBuilder();
1785        final String minMatch = PhoneNumberUtils.toCallerIDMinMatch(number);
1786        sb.append("(SELECT DISTINCT raw_contact_id" + " FROM ");
1787        appendPhoneLookupTables(sb, minMatch, false);
1788        sb.append(" WHERE ");
1789        appendPhoneLookupSelection(sb, number);
1790        sb.append(")");
1791        return sb.toString();
1792    }
1793
1794    private void appendPhoneLookupTables(StringBuilder sb, final String minMatch,
1795            boolean joinContacts) {
1796        sb.append(Tables.RAW_CONTACTS);
1797        if (joinContacts) {
1798            sb.append(" JOIN " + getContactView() + " contacts_view"
1799                    + " ON (contacts_view._id = raw_contacts.contact_id)");
1800        }
1801        sb.append(", (SELECT data_id FROM phone_lookup "
1802                + "WHERE (" + Tables.PHONE_LOOKUP + "." + PhoneLookupColumns.MIN_MATCH + " = '");
1803        sb.append(minMatch);
1804        sb.append("')) AS lookup, " + Tables.DATA);
1805    }
1806
1807    private void appendPhoneLookupSelection(StringBuilder sb, String number) {
1808        sb.append("lookup.data_id=data._id AND data.raw_contact_id=raw_contacts._id"
1809                + " AND PHONE_NUMBERS_EQUAL(data." + Phone.NUMBER + ", ");
1810        DatabaseUtils.appendEscapedSQLString(sb, number);
1811        sb.append(mUseStrictPhoneNumberComparison ? ", 1)" : ", 0)");
1812    }
1813
1814    public String getUseStrictPhoneNumberComparisonParameter() {
1815        return mUseStrictPhoneNumberComparison ? "1" : "0";
1816    }
1817
1818    /**
1819     * Loads common nickname mappings into the database.
1820     */
1821    private void loadNicknameLookupTable(SQLiteDatabase db) {
1822        String[] strings = mContext.getResources().getStringArray(
1823                com.android.internal.R.array.common_nicknames);
1824        if (strings == null || strings.length == 0) {
1825            return;
1826        }
1827
1828        SQLiteStatement nicknameLookupInsert = db.compileStatement("INSERT INTO "
1829                + Tables.NICKNAME_LOOKUP + "(" + NicknameLookupColumns.NAME + ","
1830                + NicknameLookupColumns.CLUSTER + ") VALUES (?,?)");
1831
1832        for (int clusterId = 0; clusterId < strings.length; clusterId++) {
1833            String[] names = strings[clusterId].split(",");
1834            for (int j = 0; j < names.length; j++) {
1835                String name = NameNormalizer.normalize(names[j]);
1836                try {
1837                    DatabaseUtils.bindObjectToProgram(nicknameLookupInsert, 1, name);
1838                    DatabaseUtils.bindObjectToProgram(nicknameLookupInsert, 2,
1839                            String.valueOf(clusterId));
1840                    nicknameLookupInsert.executeInsert();
1841                } catch (SQLiteException e) {
1842
1843                    // Print the exception and keep going - this is not a fatal error
1844                    Log.e(TAG, "Cannot insert nickname: " + names[j], e);
1845                }
1846            }
1847        }
1848    }
1849
1850    public static void copyStringValue(ContentValues toValues, String toKey,
1851            ContentValues fromValues, String fromKey) {
1852        if (fromValues.containsKey(fromKey)) {
1853            toValues.put(toKey, fromValues.getAsString(fromKey));
1854        }
1855    }
1856
1857    public static void copyLongValue(ContentValues toValues, String toKey,
1858            ContentValues fromValues, String fromKey) {
1859        if (fromValues.containsKey(fromKey)) {
1860            long longValue;
1861            Object value = fromValues.get(fromKey);
1862            if (value instanceof Boolean) {
1863                if ((Boolean)value) {
1864                    longValue = 1;
1865                } else {
1866                    longValue = 0;
1867                }
1868            } else if (value instanceof String) {
1869                longValue = Long.parseLong((String)value);
1870            } else {
1871                longValue = ((Number)value).longValue();
1872            }
1873            toValues.put(toKey, longValue);
1874        }
1875    }
1876
1877    public SyncStateContentProviderHelper getSyncState() {
1878        return mSyncState;
1879    }
1880
1881    /**
1882     * Delete the aggregate contact if it has no constituent raw contacts other
1883     * than the supplied one.
1884     */
1885    public void removeContactIfSingleton(long rawContactId) {
1886        SQLiteDatabase db = getWritableDatabase();
1887
1888        // Obtain contact ID from the supplied raw contact ID
1889        String contactIdFromRawContactId = "(SELECT " + RawContacts.CONTACT_ID + " FROM "
1890                + Tables.RAW_CONTACTS + " WHERE " + RawContacts._ID + "=" + rawContactId + ")";
1891
1892        // Find other raw contacts in the same aggregate contact
1893        String otherRawContacts = "(SELECT contacts1." + RawContacts._ID + " FROM "
1894                + Tables.RAW_CONTACTS + " contacts1 JOIN " + Tables.RAW_CONTACTS + " contacts2 ON ("
1895                + "contacts1." + RawContacts.CONTACT_ID + "=contacts2." + RawContacts.CONTACT_ID
1896                + ") WHERE contacts1." + RawContacts._ID + "!=" + rawContactId + ""
1897                + " AND contacts2." + RawContacts._ID + "=" + rawContactId + ")";
1898
1899        db.execSQL("DELETE FROM " + Tables.CONTACTS
1900                + " WHERE " + Contacts._ID + "=" + contactIdFromRawContactId
1901                + " AND NOT EXISTS " + otherRawContacts + ";");
1902    }
1903
1904    /**
1905     * Check if {@link Binder#getCallingUid()} should be allowed access to
1906     * {@link RawContacts#IS_RESTRICTED} data.
1907     */
1908    boolean hasAccessToRestrictedData() {
1909        final PackageManager pm = mContext.getPackageManager();
1910        final String[] callerPackages = pm.getPackagesForUid(Binder.getCallingUid());
1911
1912        // Has restricted access if caller matches any packages
1913        for (String callerPackage : callerPackages) {
1914            if (hasAccessToRestrictedData(callerPackage)) {
1915                return true;
1916            }
1917        }
1918        return false;
1919    }
1920
1921    /**
1922     * Check if requestingPackage should be allowed access to
1923     * {@link RawContacts#IS_RESTRICTED} data.
1924     */
1925    boolean hasAccessToRestrictedData(String requestingPackage) {
1926        if (mUnrestrictedPackages != null) {
1927            for (String allowedPackage : mUnrestrictedPackages) {
1928                if (allowedPackage.equals(requestingPackage)) {
1929                    return true;
1930                }
1931            }
1932        }
1933        return false;
1934    }
1935
1936    public String getDataView() {
1937        return getDataView(false);
1938    }
1939
1940    public String getDataView(boolean requireRestrictedView) {
1941        return (hasAccessToRestrictedData() && !requireRestrictedView) ?
1942                Views.DATA_ALL : Views.DATA_RESTRICTED;
1943    }
1944
1945    public String getRawContactView() {
1946        return getRawContactView(false);
1947    }
1948
1949    public String getRawContactView(boolean requireRestrictedView) {
1950        return (hasAccessToRestrictedData() && !requireRestrictedView) ?
1951                Views.RAW_CONTACTS_ALL : Views.RAW_CONTACTS_RESTRICTED;
1952    }
1953
1954    public String getContactView() {
1955        return getContactView(false);
1956    }
1957
1958    public String getContactView(boolean requireRestrictedView) {
1959        return (hasAccessToRestrictedData() && !requireRestrictedView) ?
1960                Views.CONTACTS_ALL : Views.CONTACTS_RESTRICTED;
1961    }
1962
1963    public String getGroupView() {
1964        return Views.GROUPS_ALL;
1965    }
1966
1967    public String getContactEntitiesView() {
1968        return getContactEntitiesView(false);
1969    }
1970
1971    public String getContactEntitiesView(boolean requireRestrictedView) {
1972        return (hasAccessToRestrictedData() && !requireRestrictedView) ?
1973                Tables.CONTACT_ENTITIES : Tables.CONTACT_ENTITIES_RESTRICTED;
1974    }
1975
1976    /**
1977     * Test if any of the columns appear in the given projection.
1978     */
1979    public boolean isInProjection(String[] projection, String... columns) {
1980        if (projection == null) {
1981            return true;
1982        }
1983
1984        // Optimized for a single-column test
1985        if (columns.length == 1) {
1986            String column = columns[0];
1987            for (String test : projection) {
1988                if (column.equals(test)) {
1989                    return true;
1990                }
1991            }
1992        } else {
1993            for (String test : projection) {
1994                for (String column : columns) {
1995                    if (column.equals(test)) {
1996                        return true;
1997                    }
1998                }
1999            }
2000        }
2001        return false;
2002    }
2003}
2004