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