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