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