• 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.providers.contacts.sqlite.DatabaseAnalyzer;
20 import com.android.providers.contacts.sqlite.SqlChecker;
21 import com.android.providers.contacts.sqlite.SqlChecker.InvalidSqlException;
22 import com.android.providers.contacts.util.PropertyUtils;
23 
24 import android.app.ActivityManager;
25 import android.content.ContentResolver;
26 import android.content.ContentValues;
27 import android.content.Context;
28 import android.content.Intent;
29 import android.content.pm.ApplicationInfo;
30 import android.content.pm.PackageManager;
31 import android.content.pm.PackageManager.NameNotFoundException;
32 import android.content.pm.UserInfo;
33 import android.content.res.Resources;
34 import android.database.CharArrayBuffer;
35 import android.database.Cursor;
36 import android.database.DatabaseUtils;
37 import android.database.SQLException;
38 import android.database.sqlite.SQLiteConstraintException;
39 import android.database.sqlite.SQLiteDatabase;
40 import android.database.sqlite.SQLiteDoneException;
41 import android.database.sqlite.SQLiteException;
42 import android.database.sqlite.SQLiteOpenHelper;
43 import android.database.sqlite.SQLiteQueryBuilder;
44 import android.database.sqlite.SQLiteStatement;
45 import android.net.Uri;
46 import android.os.Binder;
47 import android.os.Bundle;
48 import android.os.SystemClock;
49 import android.os.UserManager;
50 import android.provider.BaseColumns;
51 import android.provider.ContactsContract;
52 import android.provider.ContactsContract.AggregationExceptions;
53 import android.provider.ContactsContract.CommonDataKinds.Email;
54 import android.provider.ContactsContract.CommonDataKinds.Event;
55 import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
56 import android.provider.ContactsContract.CommonDataKinds.Identity;
57 import android.provider.ContactsContract.CommonDataKinds.Im;
58 import android.provider.ContactsContract.CommonDataKinds.Nickname;
59 import android.provider.ContactsContract.CommonDataKinds.Note;
60 import android.provider.ContactsContract.CommonDataKinds.Organization;
61 import android.provider.ContactsContract.CommonDataKinds.Phone;
62 import android.provider.ContactsContract.CommonDataKinds.Relation;
63 import android.provider.ContactsContract.CommonDataKinds.SipAddress;
64 import android.provider.ContactsContract.CommonDataKinds.StructuredName;
65 import android.provider.ContactsContract.CommonDataKinds.StructuredPostal;
66 import android.provider.ContactsContract.CommonDataKinds.Website;
67 import android.provider.ContactsContract.Contacts;
68 import android.provider.ContactsContract.Contacts.Photo;
69 import android.provider.ContactsContract.Data;
70 import android.provider.ContactsContract.Directory;
71 import android.provider.ContactsContract.DisplayNameSources;
72 import android.provider.ContactsContract.DisplayPhoto;
73 import android.provider.ContactsContract.FullNameStyle;
74 import android.provider.ContactsContract.Groups;
75 import android.provider.ContactsContract.MetadataSync;
76 import android.provider.ContactsContract.MetadataSyncState;
77 import android.provider.ContactsContract.PhoneticNameStyle;
78 import android.provider.ContactsContract.PhotoFiles;
79 import android.provider.ContactsContract.PinnedPositions;
80 import android.provider.ContactsContract.ProviderStatus;
81 import android.provider.ContactsContract.RawContacts;
82 import android.provider.ContactsContract.Settings;
83 import android.provider.ContactsContract.StatusUpdates;
84 import android.provider.ContactsContract.StreamItemPhotos;
85 import android.provider.ContactsContract.StreamItems;
86 import android.telephony.PhoneNumberUtils;
87 import android.telephony.SubscriptionInfo;
88 import android.telephony.SubscriptionManager;
89 import android.text.TextUtils;
90 import android.text.util.Rfc822Token;
91 import android.text.util.Rfc822Tokenizer;
92 import android.util.ArrayMap;
93 import android.util.ArraySet;
94 import android.util.Base64;
95 import android.util.Log;
96 import android.util.Slog;
97 
98 import com.android.common.content.SyncStateContentProviderHelper;
99 import com.android.internal.annotations.VisibleForTesting;
100 import com.android.providers.contacts.aggregation.util.CommonNicknameCache;
101 import com.android.providers.contacts.database.ContactsTableUtil;
102 import com.android.providers.contacts.database.DeletedContactsTableUtil;
103 import com.android.providers.contacts.database.MoreDatabaseUtils;
104 import com.android.providers.contacts.util.NeededForTesting;
105 
106 import libcore.icu.ICU;
107 
108 import java.security.MessageDigest;
109 import java.security.NoSuchAlgorithmException;
110 import java.util.ArrayList;
111 import java.util.Locale;
112 import java.util.Set;
113 
114 /**
115  * Database helper for contacts. Designed as a singleton to make sure that all
116  * {@link android.content.ContentProvider} users get the same reference.
117  * Provides handy methods for maintaining package and mime-type lookup tables.
118  */
119 public class ContactsDatabaseHelper extends SQLiteOpenHelper {
120 
121     /**
122      * Contacts DB version ranges:
123      * <pre>
124      *   0-98    Cupcake/Donut
125      *   100-199 Eclair
126      *   200-299 Eclair-MR1
127      *   300-349 Froyo
128      *   350-399 Gingerbread
129      *   400-499 Honeycomb
130      *   500-549 Honeycomb-MR1
131      *   550-599 Honeycomb-MR2
132      *   600-699 Ice Cream Sandwich
133      *   700-799 Jelly Bean
134      *   800-899 Kitkat
135      *   900-999 Lollipop
136      *   1000-1099 M
137      *   1100-1199 N
138      *   1200-1299 O
139      *   1300-1399 P
140      * </pre>
141      */
142     static final int DATABASE_VERSION = 1300;
143     private static final int MINIMUM_SUPPORTED_VERSION = 700;
144 
145     @VisibleForTesting
146     static final boolean DISALLOW_SUB_QUERIES = false;
147 
148     private static final int IDLE_CONNECTION_TIMEOUT_MS = 30000;
149 
150     public interface Tables {
151         public static final String CONTACTS = "contacts";
152         public static final String DELETED_CONTACTS = "deleted_contacts";
153         public static final String RAW_CONTACTS = "raw_contacts";
154         public static final String STREAM_ITEMS = "stream_items";
155         public static final String STREAM_ITEM_PHOTOS = "stream_item_photos";
156         public static final String PHOTO_FILES = "photo_files";
157         public static final String PACKAGES = "packages";
158         public static final String MIMETYPES = "mimetypes";
159         public static final String PHONE_LOOKUP = "phone_lookup";
160         public static final String NAME_LOOKUP = "name_lookup";
161         public static final String AGGREGATION_EXCEPTIONS = "agg_exceptions";
162         public static final String SETTINGS = "settings";
163         public static final String DATA = "data";
164         public static final String GROUPS = "groups";
165         public static final String PRESENCE = "presence";
166         public static final String AGGREGATED_PRESENCE = "agg_presence";
167         public static final String NICKNAME_LOOKUP = "nickname_lookup";
168         public static final String STATUS_UPDATES = "status_updates";
169         public static final String ACCOUNTS = "accounts";
170         public static final String VISIBLE_CONTACTS = "visible_contacts";
171         public static final String DIRECTORIES = "directories";
172         public static final String DEFAULT_DIRECTORY = "default_directory";
173         public static final String SEARCH_INDEX = "search_index";
174         public static final String METADATA_SYNC = "metadata_sync";
175         public static final String METADATA_SYNC_STATE = "metadata_sync_state";
176         public static final String PRE_AUTHORIZED_URIS = "pre_authorized_uris";
177 
178         // This list of tables contains auto-incremented sequences.
179         public static final String[] SEQUENCE_TABLES = new String[] {
180                 CONTACTS,
181                 RAW_CONTACTS,
182                 STREAM_ITEMS,
183                 STREAM_ITEM_PHOTOS,
184                 PHOTO_FILES,
185                 DATA,
186                 GROUPS,
187                 DIRECTORIES};
188 
189         /**
190          * For {@link android.provider.ContactsContract.DataUsageFeedback}. The table structure
191          * itself is not exposed outside.
192          */
193         public static final String DATA_USAGE_STAT = "data_usage_stat";
194 
195         public static final String DATA_JOIN_MIMETYPES = "data "
196                 + "JOIN mimetypes ON (data.mimetype_id = mimetypes._id)";
197 
198         public static final String DATA_JOIN_RAW_CONTACTS = "data "
199                 + "JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id)";
200 
201         // NOTE: If you want to refer to account name/type/data_set, AccountsColumns.CONCRETE_XXX
202         // MUST be used, as upgraded raw_contacts may have the account info columns too.
203         public static final String DATA_JOIN_MIMETYPE_RAW_CONTACTS = "data "
204                 + "JOIN mimetypes ON (data.mimetype_id = mimetypes._id) "
205                 + "JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id)"
206                 + " JOIN " + Tables.ACCOUNTS + " ON ("
207                     + RawContactsColumns.CONCRETE_ACCOUNT_ID + "=" + AccountsColumns.CONCRETE_ID
208                     + ")";
209 
210         // NOTE: This requires late binding of GroupMembership MIME-type
211         // TODO Consolidate settings and accounts
212         public static final String RAW_CONTACTS_JOIN_SETTINGS_DATA_GROUPS = Tables.RAW_CONTACTS
213                 + " JOIN " + Tables.ACCOUNTS + " ON ("
214                 +   RawContactsColumns.CONCRETE_ACCOUNT_ID + "=" + AccountsColumns.CONCRETE_ID
215                     + ")"
216                 + "LEFT OUTER JOIN " + Tables.SETTINGS + " ON ("
217                     + AccountsColumns.CONCRETE_ACCOUNT_NAME + "="
218                         + SettingsColumns.CONCRETE_ACCOUNT_NAME + " AND "
219                     + AccountsColumns.CONCRETE_ACCOUNT_TYPE + "="
220                         + SettingsColumns.CONCRETE_ACCOUNT_TYPE + " AND "
221                     + "((" + AccountsColumns.CONCRETE_DATA_SET + " IS NULL AND "
222                             + SettingsColumns.CONCRETE_DATA_SET + " IS NULL) OR ("
223                         + AccountsColumns.CONCRETE_DATA_SET + "="
224                             + SettingsColumns.CONCRETE_DATA_SET + "))) "
225                 + "LEFT OUTER JOIN data ON (data.mimetype_id=? AND "
226                     + "data.raw_contact_id = raw_contacts._id) "
227                 + "LEFT OUTER JOIN groups ON (groups._id = data." + GroupMembership.GROUP_ROW_ID
228                 + ")";
229 
230         // NOTE: This requires late binding of GroupMembership MIME-type
231         // TODO Add missing DATA_SET join -- or just consolidate settings and accounts
232         public static final String SETTINGS_JOIN_RAW_CONTACTS_DATA_MIMETYPES_CONTACTS = "settings "
233                 + "LEFT OUTER JOIN raw_contacts ON ("
234                     + RawContactsColumns.CONCRETE_ACCOUNT_ID + "=(SELECT "
235                         + AccountsColumns.CONCRETE_ID
236                         + " FROM " + Tables.ACCOUNTS
237                         + " WHERE "
238                             + "(" + AccountsColumns.CONCRETE_ACCOUNT_NAME
239                                 + "=" + SettingsColumns.CONCRETE_ACCOUNT_NAME + ") AND "
240                             + "(" + AccountsColumns.CONCRETE_ACCOUNT_TYPE
241                                 + "=" + SettingsColumns.CONCRETE_ACCOUNT_TYPE + ")))"
242                 + "LEFT OUTER JOIN data ON (data.mimetype_id=? AND "
243                     + "data.raw_contact_id = raw_contacts._id) "
244                 + "LEFT OUTER JOIN contacts ON (raw_contacts.contact_id = contacts._id)";
245 
246         public static final String CONTACTS_JOIN_RAW_CONTACTS_DATA_FILTERED_BY_GROUPMEMBERSHIP =
247                 Tables.CONTACTS
248                     + " INNER JOIN " + Tables.RAW_CONTACTS
249                         + " ON (" + RawContactsColumns.CONCRETE_CONTACT_ID + "="
250                             + ContactsColumns.CONCRETE_ID
251                         + ")"
252                     + " INNER JOIN " + Tables.DATA
253                         + " ON (" + DataColumns.CONCRETE_DATA1 + "=" + GroupsColumns.CONCRETE_ID
254                         + " AND "
255                         + DataColumns.CONCRETE_RAW_CONTACT_ID + "=" + RawContactsColumns.CONCRETE_ID
256                         + " AND "
257                         + DataColumns.CONCRETE_MIMETYPE_ID + "="
258                             + "(SELECT " + MimetypesColumns._ID
259                             + " FROM " + Tables.MIMETYPES
260                             + " WHERE "
261                             + MimetypesColumns.CONCRETE_MIMETYPE + "="
262                                 + "'" + GroupMembership.CONTENT_ITEM_TYPE + "'"
263                             + ")"
264                         + ")";
265 
266         // NOTE: If you want to refer to account name/type/data_set, AccountsColumns.CONCRETE_XXX
267         // MUST be used, as upgraded raw_contacts may have the account info columns too.
268         public static final String DATA_JOIN_PACKAGES_MIMETYPES_RAW_CONTACTS_GROUPS = "data "
269                 + "JOIN mimetypes ON (data.mimetype_id = mimetypes._id) "
270                 + "JOIN raw_contacts ON (data.raw_contact_id = raw_contacts._id) "
271                 + " JOIN " + Tables.ACCOUNTS + " ON ("
272                     + RawContactsColumns.CONCRETE_ACCOUNT_ID + "=" + AccountsColumns.CONCRETE_ID
273                     + ")"
274                 + "LEFT OUTER JOIN packages ON (data.package_id = packages._id) "
275                 + "LEFT OUTER JOIN groups "
276                 + "  ON (mimetypes.mimetype='" + GroupMembership.CONTENT_ITEM_TYPE + "' "
277                 + "      AND groups._id = data." + GroupMembership.GROUP_ROW_ID + ") ";
278 
279         public static final String ACTIVITIES_JOIN_MIMETYPES = "activities "
280                 + "LEFT OUTER JOIN mimetypes ON (activities.mimetype_id = mimetypes._id)";
281 
282         public static final String ACTIVITIES_JOIN_PACKAGES_MIMETYPES_RAW_CONTACTS_CONTACTS =
283                 "activities "
284                 + "LEFT OUTER JOIN packages ON (activities.package_id = packages._id) "
285                 + "LEFT OUTER JOIN mimetypes ON (activities.mimetype_id = mimetypes._id) "
286                 + "LEFT OUTER JOIN raw_contacts ON (activities.author_contact_id = " +
287                         "raw_contacts._id) "
288                 + "LEFT OUTER JOIN contacts ON (raw_contacts.contact_id = contacts._id)";
289 
290         public static final String NAME_LOOKUP_JOIN_RAW_CONTACTS = "name_lookup "
291                 + "INNER JOIN view_raw_contacts ON (name_lookup.raw_contact_id = "
292                 + "view_raw_contacts._id)";
293 
294         public static final String RAW_CONTACTS_JOIN_ACCOUNTS = Tables.RAW_CONTACTS
295                 + " JOIN " + Tables.ACCOUNTS + " ON ("
296                 + AccountsColumns.CONCRETE_ID + "=" + RawContactsColumns.CONCRETE_ACCOUNT_ID
297                 + ")";
298 
299         public static final String RAW_CONTACTS_JOIN_METADATA_SYNC = Tables.RAW_CONTACTS
300                 + " JOIN " + Tables.METADATA_SYNC + " ON ("
301                 + RawContactsColumns.CONCRETE_BACKUP_ID + "="
302                 + MetadataSyncColumns.CONCRETE_BACKUP_ID
303                 + " AND "
304                 + RawContactsColumns.CONCRETE_ACCOUNT_ID + "="
305                 + MetadataSyncColumns.CONCRETE_ACCOUNT_ID
306                 + ")";
307     }
308 
309     public interface Joins {
310         /**
311          * Join string intended to be used with the GROUPS table/view.  The main table must be named
312          * as "groups".
313          *
314          * Adds the "group_member_count column" to the query, which will be null if a group has
315          * no members.  Use ifnull(group_member_count, 0) if 0 is needed instead.
316          */
317         public static final String GROUP_MEMBER_COUNT =
318                 " LEFT OUTER JOIN (SELECT "
319                         + "data.data1 AS member_count_group_id, "
320                         + "COUNT(data.raw_contact_id) AS group_member_count "
321                     + "FROM data "
322                     + "WHERE "
323                         + "data.mimetype_id = (SELECT _id FROM mimetypes WHERE "
324                             + "mimetypes.mimetype = '" + GroupMembership.CONTENT_ITEM_TYPE + "')"
325                     + "GROUP BY member_count_group_id) AS member_count_table" // End of inner query
326                 + " ON (groups._id = member_count_table.member_count_group_id)";
327     }
328 
329     public interface Views {
330         public static final String DATA = "view_data";
331         public static final String RAW_CONTACTS = "view_raw_contacts";
332         public static final String CONTACTS = "view_contacts";
333         public static final String ENTITIES = "view_entities";
334         public static final String RAW_ENTITIES = "view_raw_entities";
335         public static final String GROUPS = "view_groups";
336 
337         /** The data_usage_stat table joined with other tables. */
338         public static final String DATA_USAGE_STAT = "view_data_usage_stat";
339 
340         /** The data_usage_stat table with the low-res columns. */
341         public static final String DATA_USAGE_LR = "view_data_usage";
342         public static final String STREAM_ITEMS = "view_stream_items";
343         public static final String METADATA_SYNC = "view_metadata_sync";
344         public static final String METADATA_SYNC_STATE = "view_metadata_sync_state";
345     }
346 
347     public interface Projections {
348         String[] ID = new String[] {BaseColumns._ID};
349         String[] LITERAL_ONE = new String[] {"1"};
350     }
351 
352     /**
353      * Property names for {@link ContactsDatabaseHelper#getProperty} and
354      * {@link ContactsDatabaseHelper#setProperty}.
355      */
356     public interface DbProperties {
357         String DIRECTORY_SCAN_COMPLETE = "directoryScanComplete";
358         String AGGREGATION_ALGORITHM = "aggregation_v2";
359         String KNOWN_ACCOUNTS = "known_accounts";
360         String ICU_VERSION = "icu_version";
361         String LOCALE = "locale";
362         String DATABASE_TIME_CREATED = "database_time_created";
363         String KNOWN_DIRECTORY_PACKAGES = "knownDirectoryPackages";
364     }
365 
366     public interface Clauses {
367         final String HAVING_NO_GROUPS = "COUNT(" + DataColumns.CONCRETE_GROUP_ID + ") == 0";
368 
369         final String GROUP_BY_ACCOUNT_CONTACT_ID = SettingsColumns.CONCRETE_ACCOUNT_NAME + ","
370                 + SettingsColumns.CONCRETE_ACCOUNT_TYPE + "," + RawContacts.CONTACT_ID;
371 
372         String LOCAL_ACCOUNT_ID =
373                 "(SELECT " + AccountsColumns._ID +
374                 " FROM " + Tables.ACCOUNTS +
375                 " WHERE " +
376                     AccountsColumns.ACCOUNT_NAME + " IS NULL AND " +
377                     AccountsColumns.ACCOUNT_TYPE + " IS NULL AND " +
378                     AccountsColumns.DATA_SET + " IS NULL)";
379 
380         final String RAW_CONTACT_IS_LOCAL = RawContactsColumns.CONCRETE_ACCOUNT_ID
381                 + "=" + LOCAL_ACCOUNT_ID;
382 
383         final String ZERO_GROUP_MEMBERSHIPS = "COUNT(" + GroupsColumns.CONCRETE_ID + ")=0";
384 
385         final String OUTER_RAW_CONTACTS = "outer_raw_contacts";
386         final String OUTER_RAW_CONTACTS_ID = OUTER_RAW_CONTACTS + "." + RawContacts._ID;
387 
388         final String CONTACT_IS_VISIBLE =
389                 "SELECT " +
390                     "MAX((SELECT (CASE WHEN " +
391                         "(CASE" +
392                             " WHEN " + RAW_CONTACT_IS_LOCAL +
393                             " THEN 1 " +
394                             " WHEN " + ZERO_GROUP_MEMBERSHIPS +
395                             " THEN " + Settings.UNGROUPED_VISIBLE +
396                             " ELSE MAX(" + Groups.GROUP_VISIBLE + ")" +
397                          "END)=1 THEN 1 ELSE 0 END)" +
398                 " FROM " + Tables.RAW_CONTACTS_JOIN_SETTINGS_DATA_GROUPS +
399                 " WHERE " + RawContactsColumns.CONCRETE_ID + "=" + OUTER_RAW_CONTACTS_ID + "))" +
400                 " FROM " + Tables.RAW_CONTACTS + " AS " + OUTER_RAW_CONTACTS +
401                 " WHERE " + RawContacts.CONTACT_ID + "=" + ContactsColumns.CONCRETE_ID +
402                 " GROUP BY " + RawContacts.CONTACT_ID;
403 
404         final String GROUP_HAS_ACCOUNT_AND_SOURCE_ID = Groups.SOURCE_ID + "=? AND "
405                 + GroupsColumns.ACCOUNT_ID + "=?";
406 
407         public static final String CONTACT_VISIBLE =
408             "EXISTS (SELECT _id FROM " + Tables.VISIBLE_CONTACTS
409                 + " WHERE " + Tables.CONTACTS +"." + Contacts._ID
410                         + "=" + Tables.VISIBLE_CONTACTS +"." + Contacts._ID + ")";
411 
412         public static final String CONTACT_IN_DEFAULT_DIRECTORY =
413                 "EXISTS (SELECT _id FROM " + Tables.DEFAULT_DIRECTORY
414                         + " WHERE " + Tables.CONTACTS +"." + Contacts._ID
415                         + "=" + Tables.DEFAULT_DIRECTORY +"." + Contacts._ID + ")";
416     }
417 
418     public interface ContactsColumns {
419         public static final String LAST_STATUS_UPDATE_ID = "status_update_id";
420 
421         public static final String CONCRETE_ID = Tables.CONTACTS + "." + BaseColumns._ID;
422 
423         public static final String CONCRETE_PHOTO_FILE_ID = Tables.CONTACTS + "."
424                 + Contacts.PHOTO_FILE_ID;
425 
426         public static final String CONCRETE_RAW_TIMES_CONTACTED = Tables.CONTACTS + "."
427                 + Contacts.RAW_TIMES_CONTACTED;
428         public static final String CONCRETE_RAW_LAST_TIME_CONTACTED = Tables.CONTACTS + "."
429                 + Contacts.RAW_LAST_TIME_CONTACTED;
430 
431         public static final String CONCRETE_STARRED = Tables.CONTACTS + "." + Contacts.STARRED;
432         public static final String CONCRETE_PINNED = Tables.CONTACTS + "." + Contacts.PINNED;
433         public static final String CONCRETE_CUSTOM_RINGTONE = Tables.CONTACTS + "."
434                 + Contacts.CUSTOM_RINGTONE;
435         public static final String CONCRETE_SEND_TO_VOICEMAIL = Tables.CONTACTS + "."
436                 + Contacts.SEND_TO_VOICEMAIL;
437         public static final String CONCRETE_LOOKUP_KEY = Tables.CONTACTS + "."
438                 + Contacts.LOOKUP_KEY;
439         public static final String CONCRETE_CONTACT_LAST_UPDATED_TIMESTAMP = Tables.CONTACTS + "."
440                 + Contacts.CONTACT_LAST_UPDATED_TIMESTAMP;
441         public static final String PHONEBOOK_LABEL_PRIMARY = "phonebook_label";
442         public static final String PHONEBOOK_BUCKET_PRIMARY = "phonebook_bucket";
443         public static final String PHONEBOOK_LABEL_ALTERNATIVE = "phonebook_label_alt";
444         public static final String PHONEBOOK_BUCKET_ALTERNATIVE = "phonebook_bucket_alt";
445     }
446 
447     public interface RawContactsColumns {
448         public static final String CONCRETE_ID =
449                 Tables.RAW_CONTACTS + "." + BaseColumns._ID;
450 
451         public static final String ACCOUNT_ID = "account_id";
452         public static final String CONCRETE_ACCOUNT_ID = Tables.RAW_CONTACTS + "." + ACCOUNT_ID;
453         public static final String CONCRETE_SOURCE_ID =
454                 Tables.RAW_CONTACTS + "." + RawContacts.SOURCE_ID;
455         public static final String CONCRETE_BACKUP_ID =
456                 Tables.RAW_CONTACTS + "." + RawContacts.BACKUP_ID;
457         public static final String CONCRETE_VERSION =
458                 Tables.RAW_CONTACTS + "." + RawContacts.VERSION;
459         public static final String CONCRETE_DIRTY =
460                 Tables.RAW_CONTACTS + "." + RawContacts.DIRTY;
461         public static final String CONCRETE_DELETED =
462                 Tables.RAW_CONTACTS + "." + RawContacts.DELETED;
463         public static final String CONCRETE_SYNC1 =
464                 Tables.RAW_CONTACTS + "." + RawContacts.SYNC1;
465         public static final String CONCRETE_SYNC2 =
466                 Tables.RAW_CONTACTS + "." + RawContacts.SYNC2;
467         public static final String CONCRETE_SYNC3 =
468                 Tables.RAW_CONTACTS + "." + RawContacts.SYNC3;
469         public static final String CONCRETE_SYNC4 =
470                 Tables.RAW_CONTACTS + "." + RawContacts.SYNC4;
471         public static final String CONCRETE_CUSTOM_RINGTONE =
472                 Tables.RAW_CONTACTS + "." + RawContacts.CUSTOM_RINGTONE;
473         public static final String CONCRETE_SEND_TO_VOICEMAIL =
474                 Tables.RAW_CONTACTS + "." + RawContacts.SEND_TO_VOICEMAIL;
475         public static final String CONCRETE_RAW_LAST_TIME_CONTACTED =
476                 Tables.RAW_CONTACTS + "." + RawContacts.RAW_LAST_TIME_CONTACTED;
477         public static final String CONCRETE_RAW_TIMES_CONTACTED =
478                 Tables.RAW_CONTACTS + "." + RawContacts.RAW_TIMES_CONTACTED;
479         public static final String CONCRETE_STARRED =
480                 Tables.RAW_CONTACTS + "." + RawContacts.STARRED;
481         public static final String CONCRETE_PINNED =
482                 Tables.RAW_CONTACTS + "." + RawContacts.PINNED;
483 
484         public static final String CONCRETE_METADATA_DIRTY =
485                 Tables.RAW_CONTACTS + "." + RawContacts.METADATA_DIRTY;
486         public static final String DISPLAY_NAME = RawContacts.DISPLAY_NAME_PRIMARY;
487         public static final String DISPLAY_NAME_SOURCE = RawContacts.DISPLAY_NAME_SOURCE;
488         public static final String AGGREGATION_NEEDED = "aggregation_needed";
489 
490         public static final String CONCRETE_DISPLAY_NAME =
491                 Tables.RAW_CONTACTS + "." + DISPLAY_NAME;
492         public static final String CONCRETE_CONTACT_ID =
493                 Tables.RAW_CONTACTS + "." + RawContacts.CONTACT_ID;
494         public static final String PHONEBOOK_LABEL_PRIMARY =
495             ContactsColumns.PHONEBOOK_LABEL_PRIMARY;
496         public static final String PHONEBOOK_BUCKET_PRIMARY =
497             ContactsColumns.PHONEBOOK_BUCKET_PRIMARY;
498         public static final String PHONEBOOK_LABEL_ALTERNATIVE =
499             ContactsColumns.PHONEBOOK_LABEL_ALTERNATIVE;
500         public static final String PHONEBOOK_BUCKET_ALTERNATIVE =
501             ContactsColumns.PHONEBOOK_BUCKET_ALTERNATIVE;
502 
503         /**
504          * This column is no longer used, but we keep it in the table so an upgraded database
505          * will look the same as a new database. This reduces the chance of OEMs adding a second
506          * column with the same name.
507          */
508         public static final String NAME_VERIFIED_OBSOLETE = "name_verified";
509     }
510 
511     public interface ViewRawContactsColumns {
512         String CONCRETE_ACCOUNT_NAME = Views.RAW_CONTACTS + "." + RawContacts.ACCOUNT_NAME;
513         String CONCRETE_ACCOUNT_TYPE = Views.RAW_CONTACTS + "." + RawContacts.ACCOUNT_TYPE;
514         String CONCRETE_DATA_SET = Views.RAW_CONTACTS + "." + RawContacts.DATA_SET;
515     }
516 
517     public interface DataColumns {
518         public static final String PACKAGE_ID = "package_id";
519         public static final String MIMETYPE_ID = "mimetype_id";
520 
521         public static final String CONCRETE_ID = Tables.DATA + "." + BaseColumns._ID;
522         public static final String CONCRETE_MIMETYPE_ID = Tables.DATA + "." + MIMETYPE_ID;
523         public static final String CONCRETE_RAW_CONTACT_ID = Tables.DATA + "."
524                 + Data.RAW_CONTACT_ID;
525         public static final String CONCRETE_GROUP_ID = Tables.DATA + "."
526                 + GroupMembership.GROUP_ROW_ID;
527 
528         public static final String CONCRETE_DATA1 = Tables.DATA + "." + Data.DATA1;
529         public static final String CONCRETE_DATA2 = Tables.DATA + "." + Data.DATA2;
530         public static final String CONCRETE_DATA3 = Tables.DATA + "." + Data.DATA3;
531         public static final String CONCRETE_DATA4 = Tables.DATA + "." + Data.DATA4;
532         public static final String CONCRETE_DATA5 = Tables.DATA + "." + Data.DATA5;
533         public static final String CONCRETE_DATA6 = Tables.DATA + "." + Data.DATA6;
534         public static final String CONCRETE_DATA7 = Tables.DATA + "." + Data.DATA7;
535         public static final String CONCRETE_DATA8 = Tables.DATA + "." + Data.DATA8;
536         public static final String CONCRETE_DATA9 = Tables.DATA + "." + Data.DATA9;
537         public static final String CONCRETE_DATA10 = Tables.DATA + "." + Data.DATA10;
538         public static final String CONCRETE_DATA11 = Tables.DATA + "." + Data.DATA11;
539         public static final String CONCRETE_DATA12 = Tables.DATA + "." + Data.DATA12;
540         public static final String CONCRETE_DATA13 = Tables.DATA + "." + Data.DATA13;
541         public static final String CONCRETE_DATA14 = Tables.DATA + "." + Data.DATA14;
542         public static final String CONCRETE_DATA15 = Tables.DATA + "." + Data.DATA15;
543         public static final String CONCRETE_IS_PRIMARY = Tables.DATA + "." + Data.IS_PRIMARY;
544         public static final String CONCRETE_PACKAGE_ID = Tables.DATA + "." + PACKAGE_ID;
545     }
546 
547     // Used only for legacy API support.
548     public interface ExtensionsColumns {
549         public static final String NAME = Data.DATA1;
550         public static final String VALUE = Data.DATA2;
551     }
552 
553     public interface GroupMembershipColumns {
554         public static final String RAW_CONTACT_ID = Data.RAW_CONTACT_ID;
555         public static final String GROUP_ROW_ID = GroupMembership.GROUP_ROW_ID;
556     }
557 
558     public interface GroupsColumns {
559         public static final String PACKAGE_ID = "package_id";
560         public static final String CONCRETE_PACKAGE_ID = Tables.GROUPS + "." + PACKAGE_ID;
561 
562         public static final String CONCRETE_ID = Tables.GROUPS + "." + BaseColumns._ID;
563         public static final String CONCRETE_SOURCE_ID = Tables.GROUPS + "." + Groups.SOURCE_ID;
564 
565         public static final String ACCOUNT_ID = "account_id";
566         public static final String CONCRETE_ACCOUNT_ID = Tables.GROUPS + "." + ACCOUNT_ID;
567     }
568 
569     public interface ViewGroupsColumns {
570         String CONCRETE_ACCOUNT_NAME = Views.GROUPS + "." + Groups.ACCOUNT_NAME;
571         String CONCRETE_ACCOUNT_TYPE = Views.GROUPS + "." + Groups.ACCOUNT_TYPE;
572         String CONCRETE_DATA_SET = Views.GROUPS + "." + Groups.DATA_SET;
573     }
574 
575     public interface ActivitiesColumns {
576         public static final String PACKAGE_ID = "package_id";
577         public static final String MIMETYPE_ID = "mimetype_id";
578     }
579 
580     public interface PhoneLookupColumns {
581         public static final String _ID = BaseColumns._ID;
582         public static final String DATA_ID = "data_id";
583         public static final String RAW_CONTACT_ID = "raw_contact_id";
584         public static final String NORMALIZED_NUMBER = "normalized_number";
585         public static final String MIN_MATCH = "min_match";
586     }
587 
588     public interface NameLookupColumns {
589         public static final String RAW_CONTACT_ID = "raw_contact_id";
590         public static final String DATA_ID = "data_id";
591         public static final String NORMALIZED_NAME = "normalized_name";
592         public static final String NAME_TYPE = "name_type";
593     }
594 
595     public interface PackagesColumns {
596         public static final String _ID = BaseColumns._ID;
597         public static final String PACKAGE = "package";
598 
599         public static final String CONCRETE_ID = Tables.PACKAGES + "." + _ID;
600     }
601 
602     public interface MimetypesColumns {
603         public static final String _ID = BaseColumns._ID;
604         public static final String MIMETYPE = "mimetype";
605 
606         public static final String CONCRETE_ID = Tables.MIMETYPES + "." + BaseColumns._ID;
607         public static final String CONCRETE_MIMETYPE = Tables.MIMETYPES + "." + MIMETYPE;
608     }
609 
610     public interface AggregationExceptionColumns {
611         public static final String _ID = BaseColumns._ID;
612     }
613 
614     public interface NicknameLookupColumns {
615         public static final String NAME = "name";
616         public static final String CLUSTER = "cluster";
617     }
618 
619     public interface SettingsColumns {
620         public static final String CONCRETE_ACCOUNT_NAME = Tables.SETTINGS + "."
621                 + Settings.ACCOUNT_NAME;
622         public static final String CONCRETE_ACCOUNT_TYPE = Tables.SETTINGS + "."
623                 + Settings.ACCOUNT_TYPE;
624         public static final String CONCRETE_DATA_SET = Tables.SETTINGS + "."
625                 + Settings.DATA_SET;
626     }
627 
628     public interface PresenceColumns {
629         String RAW_CONTACT_ID = "presence_raw_contact_id";
630         String CONTACT_ID = "presence_contact_id";
631     }
632 
633     public interface AggregatedPresenceColumns {
634         String CONTACT_ID = "presence_contact_id";
635         String CONCRETE_CONTACT_ID = Tables.AGGREGATED_PRESENCE + "." + CONTACT_ID;
636     }
637 
638     public interface StatusUpdatesColumns {
639         String DATA_ID = "status_update_data_id";
640 
641         String CONCRETE_DATA_ID = Tables.STATUS_UPDATES + "." + DATA_ID;
642 
643         String CONCRETE_PRESENCE = Tables.STATUS_UPDATES + "." + StatusUpdates.PRESENCE;
644         String CONCRETE_STATUS = Tables.STATUS_UPDATES + "." + StatusUpdates.STATUS;
645         String CONCRETE_STATUS_TIMESTAMP = Tables.STATUS_UPDATES + "."
646                 + StatusUpdates.STATUS_TIMESTAMP;
647         String CONCRETE_STATUS_RES_PACKAGE = Tables.STATUS_UPDATES + "."
648                 + StatusUpdates.STATUS_RES_PACKAGE;
649         String CONCRETE_STATUS_LABEL = Tables.STATUS_UPDATES + "." + StatusUpdates.STATUS_LABEL;
650         String CONCRETE_STATUS_ICON = Tables.STATUS_UPDATES + "." + StatusUpdates.STATUS_ICON;
651     }
652 
653     public interface ContactsStatusUpdatesColumns {
654         String ALIAS = "contacts_" + Tables.STATUS_UPDATES;
655 
656         String CONCRETE_DATA_ID = ALIAS + "." + StatusUpdatesColumns.DATA_ID;
657 
658         String CONCRETE_PRESENCE = ALIAS + "." + StatusUpdates.PRESENCE;
659         String CONCRETE_STATUS = ALIAS + "." + StatusUpdates.STATUS;
660         String CONCRETE_STATUS_TIMESTAMP = ALIAS + "." + StatusUpdates.STATUS_TIMESTAMP;
661         String CONCRETE_STATUS_RES_PACKAGE = ALIAS + "." + StatusUpdates.STATUS_RES_PACKAGE;
662         String CONCRETE_STATUS_LABEL = ALIAS + "." + StatusUpdates.STATUS_LABEL;
663         String CONCRETE_STATUS_ICON = ALIAS + "." + StatusUpdates.STATUS_ICON;
664     }
665 
666     public interface StreamItemsColumns {
667         final String CONCRETE_ID = Tables.STREAM_ITEMS + "." + BaseColumns._ID;
668         final String CONCRETE_RAW_CONTACT_ID =
669                 Tables.STREAM_ITEMS + "." + StreamItems.RAW_CONTACT_ID;
670         final String CONCRETE_PACKAGE = Tables.STREAM_ITEMS + "." + StreamItems.RES_PACKAGE;
671         final String CONCRETE_ICON = Tables.STREAM_ITEMS + "." + StreamItems.RES_ICON;
672         final String CONCRETE_LABEL = Tables.STREAM_ITEMS + "." + StreamItems.RES_LABEL;
673         final String CONCRETE_TEXT = Tables.STREAM_ITEMS + "." + StreamItems.TEXT;
674         final String CONCRETE_TIMESTAMP = Tables.STREAM_ITEMS + "." + StreamItems.TIMESTAMP;
675         final String CONCRETE_COMMENTS = Tables.STREAM_ITEMS + "." + StreamItems.COMMENTS;
676         final String CONCRETE_SYNC1 = Tables.STREAM_ITEMS + "." + StreamItems.SYNC1;
677         final String CONCRETE_SYNC2 = Tables.STREAM_ITEMS + "." + StreamItems.SYNC2;
678         final String CONCRETE_SYNC3 = Tables.STREAM_ITEMS + "." + StreamItems.SYNC3;
679         final String CONCRETE_SYNC4 = Tables.STREAM_ITEMS + "." + StreamItems.SYNC4;
680     }
681 
682     public interface StreamItemPhotosColumns {
683         final String CONCRETE_ID = Tables.STREAM_ITEM_PHOTOS + "." + BaseColumns._ID;
684         final String CONCRETE_STREAM_ITEM_ID = Tables.STREAM_ITEM_PHOTOS + "."
685                 + StreamItemPhotos.STREAM_ITEM_ID;
686         final String CONCRETE_SORT_INDEX =
687                 Tables.STREAM_ITEM_PHOTOS + "." + StreamItemPhotos.SORT_INDEX;
688         final String CONCRETE_PHOTO_FILE_ID = Tables.STREAM_ITEM_PHOTOS + "."
689                 + StreamItemPhotos.PHOTO_FILE_ID;
690         final String CONCRETE_SYNC1 = Tables.STREAM_ITEM_PHOTOS + "." + StreamItemPhotos.SYNC1;
691         final String CONCRETE_SYNC2 = Tables.STREAM_ITEM_PHOTOS + "." + StreamItemPhotos.SYNC2;
692         final String CONCRETE_SYNC3 = Tables.STREAM_ITEM_PHOTOS + "." + StreamItemPhotos.SYNC3;
693         final String CONCRETE_SYNC4 = Tables.STREAM_ITEM_PHOTOS + "." + StreamItemPhotos.SYNC4;
694     }
695 
696     public interface PhotoFilesColumns {
697         String CONCRETE_ID = Tables.PHOTO_FILES + "." + BaseColumns._ID;
698         String CONCRETE_HEIGHT = Tables.PHOTO_FILES + "." + PhotoFiles.HEIGHT;
699         String CONCRETE_WIDTH = Tables.PHOTO_FILES + "." + PhotoFiles.WIDTH;
700         String CONCRETE_FILESIZE = Tables.PHOTO_FILES + "." + PhotoFiles.FILESIZE;
701     }
702 
703     public interface AccountsColumns extends BaseColumns {
704         String CONCRETE_ID = Tables.ACCOUNTS + "." + BaseColumns._ID;
705 
706         String ACCOUNT_NAME = RawContacts.ACCOUNT_NAME;
707         String ACCOUNT_TYPE = RawContacts.ACCOUNT_TYPE;
708         String DATA_SET = RawContacts.DATA_SET;
709 
710         String CONCRETE_ACCOUNT_NAME = Tables.ACCOUNTS + "." + ACCOUNT_NAME;
711         String CONCRETE_ACCOUNT_TYPE = Tables.ACCOUNTS + "." + ACCOUNT_TYPE;
712         String CONCRETE_DATA_SET = Tables.ACCOUNTS + "." + DATA_SET;
713     }
714 
715     public interface DirectoryColumns {
716         public static final String TYPE_RESOURCE_NAME = "typeResourceName";
717     }
718 
719     public interface SearchIndexColumns {
720         public static final String CONTACT_ID = "contact_id";
721         public static final String CONTENT = "content";
722         public static final String NAME = "name";
723         public static final String TOKENS = "tokens";
724     }
725 
726     public interface PreAuthorizedUris {
727         public static final String _ID = BaseColumns._ID;
728         public static final String URI = "uri";
729         public static final String EXPIRATION = "expiration";
730     }
731 
732     /**
733      * Private table for calculating per-contact-method ranking.
734      */
735     public interface DataUsageStatColumns {
736         /** type: INTEGER (long) */
737         public static final String _ID = "stat_id";
738         public static final String CONCRETE_ID = Tables.DATA_USAGE_STAT + "." + _ID;
739 
740         /** type: INTEGER (long) */
741         public static final String DATA_ID = "data_id";
742         public static final String CONCRETE_DATA_ID = Tables.DATA_USAGE_STAT + "." + DATA_ID;
743 
744         /** type: INTEGER (long) */
745         public static final String RAW_LAST_TIME_USED = Data.RAW_LAST_TIME_USED;
746         public static final String LR_LAST_TIME_USED = Data.LR_LAST_TIME_USED;
747 
748         /** type: INTEGER */
749         public static final String RAW_TIMES_USED = Data.RAW_TIMES_USED;
750         public static final String LR_TIMES_USED = Data.LR_TIMES_USED;
751 
752         public static final String CONCRETE_RAW_LAST_TIME_USED =
753                 Tables.DATA_USAGE_STAT + "." + RAW_LAST_TIME_USED;
754 
755         public static final String CONCRETE_RAW_TIMES_USED =
756                 Tables.DATA_USAGE_STAT + "." + RAW_TIMES_USED;
757 
758         public static final String CONCRETE_LR_LAST_TIME_USED =
759                 Tables.DATA_USAGE_STAT + "." + LR_LAST_TIME_USED;
760 
761         public static final String CONCRETE_LR_TIMES_USED =
762                 Tables.DATA_USAGE_STAT + "." + LR_TIMES_USED;
763 
764         /** type: INTEGER */
765         public static final String USAGE_TYPE_INT = "usage_type";
766         public static final String CONCRETE_USAGE_TYPE =
767                 Tables.DATA_USAGE_STAT + "." + USAGE_TYPE_INT;
768 
769         /**
770          * Integer values for USAGE_TYPE.
771          *
772          * @see android.provider.ContactsContract.DataUsageFeedback#USAGE_TYPE
773          */
774         public static final int USAGE_TYPE_INT_CALL = 0;
775         public static final int USAGE_TYPE_INT_LONG_TEXT = 1;
776         public static final int USAGE_TYPE_INT_SHORT_TEXT = 2;
777     }
778 
779     public interface MetadataSyncColumns {
780         static final String CONCRETE_ID = Tables.METADATA_SYNC + "._id";
781         static final String ACCOUNT_ID = "account_id";
782         static final String CONCRETE_BACKUP_ID = Tables.METADATA_SYNC + "." +
783                 MetadataSync.RAW_CONTACT_BACKUP_ID;
784         static final String CONCRETE_ACCOUNT_ID = Tables.METADATA_SYNC + "." + ACCOUNT_ID;
785         static final String CONCRETE_DELETED = Tables.METADATA_SYNC + "." +
786                 MetadataSync.DELETED;
787     }
788 
789     public interface MetadataSyncStateColumns {
790         static final String CONCRETE_ID = Tables.METADATA_SYNC_STATE + "._id";
791         static final String ACCOUNT_ID = "account_id";
792         static final String CONCRETE_ACCOUNT_ID = Tables.METADATA_SYNC_STATE + "." + ACCOUNT_ID;
793     }
794 
795     private  interface EmailQuery {
796         public static final String TABLE = Tables.DATA;
797 
798         public static final String SELECTION =
799                 DataColumns.MIMETYPE_ID + "=? AND " + Data.DATA1 + " NOT NULL";
800 
801         public static final String COLUMNS[] = {
802                 Email._ID,
803                 Email.RAW_CONTACT_ID,
804                 Email.ADDRESS};
805 
806         public static final int ID = 0;
807         public static final int RAW_CONTACT_ID = 1;
808         public static final int ADDRESS = 2;
809     }
810 
811     private interface StructuredNameQuery {
812         public static final String TABLE = Tables.DATA;
813 
814         public static final String SELECTION =
815                 DataColumns.MIMETYPE_ID + "=? AND " + Data.DATA1 + " NOT NULL";
816 
817         public static final String COLUMNS[] = {
818                 StructuredName._ID,
819                 StructuredName.RAW_CONTACT_ID,
820                 StructuredName.DISPLAY_NAME,
821         };
822 
823         public static final int ID = 0;
824         public static final int RAW_CONTACT_ID = 1;
825         public static final int DISPLAY_NAME = 2;
826     }
827 
828     private interface NicknameQuery {
829         public static final String TABLE = Tables.DATA;
830 
831         public static final String SELECTION =
832                 DataColumns.MIMETYPE_ID + "=? AND " + Data.DATA1 + " NOT NULL";
833 
834         public static final String COLUMNS[] = {
835                 Nickname._ID,
836                 Nickname.RAW_CONTACT_ID,
837                 Nickname.NAME};
838 
839         public static final int ID = 0;
840         public static final int RAW_CONTACT_ID = 1;
841         public static final int NAME = 2;
842     }
843 
844     private interface RawContactNameQuery {
845         public static final String RAW_SQL =
846                 "SELECT "
847                         + DataColumns.MIMETYPE_ID + ","
848                         + Data.IS_PRIMARY + ","
849                         + Data.DATA1 + ","
850                         + Data.DATA2 + ","
851                         + Data.DATA3 + ","
852                         + Data.DATA4 + ","
853                         + Data.DATA5 + ","
854                         + Data.DATA6 + ","
855                         + Data.DATA7 + ","
856                         + Data.DATA8 + ","
857                         + Data.DATA9 + ","
858                         + Data.DATA10 + ","
859                         + Data.DATA11 +
860                 " FROM " + Tables.DATA +
861                 " WHERE " + Data.RAW_CONTACT_ID + "=?" +
862                         " AND (" + Data.DATA1 + " NOT NULL OR " +
863                                 Data.DATA8 + " NOT NULL OR " +
864                                 Data.DATA9 + " NOT NULL OR " +
865                                 Data.DATA10 + " NOT NULL OR " +  // Phonetic name not empty
866                                 Organization.TITLE + " NOT NULL)";
867 
868         public static final int MIMETYPE = 0;
869         public static final int IS_PRIMARY = 1;
870         public static final int DATA1 = 2;
871         public static final int GIVEN_NAME = 3;                         // data2
872         public static final int FAMILY_NAME = 4;                        // data3
873         public static final int PREFIX = 5;                             // data4
874         public static final int TITLE = 5;                              // data4
875         public static final int MIDDLE_NAME = 6;                        // data5
876         public static final int SUFFIX = 7;                             // data6
877         public static final int PHONETIC_GIVEN_NAME = 8;                // data7
878         public static final int PHONETIC_MIDDLE_NAME = 9;               // data8
879         public static final int ORGANIZATION_PHONETIC_NAME = 9;         // data8
880         public static final int PHONETIC_FAMILY_NAME = 10;              // data9
881         public static final int FULL_NAME_STYLE = 11;                   // data10
882         public static final int ORGANIZATION_PHONETIC_NAME_STYLE = 11;  // data10
883         public static final int PHONETIC_NAME_STYLE = 12;               // data11
884     }
885 
886     public final static class NameLookupType {
887         public static final int NAME_EXACT = 0;
888         public static final int NAME_VARIANT = 1;
889         public static final int NAME_COLLATION_KEY = 2;
890         public static final int NICKNAME = 3;
891         public static final int EMAIL_BASED_NICKNAME = 4;
892 
893         // The highest name-lookup type plus one.
894         public static final int TYPE_COUNT = 5;
895 
isBasedOnStructuredName(int nameLookupType)896         public static boolean isBasedOnStructuredName(int nameLookupType) {
897             return nameLookupType == NameLookupType.NAME_EXACT
898                     || nameLookupType == NameLookupType.NAME_VARIANT
899                     || nameLookupType == NameLookupType.NAME_COLLATION_KEY;
900         }
901     }
902 
903     private class StructuredNameLookupBuilder extends NameLookupBuilder {
904         // NOTE(gilad): Is in intentional that we don't use the declaration on L960?
905         private final SQLiteStatement mNameLookupInsert;
906         private final CommonNicknameCache mCommonNicknameCache;
907 
StructuredNameLookupBuilder(NameSplitter splitter, CommonNicknameCache commonNicknameCache, SQLiteStatement nameLookupInsert)908         public StructuredNameLookupBuilder(NameSplitter splitter,
909                 CommonNicknameCache commonNicknameCache, SQLiteStatement nameLookupInsert) {
910 
911             super(splitter);
912             this.mCommonNicknameCache = commonNicknameCache;
913             this.mNameLookupInsert = nameLookupInsert;
914         }
915 
916         @Override
insertNameLookup( long rawContactId, long dataId, int lookupType, String name)917         protected void insertNameLookup(
918                 long rawContactId, long dataId, int lookupType, String name) {
919 
920             if (!TextUtils.isEmpty(name)) {
921                 ContactsDatabaseHelper.this.insertNormalizedNameLookup(
922                         mNameLookupInsert, rawContactId, dataId, lookupType, name);
923             }
924         }
925 
926         @Override
getCommonNicknameClusters(String normalizedName)927         protected String[] getCommonNicknameClusters(String normalizedName) {
928             return mCommonNicknameCache.getCommonNicknameClusters(normalizedName);
929         }
930     }
931 
932     /** Placeholder for the methods to build the "low-res" SQL expressions. */
933     @VisibleForTesting
934     interface LowRes {
935         /** To be replaced with a real column name.  Only used within this interface. */
936         String TEMPLATE_PLACEHOLDER = "XX";
937 
938         /**
939          * To be replaced with a constant in the expression.
940          * Only used within this interface.
941          */
942         String CONSTANT_PLACEHOLDER = "YY";
943 
944         /** Only used within this interface. */
945         int TIMES_USED_GRANULARITY = 10;
946 
947         /** Only used within this interface. */
948         int LAST_TIME_USED_GRANULARITY = 24 * 60 * 60;
949 
950         /**
951          * Template to build the "low-res times used/contacted".  Only used within this interface.
952          * The outermost cast is needed to tell SQLite that the result is of the integer type.
953          */
954         String TEMPLATE_TIMES_USED =
955                 ("cast(ifnull((case when (XX) <= 0 then 0"
956                 + " when (XX) < (YY) then (XX)"
957                 + " else (cast((XX) as int) / (YY)) * (YY) end), 0) as int)")
958                 .replaceAll(CONSTANT_PLACEHOLDER, String.valueOf(TIMES_USED_GRANULARITY));
959 
960         /**
961          * Template to build the "low-res last time used/contacted".
962          * Only used within this interface.
963          * The outermost cast is needed to tell SQLite that the result is of the integer type.
964          */
965         String TEMPLATE_LAST_TIME_USED =
966                 ("cast((cast((XX) as int) / (YY)) * (YY) as int)")
967                 .replaceAll(CONSTANT_PLACEHOLDER, String.valueOf(LAST_TIME_USED_GRANULARITY));
968 
969         /**
970          * Build the SQL expression for the "low-res times used/contacted" expression from the
971          * give column name.
972          */
getTimesUsedExpression(String column)973         static String getTimesUsedExpression(String column) {
974             return TEMPLATE_TIMES_USED.replaceAll(TEMPLATE_PLACEHOLDER, column);
975         }
976 
977         /**
978          * Build the SQL expression for the "low-res last time used/contacted" expression from the
979          * give column name.
980          */
getLastTimeUsedExpression(String column)981         static String getLastTimeUsedExpression(String column) {
982             return TEMPLATE_LAST_TIME_USED.replaceAll(TEMPLATE_PLACEHOLDER, column);
983         }
984     }
985 
986     private static final String TAG = "ContactsDatabaseHelper";
987 
988     private static final String DATABASE_NAME = "contacts2.db";
989 
990     private static ContactsDatabaseHelper sSingleton = null;
991 
992     /** In-memory map of commonly found MIME-types to their ids in the MIMETYPES table */
993     @VisibleForTesting
994     final ArrayMap<String, Long> mCommonMimeTypeIdsCache = new ArrayMap<>();
995 
996     @VisibleForTesting
997     static final String[] COMMON_MIME_TYPES = {
998             Email.CONTENT_ITEM_TYPE,
999             Im.CONTENT_ITEM_TYPE,
1000             Nickname.CONTENT_ITEM_TYPE,
1001             Organization.CONTENT_ITEM_TYPE,
1002             Phone.CONTENT_ITEM_TYPE,
1003             SipAddress.CONTENT_ITEM_TYPE,
1004             StructuredName.CONTENT_ITEM_TYPE,
1005             StructuredPostal.CONTENT_ITEM_TYPE,
1006             Identity.CONTENT_ITEM_TYPE,
1007             android.provider.ContactsContract.CommonDataKinds.Photo.CONTENT_ITEM_TYPE,
1008             GroupMembership.CONTENT_ITEM_TYPE,
1009             Note.CONTENT_ITEM_TYPE,
1010             Event.CONTENT_ITEM_TYPE,
1011             Website.CONTENT_ITEM_TYPE,
1012             Relation.CONTENT_ITEM_TYPE,
1013             "vnd.com.google.cursor.item/contact_misc"
1014     };
1015 
1016     private final Context mContext;
1017     private final boolean mDatabaseOptimizationEnabled;
1018     private final boolean mIsTestInstance;
1019     private final SyncStateContentProviderHelper mSyncState;
1020     private final CountryMonitor mCountryMonitor;
1021 
1022     /**
1023      * Time when the DB was created.  It's persisted in {@link DbProperties#DATABASE_TIME_CREATED},
1024      * but loaded into memory so it can be accessed even when the DB is busy.
1025      */
1026     private long mDatabaseCreationTime;
1027 
1028     private MessageDigest mMessageDigest;
1029     {
1030         try {
1031             mMessageDigest = MessageDigest.getInstance("SHA-1");
1032         } catch (NoSuchAlgorithmException e) {
1033             throw new RuntimeException("No such algorithm.", e);
1034         }
1035     }
1036 
1037     private boolean mUseStrictPhoneNumberComparison;
1038 
1039     private String[] mSelectionArgs1 = new String[1];
1040     private NameSplitter.Name mName = new NameSplitter.Name();
1041     private CharArrayBuffer mCharArrayBuffer = new CharArrayBuffer(128);
1042     private NameSplitter mNameSplitter;
1043 
getInstance(Context context)1044     public static synchronized ContactsDatabaseHelper getInstance(Context context) {
1045         if (sSingleton == null) {
1046             sSingleton = new ContactsDatabaseHelper(context, DATABASE_NAME, true,
1047                     /* isTestInstance=*/ false);
1048         }
1049         return sSingleton;
1050     }
1051 
1052     /**
1053      * Returns a new instance for unit tests.
1054      */
1055     @NeededForTesting
getNewInstanceForTest(Context context, String filename)1056     public static ContactsDatabaseHelper getNewInstanceForTest(Context context, String filename) {
1057         return new ContactsDatabaseHelper(context, filename, false, /* isTestInstance=*/ true);
1058     }
1059 
ContactsDatabaseHelper( Context context, String databaseName, boolean optimizationEnabled, boolean isTestInstance)1060     protected ContactsDatabaseHelper(
1061             Context context, String databaseName, boolean optimizationEnabled,
1062             boolean isTestInstance) {
1063         super(context, databaseName, null, DATABASE_VERSION, MINIMUM_SUPPORTED_VERSION, null);
1064         boolean enableWal = android.provider.Settings.Global.getInt(context.getContentResolver(),
1065                 android.provider.Settings.Global.CONTACTS_DATABASE_WAL_ENABLED, 1) == 1;
1066         if (dbForProfile() != 0 || ActivityManager.isLowRamDeviceStatic()) {
1067             enableWal = false;
1068         }
1069         setWriteAheadLoggingEnabled(enableWal);
1070         // Memory optimization - close idle connections after 30s of inactivity
1071         setIdleConnectionTimeout(IDLE_CONNECTION_TIMEOUT_MS);
1072         mDatabaseOptimizationEnabled = optimizationEnabled;
1073         mIsTestInstance = isTestInstance;
1074         Resources resources = context.getResources();
1075         mContext = context;
1076         mSyncState = new SyncStateContentProviderHelper();
1077         mCountryMonitor = new CountryMonitor(context);
1078         mUseStrictPhoneNumberComparison = resources.getBoolean(
1079                 com.android.internal.R.bool.config_use_strict_phone_number_comparation);
1080     }
1081 
getDatabase(boolean writable)1082     public SQLiteDatabase getDatabase(boolean writable) {
1083         return writable ? getWritableDatabase() : getReadableDatabase();
1084     }
1085 
1086     /**
1087      * Populate ids of known mimetypes into a map for easy access
1088      *
1089      * @param db target database
1090      */
prepopulateCommonMimeTypes(SQLiteDatabase db)1091     private void prepopulateCommonMimeTypes(SQLiteDatabase db) {
1092         mCommonMimeTypeIdsCache.clear();
1093         for(String commonMimeType: COMMON_MIME_TYPES) {
1094             mCommonMimeTypeIdsCache.put(commonMimeType, insertMimeType(db, commonMimeType));
1095         }
1096     }
1097 
1098     @Override
onBeforeDelete(SQLiteDatabase db)1099     public void onBeforeDelete(SQLiteDatabase db) {
1100         Log.w(TAG, "Database version " + db.getVersion() + " for " + DATABASE_NAME
1101                 + " is no longer supported. Data will be lost on upgrading to " + DATABASE_VERSION);
1102     }
1103 
1104     @Override
onOpen(SQLiteDatabase db)1105     public void onOpen(SQLiteDatabase db) {
1106         Log.d(TAG, "WAL enabled for " + getDatabaseName() + ": " + db.isWriteAheadLoggingEnabled());
1107         prepopulateCommonMimeTypes(db);
1108         mSyncState.onDatabaseOpened(db);
1109         // Deleting any state from the presence tables to mimic their behavior from the time they
1110         // were in-memory tables
1111         db.execSQL("DELETE FROM " + Tables.PRESENCE + ";");
1112         db.execSQL("DELETE FROM " + Tables.AGGREGATED_PRESENCE + ";");
1113 
1114         loadDatabaseCreationTime(db);
1115     }
1116 
setDatabaseCreationTime(SQLiteDatabase db)1117     protected void setDatabaseCreationTime(SQLiteDatabase db) {
1118         // Note we don't do this in the profile DB helper.
1119         mDatabaseCreationTime = System.currentTimeMillis();
1120         PropertyUtils.setProperty(db, DbProperties.DATABASE_TIME_CREATED, String.valueOf(
1121                 mDatabaseCreationTime));
1122     }
1123 
loadDatabaseCreationTime(SQLiteDatabase db)1124     protected void loadDatabaseCreationTime(SQLiteDatabase db) {
1125         // Note we don't do this in the profile DB helper.
1126 
1127         mDatabaseCreationTime = 0;
1128         final String timestamp = PropertyUtils.getProperty(db,
1129                 DbProperties.DATABASE_TIME_CREATED, "");
1130         if (!TextUtils.isEmpty(timestamp)) {
1131             try {
1132                 mDatabaseCreationTime = Long.parseLong(timestamp);
1133             } catch (NumberFormatException e) {
1134                 Log.w(TAG, "Failed to parse timestamp: " + timestamp);
1135             }
1136         }
1137         if (AbstractContactsProvider.VERBOSE_LOGGING) {
1138             Log.v(TAG, "Open: creation time=" + mDatabaseCreationTime);
1139         }
1140         if (mDatabaseCreationTime == 0) {
1141             Log.w(TAG, "Unable to load creating time; resetting.");
1142             // Hmm, failed to load the timestamp.  Just set the current time then.
1143             mDatabaseCreationTime = System.currentTimeMillis();
1144             PropertyUtils.setProperty(db,
1145                     DbProperties.DATABASE_TIME_CREATED, Long.toString(mDatabaseCreationTime));
1146         }
1147     }
1148 
createPresenceTables(SQLiteDatabase db)1149     private void createPresenceTables(SQLiteDatabase db) {
1150         db.execSQL("CREATE TABLE IF NOT EXISTS " + Tables.PRESENCE + " ("+
1151                 StatusUpdates.DATA_ID + " INTEGER PRIMARY KEY REFERENCES data(_id)," +
1152                 StatusUpdates.PROTOCOL + " INTEGER NOT NULL," +
1153                 StatusUpdates.CUSTOM_PROTOCOL + " TEXT," +
1154                 StatusUpdates.IM_HANDLE + " TEXT," +
1155                 StatusUpdates.IM_ACCOUNT + " TEXT," +
1156                 PresenceColumns.CONTACT_ID + " INTEGER REFERENCES contacts(_id)," +
1157                 PresenceColumns.RAW_CONTACT_ID + " INTEGER REFERENCES raw_contacts(_id)," +
1158                 StatusUpdates.PRESENCE + " INTEGER," +
1159                 StatusUpdates.CHAT_CAPABILITY + " INTEGER NOT NULL DEFAULT 0," +
1160                 "UNIQUE(" + StatusUpdates.PROTOCOL + ", " + StatusUpdates.CUSTOM_PROTOCOL
1161                     + ", " + StatusUpdates.IM_HANDLE + ", " + StatusUpdates.IM_ACCOUNT + ")" +
1162         ");");
1163 
1164         db.execSQL("CREATE INDEX IF NOT EXISTS presenceIndex" + " ON "
1165                 + Tables.PRESENCE + " (" + PresenceColumns.RAW_CONTACT_ID + ");");
1166         db.execSQL("CREATE INDEX IF NOT EXISTS presenceIndex2" + " ON "
1167                 + Tables.PRESENCE + " (" + PresenceColumns.CONTACT_ID + ");");
1168 
1169         db.execSQL("CREATE TABLE IF NOT EXISTS "
1170                 + Tables.AGGREGATED_PRESENCE + " ("+
1171                 AggregatedPresenceColumns.CONTACT_ID
1172                         + " INTEGER PRIMARY KEY REFERENCES contacts(_id)," +
1173                 StatusUpdates.PRESENCE + " INTEGER," +
1174                 StatusUpdates.CHAT_CAPABILITY + " INTEGER NOT NULL DEFAULT 0" +
1175         ");");
1176 
1177         db.execSQL("CREATE TRIGGER IF NOT EXISTS " + Tables.PRESENCE + "_deleted"
1178                 + " BEFORE DELETE ON " + Tables.PRESENCE
1179                 + " BEGIN "
1180                 + "   DELETE FROM " + Tables.AGGREGATED_PRESENCE
1181                 + "     WHERE " + AggregatedPresenceColumns.CONTACT_ID + " = " +
1182                         "(SELECT " + PresenceColumns.CONTACT_ID +
1183                         " FROM " + Tables.PRESENCE +
1184                         " WHERE " + PresenceColumns.RAW_CONTACT_ID
1185                                 + "=OLD." + PresenceColumns.RAW_CONTACT_ID +
1186                         " AND NOT EXISTS" +
1187                                 "(SELECT " + PresenceColumns.RAW_CONTACT_ID +
1188                                 " FROM " + Tables.PRESENCE +
1189                                 " WHERE " + PresenceColumns.CONTACT_ID
1190                                         + "=OLD." + PresenceColumns.CONTACT_ID +
1191                                 " AND " + PresenceColumns.RAW_CONTACT_ID
1192                                         + "!=OLD." + PresenceColumns.RAW_CONTACT_ID + "));"
1193                 + " END");
1194 
1195         final String replaceAggregatePresenceSql =
1196                 "INSERT OR REPLACE INTO " + Tables.AGGREGATED_PRESENCE + "("
1197                         + AggregatedPresenceColumns.CONTACT_ID + ", "
1198                         + StatusUpdates.PRESENCE + ", "
1199                         + StatusUpdates.CHAT_CAPABILITY + ")"
1200                 + " SELECT "
1201                         + PresenceColumns.CONTACT_ID + ","
1202                         + StatusUpdates.PRESENCE + ","
1203                         + StatusUpdates.CHAT_CAPABILITY
1204                 + " FROM " + Tables.PRESENCE
1205                 + " WHERE "
1206                     + " (ifnull(" + StatusUpdates.PRESENCE + ",0)  * 10 "
1207                             + "+ ifnull(" + StatusUpdates.CHAT_CAPABILITY + ", 0))"
1208                     + " = (SELECT "
1209                         + "MAX (ifnull(" + StatusUpdates.PRESENCE + ",0)  * 10 "
1210                                 + "+ ifnull(" + StatusUpdates.CHAT_CAPABILITY + ", 0))"
1211                         + " FROM " + Tables.PRESENCE
1212                         + " WHERE " + PresenceColumns.CONTACT_ID
1213                             + "=NEW." + PresenceColumns.CONTACT_ID
1214                     + ")"
1215                 + " AND " + PresenceColumns.CONTACT_ID + "=NEW." + PresenceColumns.CONTACT_ID + ";";
1216 
1217         db.execSQL("CREATE TRIGGER IF NOT EXISTS " + Tables.PRESENCE + "_inserted"
1218                 + " AFTER INSERT ON " + Tables.PRESENCE
1219                 + " BEGIN "
1220                 + replaceAggregatePresenceSql
1221                 + " END");
1222 
1223         db.execSQL("CREATE TRIGGER IF NOT EXISTS " + Tables.PRESENCE + "_updated"
1224                 + " AFTER UPDATE ON " + Tables.PRESENCE
1225                 + " BEGIN "
1226                 + replaceAggregatePresenceSql
1227                 + " END");
1228     }
1229 
1230     @Override
onCreate(SQLiteDatabase db)1231     public void onCreate(SQLiteDatabase db) {
1232         Log.i(TAG, "Bootstrapping database " + DATABASE_NAME + " version: " + DATABASE_VERSION);
1233 
1234         mSyncState.createDatabase(db);
1235 
1236         // Create the properties table first so the create time is available as soon as possible.
1237         // The create time is needed by BOOT_COMPLETE to send broadcasts.
1238         PropertyUtils.createPropertiesTable(db);
1239 
1240         setDatabaseCreationTime(db);
1241 
1242         db.execSQL("CREATE TABLE " + Tables.ACCOUNTS + " (" +
1243                 AccountsColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
1244                 AccountsColumns.ACCOUNT_NAME + " TEXT, " +
1245                 AccountsColumns.ACCOUNT_TYPE + " TEXT, " +
1246                 AccountsColumns.DATA_SET + " TEXT" +
1247         ");");
1248 
1249         // Note, there are two sets of the usage stat columns: LR_* and RAW_*.
1250         // RAW_* contain the real values, which clients can't access.  The column names start
1251         // with a special prefix, which clients are prohibited from using in queries (including
1252         // "where" of deletes/updates.)
1253         // The LR_* columns have the original, public names.  The views have the LR columns too,
1254         // which contain the "low-res" numbers.  The tables, though, do *not* have to have these
1255         // columns, because we won't use them anyway.  However, because old versions of the tables
1256         // had those columns, and SQLite doesn't allow removing existing columns, meaning upgraded
1257         // tables will have these LR_* columns anyway.  So, in order to make a new database look
1258         // the same as an upgraded database, we create the LR columns in a new database too.
1259         // Otherwise, we would easily end up with writing SQLs that will run fine in a new DB
1260         // but not in an upgraded database, and because all unit tests will run with a new database,
1261         // we can't easily catch these sort of issues.
1262 
1263         // One row per group of contacts corresponding to the same person
1264         db.execSQL("CREATE TABLE " + Tables.CONTACTS + " (" +
1265                 BaseColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
1266                 Contacts.NAME_RAW_CONTACT_ID + " INTEGER REFERENCES raw_contacts(_id)," +
1267                 Contacts.PHOTO_ID + " INTEGER REFERENCES data(_id)," +
1268                 Contacts.PHOTO_FILE_ID + " INTEGER REFERENCES photo_files(_id)," +
1269                 Contacts.CUSTOM_RINGTONE + " TEXT," +
1270                 Contacts.SEND_TO_VOICEMAIL + " INTEGER NOT NULL DEFAULT 0," +
1271 
1272                 Contacts.RAW_TIMES_CONTACTED + " INTEGER NOT NULL DEFAULT 0," +
1273                 Contacts.RAW_LAST_TIME_CONTACTED + " INTEGER," +
1274 
1275                 Contacts.LR_TIMES_CONTACTED + " INTEGER NOT NULL DEFAULT 0," +
1276                 Contacts.LR_LAST_TIME_CONTACTED + " INTEGER," +
1277 
1278                 Contacts.STARRED + " INTEGER NOT NULL DEFAULT 0," +
1279                 Contacts.PINNED + " INTEGER NOT NULL DEFAULT " + PinnedPositions.UNPINNED + "," +
1280                 Contacts.HAS_PHONE_NUMBER + " INTEGER NOT NULL DEFAULT 0," +
1281                 Contacts.LOOKUP_KEY + " TEXT," +
1282                 ContactsColumns.LAST_STATUS_UPDATE_ID + " INTEGER REFERENCES data(_id)," +
1283                 Contacts.CONTACT_LAST_UPDATED_TIMESTAMP + " INTEGER" +
1284         ");");
1285 
1286         ContactsTableUtil.createIndexes(db);
1287 
1288         // deleted_contacts table
1289         DeletedContactsTableUtil.create(db);
1290 
1291         // Raw_contacts table
1292         db.execSQL("CREATE TABLE " + Tables.RAW_CONTACTS + " (" +
1293                 RawContacts._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
1294                 RawContactsColumns.ACCOUNT_ID + " INTEGER REFERENCES " +
1295                     Tables.ACCOUNTS + "(" + AccountsColumns._ID + ")," +
1296                 RawContacts.SOURCE_ID + " TEXT," +
1297                 RawContacts.BACKUP_ID + " TEXT," +
1298                 RawContacts.RAW_CONTACT_IS_READ_ONLY + " INTEGER NOT NULL DEFAULT 0," +
1299                 RawContacts.VERSION + " INTEGER NOT NULL DEFAULT 1," +
1300                 RawContacts.DIRTY + " INTEGER NOT NULL DEFAULT 0," +
1301                 RawContacts.DELETED + " INTEGER NOT NULL DEFAULT 0," +
1302                 RawContacts.METADATA_DIRTY + " INTEGER NOT NULL DEFAULT 0," +
1303                 RawContacts.CONTACT_ID + " INTEGER REFERENCES contacts(_id)," +
1304                 RawContacts.AGGREGATION_MODE + " INTEGER NOT NULL DEFAULT " +
1305                         RawContacts.AGGREGATION_MODE_DEFAULT + "," +
1306                 RawContactsColumns.AGGREGATION_NEEDED + " INTEGER NOT NULL DEFAULT 1," +
1307                 RawContacts.CUSTOM_RINGTONE + " TEXT," +
1308                 RawContacts.SEND_TO_VOICEMAIL + " INTEGER NOT NULL DEFAULT 0," +
1309 
1310                 RawContacts.RAW_TIMES_CONTACTED + " INTEGER NOT NULL DEFAULT 0," +
1311                 RawContacts.RAW_LAST_TIME_CONTACTED + " INTEGER," +
1312 
1313                 RawContacts.LR_TIMES_CONTACTED + " INTEGER NOT NULL DEFAULT 0," +
1314                 RawContacts.LR_LAST_TIME_CONTACTED + " INTEGER," +
1315 
1316                 RawContacts.STARRED + " INTEGER NOT NULL DEFAULT 0," +
1317                 RawContacts.PINNED + " INTEGER NOT NULL DEFAULT "  + PinnedPositions.UNPINNED +
1318                     "," + RawContacts.DISPLAY_NAME_PRIMARY + " TEXT," +
1319                 RawContacts.DISPLAY_NAME_ALTERNATIVE + " TEXT," +
1320                 RawContacts.DISPLAY_NAME_SOURCE + " INTEGER NOT NULL DEFAULT " +
1321                         DisplayNameSources.UNDEFINED + "," +
1322                 RawContacts.PHONETIC_NAME + " TEXT," +
1323                 // TODO: PHONETIC_NAME_STYLE should be INTEGER. There is a
1324                 // mismatch between how the column is created here (TEXT) and
1325                 // how it is created in upgradeToVersion205 (INTEGER).
1326                 RawContacts.PHONETIC_NAME_STYLE + " TEXT," +
1327                 RawContacts.SORT_KEY_PRIMARY + " TEXT COLLATE " +
1328                         ContactsProvider2.PHONEBOOK_COLLATOR_NAME + "," +
1329                 RawContactsColumns.PHONEBOOK_LABEL_PRIMARY + " TEXT," +
1330                 RawContactsColumns.PHONEBOOK_BUCKET_PRIMARY + " INTEGER," +
1331                 RawContacts.SORT_KEY_ALTERNATIVE + " TEXT COLLATE " +
1332                         ContactsProvider2.PHONEBOOK_COLLATOR_NAME + "," +
1333                 RawContactsColumns.PHONEBOOK_LABEL_ALTERNATIVE + " TEXT," +
1334                 RawContactsColumns.PHONEBOOK_BUCKET_ALTERNATIVE + " INTEGER," +
1335                 RawContactsColumns.NAME_VERIFIED_OBSOLETE + " INTEGER NOT NULL DEFAULT 0," +
1336                 RawContacts.SYNC1 + " TEXT, " +
1337                 RawContacts.SYNC2 + " TEXT, " +
1338                 RawContacts.SYNC3 + " TEXT, " +
1339                 RawContacts.SYNC4 + " TEXT " +
1340         ");");
1341 
1342         db.execSQL("CREATE INDEX raw_contacts_contact_id_index ON " + Tables.RAW_CONTACTS + " (" +
1343                 RawContacts.CONTACT_ID +
1344         ");");
1345 
1346         db.execSQL("CREATE INDEX raw_contacts_source_id_account_id_index ON " +
1347                 Tables.RAW_CONTACTS + " (" +
1348                 RawContacts.SOURCE_ID + ", " +
1349                 RawContactsColumns.ACCOUNT_ID +
1350         ");");
1351 
1352         db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS raw_contacts_backup_id_account_id_index ON " +
1353                 Tables.RAW_CONTACTS + " (" +
1354                 RawContacts.BACKUP_ID + ", " +
1355                 RawContactsColumns.ACCOUNT_ID +
1356         ");");
1357 
1358         db.execSQL("CREATE TABLE " + Tables.STREAM_ITEMS + " (" +
1359                 StreamItems._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
1360                 StreamItems.RAW_CONTACT_ID + " INTEGER NOT NULL, " +
1361                 StreamItems.RES_PACKAGE + " TEXT, " +
1362                 StreamItems.RES_ICON + " TEXT, " +
1363                 StreamItems.RES_LABEL + " TEXT, " +
1364                 StreamItems.TEXT + " TEXT, " +
1365                 StreamItems.TIMESTAMP + " INTEGER NOT NULL, " +
1366                 StreamItems.COMMENTS + " TEXT, " +
1367                 StreamItems.SYNC1 + " TEXT, " +
1368                 StreamItems.SYNC2 + " TEXT, " +
1369                 StreamItems.SYNC3 + " TEXT, " +
1370                 StreamItems.SYNC4 + " TEXT, " +
1371                 "FOREIGN KEY(" + StreamItems.RAW_CONTACT_ID + ") REFERENCES " +
1372                         Tables.RAW_CONTACTS + "(" + RawContacts._ID + "));");
1373 
1374         db.execSQL("CREATE TABLE " + Tables.STREAM_ITEM_PHOTOS + " (" +
1375                 StreamItemPhotos._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
1376                 StreamItemPhotos.STREAM_ITEM_ID + " INTEGER NOT NULL, " +
1377                 StreamItemPhotos.SORT_INDEX + " INTEGER, " +
1378                 StreamItemPhotos.PHOTO_FILE_ID + " INTEGER NOT NULL, " +
1379                 StreamItemPhotos.SYNC1 + " TEXT, " +
1380                 StreamItemPhotos.SYNC2 + " TEXT, " +
1381                 StreamItemPhotos.SYNC3 + " TEXT, " +
1382                 StreamItemPhotos.SYNC4 + " TEXT, " +
1383                 "FOREIGN KEY(" + StreamItemPhotos.STREAM_ITEM_ID + ") REFERENCES " +
1384                         Tables.STREAM_ITEMS + "(" + StreamItems._ID + "));");
1385 
1386         db.execSQL("CREATE TABLE " + Tables.PHOTO_FILES + " (" +
1387                 PhotoFiles._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
1388                 PhotoFiles.HEIGHT + " INTEGER NOT NULL, " +
1389                 PhotoFiles.WIDTH + " INTEGER NOT NULL, " +
1390                 PhotoFiles.FILESIZE + " INTEGER NOT NULL);");
1391 
1392         // TODO readd the index and investigate a controlled use of it
1393 //        db.execSQL("CREATE INDEX raw_contacts_agg_index ON " + Tables.RAW_CONTACTS + " (" +
1394 //                RawContactsColumns.AGGREGATION_NEEDED +
1395 //        ");");
1396 
1397         // Package name mapping table
1398         db.execSQL("CREATE TABLE " + Tables.PACKAGES + " (" +
1399                 PackagesColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
1400                 PackagesColumns.PACKAGE + " TEXT NOT NULL" +
1401         ");");
1402 
1403         // Mimetype mapping table
1404         db.execSQL("CREATE TABLE " + Tables.MIMETYPES + " (" +
1405                 MimetypesColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
1406                 MimetypesColumns.MIMETYPE + " TEXT NOT NULL" +
1407         ");");
1408 
1409         // Mimetype table requires an index on mime type
1410         db.execSQL("CREATE UNIQUE INDEX mime_type ON " + Tables.MIMETYPES + " (" +
1411                 MimetypesColumns.MIMETYPE +
1412         ");");
1413 
1414         // Public generic data table
1415         db.execSQL("CREATE TABLE " + Tables.DATA + " (" +
1416                 Data._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
1417                 DataColumns.PACKAGE_ID + " INTEGER REFERENCES package(_id)," +
1418                 DataColumns.MIMETYPE_ID + " INTEGER REFERENCES mimetype(_id) NOT NULL," +
1419                 Data.RAW_CONTACT_ID + " INTEGER REFERENCES raw_contacts(_id) NOT NULL," +
1420                 Data.HASH_ID + " TEXT," +
1421                 Data.IS_READ_ONLY + " INTEGER NOT NULL DEFAULT 0," +
1422                 Data.IS_PRIMARY + " INTEGER NOT NULL DEFAULT 0," +
1423                 Data.IS_SUPER_PRIMARY + " INTEGER NOT NULL DEFAULT 0," +
1424                 Data.DATA_VERSION + " INTEGER NOT NULL DEFAULT 0," +
1425                 Data.DATA1 + " TEXT," +
1426                 Data.DATA2 + " TEXT," +
1427                 Data.DATA3 + " TEXT," +
1428                 Data.DATA4 + " TEXT," +
1429                 Data.DATA5 + " TEXT," +
1430                 Data.DATA6 + " TEXT," +
1431                 Data.DATA7 + " TEXT," +
1432                 Data.DATA8 + " TEXT," +
1433                 Data.DATA9 + " TEXT," +
1434                 Data.DATA10 + " TEXT," +
1435                 Data.DATA11 + " TEXT," +
1436                 Data.DATA12 + " TEXT," +
1437                 Data.DATA13 + " TEXT," +
1438                 Data.DATA14 + " TEXT," +
1439                 Data.DATA15 + " TEXT," +
1440                 Data.SYNC1 + " TEXT, " +
1441                 Data.SYNC2 + " TEXT, " +
1442                 Data.SYNC3 + " TEXT, " +
1443                 Data.SYNC4 + " TEXT, " +
1444                 Data.CARRIER_PRESENCE + " INTEGER NOT NULL DEFAULT 0, " +
1445                 Data.PREFERRED_PHONE_ACCOUNT_COMPONENT_NAME + " TEXT, " +
1446                 Data.PREFERRED_PHONE_ACCOUNT_ID + " TEXT " +
1447         ");");
1448 
1449         db.execSQL("CREATE INDEX data_raw_contact_id ON " + Tables.DATA + " (" +
1450                 Data.RAW_CONTACT_ID +
1451         ");");
1452 
1453         /**
1454          * For email lookup and similar queries.
1455          */
1456         db.execSQL("CREATE INDEX data_mimetype_data1_index ON " + Tables.DATA + " (" +
1457                 DataColumns.MIMETYPE_ID + "," +
1458                 Data.DATA1 +
1459         ");");
1460 
1461         /**
1462          * For contact backup restore queries.
1463          */
1464         db.execSQL("CREATE INDEX IF NOT EXISTS data_hash_id_index ON " + Tables.DATA + " (" +
1465                 Data.HASH_ID +
1466         ");");
1467 
1468 
1469         // Private phone numbers table used for lookup
1470         db.execSQL("CREATE TABLE " + Tables.PHONE_LOOKUP + " (" +
1471                 PhoneLookupColumns.DATA_ID
1472                         + " INTEGER REFERENCES data(_id) NOT NULL," +
1473                 PhoneLookupColumns.RAW_CONTACT_ID
1474                         + " INTEGER REFERENCES raw_contacts(_id) NOT NULL," +
1475                 PhoneLookupColumns.NORMALIZED_NUMBER + " TEXT NOT NULL," +
1476                 PhoneLookupColumns.MIN_MATCH + " TEXT NOT NULL" +
1477         ");");
1478 
1479         db.execSQL("CREATE INDEX phone_lookup_index ON " + Tables.PHONE_LOOKUP + " (" +
1480                 PhoneLookupColumns.NORMALIZED_NUMBER + "," +
1481                 PhoneLookupColumns.RAW_CONTACT_ID + "," +
1482                 PhoneLookupColumns.DATA_ID +
1483         ");");
1484 
1485         db.execSQL("CREATE INDEX phone_lookup_min_match_index ON " + Tables.PHONE_LOOKUP + " (" +
1486                 PhoneLookupColumns.MIN_MATCH + "," +
1487                 PhoneLookupColumns.RAW_CONTACT_ID + "," +
1488                 PhoneLookupColumns.DATA_ID +
1489         ");");
1490 
1491         db.execSQL("CREATE INDEX phone_lookup_data_id_min_match_index ON " + Tables.PHONE_LOOKUP +
1492                 " (" + PhoneLookupColumns.DATA_ID + ", " + PhoneLookupColumns.MIN_MATCH + ");");
1493 
1494         // Private name/nickname table used for lookup.
1495         db.execSQL("CREATE TABLE " + Tables.NAME_LOOKUP + " (" +
1496                 NameLookupColumns.DATA_ID
1497                         + " INTEGER REFERENCES data(_id) NOT NULL," +
1498                 NameLookupColumns.RAW_CONTACT_ID
1499                         + " INTEGER REFERENCES raw_contacts(_id) NOT NULL," +
1500                 NameLookupColumns.NORMALIZED_NAME + " TEXT NOT NULL," +
1501                 NameLookupColumns.NAME_TYPE + " INTEGER NOT NULL," +
1502                 "PRIMARY KEY ("
1503                         + NameLookupColumns.DATA_ID + ", "
1504                         + NameLookupColumns.NORMALIZED_NAME + ", "
1505                         + NameLookupColumns.NAME_TYPE + ")" +
1506         ");");
1507 
1508         db.execSQL("CREATE INDEX name_lookup_raw_contact_id_index ON " + Tables.NAME_LOOKUP + " (" +
1509                 NameLookupColumns.RAW_CONTACT_ID +
1510         ");");
1511 
1512         db.execSQL("CREATE TABLE " + Tables.NICKNAME_LOOKUP + " (" +
1513                 NicknameLookupColumns.NAME + " TEXT," +
1514                 NicknameLookupColumns.CLUSTER + " TEXT" +
1515         ");");
1516 
1517         db.execSQL("CREATE UNIQUE INDEX nickname_lookup_index ON " + Tables.NICKNAME_LOOKUP + " (" +
1518                 NicknameLookupColumns.NAME + ", " +
1519                 NicknameLookupColumns.CLUSTER +
1520         ");");
1521 
1522         // Groups table.
1523         db.execSQL("CREATE TABLE " + Tables.GROUPS + " (" +
1524                 Groups._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
1525                 GroupsColumns.PACKAGE_ID + " INTEGER REFERENCES package(_id)," +
1526                 GroupsColumns.ACCOUNT_ID + " INTEGER REFERENCES " +
1527                     Tables.ACCOUNTS + "(" + AccountsColumns._ID + ")," +
1528                 Groups.SOURCE_ID + " TEXT," +
1529                 Groups.VERSION + " INTEGER NOT NULL DEFAULT 1," +
1530                 Groups.DIRTY + " INTEGER NOT NULL DEFAULT 0," +
1531                 Groups.TITLE + " TEXT," +
1532                 Groups.TITLE_RES + " INTEGER," +
1533                 Groups.NOTES + " TEXT," +
1534                 Groups.SYSTEM_ID + " TEXT," +
1535                 Groups.DELETED + " INTEGER NOT NULL DEFAULT 0," +
1536                 Groups.GROUP_VISIBLE + " INTEGER NOT NULL DEFAULT 0," +
1537                 Groups.SHOULD_SYNC + " INTEGER NOT NULL DEFAULT 1," +
1538                 Groups.AUTO_ADD + " INTEGER NOT NULL DEFAULT 0," +
1539                 Groups.FAVORITES + " INTEGER NOT NULL DEFAULT 0," +
1540                 Groups.GROUP_IS_READ_ONLY + " INTEGER NOT NULL DEFAULT 0," +
1541                 Groups.SYNC1 + " TEXT, " +
1542                 Groups.SYNC2 + " TEXT, " +
1543                 Groups.SYNC3 + " TEXT, " +
1544                 Groups.SYNC4 + " TEXT " +
1545         ");");
1546 
1547         db.execSQL("CREATE INDEX groups_source_id_account_id_index ON " + Tables.GROUPS + " (" +
1548                 Groups.SOURCE_ID + ", " +
1549                 GroupsColumns.ACCOUNT_ID +
1550         ");");
1551 
1552         db.execSQL("CREATE TABLE IF NOT EXISTS " + Tables.AGGREGATION_EXCEPTIONS + " (" +
1553                 AggregationExceptionColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
1554                 AggregationExceptions.TYPE + " INTEGER NOT NULL, " +
1555                 AggregationExceptions.RAW_CONTACT_ID1
1556                         + " INTEGER REFERENCES raw_contacts(_id), " +
1557                 AggregationExceptions.RAW_CONTACT_ID2
1558                         + " INTEGER REFERENCES raw_contacts(_id)" +
1559         ");");
1560 
1561         db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS aggregation_exception_index1 ON " +
1562                 Tables.AGGREGATION_EXCEPTIONS + " (" +
1563                 AggregationExceptions.RAW_CONTACT_ID1 + ", " +
1564                 AggregationExceptions.RAW_CONTACT_ID2 +
1565         ");");
1566 
1567         db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS aggregation_exception_index2 ON " +
1568                 Tables.AGGREGATION_EXCEPTIONS + " (" +
1569                 AggregationExceptions.RAW_CONTACT_ID2 + ", " +
1570                 AggregationExceptions.RAW_CONTACT_ID1 +
1571         ");");
1572 
1573         db.execSQL("CREATE TABLE IF NOT EXISTS " + Tables.SETTINGS + " (" +
1574                 Settings.ACCOUNT_NAME + " STRING NOT NULL," +
1575                 Settings.ACCOUNT_TYPE + " STRING NOT NULL," +
1576                 Settings.DATA_SET + " STRING," +
1577                 Settings.UNGROUPED_VISIBLE + " INTEGER NOT NULL DEFAULT 0," +
1578                 Settings.SHOULD_SYNC + " INTEGER NOT NULL DEFAULT 1" +
1579         ");");
1580 
1581         db.execSQL("CREATE TABLE " + Tables.VISIBLE_CONTACTS + " (" +
1582                 Contacts._ID + " INTEGER PRIMARY KEY" +
1583         ");");
1584 
1585         db.execSQL("CREATE TABLE " + Tables.DEFAULT_DIRECTORY + " (" +
1586                 Contacts._ID + " INTEGER PRIMARY KEY" +
1587         ");");
1588 
1589         db.execSQL("CREATE TABLE " + Tables.STATUS_UPDATES + " (" +
1590                 StatusUpdatesColumns.DATA_ID + " INTEGER PRIMARY KEY REFERENCES data(_id)," +
1591                 StatusUpdates.STATUS + " TEXT," +
1592                 StatusUpdates.STATUS_TIMESTAMP + " INTEGER," +
1593                 StatusUpdates.STATUS_RES_PACKAGE + " TEXT, " +
1594                 StatusUpdates.STATUS_LABEL + " INTEGER, " +
1595                 StatusUpdates.STATUS_ICON + " INTEGER" +
1596         ");");
1597 
1598         createDirectoriesTable(db);
1599         createSearchIndexTable(db, false /* we build stats table later */);
1600 
1601         db.execSQL("CREATE TABLE " + Tables.DATA_USAGE_STAT + "(" +
1602                 DataUsageStatColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
1603                 DataUsageStatColumns.DATA_ID + " INTEGER NOT NULL, " +
1604                 DataUsageStatColumns.USAGE_TYPE_INT + " INTEGER NOT NULL DEFAULT 0, " +
1605 
1606                 DataUsageStatColumns.RAW_TIMES_USED + " INTEGER NOT NULL DEFAULT 0, " +
1607                 DataUsageStatColumns.RAW_LAST_TIME_USED + " INTEGER NOT NULL DEFAULT 0, " +
1608 
1609                 DataUsageStatColumns.LR_TIMES_USED + " INTEGER NOT NULL DEFAULT 0, " +
1610                 DataUsageStatColumns.LR_LAST_TIME_USED + " INTEGER NOT NULL DEFAULT 0, " +
1611 
1612                 "FOREIGN KEY(" + DataUsageStatColumns.DATA_ID + ") REFERENCES "
1613                         + Tables.DATA + "(" + Data._ID + ")" +
1614         ");");
1615         db.execSQL("CREATE UNIQUE INDEX data_usage_stat_index ON " +
1616                 Tables.DATA_USAGE_STAT + " (" +
1617                 DataUsageStatColumns.DATA_ID + ", " +
1618                 DataUsageStatColumns.USAGE_TYPE_INT +
1619         ");");
1620 
1621         db.execSQL("CREATE TABLE IF NOT EXISTS "
1622                 + Tables.METADATA_SYNC + " (" +
1623                 MetadataSync._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
1624                 MetadataSync.RAW_CONTACT_BACKUP_ID + " TEXT NOT NULL," +
1625                 MetadataSyncColumns.ACCOUNT_ID + " INTEGER NOT NULL," +
1626                 MetadataSync.DATA + " TEXT," +
1627                 MetadataSync.DELETED + " INTEGER NOT NULL DEFAULT 0);");
1628 
1629         db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS metadata_sync_index ON " +
1630                 Tables.METADATA_SYNC + " (" +
1631                 MetadataSync.RAW_CONTACT_BACKUP_ID + ", " +
1632                 MetadataSyncColumns.ACCOUNT_ID +");");
1633 
1634         db.execSQL("CREATE TABLE " + Tables.PRE_AUTHORIZED_URIS + " ("+
1635                 PreAuthorizedUris._ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
1636                 PreAuthorizedUris.URI + " STRING NOT NULL, " +
1637                 PreAuthorizedUris.EXPIRATION + " INTEGER NOT NULL DEFAULT 0);");
1638 
1639         db.execSQL("CREATE TABLE IF NOT EXISTS "
1640                 + Tables.METADATA_SYNC_STATE + " (" +
1641                 MetadataSyncState._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
1642                 MetadataSyncStateColumns.ACCOUNT_ID + " INTEGER NOT NULL," +
1643                 MetadataSyncState.STATE + " BLOB);");
1644 
1645         db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS metadata_sync_state_index ON " +
1646                 Tables.METADATA_SYNC_STATE + " (" +
1647                 MetadataSyncColumns.ACCOUNT_ID +");");
1648 
1649         // When adding new tables, be sure to also add size-estimates in updateSqliteStats
1650         createContactsViews(db);
1651         createGroupsView(db);
1652         createContactsTriggers(db);
1653         createContactsIndexes(db, false /* we build stats table later */);
1654         createPresenceTables(db);
1655 
1656         loadNicknameLookupTable(db);
1657 
1658         // Set sequence starts.
1659         initializeAutoIncrementSequences(db);
1660 
1661         // Add the legacy API support views, etc.
1662         LegacyApiSupport.createDatabase(db);
1663 
1664         if (mDatabaseOptimizationEnabled) {
1665             // This will create a sqlite_stat1 table that is used for query optimization
1666             db.execSQL("ANALYZE;");
1667 
1668             updateSqliteStats(db);
1669         }
1670 
1671         postOnCreate();
1672     }
1673 
postOnCreate()1674     protected void postOnCreate() {
1675         // Only do this for the main DB, but not for the profile DB.
1676 
1677         notifyProviderStatusChange(mContext);
1678 
1679         // Trigger all sync adapters.
1680         ContentResolver.requestSync(null /* all accounts */,
1681                 ContactsContract.AUTHORITY, new Bundle());
1682 
1683         // Send the broadcast.
1684         final Intent dbCreatedIntent = new Intent(
1685                 ContactsContract.Intents.CONTACTS_DATABASE_CREATED);
1686         dbCreatedIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT);
1687         mContext.sendBroadcast(dbCreatedIntent, android.Manifest.permission.READ_CONTACTS);
1688     }
1689 
initializeAutoIncrementSequences(SQLiteDatabase db)1690     protected void initializeAutoIncrementSequences(SQLiteDatabase db) {
1691         // Default implementation does nothing.
1692     }
1693 
createDirectoriesTable(SQLiteDatabase db)1694     private void createDirectoriesTable(SQLiteDatabase db) {
1695         db.execSQL("CREATE TABLE " + Tables.DIRECTORIES + "(" +
1696                 Directory._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
1697                 Directory.PACKAGE_NAME + " TEXT NOT NULL," +
1698                 Directory.DIRECTORY_AUTHORITY + " TEXT NOT NULL," +
1699                 Directory.TYPE_RESOURCE_ID + " INTEGER," +
1700                 DirectoryColumns.TYPE_RESOURCE_NAME + " TEXT," +
1701                 Directory.ACCOUNT_TYPE + " TEXT," +
1702                 Directory.ACCOUNT_NAME + " TEXT," +
1703                 Directory.DISPLAY_NAME + " TEXT, " +
1704                 Directory.EXPORT_SUPPORT + " INTEGER NOT NULL" +
1705                         " DEFAULT " + Directory.EXPORT_SUPPORT_NONE + "," +
1706                 Directory.SHORTCUT_SUPPORT + " INTEGER NOT NULL" +
1707                         " DEFAULT " + Directory.SHORTCUT_SUPPORT_NONE + "," +
1708                 Directory.PHOTO_SUPPORT + " INTEGER NOT NULL" +
1709                         " DEFAULT " + Directory.PHOTO_SUPPORT_NONE +
1710         ");");
1711 
1712         // Trigger a full scan of directories in the system
1713         PropertyUtils.setProperty(db, DbProperties.DIRECTORY_SCAN_COMPLETE, "0");
1714     }
1715 
createSearchIndexTable(SQLiteDatabase db, boolean rebuildSqliteStats)1716     public void createSearchIndexTable(SQLiteDatabase db, boolean rebuildSqliteStats) {
1717         db.beginTransactionNonExclusive();
1718         try {
1719             db.execSQL("DROP TABLE IF EXISTS " + Tables.SEARCH_INDEX);
1720             db.execSQL("CREATE VIRTUAL TABLE " + Tables.SEARCH_INDEX
1721                     + " USING FTS4 ("
1722                     + SearchIndexColumns.CONTACT_ID + " INTEGER REFERENCES contacts(_id) NOT NULL,"
1723                     + SearchIndexColumns.CONTENT + " TEXT, "
1724                     + SearchIndexColumns.NAME + " TEXT, "
1725                     + SearchIndexColumns.TOKENS + " TEXT"
1726                     + ")");
1727             if (rebuildSqliteStats) {
1728                 updateSqliteStats(db);
1729             }
1730             db.setTransactionSuccessful();
1731         } finally {
1732             db.endTransaction();
1733         }
1734     }
1735 
createContactsTriggers(SQLiteDatabase db)1736     private void createContactsTriggers(SQLiteDatabase db) {
1737 
1738         // Automatically delete Data rows when a raw contact is deleted.
1739         db.execSQL("DROP TRIGGER IF EXISTS " + Tables.RAW_CONTACTS + "_deleted;");
1740         db.execSQL("CREATE TRIGGER " + Tables.RAW_CONTACTS + "_deleted "
1741                 + "   BEFORE DELETE ON " + Tables.RAW_CONTACTS
1742                 + " BEGIN "
1743                 + "   DELETE FROM " + Tables.DATA
1744                 + "     WHERE " + Data.RAW_CONTACT_ID
1745                                 + "=OLD." + RawContacts._ID + ";"
1746                 + "   DELETE FROM " + Tables.AGGREGATION_EXCEPTIONS
1747                 + "     WHERE " + AggregationExceptions.RAW_CONTACT_ID1
1748                                 + "=OLD." + RawContacts._ID
1749                 + "        OR " + AggregationExceptions.RAW_CONTACT_ID2
1750                                 + "=OLD." + RawContacts._ID + ";"
1751                 + "   DELETE FROM " + Tables.VISIBLE_CONTACTS
1752                 + "     WHERE " + Contacts._ID + "=OLD." + RawContacts.CONTACT_ID
1753                 + "       AND (SELECT COUNT(*) FROM " + Tables.RAW_CONTACTS
1754                 + "            WHERE " + RawContacts.CONTACT_ID + "=OLD." + RawContacts.CONTACT_ID
1755                 + "           )=1;"
1756                 + "   DELETE FROM " + Tables.DEFAULT_DIRECTORY
1757                 + "     WHERE " + Contacts._ID + "=OLD." + RawContacts.CONTACT_ID
1758                 + "       AND (SELECT COUNT(*) FROM " + Tables.RAW_CONTACTS
1759                 + "            WHERE " + RawContacts.CONTACT_ID + "=OLD." + RawContacts.CONTACT_ID
1760                 + "           )=1;"
1761                 + "   DELETE FROM " + Tables.CONTACTS
1762                 + "     WHERE " + Contacts._ID + "=OLD." + RawContacts.CONTACT_ID
1763                 + "       AND (SELECT COUNT(*) FROM " + Tables.RAW_CONTACTS
1764                 + "            WHERE " + RawContacts.CONTACT_ID + "=OLD." + RawContacts.CONTACT_ID
1765                 + "           )=1;"
1766                 + " END");
1767 
1768 
1769         db.execSQL("DROP TRIGGER IF EXISTS contacts_times_contacted;");
1770         db.execSQL("DROP TRIGGER IF EXISTS raw_contacts_times_contacted;");
1771 
1772         // Triggers that update {@link RawContacts#VERSION} when the contact is marked for deletion
1773         // or any time a data row is inserted, updated or deleted.
1774         db.execSQL("DROP TRIGGER IF EXISTS " + Tables.RAW_CONTACTS + "_marked_deleted;");
1775         db.execSQL("CREATE TRIGGER " + Tables.RAW_CONTACTS + "_marked_deleted "
1776                 + "   AFTER UPDATE ON " + Tables.RAW_CONTACTS
1777                 + " BEGIN "
1778                 + "   UPDATE " + Tables.RAW_CONTACTS
1779                 + "     SET "
1780                 +         RawContacts.VERSION + "=OLD." + RawContacts.VERSION + "+1 "
1781                 + "     WHERE " + RawContacts._ID + "=OLD." + RawContacts._ID
1782                 + "       AND NEW." + RawContacts.DELETED + "!= OLD." + RawContacts.DELETED + ";"
1783                 + " END");
1784 
1785         db.execSQL("DROP TRIGGER IF EXISTS " + Tables.DATA + "_updated;");
1786         db.execSQL("CREATE TRIGGER " + Tables.DATA + "_updated AFTER UPDATE ON " + Tables.DATA
1787                 + " BEGIN "
1788                 + "   UPDATE " + Tables.DATA
1789                 + "     SET " + Data.DATA_VERSION + "=OLD." + Data.DATA_VERSION + "+1 "
1790                 + "     WHERE " + Data._ID + "=OLD." + Data._ID + ";"
1791                 + "   UPDATE " + Tables.RAW_CONTACTS
1792                 + "     SET " + RawContacts.VERSION + "=" + RawContacts.VERSION + "+1 "
1793                 + "     WHERE " + RawContacts._ID + "=OLD." + Data.RAW_CONTACT_ID + ";"
1794                 + " END");
1795 
1796         db.execSQL("DROP TRIGGER IF EXISTS " + Tables.DATA + "_deleted;");
1797         db.execSQL("CREATE TRIGGER " + Tables.DATA + "_deleted BEFORE DELETE ON " + Tables.DATA
1798                 + " BEGIN "
1799                 + "   UPDATE " + Tables.RAW_CONTACTS
1800                 + "     SET " + RawContacts.VERSION + "=" + RawContacts.VERSION + "+1 "
1801                 + "     WHERE " + RawContacts._ID + "=OLD." + Data.RAW_CONTACT_ID + ";"
1802                 + "   DELETE FROM " + Tables.PHONE_LOOKUP
1803                 + "     WHERE " + PhoneLookupColumns.DATA_ID + "=OLD." + Data._ID + ";"
1804                 + "   DELETE FROM " + Tables.STATUS_UPDATES
1805                 + "     WHERE " + StatusUpdatesColumns.DATA_ID + "=OLD." + Data._ID + ";"
1806                 + "   DELETE FROM " + Tables.NAME_LOOKUP
1807                 + "     WHERE " + NameLookupColumns.DATA_ID + "=OLD." + Data._ID + ";"
1808                 + " END");
1809 
1810 
1811         db.execSQL("DROP TRIGGER IF EXISTS " + Tables.GROUPS + "_updated1;");
1812         db.execSQL("CREATE TRIGGER " + Tables.GROUPS + "_updated1 "
1813                 + "   AFTER UPDATE ON " + Tables.GROUPS
1814                 + " BEGIN "
1815                 + "   UPDATE " + Tables.GROUPS
1816                 + "     SET "
1817                 +         Groups.VERSION + "=OLD." + Groups.VERSION + "+1"
1818                 + "     WHERE " + Groups._ID + "=OLD." + Groups._ID + ";"
1819                 + " END");
1820 
1821         // Update DEFAULT_FILTER table per AUTO_ADD column update, see upgradeToVersion411.
1822         final String insertContactsWithoutAccount = (
1823                 " INSERT OR IGNORE INTO " + Tables.DEFAULT_DIRECTORY +
1824                 "     SELECT " + RawContacts.CONTACT_ID +
1825                 "     FROM " + Tables.RAW_CONTACTS +
1826                 "     WHERE " + RawContactsColumns.CONCRETE_ACCOUNT_ID +
1827                             "=" + Clauses.LOCAL_ACCOUNT_ID + ";");
1828 
1829         final String insertContactsWithAccountNoDefaultGroup = (
1830                 " INSERT OR IGNORE INTO " + Tables.DEFAULT_DIRECTORY +
1831                 "     SELECT " + RawContacts.CONTACT_ID +
1832                 "         FROM " + Tables.RAW_CONTACTS +
1833                 "     WHERE NOT EXISTS" +
1834                 "         (SELECT " + Groups._ID +
1835                 "             FROM " + Tables.GROUPS +
1836                 "             WHERE " + RawContactsColumns.CONCRETE_ACCOUNT_ID + " = " +
1837                                     GroupsColumns.CONCRETE_ACCOUNT_ID +
1838                 "             AND " + Groups.AUTO_ADD + " != 0" + ");");
1839 
1840         final String insertContactsWithAccountDefaultGroup = (
1841                 " INSERT OR IGNORE INTO " + Tables.DEFAULT_DIRECTORY +
1842                 "     SELECT " + RawContacts.CONTACT_ID +
1843                 "         FROM " + Tables.RAW_CONTACTS +
1844                 "     JOIN " + Tables.DATA +
1845                 "           ON (" + RawContactsColumns.CONCRETE_ID + "=" +
1846                         Data.RAW_CONTACT_ID + ")" +
1847                 "     WHERE " + DataColumns.MIMETYPE_ID + "=" +
1848                     "(SELECT " + MimetypesColumns._ID + " FROM " + Tables.MIMETYPES +
1849                         " WHERE " + MimetypesColumns.MIMETYPE +
1850                             "='" + GroupMembership.CONTENT_ITEM_TYPE + "')" +
1851                 "     AND EXISTS" +
1852                 "         (SELECT " + Groups._ID +
1853                 "             FROM " + Tables.GROUPS +
1854                 "                 WHERE " + RawContactsColumns.CONCRETE_ACCOUNT_ID + " = " +
1855                                         GroupsColumns.CONCRETE_ACCOUNT_ID +
1856                 "                 AND " + Groups.AUTO_ADD + " != 0" + ");");
1857 
1858         db.execSQL("DROP TRIGGER IF EXISTS " + Tables.GROUPS + "_auto_add_updated1;");
1859         db.execSQL("CREATE TRIGGER " + Tables.GROUPS + "_auto_add_updated1 "
1860                 + "   AFTER UPDATE OF " + Groups.AUTO_ADD + " ON " + Tables.GROUPS
1861                 + " BEGIN "
1862                 + "   DELETE FROM " + Tables.DEFAULT_DIRECTORY + ";"
1863                     + insertContactsWithoutAccount
1864                     + insertContactsWithAccountNoDefaultGroup
1865                     + insertContactsWithAccountDefaultGroup
1866                 + " END");
1867     }
1868 
createContactsIndexes(SQLiteDatabase db, boolean rebuildSqliteStats)1869     private void createContactsIndexes(SQLiteDatabase db, boolean rebuildSqliteStats) {
1870         db.execSQL("DROP INDEX IF EXISTS name_lookup_index");
1871         db.execSQL("CREATE INDEX name_lookup_index ON " + Tables.NAME_LOOKUP + " (" +
1872                 NameLookupColumns.NORMALIZED_NAME + "," +
1873                 NameLookupColumns.NAME_TYPE + ", " +
1874                 NameLookupColumns.RAW_CONTACT_ID + ", " +
1875                 NameLookupColumns.DATA_ID +
1876         ");");
1877 
1878         db.execSQL("DROP INDEX IF EXISTS raw_contact_sort_key1_index");
1879         db.execSQL("CREATE INDEX raw_contact_sort_key1_index ON " + Tables.RAW_CONTACTS + " (" +
1880                 RawContacts.SORT_KEY_PRIMARY +
1881         ");");
1882 
1883         db.execSQL("DROP INDEX IF EXISTS raw_contact_sort_key2_index");
1884         db.execSQL("CREATE INDEX raw_contact_sort_key2_index ON " + Tables.RAW_CONTACTS + " (" +
1885                 RawContacts.SORT_KEY_ALTERNATIVE +
1886         ");");
1887 
1888         if (rebuildSqliteStats) {
1889             updateSqliteStats(db);
1890         }
1891     }
1892 
createContactsViews(SQLiteDatabase db)1893     private void createContactsViews(SQLiteDatabase db) {
1894         db.execSQL("DROP VIEW IF EXISTS " + Views.CONTACTS + ";");
1895         db.execSQL("DROP VIEW IF EXISTS " + Views.DATA + ";");
1896         db.execSQL("DROP VIEW IF EXISTS " + Views.RAW_CONTACTS + ";");
1897         db.execSQL("DROP VIEW IF EXISTS " + Views.RAW_ENTITIES + ";");
1898         db.execSQL("DROP VIEW IF EXISTS " + Views.ENTITIES + ";");
1899         db.execSQL("DROP VIEW IF EXISTS " + Views.DATA_USAGE_STAT + ";");
1900         db.execSQL("DROP VIEW IF EXISTS " + Views.DATA_USAGE_LR + ";");
1901         db.execSQL("DROP VIEW IF EXISTS " + Views.STREAM_ITEMS + ";");
1902         db.execSQL("DROP VIEW IF EXISTS " + Views.METADATA_SYNC_STATE + ";");
1903         db.execSQL("DROP VIEW IF EXISTS " + Views.METADATA_SYNC + ";");
1904 
1905         String dataColumns =
1906                 Data.IS_PRIMARY + ", "
1907                 + Data.IS_SUPER_PRIMARY + ", "
1908                 + Data.DATA_VERSION + ", "
1909                 + DataColumns.CONCRETE_PACKAGE_ID + ","
1910                 + PackagesColumns.PACKAGE + " AS " + Data.RES_PACKAGE + ","
1911                 + DataColumns.CONCRETE_MIMETYPE_ID + ","
1912                 + MimetypesColumns.MIMETYPE + " AS " + Data.MIMETYPE + ", "
1913                 + Data.IS_READ_ONLY + ", "
1914                 + Data.DATA1 + ", "
1915                 + Data.DATA2 + ", "
1916                 + Data.DATA3 + ", "
1917                 + Data.DATA4 + ", "
1918                 + Data.DATA5 + ", "
1919                 + Data.DATA6 + ", "
1920                 + Data.DATA7 + ", "
1921                 + Data.DATA8 + ", "
1922                 + Data.DATA9 + ", "
1923                 + Data.DATA10 + ", "
1924                 + Data.DATA11 + ", "
1925                 + Data.DATA12 + ", "
1926                 + Data.DATA13 + ", "
1927                 + Data.DATA14 + ", "
1928                 + Data.DATA15 + ", "
1929                 + Data.CARRIER_PRESENCE + ", "
1930                 + Data.PREFERRED_PHONE_ACCOUNT_COMPONENT_NAME + ", "
1931                 + Data.PREFERRED_PHONE_ACCOUNT_ID + ", "
1932                 + Data.SYNC1 + ", "
1933                 + Data.SYNC2 + ", "
1934                 + Data.SYNC3 + ", "
1935                 + Data.SYNC4;
1936 
1937         String syncColumns =
1938                 RawContactsColumns.CONCRETE_ACCOUNT_ID + ","
1939                 + AccountsColumns.CONCRETE_ACCOUNT_NAME + " AS " + RawContacts.ACCOUNT_NAME + ","
1940                 + AccountsColumns.CONCRETE_ACCOUNT_TYPE + " AS " + RawContacts.ACCOUNT_TYPE + ","
1941                 + AccountsColumns.CONCRETE_DATA_SET + " AS " + RawContacts.DATA_SET + ","
1942                 + "(CASE WHEN " + AccountsColumns.CONCRETE_DATA_SET + " IS NULL THEN "
1943                             + AccountsColumns.CONCRETE_ACCOUNT_TYPE
1944                         + " ELSE " + AccountsColumns.CONCRETE_ACCOUNT_TYPE + "||'/'||"
1945                             + AccountsColumns.CONCRETE_DATA_SET + " END) AS "
1946                                 + RawContacts.ACCOUNT_TYPE_AND_DATA_SET + ","
1947                 + RawContactsColumns.CONCRETE_SOURCE_ID + " AS " + RawContacts.SOURCE_ID + ","
1948                 + RawContactsColumns.CONCRETE_BACKUP_ID + " AS " + RawContacts.BACKUP_ID + ","
1949                 + RawContactsColumns.CONCRETE_VERSION + " AS " + RawContacts.VERSION + ","
1950                 + RawContactsColumns.CONCRETE_DIRTY + " AS " + RawContacts.DIRTY + ","
1951                 + RawContactsColumns.CONCRETE_SYNC1 + " AS " + RawContacts.SYNC1 + ","
1952                 + RawContactsColumns.CONCRETE_SYNC2 + " AS " + RawContacts.SYNC2 + ","
1953                 + RawContactsColumns.CONCRETE_SYNC3 + " AS " + RawContacts.SYNC3 + ","
1954                 + RawContactsColumns.CONCRETE_SYNC4 + " AS " + RawContacts.SYNC4;
1955 
1956         String baseContactColumns =
1957                 Contacts.HAS_PHONE_NUMBER + ", "
1958                 + Contacts.NAME_RAW_CONTACT_ID + ", "
1959                 + Contacts.LOOKUP_KEY + ", "
1960                 + Contacts.PHOTO_ID + ", "
1961                 + Contacts.PHOTO_FILE_ID + ", "
1962                 + "CAST(" + Clauses.CONTACT_VISIBLE + " AS INTEGER) AS "
1963                         + Contacts.IN_VISIBLE_GROUP + ", "
1964                 + "CAST(" + Clauses.CONTACT_IN_DEFAULT_DIRECTORY + " AS INTEGER) AS "
1965                         + Contacts.IN_DEFAULT_DIRECTORY + ", "
1966                 + ContactsColumns.LAST_STATUS_UPDATE_ID + ", "
1967                 + ContactsColumns.CONCRETE_CONTACT_LAST_UPDATED_TIMESTAMP;
1968 
1969         String contactOptionColumns =
1970                 ContactsColumns.CONCRETE_CUSTOM_RINGTONE
1971                         + " AS " + Contacts.CUSTOM_RINGTONE + ","
1972                 + ContactsColumns.CONCRETE_SEND_TO_VOICEMAIL
1973                         + " AS " + Contacts.SEND_TO_VOICEMAIL + ","
1974 
1975                 + ContactsColumns.CONCRETE_RAW_LAST_TIME_CONTACTED
1976                         + " AS " + Contacts.RAW_LAST_TIME_CONTACTED + ","
1977                 + ContactsColumns.CONCRETE_RAW_TIMES_CONTACTED
1978                         + " AS " + Contacts.RAW_TIMES_CONTACTED + ","
1979 
1980                 + LowRes.getLastTimeUsedExpression(ContactsColumns.CONCRETE_RAW_LAST_TIME_CONTACTED)
1981                         + " AS " + Contacts.LR_LAST_TIME_CONTACTED + ","
1982                 + LowRes.getTimesUsedExpression(ContactsColumns.CONCRETE_RAW_TIMES_CONTACTED)
1983                         + " AS " + Contacts.LR_TIMES_CONTACTED + ","
1984 
1985                 + ContactsColumns.CONCRETE_STARRED
1986                         + " AS " + Contacts.STARRED + ","
1987                 + ContactsColumns.CONCRETE_PINNED
1988                         + " AS " + Contacts.PINNED;
1989 
1990         String contactNameColumns =
1991                 "name_raw_contact." + RawContacts.DISPLAY_NAME_SOURCE
1992                         + " AS " + Contacts.DISPLAY_NAME_SOURCE + ", "
1993                 + "name_raw_contact." + RawContacts.DISPLAY_NAME_PRIMARY
1994                         + " AS " + Contacts.DISPLAY_NAME_PRIMARY + ", "
1995                 + "name_raw_contact." + RawContacts.DISPLAY_NAME_ALTERNATIVE
1996                         + " AS " + Contacts.DISPLAY_NAME_ALTERNATIVE + ", "
1997                 + "name_raw_contact." + RawContacts.PHONETIC_NAME
1998                         + " AS " + Contacts.PHONETIC_NAME + ", "
1999                 + "name_raw_contact." + RawContacts.PHONETIC_NAME_STYLE
2000                         + " AS " + Contacts.PHONETIC_NAME_STYLE + ", "
2001                 + "name_raw_contact." + RawContacts.SORT_KEY_PRIMARY
2002                         + " AS " + Contacts.SORT_KEY_PRIMARY + ", "
2003                 + "name_raw_contact." + RawContactsColumns.PHONEBOOK_LABEL_PRIMARY
2004                         + " AS " + ContactsColumns.PHONEBOOK_LABEL_PRIMARY + ", "
2005                 + "name_raw_contact." + RawContactsColumns.PHONEBOOK_BUCKET_PRIMARY
2006                         + " AS " + ContactsColumns.PHONEBOOK_BUCKET_PRIMARY + ", "
2007                 + "name_raw_contact." + RawContacts.SORT_KEY_ALTERNATIVE
2008                         + " AS " + Contacts.SORT_KEY_ALTERNATIVE + ", "
2009                 + "name_raw_contact." + RawContactsColumns.PHONEBOOK_LABEL_ALTERNATIVE
2010                         + " AS " + ContactsColumns.PHONEBOOK_LABEL_ALTERNATIVE + ", "
2011                 + "name_raw_contact." + RawContactsColumns.PHONEBOOK_BUCKET_ALTERNATIVE
2012                         + " AS " + ContactsColumns.PHONEBOOK_BUCKET_ALTERNATIVE;
2013 
2014         String dataSelect = "SELECT "
2015                 + DataColumns.CONCRETE_ID + " AS " + Data._ID + ","
2016                 + Data.HASH_ID + ", "
2017                 + Data.RAW_CONTACT_ID + ", "
2018                 + RawContactsColumns.CONCRETE_CONTACT_ID + " AS " + RawContacts.CONTACT_ID + ", "
2019                 + syncColumns + ", "
2020                 + dataColumns + ", "
2021                 + contactOptionColumns + ", "
2022                 + contactNameColumns + ", "
2023                 + baseContactColumns + ", "
2024                 + buildDisplayPhotoUriAlias(RawContactsColumns.CONCRETE_CONTACT_ID,
2025                         Contacts.PHOTO_URI) + ", "
2026                 + buildThumbnailPhotoUriAlias(RawContactsColumns.CONCRETE_CONTACT_ID,
2027                         Contacts.PHOTO_THUMBNAIL_URI) + ", "
2028                 + dbForProfile() + " AS " + RawContacts.RAW_CONTACT_IS_USER_PROFILE + ", "
2029                 + Tables.GROUPS + "." + Groups.SOURCE_ID + " AS " + GroupMembership.GROUP_SOURCE_ID
2030                 + " FROM " + Tables.DATA
2031                 + " JOIN " + Tables.MIMETYPES + " ON ("
2032                 +   DataColumns.CONCRETE_MIMETYPE_ID + "=" + MimetypesColumns.CONCRETE_ID + ")"
2033                 + " JOIN " + Tables.RAW_CONTACTS + " ON ("
2034                 +   DataColumns.CONCRETE_RAW_CONTACT_ID + "=" + RawContactsColumns.CONCRETE_ID + ")"
2035                 + " JOIN " + Tables.ACCOUNTS + " ON ("
2036                 +   RawContactsColumns.CONCRETE_ACCOUNT_ID + "=" + AccountsColumns.CONCRETE_ID
2037                     + ")"
2038                 + " JOIN " + Tables.CONTACTS + " ON ("
2039                 +   RawContactsColumns.CONCRETE_CONTACT_ID + "=" + ContactsColumns.CONCRETE_ID + ")"
2040                 + " JOIN " + Tables.RAW_CONTACTS + " AS name_raw_contact ON("
2041                 +   Contacts.NAME_RAW_CONTACT_ID + "=name_raw_contact." + RawContacts._ID + ")"
2042                 + " LEFT OUTER JOIN " + Tables.PACKAGES + " ON ("
2043                 +   DataColumns.CONCRETE_PACKAGE_ID + "=" + PackagesColumns.CONCRETE_ID + ")"
2044                 + " LEFT OUTER JOIN " + Tables.GROUPS + " ON ("
2045                 +   MimetypesColumns.CONCRETE_MIMETYPE + "='" + GroupMembership.CONTENT_ITEM_TYPE
2046                 +   "' AND " + GroupsColumns.CONCRETE_ID + "="
2047                         + Tables.DATA + "." + GroupMembership.GROUP_ROW_ID + ")";
2048 
2049         db.execSQL("CREATE VIEW " + Views.DATA + " AS " + dataSelect);
2050 
2051         String rawContactOptionColumns =
2052                 RawContacts.CUSTOM_RINGTONE + ","
2053                 + RawContacts.SEND_TO_VOICEMAIL + ","
2054                 + RawContacts.RAW_LAST_TIME_CONTACTED + ","
2055                 + LowRes.getLastTimeUsedExpression(RawContacts.RAW_LAST_TIME_CONTACTED)
2056                         + " AS " + RawContacts.LR_LAST_TIME_CONTACTED + ","
2057                 + RawContacts.RAW_TIMES_CONTACTED + ","
2058                 + LowRes.getTimesUsedExpression(RawContacts.RAW_TIMES_CONTACTED)
2059                         + " AS " + RawContacts.LR_TIMES_CONTACTED + ","
2060                 + RawContacts.STARRED + ","
2061                 + RawContacts.PINNED;
2062 
2063         String rawContactsSelect = "SELECT "
2064                 + RawContactsColumns.CONCRETE_ID + " AS " + RawContacts._ID + ","
2065                 + RawContacts.CONTACT_ID + ", "
2066                 + RawContacts.AGGREGATION_MODE + ", "
2067                 + RawContacts.RAW_CONTACT_IS_READ_ONLY + ", "
2068                 + RawContacts.DELETED + ", "
2069                 + RawContactsColumns.CONCRETE_METADATA_DIRTY + ", "
2070                 + RawContacts.DISPLAY_NAME_SOURCE  + ", "
2071                 + RawContacts.DISPLAY_NAME_PRIMARY  + ", "
2072                 + RawContacts.DISPLAY_NAME_ALTERNATIVE  + ", "
2073                 + RawContacts.PHONETIC_NAME  + ", "
2074                 + RawContacts.PHONETIC_NAME_STYLE  + ", "
2075                 + RawContacts.SORT_KEY_PRIMARY  + ", "
2076                 + RawContactsColumns.PHONEBOOK_LABEL_PRIMARY  + ", "
2077                 + RawContactsColumns.PHONEBOOK_BUCKET_PRIMARY  + ", "
2078                 + RawContacts.SORT_KEY_ALTERNATIVE + ", "
2079                 + RawContactsColumns.PHONEBOOK_LABEL_ALTERNATIVE  + ", "
2080                 + RawContactsColumns.PHONEBOOK_BUCKET_ALTERNATIVE  + ", "
2081                 + dbForProfile() + " AS " + RawContacts.RAW_CONTACT_IS_USER_PROFILE + ", "
2082                 + rawContactOptionColumns + ", "
2083                 + syncColumns
2084                 + " FROM " + Tables.RAW_CONTACTS
2085                 + " JOIN " + Tables.ACCOUNTS + " ON ("
2086                 +   RawContactsColumns.CONCRETE_ACCOUNT_ID + "=" + AccountsColumns.CONCRETE_ID
2087                     + ")";
2088 
2089         db.execSQL("CREATE VIEW " + Views.RAW_CONTACTS + " AS " + rawContactsSelect);
2090 
2091         String contactsColumns =
2092                 ContactsColumns.CONCRETE_CUSTOM_RINGTONE
2093                         + " AS " + Contacts.CUSTOM_RINGTONE + ", "
2094                 + contactNameColumns + ", "
2095                 + baseContactColumns + ", "
2096 
2097                 + ContactsColumns.CONCRETE_RAW_LAST_TIME_CONTACTED
2098                         + " AS " + Contacts.RAW_LAST_TIME_CONTACTED + ", "
2099                 + LowRes.getLastTimeUsedExpression(ContactsColumns.CONCRETE_RAW_LAST_TIME_CONTACTED)
2100                         + " AS " + Contacts.LR_LAST_TIME_CONTACTED + ", "
2101 
2102                 + ContactsColumns.CONCRETE_SEND_TO_VOICEMAIL
2103                         + " AS " + Contacts.SEND_TO_VOICEMAIL + ", "
2104                 + ContactsColumns.CONCRETE_STARRED
2105                         + " AS " + Contacts.STARRED + ", "
2106                 + ContactsColumns.CONCRETE_PINNED
2107                 + " AS " + Contacts.PINNED + ", "
2108 
2109                 + ContactsColumns.CONCRETE_RAW_TIMES_CONTACTED
2110                         + " AS " + Contacts.RAW_TIMES_CONTACTED + ", "
2111                 + LowRes.getTimesUsedExpression(ContactsColumns.CONCRETE_RAW_TIMES_CONTACTED)
2112                         + " AS " + Contacts.LR_TIMES_CONTACTED;
2113 
2114         String contactsSelect = "SELECT "
2115                 + ContactsColumns.CONCRETE_ID + " AS " + Contacts._ID + ","
2116                 + contactsColumns + ", "
2117                 + buildDisplayPhotoUriAlias(ContactsColumns.CONCRETE_ID, Contacts.PHOTO_URI) + ", "
2118                 + buildThumbnailPhotoUriAlias(ContactsColumns.CONCRETE_ID,
2119                         Contacts.PHOTO_THUMBNAIL_URI) + ", "
2120                 + dbForProfile() + " AS " + Contacts.IS_USER_PROFILE
2121                 + " FROM " + Tables.CONTACTS
2122                 + " JOIN " + Tables.RAW_CONTACTS + " AS name_raw_contact ON("
2123                 +   Contacts.NAME_RAW_CONTACT_ID + "=name_raw_contact." + RawContacts._ID + ")";
2124 
2125         db.execSQL("CREATE VIEW " + Views.CONTACTS + " AS " + contactsSelect);
2126 
2127         String rawEntitiesSelect = "SELECT "
2128                 + RawContacts.CONTACT_ID + ", "
2129                 + RawContactsColumns.CONCRETE_DELETED + " AS " + RawContacts.DELETED + ","
2130                 + RawContactsColumns.CONCRETE_METADATA_DIRTY + ", "
2131                 + dataColumns + ", "
2132                 + syncColumns + ", "
2133                 + Data.SYNC1 + ", "
2134                 + Data.SYNC2 + ", "
2135                 + Data.SYNC3 + ", "
2136                 + Data.SYNC4 + ", "
2137                 + RawContactsColumns.CONCRETE_ID + " AS " + RawContacts._ID + ", "
2138                 + DataColumns.CONCRETE_ID + " AS " + RawContacts.Entity.DATA_ID + ","
2139                 + RawContactsColumns.CONCRETE_STARRED + " AS " + RawContacts.STARRED + ","
2140                 + dbForProfile() + " AS " + RawContacts.RAW_CONTACT_IS_USER_PROFILE + ","
2141                 + Tables.GROUPS + "." + Groups.SOURCE_ID + " AS " + GroupMembership.GROUP_SOURCE_ID
2142                 + " FROM " + Tables.RAW_CONTACTS
2143                 + " JOIN " + Tables.ACCOUNTS + " ON ("
2144                 +   RawContactsColumns.CONCRETE_ACCOUNT_ID + "=" + AccountsColumns.CONCRETE_ID
2145                     + ")"
2146                 + " LEFT OUTER JOIN " + Tables.DATA + " ON ("
2147                 +   DataColumns.CONCRETE_RAW_CONTACT_ID + "=" + RawContactsColumns.CONCRETE_ID + ")"
2148                 + " LEFT OUTER JOIN " + Tables.PACKAGES + " ON ("
2149                 +   DataColumns.CONCRETE_PACKAGE_ID + "=" + PackagesColumns.CONCRETE_ID + ")"
2150                 + " LEFT OUTER JOIN " + Tables.MIMETYPES + " ON ("
2151                 +   DataColumns.CONCRETE_MIMETYPE_ID + "=" + MimetypesColumns.CONCRETE_ID + ")"
2152                 + " LEFT OUTER JOIN " + Tables.GROUPS + " ON ("
2153                 +   MimetypesColumns.CONCRETE_MIMETYPE + "='" + GroupMembership.CONTENT_ITEM_TYPE
2154                 +   "' AND " + GroupsColumns.CONCRETE_ID + "="
2155                 + Tables.DATA + "." + GroupMembership.GROUP_ROW_ID + ")";
2156 
2157         db.execSQL("CREATE VIEW " + Views.RAW_ENTITIES + " AS "
2158                 + rawEntitiesSelect);
2159 
2160         String entitiesSelect = "SELECT "
2161                 + RawContactsColumns.CONCRETE_CONTACT_ID + " AS " + Contacts._ID + ", "
2162                 + RawContactsColumns.CONCRETE_CONTACT_ID + " AS " + RawContacts.CONTACT_ID + ", "
2163                 + RawContactsColumns.CONCRETE_DELETED + " AS " + RawContacts.DELETED + ","
2164                 + RawContactsColumns.CONCRETE_METADATA_DIRTY + ", "
2165                 + dataColumns + ", "
2166                 + syncColumns + ", "
2167                 + contactsColumns + ", "
2168                 + buildDisplayPhotoUriAlias(RawContactsColumns.CONCRETE_CONTACT_ID,
2169                         Contacts.PHOTO_URI) + ", "
2170                 + buildThumbnailPhotoUriAlias(RawContactsColumns.CONCRETE_CONTACT_ID,
2171                         Contacts.PHOTO_THUMBNAIL_URI) + ", "
2172                 + dbForProfile() + " AS " + Contacts.IS_USER_PROFILE + ", "
2173                 + Data.SYNC1 + ", "
2174                 + Data.SYNC2 + ", "
2175                 + Data.SYNC3 + ", "
2176                 + Data.SYNC4 + ", "
2177                 + RawContactsColumns.CONCRETE_ID + " AS " + Contacts.Entity.RAW_CONTACT_ID + ", "
2178                 + DataColumns.CONCRETE_ID + " AS " + Contacts.Entity.DATA_ID + ","
2179                 + Tables.GROUPS + "." + Groups.SOURCE_ID + " AS " + GroupMembership.GROUP_SOURCE_ID
2180                 + " FROM " + Tables.RAW_CONTACTS
2181                 + " JOIN " + Tables.ACCOUNTS + " ON ("
2182                 +   RawContactsColumns.CONCRETE_ACCOUNT_ID + "=" + AccountsColumns.CONCRETE_ID
2183                     + ")"
2184                 + " JOIN " + Tables.CONTACTS + " ON ("
2185                 +   RawContactsColumns.CONCRETE_CONTACT_ID + "=" + ContactsColumns.CONCRETE_ID + ")"
2186                 + " JOIN " + Tables.RAW_CONTACTS + " AS name_raw_contact ON("
2187                 +   Contacts.NAME_RAW_CONTACT_ID + "=name_raw_contact." + RawContacts._ID + ")"
2188                 + " LEFT OUTER JOIN " + Tables.DATA + " ON ("
2189                 +   DataColumns.CONCRETE_RAW_CONTACT_ID + "=" + RawContactsColumns.CONCRETE_ID + ")"
2190                 + " LEFT OUTER JOIN " + Tables.PACKAGES + " ON ("
2191                 +   DataColumns.CONCRETE_PACKAGE_ID + "=" + PackagesColumns.CONCRETE_ID + ")"
2192                 + " LEFT OUTER JOIN " + Tables.MIMETYPES + " ON ("
2193                 +   DataColumns.CONCRETE_MIMETYPE_ID + "=" + MimetypesColumns.CONCRETE_ID + ")"
2194                 + " LEFT OUTER JOIN " + Tables.GROUPS + " ON ("
2195                 +   MimetypesColumns.CONCRETE_MIMETYPE + "='" + GroupMembership.CONTENT_ITEM_TYPE
2196                 +   "' AND " + GroupsColumns.CONCRETE_ID + "="
2197                 + Tables.DATA + "." + GroupMembership.GROUP_ROW_ID + ")";
2198 
2199         db.execSQL("CREATE VIEW " + Views.ENTITIES + " AS "
2200                 + entitiesSelect);
2201 
2202         // Data usage view, with the low res columns, with no joins.
2203         final String dataUsageViewSelect = "SELECT "
2204                 + DataUsageStatColumns._ID + ", "
2205                 + DataUsageStatColumns.DATA_ID + ", "
2206                 + DataUsageStatColumns.USAGE_TYPE_INT + ", "
2207                 + DataUsageStatColumns.RAW_TIMES_USED + ", "
2208                 + DataUsageStatColumns.RAW_LAST_TIME_USED + ","
2209                 + LowRes.getTimesUsedExpression(DataUsageStatColumns.RAW_TIMES_USED)
2210                     + " AS " + DataUsageStatColumns.LR_TIMES_USED + ","
2211                 + LowRes.getLastTimeUsedExpression(DataUsageStatColumns.RAW_LAST_TIME_USED)
2212                     + " AS " + DataUsageStatColumns.LR_LAST_TIME_USED
2213                 + " FROM " + Tables.DATA_USAGE_STAT;
2214 
2215         // When the data_usage_stat table is needed with the low-res columns, use this, which is
2216         // faster than the DATA_USAGE_STAT view since it doesn't involve joins.
2217         db.execSQL("CREATE VIEW " + Views.DATA_USAGE_LR + " AS " + dataUsageViewSelect);
2218 
2219         String dataUsageStatSelect = "SELECT "
2220                 + DataUsageStatColumns.CONCRETE_ID + " AS " + DataUsageStatColumns._ID + ", "
2221                 + DataUsageStatColumns.DATA_ID + ", "
2222                 + RawContactsColumns.CONCRETE_CONTACT_ID + " AS " + RawContacts.CONTACT_ID + ", "
2223                 + MimetypesColumns.CONCRETE_MIMETYPE + " AS " + Data.MIMETYPE + ", "
2224                 + DataUsageStatColumns.USAGE_TYPE_INT + ", "
2225                 + DataUsageStatColumns.RAW_TIMES_USED + ", "
2226                 + DataUsageStatColumns.RAW_LAST_TIME_USED + ", "
2227                 + DataUsageStatColumns.LR_TIMES_USED + ", "
2228                 + DataUsageStatColumns.LR_LAST_TIME_USED
2229                 + " FROM " + Views.DATA_USAGE_LR + " AS " + Tables.DATA_USAGE_STAT
2230                 + " JOIN " + Tables.DATA + " ON ("
2231                 +   DataColumns.CONCRETE_ID + "=" + DataUsageStatColumns.CONCRETE_DATA_ID + ")"
2232                 + " JOIN " + Tables.RAW_CONTACTS + " ON ("
2233                 +   RawContactsColumns.CONCRETE_ID + "=" + DataColumns.CONCRETE_RAW_CONTACT_ID
2234                     + " )"
2235                 + " JOIN " + Tables.MIMETYPES + " ON ("
2236                 +   MimetypesColumns.CONCRETE_ID + "=" + DataColumns.CONCRETE_MIMETYPE_ID + ")";
2237 
2238         db.execSQL("CREATE VIEW " + Views.DATA_USAGE_STAT + " AS " + dataUsageStatSelect);
2239 
2240         String streamItemSelect = "SELECT " +
2241                 StreamItemsColumns.CONCRETE_ID + ", " +
2242                 ContactsColumns.CONCRETE_ID + " AS " + StreamItems.CONTACT_ID + ", " +
2243                 ContactsColumns.CONCRETE_LOOKUP_KEY +
2244                         " AS " + StreamItems.CONTACT_LOOKUP_KEY + ", " +
2245                 AccountsColumns.CONCRETE_ACCOUNT_NAME + ", " +
2246                 AccountsColumns.CONCRETE_ACCOUNT_TYPE + ", " +
2247                 AccountsColumns.CONCRETE_DATA_SET + ", " +
2248                 StreamItemsColumns.CONCRETE_RAW_CONTACT_ID +
2249                         " as " + StreamItems.RAW_CONTACT_ID + ", " +
2250                 RawContactsColumns.CONCRETE_SOURCE_ID +
2251                         " as " + StreamItems.RAW_CONTACT_SOURCE_ID + ", " +
2252                 StreamItemsColumns.CONCRETE_PACKAGE + ", " +
2253                 StreamItemsColumns.CONCRETE_ICON + ", " +
2254                 StreamItemsColumns.CONCRETE_LABEL + ", " +
2255                 StreamItemsColumns.CONCRETE_TEXT + ", " +
2256                 StreamItemsColumns.CONCRETE_TIMESTAMP + ", " +
2257                 StreamItemsColumns.CONCRETE_COMMENTS + ", " +
2258                 StreamItemsColumns.CONCRETE_SYNC1 + ", " +
2259                 StreamItemsColumns.CONCRETE_SYNC2 + ", " +
2260                 StreamItemsColumns.CONCRETE_SYNC3 + ", " +
2261                 StreamItemsColumns.CONCRETE_SYNC4 +
2262                 " FROM " + Tables.STREAM_ITEMS
2263                 + " JOIN " + Tables.RAW_CONTACTS + " ON ("
2264                 + StreamItemsColumns.CONCRETE_RAW_CONTACT_ID + "=" + RawContactsColumns.CONCRETE_ID
2265                     + ")"
2266                 + " JOIN " + Tables.ACCOUNTS + " ON ("
2267                 +   RawContactsColumns.CONCRETE_ACCOUNT_ID + "=" + AccountsColumns.CONCRETE_ID
2268                     + ")"
2269                 + " JOIN " + Tables.CONTACTS + " ON ("
2270                 + RawContactsColumns.CONCRETE_CONTACT_ID + "=" + ContactsColumns.CONCRETE_ID + ")";
2271 
2272         db.execSQL("CREATE VIEW " + Views.STREAM_ITEMS + " AS " + streamItemSelect);
2273 
2274         String metadataSyncSelect = "SELECT " +
2275                 MetadataSyncColumns.CONCRETE_ID + ", " +
2276                 MetadataSync.RAW_CONTACT_BACKUP_ID + ", " +
2277                 AccountsColumns.ACCOUNT_NAME + ", " +
2278                 AccountsColumns.ACCOUNT_TYPE + ", " +
2279                 AccountsColumns.DATA_SET + ", " +
2280                 MetadataSync.DATA + ", " +
2281                 MetadataSync.DELETED +
2282                 " FROM " + Tables.METADATA_SYNC
2283                 + " JOIN " + Tables.ACCOUNTS + " ON ("
2284                 +   MetadataSyncColumns.CONCRETE_ACCOUNT_ID + "=" + AccountsColumns.CONCRETE_ID
2285                 + ")";
2286 
2287         db.execSQL("CREATE VIEW " + Views.METADATA_SYNC + " AS " + metadataSyncSelect);
2288 
2289         String metadataSyncStateSelect = "SELECT " +
2290                 MetadataSyncStateColumns.CONCRETE_ID + ", " +
2291                 AccountsColumns.ACCOUNT_NAME + ", " +
2292                 AccountsColumns.ACCOUNT_TYPE + ", " +
2293                 AccountsColumns.DATA_SET + ", " +
2294                 MetadataSyncState.STATE +
2295                 " FROM " + Tables.METADATA_SYNC_STATE
2296                 + " JOIN " + Tables.ACCOUNTS + " ON ("
2297                 +   MetadataSyncStateColumns.CONCRETE_ACCOUNT_ID + "=" + AccountsColumns.CONCRETE_ID
2298                 + ")";
2299 
2300         db.execSQL("CREATE VIEW " + Views.METADATA_SYNC_STATE + " AS " + metadataSyncStateSelect);
2301     }
2302 
buildDisplayPhotoUriAlias(String contactIdColumn, String alias)2303     private static String buildDisplayPhotoUriAlias(String contactIdColumn, String alias) {
2304         return "(CASE WHEN " + Contacts.PHOTO_FILE_ID + " IS NULL THEN (CASE WHEN "
2305                 + Contacts.PHOTO_ID + " IS NULL"
2306                 + " OR " + Contacts.PHOTO_ID + "=0"
2307                 + " THEN NULL"
2308                 + " ELSE '" + Contacts.CONTENT_URI + "/'||"
2309                         + contactIdColumn + "|| '/" + Photo.CONTENT_DIRECTORY + "'"
2310                 + " END) ELSE '" + DisplayPhoto.CONTENT_URI + "/'||"
2311                         + Contacts.PHOTO_FILE_ID + " END)"
2312                 + " AS " + alias;
2313     }
2314 
buildThumbnailPhotoUriAlias(String contactIdColumn, String alias)2315     private static String buildThumbnailPhotoUriAlias(String contactIdColumn, String alias) {
2316         return "(CASE WHEN "
2317                 + Contacts.PHOTO_ID + " IS NULL"
2318                 + " OR " + Contacts.PHOTO_ID + "=0"
2319                 + " THEN NULL"
2320                 + " ELSE '" + Contacts.CONTENT_URI + "/'||"
2321                         + contactIdColumn + "|| '/" + Photo.CONTENT_DIRECTORY + "'"
2322                 + " END)"
2323                 + " AS " + alias;
2324     }
2325 
2326     /**
2327      * Returns the value to be returned when querying the column indicating that the contact
2328      * or raw contact belongs to the user's personal profile.  Overridden in the profile
2329      * DB helper subclass.
2330      */
dbForProfile()2331     protected int dbForProfile() {
2332         return 0;
2333     }
2334 
createGroupsView(SQLiteDatabase db)2335     private void createGroupsView(SQLiteDatabase db) {
2336         db.execSQL("DROP VIEW IF EXISTS " + Views.GROUPS + ";");
2337 
2338         String groupsColumns =
2339                 GroupsColumns.CONCRETE_ACCOUNT_ID + " AS " + GroupsColumns.ACCOUNT_ID + ","
2340                 + AccountsColumns.CONCRETE_ACCOUNT_NAME + " AS " + Groups.ACCOUNT_NAME + ","
2341                 + AccountsColumns.CONCRETE_ACCOUNT_TYPE + " AS " + Groups.ACCOUNT_TYPE + ","
2342                 + AccountsColumns.CONCRETE_DATA_SET + " AS " + Groups.DATA_SET + ","
2343                 + "(CASE WHEN " + AccountsColumns.CONCRETE_DATA_SET
2344                     + " IS NULL THEN " + AccountsColumns.CONCRETE_ACCOUNT_TYPE
2345                     + " ELSE " + AccountsColumns.CONCRETE_ACCOUNT_TYPE
2346                         + "||'/'||" + AccountsColumns.CONCRETE_DATA_SET + " END) AS "
2347                             + Groups.ACCOUNT_TYPE_AND_DATA_SET + ","
2348                 + Groups.SOURCE_ID + ","
2349                 + Groups.VERSION + ","
2350                 + Groups.DIRTY + ","
2351                 + Groups.TITLE + ","
2352                 + Groups.TITLE_RES + ","
2353                 + Groups.NOTES + ","
2354                 + Groups.SYSTEM_ID + ","
2355                 + Groups.DELETED + ","
2356                 + Groups.GROUP_VISIBLE + ","
2357                 + Groups.SHOULD_SYNC + ","
2358                 + Groups.AUTO_ADD + ","
2359                 + Groups.FAVORITES + ","
2360                 + Groups.GROUP_IS_READ_ONLY + ","
2361                 + Groups.SYNC1 + ","
2362                 + Groups.SYNC2 + ","
2363                 + Groups.SYNC3 + ","
2364                 + Groups.SYNC4 + ","
2365                 + PackagesColumns.PACKAGE + " AS " + Groups.RES_PACKAGE;
2366 
2367         String groupsSelect = "SELECT "
2368                 + GroupsColumns.CONCRETE_ID + " AS " + Groups._ID + ","
2369                 + groupsColumns
2370                 + " FROM " + Tables.GROUPS
2371                 + " JOIN " + Tables.ACCOUNTS + " ON ("
2372                     + GroupsColumns.CONCRETE_ACCOUNT_ID + "=" + AccountsColumns.CONCRETE_ID + ")"
2373                 + " LEFT OUTER JOIN " + Tables.PACKAGES + " ON ("
2374                     + GroupsColumns.CONCRETE_PACKAGE_ID + "=" + PackagesColumns.CONCRETE_ID + ")";
2375 
2376         db.execSQL("CREATE VIEW " + Views.GROUPS + " AS " + groupsSelect);
2377     }
2378 
2379     @Override
onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion)2380     public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) {
2381         Log.i(TAG, "ContactsProvider cannot proceed because downgrading your database is not " +
2382                 "supported. To continue, please either re-upgrade to your previous Android " +
2383                 "version, or clear all application data in Contacts Storage (this will result " +
2384                 "in the loss of all local contacts that are not synced). To avoid data loss, " +
2385                 "your contacts database will not be wiped automatically.");
2386         super.onDowngrade(db, oldVersion, newVersion);
2387     }
2388 
2389     @Override
onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)2390     public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
2391         Log.i(TAG,
2392                 "Upgrading " + DATABASE_NAME + " from version " + oldVersion + " to " + newVersion);
2393 
2394         prepopulateCommonMimeTypes(db);
2395 
2396         boolean upgradeViewsAndTriggers = false;
2397         boolean upgradeNameLookup = false;
2398         boolean upgradeLegacyApiSupport = false;
2399         boolean upgradeSearchIndex = false;
2400         boolean rescanDirectories = false;
2401         boolean rebuildSqliteStats = false;
2402         boolean upgradeLocaleSpecificData = false;
2403 
2404         if (oldVersion < 701) {
2405             upgradeToVersion701(db);
2406             oldVersion = 701;
2407         }
2408 
2409         if (oldVersion < 702) {
2410             upgradeToVersion702(db);
2411             oldVersion = 702;
2412         }
2413 
2414         if (oldVersion < 703) {
2415             // Now names like "L'Image" will be searchable.
2416             upgradeSearchIndex = true;
2417             oldVersion = 703;
2418         }
2419 
2420         if (oldVersion < 704) {
2421             db.execSQL("DROP TABLE IF EXISTS activities;");
2422             oldVersion = 704;
2423         }
2424 
2425         if (oldVersion < 705) {
2426             // Before this version, we didn't rebuild the search index on locale changes, so
2427             // if the locale has changed after sync, the index contains gets stale.
2428             // To correct the issue we have to rebuild the index here.
2429             upgradeSearchIndex = true;
2430             oldVersion = 705;
2431         }
2432 
2433         if (oldVersion < 706) {
2434             // Prior to this version, we didn't rebuild the stats table after drop operations,
2435             // which resulted in losing some of the rows from the stats table.
2436             rebuildSqliteStats = true;
2437             oldVersion = 706;
2438         }
2439 
2440         if (oldVersion < 707) {
2441             upgradeToVersion707(db);
2442             upgradeViewsAndTriggers = true;
2443             oldVersion = 707;
2444         }
2445 
2446         if (oldVersion < 708) {
2447             // Sort keys, phonebook labels and buckets, and search keys have
2448             // changed so force a rebuild.
2449             upgradeLocaleSpecificData = true;
2450             oldVersion = 708;
2451         }
2452         if (oldVersion < 709) {
2453             // Added secondary locale phonebook labels; changed Japanese
2454             // and Chinese sort keys.
2455             upgradeLocaleSpecificData = true;
2456             oldVersion = 709;
2457         }
2458 
2459         if (oldVersion < 710) {
2460             upgradeToVersion710(db);
2461             upgradeViewsAndTriggers = true;
2462             oldVersion = 710;
2463         }
2464 
2465         if (oldVersion < 800) {
2466             upgradeToVersion800(db);
2467             oldVersion = 800;
2468         }
2469 
2470         if (oldVersion < 801) {
2471             PropertyUtils.setProperty(db, DbProperties.DATABASE_TIME_CREATED, String.valueOf(
2472                     System.currentTimeMillis()));
2473             oldVersion = 801;
2474         }
2475 
2476         if (oldVersion < 802) {
2477             upgradeToVersion802(db);
2478             upgradeViewsAndTriggers = true;
2479             oldVersion = 802;
2480         }
2481 
2482         if (oldVersion < 803) {
2483             // Rebuild the search index so that names, organizations and nicknames are
2484             // now indexed as names.
2485             upgradeSearchIndex = true;
2486             oldVersion = 803;
2487         }
2488 
2489         if (oldVersion < 804) {
2490             // Reserved.
2491             oldVersion = 804;
2492         }
2493 
2494         if (oldVersion < 900) {
2495             upgradeViewsAndTriggers = true;
2496             oldVersion = 900;
2497         }
2498 
2499         if (oldVersion < 901) {
2500             // Rebuild the search index to fix any search index that was previously in a
2501             // broken state due to b/11059351
2502             upgradeSearchIndex = true;
2503             oldVersion = 901;
2504         }
2505 
2506         if (oldVersion < 902) {
2507             upgradeToVersion902(db);
2508             oldVersion = 902;
2509         }
2510 
2511         if (oldVersion < 903) {
2512             upgradeToVersion903(db);
2513             oldVersion = 903;
2514         }
2515 
2516         if (oldVersion < 904) {
2517             upgradeToVersion904(db);
2518             oldVersion = 904;
2519         }
2520 
2521         if (oldVersion < 905) {
2522             upgradeToVersion905(db);
2523             oldVersion = 905;
2524         }
2525 
2526         if (oldVersion < 906) {
2527             upgradeToVersion906(db);
2528             oldVersion = 906;
2529         }
2530 
2531         if (oldVersion < 907) {
2532             // Rebuild NAME_LOOKUP.
2533             upgradeNameLookup = true;
2534             oldVersion = 907;
2535         }
2536 
2537         if (oldVersion < 908) {
2538             upgradeToVersion908(db);
2539             oldVersion = 908;
2540         }
2541 
2542         if (oldVersion < 909) {
2543             upgradeToVersion909(db);
2544             oldVersion = 909;
2545         }
2546 
2547         if (oldVersion < 910) {
2548             upgradeToVersion910(db);
2549             oldVersion = 910;
2550         }
2551         if (oldVersion < 1000) {
2552             upgradeToVersion1000(db);
2553             upgradeViewsAndTriggers = true;
2554             oldVersion = 1000;
2555         }
2556 
2557         if (oldVersion < 1002) {
2558             rebuildSqliteStats = true;
2559             upgradeToVersion1002(db);
2560             oldVersion = 1002;
2561         }
2562 
2563         if (oldVersion < 1003) {
2564             upgradeToVersion1003(db);
2565             oldVersion = 1003;
2566         }
2567 
2568         if (oldVersion < 1004) {
2569             upgradeToVersion1004(db);
2570             oldVersion = 1004;
2571         }
2572 
2573         if (oldVersion < 1005) {
2574             upgradeToVersion1005(db);
2575             oldVersion = 1005;
2576         }
2577 
2578         if (oldVersion < 1006) {
2579             upgradeViewsAndTriggers = true;
2580             oldVersion = 1006;
2581         }
2582 
2583         if (oldVersion < 1007) {
2584             upgradeToVersion1007(db);
2585             oldVersion = 1007;
2586         }
2587 
2588         if (oldVersion < 1009) {
2589             upgradeToVersion1009(db);
2590             oldVersion = 1009;
2591         }
2592 
2593         if (oldVersion < 1100) {
2594             upgradeToVersion1100(db);
2595             upgradeViewsAndTriggers = true;
2596             oldVersion = 1100;
2597         }
2598 
2599         if (oldVersion < 1101) {
2600             upgradeToVersion1101(db);
2601             oldVersion = 1101;
2602         }
2603 
2604         if (oldVersion < 1102) {
2605             // Version 1009 was added *after* 1100/1101.  For master devices
2606             // that have already been updated to 1101, we do it again.
2607             upgradeToVersion1009(db);
2608             oldVersion = 1102;
2609         }
2610 
2611         if (oldVersion < 1103) {
2612             upgradeViewsAndTriggers = true;
2613             oldVersion = 1103;
2614         }
2615 
2616         if (oldVersion < 1104) {
2617             upgradeToVersion1104(db);
2618             upgradeViewsAndTriggers = true;
2619             oldVersion = 1104;
2620         }
2621 
2622         if (oldVersion < 1105) {
2623             upgradeToVersion1105(db);
2624             upgradeViewsAndTriggers = true;
2625             oldVersion = 1105;
2626         }
2627 
2628         if (oldVersion < 1106) {
2629             upgradeToVersion1106(db);
2630             oldVersion = 1106;
2631         }
2632 
2633         if (oldVersion < 1107) {
2634             upgradeToVersion1107(db);
2635             oldVersion = 1107;
2636         }
2637 
2638         if (oldVersion < 1108) {
2639             upgradeToVersion1108(db);
2640             oldVersion = 1108;
2641         }
2642 
2643         if (isUpgradeRequired(oldVersion, newVersion, 1109)) {
2644             upgradeToVersion1109(db);
2645             oldVersion = 1109;
2646         }
2647 
2648         if (isUpgradeRequired(oldVersion, newVersion, 1110)) {
2649             upgradeToVersion1110(db);
2650             oldVersion = 1110;
2651         }
2652 
2653         if (isUpgradeRequired(oldVersion, newVersion, 1111)) {
2654             upgradeToVersion1111(db);
2655             oldVersion = 1111;
2656         }
2657 
2658         if (isUpgradeRequired(oldVersion, newVersion, 1200)) {
2659             createPresenceTables(db);
2660             oldVersion = 1200;
2661         }
2662 
2663         if (isUpgradeRequired(oldVersion, newVersion, 1201)) {
2664             upgradeToVersion1201(db);
2665             upgradeViewsAndTriggers = true;
2666             oldVersion = 1201;
2667         }
2668 
2669         if (isUpgradeRequired(oldVersion, newVersion, 1202)) {
2670             upgradeViewsAndTriggers = true;
2671             oldVersion = 1202;
2672         }
2673 
2674         if (isUpgradeRequired(oldVersion,newVersion, 1300)) {
2675             upgradeToVersion1300(db);
2676             upgradeViewsAndTriggers = true;
2677             oldVersion = 1300;
2678         }
2679 
2680         // We extracted "calls" and "voicemail_status" at this point, but we can't remove them here
2681         // yet, until CallLogDatabaseHelper moves the data.
2682 
2683         if (upgradeViewsAndTriggers) {
2684             createContactsViews(db);
2685             createGroupsView(db);
2686             createContactsTriggers(db);
2687             createContactsIndexes(db, false /* we build stats table later */);
2688             upgradeLegacyApiSupport = true;
2689             rebuildSqliteStats = true;
2690         }
2691 
2692         if (upgradeLegacyApiSupport) {
2693             LegacyApiSupport.createViews(db);
2694         }
2695 
2696         if (upgradeLocaleSpecificData) {
2697             upgradeLocaleData(db, false /* we build stats table later */);
2698             // Name lookups are rebuilt as part of the full locale rebuild
2699             upgradeNameLookup = false;
2700             upgradeSearchIndex = true;
2701             rebuildSqliteStats = true;
2702         }
2703 
2704         if (upgradeNameLookup) {
2705             rebuildNameLookup(db, false /* we build stats table later */);
2706             rebuildSqliteStats = true;
2707         }
2708 
2709         if (upgradeSearchIndex) {
2710             rebuildSearchIndex(db, false /* we build stats table later */);
2711             rebuildSqliteStats = true;
2712         }
2713 
2714         if (rescanDirectories) {
2715             // Force the next ContactDirectoryManager.scanAllPackages() to rescan all packages.
2716             // (It's called from the BACKGROUND_TASK_UPDATE_ACCOUNTS background task.)
2717             PropertyUtils.setProperty(db, DbProperties.DIRECTORY_SCAN_COMPLETE, "0");
2718         }
2719 
2720         if (rebuildSqliteStats) {
2721             updateSqliteStats(db);
2722         }
2723 
2724         if (oldVersion != newVersion) {
2725             throw new IllegalStateException(
2726                     "error upgrading the database to version " + newVersion);
2727         }
2728     }
2729 
isUpgradeRequired(int oldVersion, int newVersion, int version)2730     private static boolean isUpgradeRequired(int oldVersion, int newVersion, int version) {
2731         return oldVersion < version && newVersion >= version;
2732     }
2733 
rebuildNameLookup(SQLiteDatabase db, boolean rebuildSqliteStats)2734     private void rebuildNameLookup(SQLiteDatabase db, boolean rebuildSqliteStats) {
2735         db.execSQL("DROP INDEX IF EXISTS name_lookup_index");
2736         insertNameLookup(db);
2737         createContactsIndexes(db, rebuildSqliteStats);
2738     }
2739 
rebuildSearchIndex()2740     protected void rebuildSearchIndex() {
2741         rebuildSearchIndex(getWritableDatabase(), true);
2742     }
2743 
rebuildSearchIndex(SQLiteDatabase db, boolean rebuildSqliteStats)2744     private void rebuildSearchIndex(SQLiteDatabase db, boolean rebuildSqliteStats) {
2745         createSearchIndexTable(db, rebuildSqliteStats);
2746         PropertyUtils.setProperty(db, SearchIndexManager.PROPERTY_SEARCH_INDEX_VERSION, "0");
2747     }
2748 
2749     /**
2750      * Checks whether the current ICU code version matches that used to build
2751      * the locale specific data in the ContactsDB.
2752      */
needsToUpdateLocaleData(LocaleSet locales)2753     public boolean needsToUpdateLocaleData(LocaleSet locales) {
2754         final String dbLocale = getProperty(DbProperties.LOCALE, "");
2755         if (!dbLocale.equals(locales.toString())) {
2756             return true;
2757         }
2758         final String curICUVersion = ICU.getIcuVersion();
2759         final String dbICUVersion = getProperty(DbProperties.ICU_VERSION,
2760                 "(unknown)");
2761         if (!curICUVersion.equals(dbICUVersion)) {
2762             Log.i(TAG, "ICU version has changed. Current version is "
2763                     + curICUVersion + "; DB was built with " + dbICUVersion);
2764             return true;
2765         }
2766         return false;
2767     }
2768 
upgradeLocaleData(SQLiteDatabase db, boolean rebuildSqliteStats)2769     private void upgradeLocaleData(SQLiteDatabase db, boolean rebuildSqliteStats) {
2770         final LocaleSet locales = LocaleSet.newDefault();
2771         Log.i(TAG, "Upgrading locale data for " + locales
2772                 + " (ICU v" + ICU.getIcuVersion() + ")");
2773         final long start = SystemClock.elapsedRealtime();
2774         rebuildLocaleData(db, locales, rebuildSqliteStats);
2775         Log.i(TAG, "Locale update completed in " + (SystemClock.elapsedRealtime() - start) + "ms");
2776     }
2777 
rebuildLocaleData(SQLiteDatabase db, LocaleSet locales, boolean rebuildSqliteStats)2778     private void rebuildLocaleData(SQLiteDatabase db, LocaleSet locales, boolean rebuildSqliteStats) {
2779         db.execSQL("DROP INDEX raw_contact_sort_key1_index");
2780         db.execSQL("DROP INDEX raw_contact_sort_key2_index");
2781         db.execSQL("DROP INDEX IF EXISTS name_lookup_index");
2782 
2783         loadNicknameLookupTable(db);
2784         insertNameLookup(db);
2785         rebuildSortKeys(db);
2786         createContactsIndexes(db, rebuildSqliteStats);
2787 
2788         FastScrollingIndexCache.getInstance(mContext).invalidate();
2789         // Update the ICU version used to generate the locale derived data
2790         // so we can tell when we need to rebuild with new ICU versions.
2791         PropertyUtils.setProperty(db, DbProperties.ICU_VERSION, ICU.getIcuVersion());
2792         PropertyUtils.setProperty(db, DbProperties.LOCALE, locales.toString());
2793     }
2794 
2795     /**
2796      * Regenerates all locale-sensitive data if needed:
2797      * nickname_lookup, name_lookup and sort keys. Invalidates the fast
2798      * scrolling index cache.
2799      */
setLocale(LocaleSet locales)2800     public void setLocale(LocaleSet locales) {
2801         if (!needsToUpdateLocaleData(locales)) {
2802             return;
2803         }
2804         Log.i(TAG, "Switching to locale " + locales
2805                 + " (ICU v" + ICU.getIcuVersion() + ")");
2806 
2807         final long start = SystemClock.elapsedRealtime();
2808         SQLiteDatabase db = getWritableDatabase();
2809         db.setLocale(locales.getPrimaryLocale());
2810         db.beginTransaction();
2811         try {
2812             rebuildLocaleData(db, locales, true);
2813             db.setTransactionSuccessful();
2814         } finally {
2815             db.endTransaction();
2816         }
2817 
2818         Log.i(TAG, "Locale change completed in " + (SystemClock.elapsedRealtime() - start) + "ms");
2819     }
2820 
2821     /**
2822      * Regenerates sort keys for all contacts.
2823      */
rebuildSortKeys(SQLiteDatabase db)2824     private void rebuildSortKeys(SQLiteDatabase db) {
2825         Cursor cursor = db.query(Tables.RAW_CONTACTS, new String[] {RawContacts._ID},
2826                 null, null, null, null, null);
2827         try {
2828             while (cursor.moveToNext()) {
2829                 long rawContactId = cursor.getLong(0);
2830                 updateRawContactDisplayName(db, rawContactId);
2831             }
2832         } finally {
2833             cursor.close();
2834         }
2835     }
2836 
insertNameLookup(SQLiteDatabase db)2837     private void insertNameLookup(SQLiteDatabase db) {
2838         db.execSQL("DELETE FROM " + Tables.NAME_LOOKUP);
2839 
2840         final SQLiteStatement nameLookupInsert = db.compileStatement(
2841                 "INSERT OR IGNORE INTO " + Tables.NAME_LOOKUP + "("
2842                         + NameLookupColumns.RAW_CONTACT_ID + ","
2843                         + NameLookupColumns.DATA_ID + ","
2844                         + NameLookupColumns.NAME_TYPE + ","
2845                         + NameLookupColumns.NORMALIZED_NAME +
2846                 ") VALUES (?,?,?,?)");
2847 
2848         try {
2849             insertStructuredNameLookup(db, nameLookupInsert);
2850             insertEmailLookup(db, nameLookupInsert);
2851             insertNicknameLookup(db, nameLookupInsert);
2852         } finally {
2853             nameLookupInsert.close();
2854         }
2855     }
2856 
2857     /**
2858      * Inserts name lookup rows for all structured names in the database.
2859      */
insertStructuredNameLookup(SQLiteDatabase db, SQLiteStatement nameLookupInsert)2860     private void insertStructuredNameLookup(SQLiteDatabase db, SQLiteStatement nameLookupInsert) {
2861         NameSplitter nameSplitter = createNameSplitter();
2862         NameLookupBuilder nameLookupBuilder = new StructuredNameLookupBuilder(nameSplitter,
2863                 new CommonNicknameCache(db), nameLookupInsert);
2864         final long mimeTypeId = lookupMimeTypeId(db, StructuredName.CONTENT_ITEM_TYPE);
2865         Cursor cursor = db.query(StructuredNameQuery.TABLE, StructuredNameQuery.COLUMNS,
2866                 StructuredNameQuery.SELECTION, new String[] {String.valueOf(mimeTypeId)},
2867                 null, null, null);
2868         try {
2869             while (cursor.moveToNext()) {
2870                 long dataId = cursor.getLong(StructuredNameQuery.ID);
2871                 long rawContactId = cursor.getLong(StructuredNameQuery.RAW_CONTACT_ID);
2872                 String name = cursor.getString(StructuredNameQuery.DISPLAY_NAME);
2873                 int fullNameStyle = nameSplitter.guessFullNameStyle(name);
2874                 fullNameStyle = nameSplitter.getAdjustedFullNameStyle(fullNameStyle);
2875                 nameLookupBuilder.insertNameLookup(rawContactId, dataId, name, fullNameStyle);
2876             }
2877         } finally {
2878             cursor.close();
2879         }
2880     }
2881 
2882     /**
2883      * Inserts name lookup rows for all email addresses in the database.
2884      */
insertEmailLookup(SQLiteDatabase db, SQLiteStatement nameLookupInsert)2885     private void insertEmailLookup(SQLiteDatabase db, SQLiteStatement nameLookupInsert) {
2886         final long mimeTypeId = lookupMimeTypeId(db, Email.CONTENT_ITEM_TYPE);
2887         Cursor cursor = db.query(EmailQuery.TABLE, EmailQuery.COLUMNS,
2888                 EmailQuery.SELECTION, new String[] {String.valueOf(mimeTypeId)},
2889                 null, null, null);
2890         try {
2891             while (cursor.moveToNext()) {
2892                 long dataId = cursor.getLong(EmailQuery.ID);
2893                 long rawContactId = cursor.getLong(EmailQuery.RAW_CONTACT_ID);
2894                 String address = cursor.getString(EmailQuery.ADDRESS);
2895                 address = extractHandleFromEmailAddress(address);
2896                 insertNameLookup(nameLookupInsert, rawContactId, dataId,
2897                         NameLookupType.EMAIL_BASED_NICKNAME, address);
2898             }
2899         } finally {
2900             cursor.close();
2901         }
2902     }
2903 
2904     /**
2905      * Inserts name lookup rows for all nicknames in the database.
2906      */
insertNicknameLookup(SQLiteDatabase db, SQLiteStatement nameLookupInsert)2907     private void insertNicknameLookup(SQLiteDatabase db, SQLiteStatement nameLookupInsert) {
2908         final long mimeTypeId = lookupMimeTypeId(db, Nickname.CONTENT_ITEM_TYPE);
2909         Cursor cursor = db.query(NicknameQuery.TABLE, NicknameQuery.COLUMNS,
2910                 NicknameQuery.SELECTION, new String[]{String.valueOf(mimeTypeId)},
2911                 null, null, null);
2912         try {
2913             while (cursor.moveToNext()) {
2914                 long dataId = cursor.getLong(NicknameQuery.ID);
2915                 long rawContactId = cursor.getLong(NicknameQuery.RAW_CONTACT_ID);
2916                 String nickname = cursor.getString(NicknameQuery.NAME);
2917                 insertNameLookup(nameLookupInsert, rawContactId, dataId,
2918                         NameLookupType.NICKNAME, nickname);
2919             }
2920         } finally {
2921             cursor.close();
2922         }
2923     }
2924 
2925     /**
2926      * Inserts a record in the {@link Tables#NAME_LOOKUP} table.
2927      */
insertNameLookup(SQLiteStatement stmt, long rawContactId, long dataId, int lookupType, String name)2928     public void insertNameLookup(SQLiteStatement stmt, long rawContactId, long dataId,
2929             int lookupType, String name) {
2930         if (TextUtils.isEmpty(name)) {
2931             return;
2932         }
2933 
2934         String normalized = NameNormalizer.normalize(name);
2935         if (TextUtils.isEmpty(normalized)) {
2936             return;
2937         }
2938 
2939         insertNormalizedNameLookup(stmt, rawContactId, dataId, lookupType, normalized);
2940     }
2941 
insertNormalizedNameLookup(SQLiteStatement stmt, long rawContactId, long dataId, int lookupType, String normalizedName)2942     private void insertNormalizedNameLookup(SQLiteStatement stmt, long rawContactId, long dataId,
2943             int lookupType, String normalizedName) {
2944         stmt.bindLong(1, rawContactId);
2945         stmt.bindLong(2, dataId);
2946         stmt.bindLong(3, lookupType);
2947         stmt.bindString(4, normalizedName);
2948         stmt.executeInsert();
2949     }
2950 
upgradeToVersion701(SQLiteDatabase db)2951     private void upgradeToVersion701(SQLiteDatabase db) {
2952         db.execSQL("UPDATE raw_contacts SET last_time_contacted =" +
2953                 " max(ifnull(last_time_contacted, 0), " +
2954                 " ifnull((SELECT max(last_time_used) " +
2955                     " FROM data JOIN data_usage_stat ON (data._id = data_usage_stat.data_id)" +
2956                     " WHERE data.raw_contact_id = raw_contacts._id), 0))");
2957         // Replace 0 with null.  This isn't really necessary, but we do this anyway for consistency.
2958         db.execSQL("UPDATE raw_contacts SET last_time_contacted = null" +
2959                 " where last_time_contacted = 0");
2960     }
2961 
2962     /**
2963      * Pre-HC devices don't have correct "NORMALIZED_NUMBERS".  Clear them up.
2964      */
upgradeToVersion702(SQLiteDatabase db)2965     private void upgradeToVersion702(SQLiteDatabase db) {
2966         // All the "correct" Phone.NORMALIZED_NUMBERS should begin with "+".  The upgraded data
2967         // don't.  Find all Phone.NORMALIZED_NUMBERS that don't begin with "+".
2968         final int count;
2969         final long[] dataIds;
2970         final long[] rawContactIds;
2971         final String[] phoneNumbers;
2972         final StringBuilder sbDataIds;
2973         final Cursor c = db.rawQuery(
2974                 "SELECT _id, raw_contact_id, data1 FROM data " +
2975                 " WHERE mimetype_id=" +
2976                     "(SELECT _id FROM mimetypes" +
2977                     " WHERE mimetype='vnd.android.cursor.item/phone_v2')" +
2978                 " AND data4 not like '+%'", // "Not like" will exclude nulls too.
2979                 null);
2980         try {
2981             count = c.getCount();
2982             if (count == 0) {
2983                 return;
2984             }
2985             dataIds = new long[count];
2986             rawContactIds = new long[count];
2987             phoneNumbers = new String[count];
2988             sbDataIds = new StringBuilder();
2989 
2990             c.moveToPosition(-1);
2991             while (c.moveToNext()) {
2992                 final int i = c.getPosition();
2993                 dataIds[i] = c.getLong(0);
2994                 rawContactIds[i] = c.getLong(1);
2995                 phoneNumbers[i] = c.getString(2);
2996 
2997                 if (sbDataIds.length() > 0) {
2998                     sbDataIds.append(",");
2999                 }
3000                 sbDataIds.append(dataIds[i]);
3001             }
3002         } finally {
3003             c.close();
3004         }
3005 
3006         final String dataIdList = sbDataIds.toString();
3007 
3008         // Then, update the Data and PhoneLookup tables.
3009 
3010         // First, just null out all Phone.NORMALIZED_NUMBERS for those.
3011         db.execSQL("UPDATE data SET data4 = null" +
3012                 " WHERE _id IN (" + dataIdList + ")");
3013 
3014         // Then, re-create phone_lookup for them.
3015         db.execSQL("DELETE FROM phone_lookup" +
3016                 " WHERE data_id IN (" + dataIdList + ")");
3017 
3018         for (int i = 0; i < count; i++) {
3019             // Mimic how DataRowHandlerForPhoneNumber.insert() works when it can't normalize
3020             // numbers.
3021             final String phoneNumber = phoneNumbers[i];
3022             if (TextUtils.isEmpty(phoneNumber)) continue;
3023 
3024             final String normalized = PhoneNumberUtils.normalizeNumber(phoneNumber);
3025             if (!TextUtils.isEmpty(normalized)) {
3026                 db.execSQL("INSERT INTO phone_lookup" +
3027                         "(data_id, raw_contact_id, normalized_number, min_match)" +
3028                         " VALUES(?,?,?,?)",
3029                         new String[] {
3030                             String.valueOf(dataIds[i]),
3031                             String.valueOf(rawContactIds[i]),
3032                             normalized,
3033                             PhoneNumberUtils.toCallerIDMinMatch(normalized)});
3034             }
3035         }
3036     }
3037 
upgradeToVersion707(SQLiteDatabase db)3038     private void upgradeToVersion707(SQLiteDatabase db) {
3039         db.execSQL("ALTER TABLE raw_contacts ADD phonebook_label TEXT;");
3040         db.execSQL("ALTER TABLE raw_contacts ADD phonebook_bucket INTEGER;");
3041         db.execSQL("ALTER TABLE raw_contacts ADD phonebook_label_alt TEXT;");
3042         db.execSQL("ALTER TABLE raw_contacts ADD phonebook_bucket_alt INTEGER;");
3043     }
3044 
upgradeToVersion710(SQLiteDatabase db)3045     private void upgradeToVersion710(SQLiteDatabase db) {
3046 
3047         // Adding timestamp to contacts table.
3048         db.execSQL("ALTER TABLE contacts"
3049                 + " ADD contact_last_updated_timestamp INTEGER;");
3050 
3051         db.execSQL("UPDATE contacts"
3052                 + " SET contact_last_updated_timestamp"
3053                 + " = " + System.currentTimeMillis());
3054 
3055         db.execSQL("CREATE INDEX contacts_contact_last_updated_timestamp_index "
3056                 + "ON contacts(contact_last_updated_timestamp)");
3057 
3058         // New deleted contacts table.
3059         db.execSQL("CREATE TABLE deleted_contacts (" +
3060                 "contact_id INTEGER PRIMARY KEY," +
3061                 "contact_deleted_timestamp INTEGER NOT NULL default 0"
3062                 + ");");
3063 
3064         db.execSQL("CREATE INDEX deleted_contacts_contact_deleted_timestamp_index "
3065                 + "ON deleted_contacts(contact_deleted_timestamp)");
3066     }
3067 
upgradeToVersion800(SQLiteDatabase db)3068     private void upgradeToVersion800(SQLiteDatabase db) {
3069         // Default Calls.PRESENTATION_ALLOWED=1
3070         db.execSQL("ALTER TABLE calls ADD presentation INTEGER NOT NULL DEFAULT 1;");
3071 
3072         // Re-map CallerInfo.{..}_NUMBER strings to Calls.PRESENTATION_{..} ints
3073         //  PRIVATE_NUMBER="-2" -> PRESENTATION_RESTRICTED=2
3074         //  UNKNOWN_NUMBER="-1" -> PRESENTATION_UNKNOWN   =3
3075         // PAYPHONE_NUMBER="-3" -> PRESENTATION_PAYPHONE  =4
3076         db.execSQL("UPDATE calls SET presentation=2, number='' WHERE number='-2';");
3077         db.execSQL("UPDATE calls SET presentation=3, number='' WHERE number='-1';");
3078         db.execSQL("UPDATE calls SET presentation=4, number='' WHERE number='-3';");
3079     }
3080 
upgradeToVersion802(SQLiteDatabase db)3081     private void upgradeToVersion802(SQLiteDatabase db) {
3082         db.execSQL("ALTER TABLE contacts ADD pinned INTEGER NOT NULL DEFAULT " +
3083                 ContactsContract.PinnedPositions.UNPINNED + ";");
3084         db.execSQL("ALTER TABLE raw_contacts ADD pinned INTEGER NOT NULL DEFAULT  " +
3085                 ContactsContract.PinnedPositions.UNPINNED + ";");
3086     }
3087 
upgradeToVersion902(SQLiteDatabase db)3088     private void upgradeToVersion902(SQLiteDatabase db) {
3089         // adding account identifier to call log table
3090         db.execSQL("ALTER TABLE calls ADD subscription_component_name TEXT;");
3091         db.execSQL("ALTER TABLE calls ADD subscription_id TEXT;");
3092     }
3093 
3094     /**
3095      * Searches for any calls in the call log with no normalized phone number and attempts to add
3096      * one if the number can be normalized.
3097      *
3098      * @param db The database.
3099      */
upgradeToVersion903(SQLiteDatabase db)3100     private void upgradeToVersion903(SQLiteDatabase db) {
3101         // Find the calls in the call log with no normalized phone number.
3102         final Cursor c = db.rawQuery(
3103                 "SELECT _id, number, countryiso FROM calls " +
3104                         " WHERE (normalized_number is null OR normalized_number = '') " +
3105                         " AND countryiso != '' AND countryiso is not null " +
3106                         " AND number != '' AND number is not null;",
3107                 null
3108         );
3109 
3110         try {
3111             if (c.getCount() == 0) {
3112                 return;
3113             }
3114 
3115             db.beginTransaction();
3116             try {
3117                 c.moveToPosition(-1);
3118                 while (c.moveToNext()) {
3119                     final long callId = c.getLong(0);
3120                     final String unNormalizedNumber = c.getString(1);
3121                     final String countryIso = c.getString(2);
3122 
3123                     // Attempt to get normalized number.
3124                     String normalizedNumber = PhoneNumberUtils
3125                             .formatNumberToE164(unNormalizedNumber, countryIso);
3126 
3127                     if (!TextUtils.isEmpty(normalizedNumber)) {
3128                         db.execSQL("UPDATE calls set normalized_number = ? " +
3129                                         "where _id = ?;",
3130                                 new String[]{
3131                                         normalizedNumber,
3132                                         String.valueOf(callId),
3133                                 }
3134                         );
3135                     }
3136                 }
3137 
3138                 db.setTransactionSuccessful();
3139             } finally {
3140                 db.endTransaction();
3141             }
3142         } finally {
3143             c.close();
3144         }
3145     }
3146 
3147     /**
3148      * Updates the calls table in the database to include the call_duration and features columns.
3149      * @param db The database to update.
3150      */
upgradeToVersion904(SQLiteDatabase db)3151     private void upgradeToVersion904(SQLiteDatabase db) {
3152         db.execSQL("ALTER TABLE calls ADD features INTEGER NOT NULL DEFAULT 0;");
3153         db.execSQL("ALTER TABLE calls ADD data_usage INTEGER;");
3154     }
3155 
3156     /**
3157      * Adds the voicemail transcription to the Table.Calls
3158      */
upgradeToVersion905(SQLiteDatabase db)3159     private void upgradeToVersion905(SQLiteDatabase db) {
3160         db.execSQL("ALTER TABLE calls ADD transcription TEXT;");
3161     }
3162 
3163     /**
3164      * Upgrades the database with the new value for {@link PinnedPositions#UNPINNED}. In this
3165      * database version upgrade, the value is changed from 2147483647 (Integer.MAX_VALUE) to 0.
3166      *
3167      * The first pinned contact now starts from position 1.
3168      */
3169     @VisibleForTesting
upgradeToVersion906(SQLiteDatabase db)3170     public void upgradeToVersion906(SQLiteDatabase db) {
3171         db.execSQL("UPDATE contacts SET pinned = pinned + 1"
3172                 + " WHERE pinned >= 0 AND pinned < 2147483647;");
3173         db.execSQL("UPDATE raw_contacts SET pinned = pinned + 1"
3174                 + " WHERE pinned >= 0 AND pinned < 2147483647;");
3175 
3176         db.execSQL("UPDATE contacts SET pinned = 0"
3177                 + " WHERE pinned = 2147483647;");
3178         db.execSQL("UPDATE raw_contacts SET pinned = 0"
3179                 + " WHERE pinned = 2147483647;");
3180     }
3181 
upgradeToVersion908(SQLiteDatabase db)3182     private void upgradeToVersion908(SQLiteDatabase db) {
3183         db.execSQL("UPDATE contacts SET pinned = 0 WHERE pinned = 2147483647;");
3184         db.execSQL("UPDATE raw_contacts SET pinned = 0 WHERE pinned = 2147483647;");
3185     }
3186 
upgradeToVersion909(SQLiteDatabase db)3187     private void upgradeToVersion909(SQLiteDatabase db) {
3188         try {
3189             db.execSQL("ALTER TABLE calls ADD sub_id INTEGER DEFAULT -1;");
3190         } catch (SQLiteException e) {
3191             // The column already exists--copy over data
3192             db.execSQL("UPDATE calls SET subscription_component_name='com.android.phone/"
3193                     + "com.android.services.telephony.TelephonyConnectionService';");
3194             db.execSQL("UPDATE calls SET subscription_id=sub_id;");
3195         }
3196     }
3197 
3198     /**
3199      * Delete any remaining rows in the calls table if the user is a profile of another user.
3200      * b/17096027
3201      */
upgradeToVersion910(SQLiteDatabase db)3202     private void upgradeToVersion910(SQLiteDatabase db) {
3203         final UserManager userManager = (UserManager) mContext.getSystemService(
3204                 Context.USER_SERVICE);
3205         final UserInfo user = userManager.getUserInfo(userManager.getUserHandle());
3206         if (user.isManagedProfile()) {
3207             db.execSQL("DELETE FROM calls;");
3208         }
3209     }
3210 
3211     /**
3212      * Add backup_id column to raw_contacts table and hash_id column to data table.
3213      */
upgradeToVersion1000(SQLiteDatabase db)3214     private void upgradeToVersion1000(SQLiteDatabase db) {
3215         db.execSQL("ALTER TABLE raw_contacts ADD backup_id TEXT;");
3216         db.execSQL("ALTER TABLE data ADD hash_id TEXT;");
3217         db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS raw_contacts_backup_id_account_id_index ON " +
3218                 "raw_contacts (backup_id, account_id);");
3219         db.execSQL("CREATE INDEX IF NOT EXISTS data_hash_id_index ON data (hash_id);");
3220     }
3221 
3222     @VisibleForTesting
upgradeToVersion1002(SQLiteDatabase db)3223     public void upgradeToVersion1002(SQLiteDatabase db) {
3224         db.execSQL("DROP TABLE IF EXISTS pre_authorized_uris;");
3225         db.execSQL("CREATE TABLE pre_authorized_uris ("+
3226                 "_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
3227                 "uri STRING NOT NULL, " +
3228                 "expiration INTEGER NOT NULL DEFAULT 0);");
3229     }
3230 
upgradeToVersion1003(SQLiteDatabase db)3231     public void upgradeToVersion1003(SQLiteDatabase db) {
3232         db.execSQL("ALTER TABLE calls ADD phone_account_address TEXT;");
3233 
3234         // After version 1003, we are using the ICC ID as the phone-account ID. This code updates
3235         // any existing telephony connection-service calllog entries to the ICC ID from the
3236         // previously used subscription ID.
3237         // TODO: This is inconsistent, depending on the initialization state of SubscriptionManager.
3238         //       Sometimes it returns zero subscriptions. May want to move this upgrade to run after
3239         //       ON_BOOT_COMPLETE instead of PRE_BOOT_COMPLETE.
3240         SubscriptionManager sm = SubscriptionManager.from(mContext);
3241         if (sm != null) {
3242             Log.i(TAG, "count: " + sm.getAllSubscriptionInfoCount());
3243             for (SubscriptionInfo info : sm.getAllSubscriptionInfoList()) {
3244                 String iccId = info.getIccId();
3245                 int subId = info.getSubscriptionId();
3246                 if (!TextUtils.isEmpty(iccId) &&
3247                         subId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
3248                     StringBuilder sb = new StringBuilder();
3249                     sb.append("UPDATE calls SET subscription_id=");
3250                     DatabaseUtils.appendEscapedSQLString(sb, iccId);
3251                     sb.append(" WHERE subscription_id=");
3252                     sb.append(subId);
3253                     sb.append(" AND subscription_component_name='com.android.phone/"
3254                             + "com.android.services.telephony.TelephonyConnectionService';");
3255 
3256                     db.execSQL(sb.toString());
3257                 }
3258             }
3259         }
3260     }
3261 
3262     /**
3263      * Add a "hidden" column for call log entries we want to hide after an upgrade until the user
3264      * adds the right phone account to the device.
3265      */
upgradeToVersion1004(SQLiteDatabase db)3266     public void upgradeToVersion1004(SQLiteDatabase db) {
3267         db.execSQL("ALTER TABLE calls ADD phone_account_hidden INTEGER NOT NULL DEFAULT 0;");
3268     }
3269 
upgradeToVersion1005(SQLiteDatabase db)3270     public void upgradeToVersion1005(SQLiteDatabase db) {
3271         db.execSQL("ALTER TABLE calls ADD photo_uri TEXT;");
3272     }
3273 
3274     /**
3275      * The try/catch pattern exists because some devices have the upgrade and some do not. This is
3276      * because the below updates were merged into version 1005 after some devices had already
3277      * upgraded to version 1005 and hence did not receive the below upgrades.
3278      */
upgradeToVersion1007(SQLiteDatabase db)3279     public void upgradeToVersion1007(SQLiteDatabase db) {
3280         try {
3281             // Add multi-sim fields
3282             db.execSQL("ALTER TABLE voicemail_status ADD phone_account_component_name TEXT;");
3283             db.execSQL("ALTER TABLE voicemail_status ADD phone_account_id TEXT;");
3284 
3285             // For use by the sync adapter
3286             db.execSQL("ALTER TABLE calls ADD dirty INTEGER NOT NULL DEFAULT 0;");
3287             db.execSQL("ALTER TABLE calls ADD deleted INTEGER NOT NULL DEFAULT 0;");
3288         } catch (SQLiteException e) {
3289             // These columns already exist. Do nothing.
3290             // Log verbose because this should be the majority case.
3291             Log.v(TAG, "Version 1007: Columns already exist, skipping upgrade steps.");
3292         }
3293   }
3294 
3295 
upgradeToVersion1009(SQLiteDatabase db)3296     public void upgradeToVersion1009(SQLiteDatabase db) {
3297         try {
3298             db.execSQL("ALTER TABLE data ADD carrier_presence INTEGER NOT NULL DEFAULT 0");
3299         } catch (SQLiteException ignore) {
3300         }
3301     }
3302 
upgradeToVersion1100(SQLiteDatabase db)3303     private void upgradeToVersion1100(SQLiteDatabase db) {
3304         db.execSQL("ALTER TABLE raw_contacts ADD metadata_dirty INTEGER NOT NULL DEFAULT 0;");
3305     }
3306 
3307     // Data.hash_id column is used for metadata backup, and this upgrade is to generate
3308     // hash_id column. Usually data1 and data2 are two main columns to store data info.
3309     // But for photo, we don't use data1 and data2, instead, use data15 to store photo blob.
3310     // So this upgrade generates hash_id from (data1 + data2) or (data15) using sha-1.
upgradeToVersion1101(SQLiteDatabase db)3311     public void upgradeToVersion1101(SQLiteDatabase db) {
3312         final SQLiteStatement update = db.compileStatement(
3313                 "UPDATE " + Tables.DATA +
3314                 " SET " + Data.HASH_ID + "=?" +
3315                 " WHERE " + Data._ID + "=?"
3316         );
3317         final Cursor c = db.query(Tables.DATA,
3318                 new String[] {Data._ID, Data.DATA1, Data.DATA2, Data.DATA15},
3319                 null, null, null, null, Data._ID);
3320         try {
3321             while (c.moveToNext()) {
3322                 final long dataId = c.getLong(0);
3323                 final String data1 = c.getString(1);
3324                 final String data2 = c.getString(2);
3325                 final byte[] data15 = c.getBlob(3);
3326                 final String hashId = legacyGenerateHashId(data1, data2, data15);
3327                 if (!TextUtils.isEmpty(hashId)) {
3328                     update.bindString(1, hashId);
3329                     update.bindLong(2, dataId);
3330                     update.execute();
3331                 }
3332             }
3333         } finally {
3334             c.close();
3335         }
3336     }
3337 
3338     /**
3339      * Add new metadata_sync table to cache the meta data on raw contacts level from server before
3340      * they are merged into other CP2 tables. The data column is the blob column containing all
3341      * the backed up metadata for this raw_contact. This table should only be used by metadata
3342      * sync adapter.
3343      */
upgradeToVersion1104(SQLiteDatabase db)3344     public void upgradeToVersion1104(SQLiteDatabase db) {
3345         db.execSQL("DROP TABLE IF EXISTS metadata_sync;");
3346         db.execSQL("CREATE TABLE metadata_sync (" +
3347                 "_id INTEGER PRIMARY KEY AUTOINCREMENT, raw_contact_backup_id TEXT NOT NULL, " +
3348                 "account_id INTEGER NOT NULL, data TEXT, deleted INTEGER NOT NULL DEFAULT 0);");
3349         db.execSQL("CREATE UNIQUE INDEX metadata_sync_index ON metadata_sync (" +
3350                 "raw_contact_backup_id, account_id);");
3351     }
3352 
3353     /**
3354      * Add new metadata_sync_state table to store the metadata sync state for a set of accounts.
3355      */
upgradeToVersion1105(SQLiteDatabase db)3356     public void upgradeToVersion1105(SQLiteDatabase db) {
3357         db.execSQL("DROP TABLE IF EXISTS metadata_sync_state;");
3358         db.execSQL("CREATE TABLE metadata_sync_state (" +
3359                 "_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
3360                 "account_id INTEGER NOT NULL, state BLOB);");
3361         db.execSQL("CREATE UNIQUE INDEX metadata_sync_state_index ON metadata_sync_state (" +
3362                 "account_id);");
3363     }
3364 
upgradeToVersion1106(SQLiteDatabase db)3365     public void upgradeToVersion1106(SQLiteDatabase db) {
3366         db.execSQL("ALTER TABLE calls ADD post_dial_digits TEXT NOT NULL DEFAULT ''");
3367     }
3368 
upgradeToVersion1107(SQLiteDatabase db)3369     public void upgradeToVersion1107(SQLiteDatabase db) {
3370         try {
3371             db.execSQL("ALTER TABLE calls ADD post_dial_digits TEXT NOT NULL DEFAULT ''");
3372         } catch (SQLiteException ignore) {
3373             // This is for devices which got initialized without a post_dial_digits
3374             // column from version 1106. The exception indicates that the column is
3375             // already present, so nothing needs to be done.
3376         }
3377     }
3378 
upgradeToVersion1108(SQLiteDatabase db)3379     public void upgradeToVersion1108(SQLiteDatabase db) {
3380         db.execSQL(
3381                 "ALTER TABLE calls ADD add_for_all_users INTEGER NOT NULL DEFAULT 1");
3382     }
3383 
upgradeToVersion1109(SQLiteDatabase db)3384     public void upgradeToVersion1109(SQLiteDatabase db) {
3385         db.execSQL("ALTER TABLE voicemail_status ADD quota_occupied INTEGER DEFAULT -1;");
3386         db.execSQL("ALTER TABLE voicemail_status ADD quota_total INTEGER DEFAULT -1;");
3387         db.execSQL("ALTER TABLE calls ADD last_modified INTEGER DEFAULT 0;");
3388     }
3389 
3390     /**
3391      * Update hash_id for photo data. Generates the same value for all photo mimetype data, since
3392      * there's only one photo for each raw_contact.
3393      */
upgradeToVersion1110(SQLiteDatabase db)3394     public void upgradeToVersion1110(SQLiteDatabase db) {
3395         final long mimeTypeId = lookupMimeTypeId(db,
3396                 ContactsContract.CommonDataKinds.Photo.CONTENT_ITEM_TYPE);
3397         final ContentValues values = new ContentValues();
3398         values.put(Data.HASH_ID, getPhotoHashId());
3399         db.update(Tables.DATA, values, DataColumns.MIMETYPE_ID + " = " + mimeTypeId, null);
3400     }
3401 
getPhotoHashId()3402     public String getPhotoHashId() {
3403         return generateHashId(ContactsContract.CommonDataKinds.Photo.CONTENT_ITEM_TYPE, null);
3404     }
3405 
3406     @VisibleForTesting
upgradeToVersion1111(SQLiteDatabase db)3407     public void upgradeToVersion1111(SQLiteDatabase db) {
3408         // Re-order contacts with no display name to the phone number bucket and give
3409         // them the phone number label. See b/21736630.
3410         final ContactLocaleUtils localeUtils = ContactLocaleUtils.getInstance();
3411         final int index = localeUtils.getNumberBucketIndex();
3412         final String label = localeUtils.getBucketLabel(index);
3413         // Note, sort_key = null is equivalent to display_name = null
3414         db.execSQL("UPDATE raw_contacts SET phonebook_bucket = " + index +
3415                 ", phonebook_label='" + label + "' WHERE sort_key IS NULL AND phonebook_bucket=0;");
3416         db.execSQL("UPDATE raw_contacts SET phonebook_bucket_alt = " + index +
3417                 ", phonebook_label_alt='" + label +
3418                 "' WHERE sort_key_alt IS NULL AND phonebook_bucket_alt=0;");
3419 
3420         FastScrollingIndexCache.getInstance(mContext).invalidate();
3421     }
3422 
upgradeToVersion1201(SQLiteDatabase db)3423     private void upgradeToVersion1201(SQLiteDatabase db) {
3424         db.execSQL("ALTER TABLE contacts ADD x_times_contacted INTEGER NOT NULL DEFAULT 0");
3425         db.execSQL("ALTER TABLE contacts ADD x_last_time_contacted INTEGER");
3426 
3427         db.execSQL("ALTER TABLE raw_contacts ADD x_times_contacted INTEGER NOT NULL DEFAULT 0");
3428         db.execSQL("ALTER TABLE raw_contacts ADD x_last_time_contacted INTEGER");
3429 
3430         db.execSQL("ALTER TABLE data_usage_stat ADD x_times_used INTEGER NOT NULL DEFAULT 0");
3431         db.execSQL("ALTER TABLE data_usage_stat ADD x_last_time_used INTEGER NOT NULL DEFAULT 0");
3432 
3433         db.execSQL("UPDATE contacts SET "
3434                 + "x_times_contacted = ifnull(times_contacted,0),"
3435                 + "x_last_time_contacted = ifnull(last_time_contacted,0),"
3436                 + "times_contacted = 0,"
3437                 + "last_time_contacted = 0");
3438 
3439         db.execSQL("UPDATE raw_contacts SET "
3440                 + "x_times_contacted = ifnull(times_contacted,0),"
3441                 + "x_last_time_contacted = ifnull(last_time_contacted,0),"
3442                 + "times_contacted = 0,"
3443                 + "last_time_contacted = 0");
3444 
3445         db.execSQL("UPDATE data_usage_stat SET "
3446                 + "x_times_used = ifnull(times_used,0),"
3447                 + "x_last_time_used = ifnull(last_time_used,0),"
3448                 + "times_used = 0,"
3449                 + "last_time_used = 0");
3450     }
3451 
upgradeToVersion1300(SQLiteDatabase db)3452     public void upgradeToVersion1300(SQLiteDatabase db) {
3453         try {
3454             db.execSQL("ALTER TABLE data ADD preferred_phone_account_component_name "
3455                     + "TEXT;");
3456             db.execSQL("ALTER TABLE data ADD preferred_phone_account_id TEXT;");
3457         } catch (SQLiteException ignore) {
3458         }
3459     }
3460 
3461     /**
3462      * This method is only used in upgradeToVersion1101 method, and should not be used in other
3463      * places now. Because data15 is not used to generate hash_id for photo, and the new generating
3464      * method for photos should be getPhotoHashId().
3465      *
3466      * Generate hash_id from data1, data2 and data15 columns.
3467      * If one of data1 and data2 is not null, using data1 and data2 to get hash_id,
3468      * otherwise, using data15 to generate.
3469      */
legacyGenerateHashId(String data1, String data2, byte[] data15)3470     public String legacyGenerateHashId(String data1, String data2, byte[] data15) {
3471         final StringBuilder sb = new StringBuilder();
3472         byte[] hashInput = null;
3473         if (!TextUtils.isEmpty(data1) || !TextUtils.isEmpty(data2)) {
3474             sb.append(data1);
3475             sb.append(data2);
3476             hashInput = sb.toString().getBytes();
3477         } else if (data15 != null) {
3478             hashInput = data15;
3479         }
3480         if (hashInput != null) {
3481             final String hashId = generateHashIdForData(hashInput);
3482             return hashId;
3483         } else {
3484             return null;
3485         }
3486     }
3487 
generateHashId(String data1, String data2)3488     public String generateHashId(String data1, String data2) {
3489         final StringBuilder sb = new StringBuilder();
3490         byte[] hashInput = null;
3491         if (!TextUtils.isEmpty(data1) || !TextUtils.isEmpty(data2)) {
3492             sb.append(data1);
3493             sb.append(data2);
3494             hashInput = sb.toString().getBytes();
3495         }
3496         if (hashInput != null) {
3497             final String hashId = generateHashIdForData(hashInput);
3498             return hashId;
3499         } else {
3500             return null;
3501         }
3502     }
3503 
3504     // Use SHA-1 hash method to generate hash string for the input.
3505     @VisibleForTesting
generateHashIdForData(byte[] input)3506     String generateHashIdForData(byte[] input) {
3507         synchronized (mMessageDigest) {
3508             final byte[] hashResult = mMessageDigest.digest(input);
3509             return Base64.encodeToString(hashResult, Base64.DEFAULT);
3510         }
3511     }
3512 
extractHandleFromEmailAddress(String email)3513     public String extractHandleFromEmailAddress(String email) {
3514         Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(email);
3515         if (tokens.length == 0) {
3516             return null;
3517         }
3518 
3519         String address = tokens[0].getAddress();
3520         int index = address.indexOf('@');
3521         if (index != -1) {
3522             return address.substring(0, index);
3523         }
3524         return null;
3525     }
3526 
extractAddressFromEmailAddress(String email)3527     public String extractAddressFromEmailAddress(String email) {
3528         Rfc822Token[] tokens = Rfc822Tokenizer.tokenize(email);
3529         if (tokens.length == 0) {
3530             return null;
3531         }
3532         return tokens[0].getAddress().trim();
3533     }
3534 
3535     /**
3536      * Inserts a new mimetype into the table Tables.MIMETYPES and returns its id. Use
3537      * {@link #lookupMimeTypeId(SQLiteDatabase, String)} to lookup id of a mimetype that is
3538      * guaranteed to be in the database
3539      *
3540      * @param db the SQLiteDatabase object returned by {@link #getWritableDatabase()}
3541      * @param mimeType The mimetype to insert
3542      * @return the id of the newly inserted row
3543      */
insertMimeType(SQLiteDatabase db, String mimeType)3544     private long insertMimeType(SQLiteDatabase db, String mimeType) {
3545         final String insert = "INSERT INTO " + Tables.MIMETYPES + "("
3546                 + MimetypesColumns.MIMETYPE +
3547                 ") VALUES (?)";
3548         long id = insertWithOneArgAndReturnId(db, insert, mimeType);
3549         if (id >= 0) {
3550             return id;
3551         }
3552         return lookupMimeTypeId(db, mimeType);
3553     }
3554 
3555     /**
3556      * Looks up Tables.MIMETYPES for the mime type and returns its id. Returns -1 if the mime type
3557      * is not found. Use {@link #insertMimeType(SQLiteDatabase, String)} when it is doubtful whether
3558      * the mimetype already exists in the table or not.
3559      *
3560      * @param db
3561      * @param mimeType
3562      * @return the id of the row containing the mime type or -1 if the mime type was not found.
3563      */
lookupMimeTypeId(SQLiteDatabase db, String mimeType)3564     private long lookupMimeTypeId(SQLiteDatabase db, String mimeType) {
3565         Long id = mCommonMimeTypeIdsCache.get(mimeType);
3566         if (id != null) {
3567             return id;
3568         }
3569         final String query = "SELECT " +
3570                 MimetypesColumns._ID + " FROM " + Tables.MIMETYPES + " WHERE "
3571                 + MimetypesColumns.MIMETYPE +
3572                 "=?";
3573         id = queryIdWithOneArg(db, query, mimeType);
3574         if (id < 0) {
3575             Log.e(TAG, "Mimetype " + mimeType + " not found in the MIMETYPES table");
3576         }
3577         return id;
3578     }
3579 
bindString(SQLiteStatement stmt, int index, String value)3580     private static void bindString(SQLiteStatement stmt, int index, String value) {
3581         if (value == null) {
3582             stmt.bindNull(index);
3583         } else {
3584             stmt.bindString(index, value);
3585         }
3586     }
3587 
bindLong(SQLiteStatement stmt, int index, Number value)3588     private void bindLong(SQLiteStatement stmt, int index, Number value) {
3589         if (value == null) {
3590             stmt.bindNull(index);
3591         } else {
3592             stmt.bindLong(index, value.longValue());
3593         }
3594     }
3595 
3596     /**
3597      * Adds index stats into the SQLite database to force it to always use the lookup indexes.
3598      *
3599      * Note if you drop a table or an index, the corresponding row will be removed from this table.
3600      * Make sure to call this method after such operations.
3601      */
updateSqliteStats(SQLiteDatabase db)3602     private void updateSqliteStats(SQLiteDatabase db) {
3603         if (!mDatabaseOptimizationEnabled) {
3604             return;  // We don't use sqlite_stat1 during tests.
3605         }
3606 
3607         // Specific stats strings are based on an actual large database after running ANALYZE
3608         // Important here are relative sizes. Raw-Contacts is slightly bigger than Contacts
3609         // Warning: Missing tables in here will make SQLite assume to contain 1000000 rows,
3610         // which can lead to catastrophic query plans for small tables
3611 
3612         // What these numbers mean is described in this file.
3613         // http://www.sqlite.org/cgi/src/finfo?name=src/analyze.c
3614 
3615         // Excerpt:
3616         /*
3617         ** Format of sqlite_stat1:
3618         **
3619         ** There is normally one row per index, with the index identified by the
3620         ** name in the idx column.  The tbl column is the name of the table to
3621         ** which the index belongs.  In each such row, the stat column will be
3622         ** a string consisting of a list of integers.  The first integer in this
3623         ** list is the number of rows in the index and in the table.  The second
3624         ** integer is the average number of rows in the index that have the same
3625         ** value in the first column of the index.  The third integer is the average
3626         ** number of rows in the index that have the same value for the first two
3627         ** columns.  The N-th integer (for N>1) is the average number of rows in
3628         ** the index which have the same value for the first N-1 columns.  For
3629         ** a K-column index, there will be K+1 integers in the stat column.  If
3630         ** the index is unique, then the last integer will be 1.
3631         **
3632         ** The list of integers in the stat column can optionally be followed
3633         ** by the keyword "unordered".  The "unordered" keyword, if it is present,
3634         ** must be separated from the last integer by a single space.  If the
3635         ** "unordered" keyword is present, then the query planner assumes that
3636         ** the index is unordered and will not use the index for a range query.
3637         **
3638         ** If the sqlite_stat1.idx column is NULL, then the sqlite_stat1.stat
3639         ** column contains a single integer which is the (estimated) number of
3640         ** rows in the table identified by sqlite_stat1.tbl.
3641         */
3642 
3643         try {
3644             db.execSQL("DELETE FROM sqlite_stat1");
3645             updateIndexStats(db, Tables.CONTACTS,
3646                     "contacts_has_phone_index", "9000 500");
3647             updateIndexStats(db, Tables.CONTACTS,
3648                     "contacts_name_raw_contact_id_index", "9000 1");
3649             updateIndexStats(db, Tables.CONTACTS, MoreDatabaseUtils.buildIndexName(Tables.CONTACTS,
3650                     Contacts.CONTACT_LAST_UPDATED_TIMESTAMP), "9000 10");
3651 
3652             updateIndexStats(db, Tables.RAW_CONTACTS,
3653                     "raw_contacts_contact_id_index", "10000 2");
3654             updateIndexStats(db, Tables.RAW_CONTACTS,
3655                     "raw_contact_sort_key2_index", "10000 2");
3656             updateIndexStats(db, Tables.RAW_CONTACTS,
3657                     "raw_contact_sort_key1_index", "10000 2");
3658             updateIndexStats(db, Tables.RAW_CONTACTS,
3659                     "raw_contacts_source_id_account_id_index", "10000 1 1");
3660 
3661             updateIndexStats(db, Tables.NAME_LOOKUP,
3662                     "name_lookup_raw_contact_id_index", "35000 4");
3663             updateIndexStats(db, Tables.NAME_LOOKUP,
3664                     "name_lookup_index", "35000 2 2 2 1");
3665             updateIndexStats(db, Tables.NAME_LOOKUP,
3666                     "sqlite_autoindex_name_lookup_1", "35000 3 2 1");
3667 
3668             updateIndexStats(db, Tables.PHONE_LOOKUP,
3669                     "phone_lookup_index", "3500 3 2 1");
3670             updateIndexStats(db, Tables.PHONE_LOOKUP,
3671                     "phone_lookup_min_match_index", "3500 3 2 2");
3672             updateIndexStats(db, Tables.PHONE_LOOKUP,
3673                     "phone_lookup_data_id_min_match_index", "3500 2 2");
3674 
3675             updateIndexStats(db, Tables.DATA,
3676                     "data_mimetype_data1_index", "60000 5000 2");
3677             updateIndexStats(db, Tables.DATA,
3678                     "data_raw_contact_id", "60000 10");
3679 
3680             updateIndexStats(db, Tables.GROUPS,
3681                     "groups_source_id_account_id_index", "50 2 2 1 1");
3682 
3683             updateIndexStats(db, Tables.NICKNAME_LOOKUP,
3684                     "nickname_lookup_index", "500 2 1");
3685 
3686             updateIndexStats(db, Tables.STATUS_UPDATES,
3687                     null, "100");
3688 
3689             updateIndexStats(db, Tables.STREAM_ITEMS,
3690                     null, "500");
3691             updateIndexStats(db, Tables.STREAM_ITEM_PHOTOS,
3692                     null, "50");
3693 
3694             updateIndexStats(db, Tables.ACCOUNTS,
3695                     null, "3");
3696 
3697             updateIndexStats(db, Tables.PRE_AUTHORIZED_URIS,
3698                     null, "1");
3699 
3700             updateIndexStats(db, Tables.VISIBLE_CONTACTS,
3701                     null, "2000");
3702 
3703             updateIndexStats(db, Tables.PHOTO_FILES,
3704                     null, "50");
3705 
3706             updateIndexStats(db, Tables.DEFAULT_DIRECTORY,
3707                     null, "1500");
3708 
3709             updateIndexStats(db, Tables.MIMETYPES,
3710                     "mime_type", "18 1");
3711 
3712             updateIndexStats(db, Tables.DATA_USAGE_STAT,
3713                     "data_usage_stat_index", "20 2 1");
3714 
3715             updateIndexStats(db, Tables.METADATA_SYNC,
3716                     "metadata_sync_index", "10000 1 1");
3717 
3718             // Tiny tables
3719             updateIndexStats(db, Tables.AGGREGATION_EXCEPTIONS,
3720                     null, "10");
3721             updateIndexStats(db, Tables.SETTINGS,
3722                     null, "10");
3723             updateIndexStats(db, Tables.PACKAGES,
3724                     null, "0");
3725             updateIndexStats(db, Tables.DIRECTORIES,
3726                     null, "3");
3727             updateIndexStats(db, LegacyApiSupport.LegacyTables.SETTINGS,
3728                     null, "0");
3729             updateIndexStats(db, "android_metadata",
3730                     null, "1");
3731             updateIndexStats(db, "_sync_state",
3732                     "sqlite_autoindex__sync_state_1", "2 1 1");
3733             updateIndexStats(db, "_sync_state_metadata",
3734                     null, "1");
3735             updateIndexStats(db, "properties",
3736                     "sqlite_autoindex_properties_1", "4 1");
3737 
3738             updateIndexStats(db, Tables.METADATA_SYNC_STATE,
3739                     "metadata_sync_state_index", "2 1 1");
3740 
3741             // Search index
3742             updateIndexStats(db, "search_index_docsize",
3743                     null, "9000");
3744             updateIndexStats(db, "search_index_content",
3745                     null, "9000");
3746             updateIndexStats(db, "search_index_stat",
3747                     null, "1");
3748             updateIndexStats(db, "search_index_segments",
3749                     null, "450");
3750             updateIndexStats(db, "search_index_segdir",
3751                     "sqlite_autoindex_search_index_segdir_1", "9 5 1");
3752 
3753             updateIndexStats(db, Tables.PRESENCE, "presenceIndex", "1 1");
3754             updateIndexStats(db, Tables.PRESENCE, "presenceIndex2", "1 1");
3755             updateIndexStats(db, Tables.AGGREGATED_PRESENCE, null, "1");
3756 
3757             // Force SQLite to reload sqlite_stat1.
3758             db.execSQL("ANALYZE sqlite_master;");
3759         } catch (SQLException e) {
3760             Log.e(TAG, "Could not update index stats", e);
3761         }
3762     }
3763 
3764     /**
3765      * Stores statistics for a given index.
3766      *
3767      * @param stats has the following structure: the first index is the expected size of
3768      * the table.  The following integer(s) are the expected number of records selected with the
3769      * index.  There should be one integer per indexed column.
3770      */
updateIndexStats(SQLiteDatabase db, String table, String index, String stats)3771     private void updateIndexStats(SQLiteDatabase db, String table, String index, String stats) {
3772         if (index == null) {
3773             db.execSQL("DELETE FROM sqlite_stat1 WHERE tbl=? AND idx IS NULL",
3774                     new String[] {table});
3775         } else {
3776             db.execSQL("DELETE FROM sqlite_stat1 WHERE tbl=? AND idx=?",
3777                     new String[] {table, index});
3778         }
3779         db.execSQL("INSERT INTO sqlite_stat1 (tbl,idx,stat) VALUES (?,?,?)",
3780                 new String[] {table, index, stats});
3781     }
3782 
3783     /**
3784      * Wipes all data except mime type and package lookup tables.
3785      */
wipeData()3786     public void wipeData() {
3787         SQLiteDatabase db = getWritableDatabase();
3788 
3789         db.execSQL("DELETE FROM " + Tables.ACCOUNTS + ";");
3790         db.execSQL("DELETE FROM " + Tables.CONTACTS + ";");
3791         db.execSQL("DELETE FROM " + Tables.RAW_CONTACTS + ";");
3792         db.execSQL("DELETE FROM " + Tables.STREAM_ITEMS + ";");
3793         db.execSQL("DELETE FROM " + Tables.STREAM_ITEM_PHOTOS + ";");
3794         db.execSQL("DELETE FROM " + Tables.PHOTO_FILES + ";");
3795         db.execSQL("DELETE FROM " + Tables.DATA + ";");
3796         db.execSQL("DELETE FROM " + Tables.PHONE_LOOKUP + ";");
3797         db.execSQL("DELETE FROM " + Tables.NAME_LOOKUP + ";");
3798         db.execSQL("DELETE FROM " + Tables.GROUPS + ";");
3799         db.execSQL("DELETE FROM " + Tables.AGGREGATION_EXCEPTIONS + ";");
3800         db.execSQL("DELETE FROM " + Tables.SETTINGS + ";");
3801         db.execSQL("DELETE FROM " + Tables.DIRECTORIES + ";");
3802         db.execSQL("DELETE FROM " + Tables.SEARCH_INDEX + ";");
3803         db.execSQL("DELETE FROM " + Tables.DELETED_CONTACTS + ";");
3804         db.execSQL("DELETE FROM " + Tables.MIMETYPES + ";");
3805         db.execSQL("DELETE FROM " + Tables.PACKAGES + ";");
3806         db.execSQL("DELETE FROM " + Tables.PRESENCE + ";");
3807         db.execSQL("DELETE FROM " + Tables.AGGREGATED_PRESENCE + ";");
3808 
3809         prepopulateCommonMimeTypes(db);
3810         // Note: we are not removing reference data from Tables.NICKNAME_LOOKUP
3811     }
3812 
createNameSplitter()3813     public NameSplitter createNameSplitter() {
3814         return createNameSplitter(Locale.getDefault());
3815     }
3816 
createNameSplitter(Locale locale)3817     public NameSplitter createNameSplitter(Locale locale) {
3818         mNameSplitter = new NameSplitter(
3819                 mContext.getString(com.android.internal.R.string.common_name_prefixes),
3820                 mContext.getString(com.android.internal.R.string.common_last_name_prefixes),
3821                 mContext.getString(com.android.internal.R.string.common_name_suffixes),
3822                 mContext.getString(com.android.internal.R.string.common_name_conjunctions),
3823                 locale);
3824         return mNameSplitter;
3825     }
3826 
3827     /**
3828      * Return the {@link ApplicationInfo#uid} for the given package name.
3829      */
getUidForPackageName(PackageManager pm, String packageName)3830     public static int getUidForPackageName(PackageManager pm, String packageName) {
3831         try {
3832             ApplicationInfo clientInfo = pm.getApplicationInfo(packageName, 0 /* no flags */);
3833             return clientInfo.uid;
3834         } catch (NameNotFoundException e) {
3835             throw new RuntimeException(e);
3836         }
3837     }
3838 
3839     @VisibleForTesting
queryIdWithOneArg(SQLiteDatabase db, String sql, String sqlArgument)3840     static long queryIdWithOneArg(SQLiteDatabase db, String sql, String sqlArgument) {
3841         final SQLiteStatement query = db.compileStatement(sql);
3842         try {
3843             bindString(query, 1, sqlArgument);
3844             try {
3845                 return query.simpleQueryForLong();
3846             } catch (SQLiteDoneException notFound) {
3847                 return -1;
3848             }
3849         } finally {
3850             query.close();
3851         }
3852     }
3853 
3854     @VisibleForTesting
insertWithOneArgAndReturnId(SQLiteDatabase db, String sql, String sqlArgument)3855     static long insertWithOneArgAndReturnId(SQLiteDatabase db, String sql, String sqlArgument) {
3856         final SQLiteStatement insert = db.compileStatement(sql);
3857         try {
3858             bindString(insert, 1, sqlArgument);
3859             try {
3860                 return insert.executeInsert();
3861             } catch (SQLiteConstraintException conflict) {
3862                 return -1;
3863             }
3864         } finally {
3865             insert.close();
3866         }
3867     }
3868 
3869     /**
3870      * Convert a package name into an integer, using {@link Tables#PACKAGES} for
3871      * lookups and possible allocation of new IDs as needed.
3872      */
getPackageId(String packageName)3873     public long getPackageId(String packageName) {
3874         final String query =
3875                 "SELECT " + PackagesColumns._ID +
3876                 " FROM " + Tables.PACKAGES +
3877                 " WHERE " + PackagesColumns.PACKAGE + "=?";
3878 
3879         final String insert =
3880                 "INSERT INTO " + Tables.PACKAGES + "("
3881                         + PackagesColumns.PACKAGE +
3882                 ") VALUES (?)";
3883 
3884         SQLiteDatabase db = getWritableDatabase();
3885         long id = queryIdWithOneArg(db, query, packageName);
3886         if (id >= 0) {
3887             return id;
3888         }
3889         id = insertWithOneArgAndReturnId(db, insert, packageName);
3890         if (id >= 0) {
3891             return id;
3892         }
3893         // just in case there was a race while doing insert above
3894         return queryIdWithOneArg(db, query, packageName);
3895     }
3896 
3897     /**
3898      * Convert a mimetype into an integer, using {@link Tables#MIMETYPES} for
3899      * lookups and possible allocation of new IDs as needed.
3900      */
getMimeTypeId(String mimeType)3901     public long getMimeTypeId(String mimeType) {
3902         SQLiteDatabase db = getWritableDatabase();
3903         long id = lookupMimeTypeId(db, mimeType);
3904         if (id < 0) {
3905             return insertMimeType(db, mimeType);
3906         }
3907         return id;
3908     }
3909 
getMimeTypeIdForStructuredName()3910     public long getMimeTypeIdForStructuredName() {
3911         return lookupMimeTypeId(getWritableDatabase(), StructuredName.CONTENT_ITEM_TYPE);
3912     }
3913 
getMimeTypeIdForStructuredPostal()3914     public long getMimeTypeIdForStructuredPostal() {
3915         return lookupMimeTypeId(getWritableDatabase(), StructuredPostal.CONTENT_ITEM_TYPE);
3916     }
3917 
getMimeTypeIdForOrganization()3918     public long getMimeTypeIdForOrganization() {
3919         return lookupMimeTypeId(getWritableDatabase(), Organization.CONTENT_ITEM_TYPE);
3920     }
3921 
getMimeTypeIdForIm()3922     public long getMimeTypeIdForIm() {
3923         return lookupMimeTypeId(getWritableDatabase(), Im.CONTENT_ITEM_TYPE);
3924     }
3925 
getMimeTypeIdForEmail()3926     public long getMimeTypeIdForEmail() {
3927         return lookupMimeTypeId(getWritableDatabase(), Email.CONTENT_ITEM_TYPE);
3928     }
3929 
getMimeTypeIdForPhone()3930     public long getMimeTypeIdForPhone() {
3931         return lookupMimeTypeId(getWritableDatabase(), Phone.CONTENT_ITEM_TYPE);
3932     }
3933 
getMimeTypeIdForSip()3934     public long getMimeTypeIdForSip() {
3935         return lookupMimeTypeId(getWritableDatabase(), SipAddress.CONTENT_ITEM_TYPE);
3936     }
3937 
3938     /**
3939      * Returns a {@link ContactsContract.DisplayNameSources} value based on {@param mimeTypeId}.
3940      * This does not return {@link ContactsContract.DisplayNameSources#STRUCTURED_PHONETIC_NAME}.
3941      * The calling client needs to inspect the structured name itself to distinguish between
3942      * {@link ContactsContract.DisplayNameSources#STRUCTURED_NAME} and
3943      * {@code STRUCTURED_PHONETIC_NAME}.
3944      */
getDisplayNameSourceForMimeTypeId(int mimeTypeId)3945     private int getDisplayNameSourceForMimeTypeId(int mimeTypeId) {
3946         if (mimeTypeId == mCommonMimeTypeIdsCache.get(StructuredName.CONTENT_ITEM_TYPE)) {
3947             return DisplayNameSources.STRUCTURED_NAME;
3948         }
3949         if (mimeTypeId == mCommonMimeTypeIdsCache.get(Email.CONTENT_ITEM_TYPE)) {
3950             return DisplayNameSources.EMAIL;
3951         }
3952         if (mimeTypeId == mCommonMimeTypeIdsCache.get(Phone.CONTENT_ITEM_TYPE)) {
3953             return DisplayNameSources.PHONE;
3954         }
3955         if (mimeTypeId == mCommonMimeTypeIdsCache.get(Organization.CONTENT_ITEM_TYPE)) {
3956             return DisplayNameSources.ORGANIZATION;
3957         }
3958         if (mimeTypeId == mCommonMimeTypeIdsCache.get(Nickname.CONTENT_ITEM_TYPE)) {
3959             return DisplayNameSources.NICKNAME;
3960         }
3961         return DisplayNameSources.UNDEFINED;
3962     }
3963 
3964     /**
3965      * Find the mimetype for the given {@link Data#_ID}.
3966      */
getDataMimeType(long dataId)3967     public String getDataMimeType(long dataId) {
3968         final SQLiteStatement dataMimetypeQuery = getWritableDatabase().compileStatement(
3969                     "SELECT " + MimetypesColumns.MIMETYPE +
3970                     " FROM " + Tables.DATA_JOIN_MIMETYPES +
3971                     " WHERE " + Tables.DATA + "." + Data._ID + "=?");
3972         try {
3973             // Try database query to find mimetype
3974             dataMimetypeQuery.bindLong(1, dataId);
3975             return dataMimetypeQuery.simpleQueryForString();
3976         } catch (SQLiteDoneException e) {
3977             // No valid mapping found, so return null
3978             return null;
3979         }
3980     }
3981 
3982     /**
3983      * Gets all accounts in the accounts table.
3984      */
getAllAccountsWithDataSets()3985     public Set<AccountWithDataSet> getAllAccountsWithDataSets() {
3986         final ArraySet<AccountWithDataSet> result = new ArraySet<>();
3987         Cursor c = getReadableDatabase().rawQuery(
3988                 "SELECT DISTINCT " +  AccountsColumns._ID + "," + AccountsColumns.ACCOUNT_NAME +
3989                 "," + AccountsColumns.ACCOUNT_TYPE + "," + AccountsColumns.DATA_SET +
3990                 " FROM " + Tables.ACCOUNTS, null);
3991         try {
3992             while (c.moveToNext()) {
3993                 result.add(AccountWithDataSet.get(c.getString(1), c.getString(2), c.getString(3)));
3994             }
3995         } finally {
3996             c.close();
3997         }
3998         return result;
3999     }
4000 
4001     /**
4002      * @return ID of the specified account, or null if the account doesn't exist.
4003      */
getAccountIdOrNull(AccountWithDataSet accountWithDataSet)4004     public Long getAccountIdOrNull(AccountWithDataSet accountWithDataSet) {
4005         if (accountWithDataSet == null) {
4006             accountWithDataSet = AccountWithDataSet.LOCAL;
4007         }
4008         final SQLiteStatement select = getWritableDatabase().compileStatement(
4009                 "SELECT " + AccountsColumns._ID +
4010                         " FROM " + Tables.ACCOUNTS +
4011                         " WHERE " +
4012                         "((?1 IS NULL AND " + AccountsColumns.ACCOUNT_NAME + " IS NULL) OR " +
4013                         "(" + AccountsColumns.ACCOUNT_NAME + "=?1)) AND " +
4014                         "((?2 IS NULL AND " + AccountsColumns.ACCOUNT_TYPE + " IS NULL) OR " +
4015                         "(" + AccountsColumns.ACCOUNT_TYPE + "=?2)) AND " +
4016                         "((?3 IS NULL AND " + AccountsColumns.DATA_SET + " IS NULL) OR " +
4017                         "(" + AccountsColumns.DATA_SET + "=?3))");
4018         try {
4019             DatabaseUtils.bindObjectToProgram(select, 1, accountWithDataSet.getAccountName());
4020             DatabaseUtils.bindObjectToProgram(select, 2, accountWithDataSet.getAccountType());
4021             DatabaseUtils.bindObjectToProgram(select, 3, accountWithDataSet.getDataSet());
4022             try {
4023                 return select.simpleQueryForLong();
4024             } catch (SQLiteDoneException notFound) {
4025                 return null;
4026             }
4027         } finally {
4028             select.close();
4029         }
4030     }
4031 
4032     /**
4033      * @return ID of the specified account.  This method will create a record in the accounts table
4034      *     if the account doesn't exist in the accounts table.
4035      *
4036      * This must be used in a transaction, so there's no need for synchronization.
4037      */
getOrCreateAccountIdInTransaction(AccountWithDataSet accountWithDataSet)4038     public long getOrCreateAccountIdInTransaction(AccountWithDataSet accountWithDataSet) {
4039         if (accountWithDataSet == null) {
4040             accountWithDataSet = AccountWithDataSet.LOCAL;
4041         }
4042         Long id = getAccountIdOrNull(accountWithDataSet);
4043         if (id != null) {
4044             return id;
4045         }
4046         final SQLiteStatement insert = getWritableDatabase().compileStatement(
4047                 "INSERT INTO " + Tables.ACCOUNTS +
4048                 " (" + AccountsColumns.ACCOUNT_NAME + ", " +
4049                 AccountsColumns.ACCOUNT_TYPE + ", " +
4050                 AccountsColumns.DATA_SET + ") VALUES (?, ?, ?)");
4051         try {
4052             DatabaseUtils.bindObjectToProgram(insert, 1, accountWithDataSet.getAccountName());
4053             DatabaseUtils.bindObjectToProgram(insert, 2, accountWithDataSet.getAccountType());
4054             DatabaseUtils.bindObjectToProgram(insert, 3, accountWithDataSet.getDataSet());
4055             id = insert.executeInsert();
4056         } finally {
4057             insert.close();
4058         }
4059 
4060         return id;
4061     }
4062 
4063     /**
4064      * Update {@link Contacts#IN_VISIBLE_GROUP} for all contacts.
4065      */
updateAllVisible()4066     public void updateAllVisible() {
4067         updateCustomContactVisibility(getWritableDatabase(), -1);
4068     }
4069 
4070     /**
4071      * Updates contact visibility and return true iff the visibility was actually changed.
4072      */
updateContactVisibleOnlyIfChanged(TransactionContext txContext, long contactId)4073     public boolean updateContactVisibleOnlyIfChanged(TransactionContext txContext, long contactId) {
4074         return updateContactVisible(txContext, contactId, true);
4075     }
4076 
4077     /**
4078      * Update {@link Contacts#IN_VISIBLE_GROUP} and
4079      * {@link Tables#DEFAULT_DIRECTORY} for a specific contact.
4080      */
updateContactVisible(TransactionContext txContext, long contactId)4081     public void updateContactVisible(TransactionContext txContext, long contactId) {
4082         updateContactVisible(txContext, contactId, false);
4083     }
4084 
updateContactVisible( TransactionContext txContext, long contactId, boolean onlyIfChanged)4085     public boolean updateContactVisible(
4086             TransactionContext txContext, long contactId, boolean onlyIfChanged) {
4087         SQLiteDatabase db = getWritableDatabase();
4088         updateCustomContactVisibility(db, contactId);
4089 
4090         String contactIdAsString = String.valueOf(contactId);
4091         long mimetype = getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE);
4092 
4093         // The contact will be included in the default directory if contains a raw contact that is
4094         // in any group or in an account that does not have any AUTO_ADD groups.
4095         boolean newVisibility = DatabaseUtils.longForQuery(db,
4096                 "SELECT EXISTS (" +
4097                     "SELECT " + RawContacts.CONTACT_ID +
4098                     " FROM " + Tables.RAW_CONTACTS +
4099                     " JOIN " + Tables.DATA +
4100                     "   ON (" + RawContactsColumns.CONCRETE_ID + "="
4101                             + Data.RAW_CONTACT_ID + ")" +
4102                     " WHERE " + RawContacts.CONTACT_ID + "=?1" +
4103                     "   AND " + DataColumns.MIMETYPE_ID + "=?2" +
4104                 ") OR EXISTS (" +
4105                     "SELECT " + RawContacts._ID +
4106                     " FROM " + Tables.RAW_CONTACTS +
4107                     " WHERE " + RawContacts.CONTACT_ID + "=?1" +
4108                     "   AND NOT EXISTS" +
4109                         " (SELECT " + Groups._ID +
4110                         "  FROM " + Tables.GROUPS +
4111                         "  WHERE " + RawContactsColumns.CONCRETE_ACCOUNT_ID + " = "
4112                                 + GroupsColumns.CONCRETE_ACCOUNT_ID +
4113                         "  AND " + Groups.AUTO_ADD + " != 0" +
4114                         ")" +
4115                 ") OR EXISTS (" +
4116                     "SELECT " + RawContacts._ID +
4117                     " FROM " + Tables.RAW_CONTACTS +
4118                     " WHERE " + RawContacts.CONTACT_ID + "=?1" +
4119                     "   AND " + RawContactsColumns.CONCRETE_ACCOUNT_ID + "=" +
4120                         Clauses.LOCAL_ACCOUNT_ID +
4121                 ")",
4122                 new String[] {
4123                     contactIdAsString,
4124                     String.valueOf(mimetype)
4125                 }) != 0;
4126 
4127         if (onlyIfChanged) {
4128             boolean oldVisibility = isContactInDefaultDirectory(db, contactId);
4129             if (oldVisibility == newVisibility) {
4130                 return false;
4131             }
4132         }
4133 
4134         if (newVisibility) {
4135             db.execSQL("INSERT OR IGNORE INTO " + Tables.DEFAULT_DIRECTORY + " VALUES(?)",
4136                     new String[] {contactIdAsString});
4137             txContext.invalidateSearchIndexForContact(contactId);
4138         } else {
4139             db.execSQL("DELETE FROM " + Tables.DEFAULT_DIRECTORY +
4140                         " WHERE " + Contacts._ID + "=?",
4141                     new String[] {contactIdAsString});
4142             db.execSQL("DELETE FROM " + Tables.SEARCH_INDEX +
4143                         " WHERE " + SearchIndexColumns.CONTACT_ID + "=CAST(? AS int)",
4144                     new String[] {contactIdAsString});
4145         }
4146         return true;
4147     }
4148 
isContactInDefaultDirectory(SQLiteDatabase db, long contactId)4149     public boolean isContactInDefaultDirectory(SQLiteDatabase db, long contactId) {
4150         final SQLiteStatement contactInDefaultDirectoryQuery = db.compileStatement(
4151                     "SELECT EXISTS (" +
4152                             "SELECT 1 FROM " + Tables.DEFAULT_DIRECTORY +
4153                             " WHERE " + Contacts._ID + "=?)");
4154         contactInDefaultDirectoryQuery.bindLong(1, contactId);
4155         return contactInDefaultDirectoryQuery.simpleQueryForLong() != 0;
4156     }
4157 
4158     /**
4159      * Update the visible_contacts table according to the current visibility of contacts, which
4160      * is defined by {@link Clauses#CONTACT_IS_VISIBLE}.
4161      *
4162      * If {@code optionalContactId} is non-negative, it'll update only for the specified contact.
4163      */
updateCustomContactVisibility(SQLiteDatabase db, long optionalContactId)4164     private void updateCustomContactVisibility(SQLiteDatabase db, long optionalContactId) {
4165         final long groupMembershipMimetypeId = getMimeTypeId(GroupMembership.CONTENT_ITEM_TYPE);
4166         String[] selectionArgs = new String[] {String.valueOf(groupMembershipMimetypeId)};
4167 
4168         final String contactIdSelect = (optionalContactId < 0) ? "" :
4169                 (Contacts._ID + "=" + optionalContactId + " AND ");
4170 
4171         // First delete what needs to be deleted, then insert what needs to be added.
4172         // Since flash writes are very expensive, this approach is much better than
4173         // delete-all-insert-all.
4174         db.execSQL(
4175                 "DELETE FROM " + Tables.VISIBLE_CONTACTS +
4176                 " WHERE " + Contacts._ID + " IN" +
4177                     "(SELECT " + Contacts._ID +
4178                     " FROM " + Tables.CONTACTS +
4179                     " WHERE " + contactIdSelect + "(" + Clauses.CONTACT_IS_VISIBLE + ")=0) ",
4180                 selectionArgs);
4181 
4182         db.execSQL(
4183                 "INSERT INTO " + Tables.VISIBLE_CONTACTS +
4184                 " SELECT " + Contacts._ID +
4185                 " FROM " + Tables.CONTACTS +
4186                 " WHERE " +
4187                     contactIdSelect +
4188                     Contacts._ID + " NOT IN " + Tables.VISIBLE_CONTACTS +
4189                     " AND (" + Clauses.CONTACT_IS_VISIBLE + ")=1 ",
4190                 selectionArgs);
4191     }
4192 
4193     /**
4194      * Returns contact ID for the given contact or zero if it is NULL.
4195      */
getContactId(long rawContactId)4196     public long getContactId(long rawContactId) {
4197         final SQLiteStatement contactIdQuery = getWritableDatabase().compileStatement(
4198                     "SELECT " + RawContacts.CONTACT_ID +
4199                     " FROM " + Tables.RAW_CONTACTS +
4200                     " WHERE " + RawContacts._ID + "=?");
4201         try {
4202             contactIdQuery.bindLong(1, rawContactId);
4203             return contactIdQuery.simpleQueryForLong();
4204         } catch (SQLiteDoneException e) {
4205             return 0;  // No valid mapping found.
4206         }
4207     }
4208 
getAggregationMode(long rawContactId)4209     public int getAggregationMode(long rawContactId) {
4210         final SQLiteStatement aggregationModeQuery = getWritableDatabase().compileStatement(
4211                     "SELECT " + RawContacts.AGGREGATION_MODE +
4212                     " FROM " + Tables.RAW_CONTACTS +
4213                     " WHERE " + RawContacts._ID + "=?");
4214         try {
4215             aggregationModeQuery.bindLong(1, rawContactId);
4216             return (int) aggregationModeQuery.simpleQueryForLong();
4217         } catch (SQLiteDoneException e) {
4218             return RawContacts.AGGREGATION_MODE_DISABLED;  // No valid row found.
4219         }
4220     }
4221 
buildPhoneLookupAndContactQuery( SQLiteQueryBuilder qb, String normalizedNumber, String numberE164)4222     public void buildPhoneLookupAndContactQuery(
4223             SQLiteQueryBuilder qb, String normalizedNumber, String numberE164) {
4224 
4225         String minMatch = PhoneNumberUtils.toCallerIDMinMatch(normalizedNumber);
4226         StringBuilder sb = new StringBuilder();
4227         appendPhoneLookupTables(sb, minMatch, true);
4228         qb.setTables(sb.toString());
4229 
4230         sb = new StringBuilder();
4231         appendPhoneLookupSelection(sb, normalizedNumber, numberE164);
4232         qb.appendWhere(sb.toString());
4233     }
4234 
4235     /**
4236      * Phone lookup method that uses the custom SQLite function phone_number_compare_loose
4237      * that serves as a fallback in case the regular lookup does not return any results.
4238      * @param qb The query builder.
4239      * @param number The phone number to search for.
4240      */
buildFallbackPhoneLookupAndContactQuery(SQLiteQueryBuilder qb, String number)4241     public void buildFallbackPhoneLookupAndContactQuery(SQLiteQueryBuilder qb, String number) {
4242         final String minMatch = PhoneNumberUtils.toCallerIDMinMatch(number);
4243         final StringBuilder sb = new StringBuilder();
4244         // Append lookup tables.
4245         sb.append(Tables.RAW_CONTACTS);
4246         sb.append(" JOIN " + Views.CONTACTS + " as contacts_view"
4247                 + " ON (contacts_view._id = " + Tables.RAW_CONTACTS
4248                 + "." + RawContacts.CONTACT_ID + ")" +
4249                 " JOIN (SELECT " + PhoneLookupColumns.DATA_ID + "," +
4250                 PhoneLookupColumns.NORMALIZED_NUMBER + " FROM "+ Tables.PHONE_LOOKUP + " "
4251                 + "WHERE (" + Tables.PHONE_LOOKUP + "." + PhoneLookupColumns.MIN_MATCH + " = '");
4252         sb.append(minMatch);
4253         sb.append("')) AS lookup " +
4254                 "ON lookup." + PhoneLookupColumns.DATA_ID + "=" + Tables.DATA + "." + Data._ID
4255                 + " JOIN " + Tables.DATA + " "
4256                 + "ON " + Tables.DATA + "." + Data.RAW_CONTACT_ID + "=" + Tables.RAW_CONTACTS + "."
4257                 + RawContacts._ID);
4258 
4259         qb.setTables(sb.toString());
4260 
4261         sb.setLength(0);
4262         sb.append("PHONE_NUMBERS_EQUAL(" + Tables.DATA + "." + Phone.NUMBER + ", ");
4263         DatabaseUtils.appendEscapedSQLString(sb, number);
4264         sb.append(mUseStrictPhoneNumberComparison ? ", 1)" : ", 0)");
4265         qb.appendWhere(sb.toString());
4266     }
4267 
4268     /**
4269      * Adds query for selecting the contact with the given {@code sipAddress} to the given
4270      * {@link StringBuilder}.
4271      *
4272      * @return the query arguments to be passed in with the query
4273      */
buildSipContactQuery(StringBuilder sb, String sipAddress)4274     public String[] buildSipContactQuery(StringBuilder sb, String sipAddress) {
4275         sb.append("upper(");
4276         sb.append(Data.DATA1);
4277         sb.append(")=upper(?) AND ");
4278         sb.append(DataColumns.MIMETYPE_ID);
4279         sb.append("=");
4280         sb.append(Long.toString(getMimeTypeIdForSip()));
4281         // Return the arguments to be passed to the query.
4282         return new String[] {sipAddress};
4283     }
4284 
buildPhoneLookupAsNestedQuery(String number)4285     public String buildPhoneLookupAsNestedQuery(String number) {
4286         StringBuilder sb = new StringBuilder();
4287         final String minMatch = PhoneNumberUtils.toCallerIDMinMatch(number);
4288         sb.append("(SELECT DISTINCT raw_contact_id" + " FROM ");
4289         appendPhoneLookupTables(sb, minMatch, false);
4290         sb.append(" WHERE ");
4291         appendPhoneLookupSelection(sb, number, null);
4292         sb.append(")");
4293         return sb.toString();
4294     }
4295 
appendPhoneLookupTables( StringBuilder sb, final String minMatch, boolean joinContacts)4296     private void appendPhoneLookupTables(
4297             StringBuilder sb, final String minMatch, boolean joinContacts) {
4298 
4299         sb.append(Tables.RAW_CONTACTS);
4300         if (joinContacts) {
4301             sb.append(" JOIN " + Views.CONTACTS + " contacts_view"
4302                     + " ON (contacts_view._id = raw_contacts.contact_id)");
4303         }
4304         sb.append(", (SELECT data_id, normalized_number, length(normalized_number) as len "
4305                 + " FROM phone_lookup " + " WHERE (" + Tables.PHONE_LOOKUP + "."
4306                 + PhoneLookupColumns.MIN_MATCH + " = '");
4307         sb.append(minMatch);
4308         sb.append("')) AS lookup, " + Tables.DATA);
4309     }
4310 
appendPhoneLookupSelection(StringBuilder sb, String number, String numberE164)4311     private void appendPhoneLookupSelection(StringBuilder sb, String number, String numberE164) {
4312         sb.append("lookup.data_id=data._id AND data.raw_contact_id=raw_contacts._id");
4313         boolean hasNumberE164 = !TextUtils.isEmpty(numberE164);
4314         boolean hasNumber = !TextUtils.isEmpty(number);
4315         if (hasNumberE164 || hasNumber) {
4316             sb.append(" AND ( ");
4317             if (hasNumberE164) {
4318                 sb.append(" lookup.normalized_number = ");
4319                 DatabaseUtils.appendEscapedSQLString(sb, numberE164);
4320             }
4321             if (hasNumberE164 && hasNumber) {
4322                 sb.append(" OR ");
4323             }
4324             if (hasNumber) {
4325                 // Skip the suffix match entirely if we are using strict number comparison.
4326                 if (!mUseStrictPhoneNumberComparison) {
4327                     int numberLen = number.length();
4328                     sb.append(" lookup.len <= ");
4329                     sb.append(numberLen);
4330                     sb.append(" AND substr(");
4331                     DatabaseUtils.appendEscapedSQLString(sb, number);
4332                     sb.append(',');
4333                     sb.append(numberLen);
4334                     sb.append(" - lookup.len + 1) = lookup.normalized_number");
4335 
4336                     // Some countries (e.g. Brazil) can have incoming calls which contain only
4337                     // the local number (no country calling code and no area code).  This case
4338                     // is handled below, see b/5197612.
4339                     // This also handles a Gingerbread -> ICS upgrade issue; see b/5638376.
4340                     sb.append(" OR (");
4341                     sb.append(" lookup.len > ");
4342                     sb.append(numberLen);
4343                     sb.append(" AND substr(lookup.normalized_number,");
4344                     sb.append("lookup.len + 1 - ");
4345                     sb.append(numberLen);
4346                     sb.append(") = ");
4347                     DatabaseUtils.appendEscapedSQLString(sb, number);
4348                     sb.append(")");
4349                 } else {
4350                     sb.append("0");
4351                 }
4352             }
4353             sb.append(')');
4354         }
4355     }
4356 
getUseStrictPhoneNumberComparisonParameter()4357     public String getUseStrictPhoneNumberComparisonParameter() {
4358         return mUseStrictPhoneNumberComparison ? "1" : "0";
4359     }
4360 
4361     /**
4362      * Loads common nickname mappings into the database.
4363      */
loadNicknameLookupTable(SQLiteDatabase db)4364     private void loadNicknameLookupTable(SQLiteDatabase db) {
4365         db.execSQL("DELETE FROM " + Tables.NICKNAME_LOOKUP);
4366 
4367         String[] strings = mContext.getResources().getStringArray(
4368                 com.android.internal.R.array.common_nicknames);
4369         if (strings == null || strings.length == 0) {
4370             return;
4371         }
4372 
4373         final SQLiteStatement nicknameLookupInsert = db.compileStatement("INSERT INTO "
4374                 + Tables.NICKNAME_LOOKUP + "(" + NicknameLookupColumns.NAME + ","
4375                 + NicknameLookupColumns.CLUSTER + ") VALUES (?,?)");
4376 
4377         try {
4378             for (int clusterId = 0; clusterId < strings.length; clusterId++) {
4379                 String[] names = strings[clusterId].split(",");
4380                 for (String name : names) {
4381                     String normalizedName = NameNormalizer.normalize(name);
4382                     try {
4383                         nicknameLookupInsert.bindString(1, normalizedName);
4384                         nicknameLookupInsert.bindString(2, String.valueOf(clusterId));
4385                         nicknameLookupInsert.executeInsert();
4386                     } catch (SQLiteException e) {
4387                         // Print the exception and keep going (this is not a fatal error).
4388                         Log.e(TAG, "Cannot insert nickname: " + name, e);
4389                     }
4390                 }
4391             }
4392         } finally {
4393             nicknameLookupInsert.close();
4394         }
4395     }
4396 
copyStringValue( ContentValues toValues, String toKey, ContentValues fromValues, String fromKey)4397     public static void copyStringValue(
4398             ContentValues toValues, String toKey, ContentValues fromValues, String fromKey) {
4399 
4400         if (fromValues.containsKey(fromKey)) {
4401             toValues.put(toKey, fromValues.getAsString(fromKey));
4402         }
4403     }
4404 
copyLongValue( ContentValues toValues, String toKey, ContentValues fromValues, String fromKey)4405     public static void copyLongValue(
4406             ContentValues toValues, String toKey, ContentValues fromValues, String fromKey) {
4407 
4408         if (fromValues.containsKey(fromKey)) {
4409             long longValue;
4410             Object value = fromValues.get(fromKey);
4411             if (value instanceof Boolean) {
4412                 longValue = (Boolean) value ? 1 : 0;
4413             } else if (value instanceof String) {
4414                 longValue = Long.parseLong((String)value);
4415             } else {
4416                 longValue = ((Number)value).longValue();
4417             }
4418             toValues.put(toKey, longValue);
4419         }
4420     }
4421 
getSyncState()4422     public SyncStateContentProviderHelper getSyncState() {
4423         return mSyncState;
4424     }
4425 
4426     /**
4427      * Returns the value from the {@link PropertyUtils.Tables#PROPERTIES} table.
4428      */
getProperty(String key, String defaultValue)4429     public String getProperty(String key, String defaultValue) {
4430         return PropertyUtils.getProperty(getReadableDatabase(), key, defaultValue);
4431     }
4432 
4433     /**
4434      * Stores a key-value pair in the {@link PropertyUtils.Tables#PROPERTIES} table.
4435      */
setProperty(String key, String value)4436     public void setProperty(String key, String value) {
4437         PropertyUtils.setProperty(getWritableDatabase(), key, value);
4438     }
4439 
forceDirectoryRescan()4440     public void forceDirectoryRescan() {
4441         setProperty(DbProperties.DIRECTORY_SCAN_COMPLETE, "0");
4442     }
4443 
4444     /**
4445      * Test if the given column appears in the given projection.
4446      */
isInProjection(String[] projection, String column)4447     public static boolean isInProjection(String[] projection, String column) {
4448         if (projection == null) {
4449             return true;  // Null means "all columns".  We can't really tell if it's in there.
4450         }
4451         for (String test : projection) {
4452             if (column.equals(test)) {
4453                 return true;
4454             }
4455         }
4456         return false;
4457     }
4458 
4459     /**
4460      * Tests if any of the columns appear in the given projection.
4461      */
isInProjection(String[] projection, String... columns)4462     public static boolean isInProjection(String[] projection, String... columns) {
4463         if (projection == null) {
4464             return true;
4465         }
4466 
4467         // Optimized for a single-column test
4468         if (columns.length == 1) {
4469             return isInProjection(projection, columns[0]);
4470         }
4471         for (String test : projection) {
4472             for (String column : columns) {
4473                 if (column.equals(test)) {
4474                     return true;
4475                 }
4476             }
4477         }
4478         return false;
4479     }
4480 
4481     /**
4482      * Returns a detailed exception message for the supplied URI.  It includes the calling
4483      * user and calling package(s).
4484      */
exceptionMessage(Uri uri)4485     public String exceptionMessage(Uri uri) {
4486         return exceptionMessage(null, uri);
4487     }
4488 
4489     /**
4490      * Returns a detailed exception message for the supplied URI.  It includes the calling
4491      * user and calling package(s).
4492      */
exceptionMessage(String message, Uri uri)4493     public String exceptionMessage(String message, Uri uri) {
4494         StringBuilder sb = new StringBuilder();
4495         if (message != null) {
4496             sb.append(message).append("; ");
4497         }
4498         sb.append("URI: ").append(uri);
4499         final PackageManager pm = mContext.getPackageManager();
4500         int callingUid = Binder.getCallingUid();
4501         sb.append(", calling user: ");
4502         String userName = pm.getNameForUid(callingUid);
4503         sb.append(userName == null ? callingUid : userName);
4504 
4505         final String[] callerPackages = pm.getPackagesForUid(callingUid);
4506         if (callerPackages != null && callerPackages.length > 0) {
4507             if (callerPackages.length == 1) {
4508                 sb.append(", calling package:");
4509                 sb.append(callerPackages[0]);
4510             } else {
4511                 sb.append(", calling package is one of: [");
4512                 for (int i = 0; i < callerPackages.length; i++) {
4513                     if (i != 0) {
4514                         sb.append(", ");
4515                     }
4516                     sb.append(callerPackages[i]);
4517                 }
4518                 sb.append("]");
4519             }
4520         }
4521         return sb.toString();
4522     }
4523 
deleteStatusUpdate(long dataId)4524     public void deleteStatusUpdate(long dataId) {
4525         final SQLiteStatement statusUpdateDelete = getWritableDatabase().compileStatement(
4526                     "DELETE FROM " + Tables.STATUS_UPDATES +
4527                     " WHERE " + StatusUpdatesColumns.DATA_ID + "=?");
4528         statusUpdateDelete.bindLong(1, dataId);
4529         statusUpdateDelete.execute();
4530     }
4531 
replaceStatusUpdate(Long dataId, long timestamp, String status, String resPackage, Integer iconResource, Integer labelResource)4532     public void replaceStatusUpdate(Long dataId, long timestamp, String status, String resPackage,
4533             Integer iconResource, Integer labelResource) {
4534         final SQLiteStatement statusUpdateReplace = getWritableDatabase().compileStatement(
4535                     "INSERT OR REPLACE INTO " + Tables.STATUS_UPDATES + "("
4536                             + StatusUpdatesColumns.DATA_ID + ", "
4537                             + StatusUpdates.STATUS_TIMESTAMP + ","
4538                             + StatusUpdates.STATUS + ","
4539                             + StatusUpdates.STATUS_RES_PACKAGE + ","
4540                             + StatusUpdates.STATUS_ICON + ","
4541                             + StatusUpdates.STATUS_LABEL + ")" +
4542                     " VALUES (?,?,?,?,?,?)");
4543         statusUpdateReplace.bindLong(1, dataId);
4544         statusUpdateReplace.bindLong(2, timestamp);
4545         bindString(statusUpdateReplace, 3, status);
4546         bindString(statusUpdateReplace, 4, resPackage);
4547         bindLong(statusUpdateReplace, 5, iconResource);
4548         bindLong(statusUpdateReplace, 6, labelResource);
4549         statusUpdateReplace.execute();
4550     }
4551 
insertStatusUpdate(Long dataId, String status, String resPackage, Integer iconResource, Integer labelResource)4552     public void insertStatusUpdate(Long dataId, String status, String resPackage,
4553             Integer iconResource, Integer labelResource) {
4554         final SQLiteStatement statusUpdateInsert = getWritableDatabase().compileStatement(
4555                     "INSERT INTO " + Tables.STATUS_UPDATES + "("
4556                             + StatusUpdatesColumns.DATA_ID + ", "
4557                             + StatusUpdates.STATUS + ","
4558                             + StatusUpdates.STATUS_RES_PACKAGE + ","
4559                             + StatusUpdates.STATUS_ICON + ","
4560                             + StatusUpdates.STATUS_LABEL + ")" +
4561                     " VALUES (?,?,?,?,?)");
4562         try {
4563             statusUpdateInsert.bindLong(1, dataId);
4564             bindString(statusUpdateInsert, 2, status);
4565             bindString(statusUpdateInsert, 3, resPackage);
4566             bindLong(statusUpdateInsert, 4, iconResource);
4567             bindLong(statusUpdateInsert, 5, labelResource);
4568             statusUpdateInsert.executeInsert();
4569         } catch (SQLiteConstraintException e) {
4570             // The row already exists - update it
4571             final SQLiteStatement statusUpdateAutoTimestamp = getWritableDatabase()
4572                     .compileStatement(
4573                         "UPDATE " + Tables.STATUS_UPDATES +
4574                         " SET " + StatusUpdates.STATUS_TIMESTAMP + "=?,"
4575                                 + StatusUpdates.STATUS + "=?" +
4576                         " WHERE " + StatusUpdatesColumns.DATA_ID + "=?"
4577                                 + " AND " + StatusUpdates.STATUS + "!=?");
4578 
4579             long timestamp = System.currentTimeMillis();
4580             statusUpdateAutoTimestamp.bindLong(1, timestamp);
4581             bindString(statusUpdateAutoTimestamp, 2, status);
4582             statusUpdateAutoTimestamp.bindLong(3, dataId);
4583             bindString(statusUpdateAutoTimestamp, 4, status);
4584             statusUpdateAutoTimestamp.execute();
4585 
4586             final SQLiteStatement statusAttributionUpdate = getWritableDatabase().compileStatement(
4587                         "UPDATE " + Tables.STATUS_UPDATES +
4588                         " SET " + StatusUpdates.STATUS_RES_PACKAGE + "=?,"
4589                                 + StatusUpdates.STATUS_ICON + "=?,"
4590                                 + StatusUpdates.STATUS_LABEL + "=?" +
4591                         " WHERE " + StatusUpdatesColumns.DATA_ID + "=?");
4592             bindString(statusAttributionUpdate, 1, resPackage);
4593             bindLong(statusAttributionUpdate, 2, iconResource);
4594             bindLong(statusAttributionUpdate, 3, labelResource);
4595             statusAttributionUpdate.bindLong(4, dataId);
4596             statusAttributionUpdate.execute();
4597         }
4598     }
4599 
4600     /**
4601      * Updates a raw contact display name based on data rows, e.g. structured name,
4602      * organization, email etc.
4603      */
updateRawContactDisplayName(SQLiteDatabase db, long rawContactId)4604     public void updateRawContactDisplayName(SQLiteDatabase db, long rawContactId) {
4605         if (mNameSplitter == null) {
4606             createNameSplitter();
4607         }
4608 
4609         int bestDisplayNameSource = DisplayNameSources.UNDEFINED;
4610         NameSplitter.Name bestName = null;
4611         String bestDisplayName = null;
4612         String bestPhoneticName = null;
4613         int bestPhoneticNameStyle = PhoneticNameStyle.UNDEFINED;
4614 
4615         mSelectionArgs1[0] = String.valueOf(rawContactId);
4616         Cursor c = db.rawQuery(RawContactNameQuery.RAW_SQL, mSelectionArgs1);
4617         try {
4618             while (c.moveToNext()) {
4619                 int mimeType = c.getInt(RawContactNameQuery.MIMETYPE);
4620                 int source = getDisplayNameSourceForMimeTypeId(mimeType);
4621 
4622                 if (source == DisplayNameSources.STRUCTURED_NAME) {
4623                     final String given = c.getString(RawContactNameQuery.GIVEN_NAME);
4624                     final String middle = c.getString(RawContactNameQuery.MIDDLE_NAME);
4625                     final String family = c.getString(RawContactNameQuery.FAMILY_NAME);
4626                     final String suffix = c.getString(RawContactNameQuery.SUFFIX);
4627                     final String prefix = c.getString(RawContactNameQuery.PREFIX);
4628                     if (TextUtils.isEmpty(given) && TextUtils.isEmpty(middle)
4629                             && TextUtils.isEmpty(family) && TextUtils.isEmpty(suffix)
4630                             && TextUtils.isEmpty(prefix)) {
4631                         // Every non-phonetic name component is empty. Therefore, lets lower the
4632                         // source score to STRUCTURED_PHONETIC_NAME.
4633                         source = DisplayNameSources.STRUCTURED_PHONETIC_NAME;
4634                     }
4635                 }
4636 
4637                 if (source < bestDisplayNameSource || source == DisplayNameSources.UNDEFINED) {
4638                     continue;
4639                 }
4640 
4641                 if (source == bestDisplayNameSource
4642                         && c.getInt(RawContactNameQuery.IS_PRIMARY) == 0) {
4643                     continue;
4644                 }
4645 
4646                 if (mimeType == getMimeTypeIdForStructuredName()) {
4647                     NameSplitter.Name name;
4648                     if (bestName != null) {
4649                         name = new NameSplitter.Name();
4650                     } else {
4651                         name = mName;
4652                         name.clear();
4653                     }
4654                     name.prefix = c.getString(RawContactNameQuery.PREFIX);
4655                     name.givenNames = c.getString(RawContactNameQuery.GIVEN_NAME);
4656                     name.middleName = c.getString(RawContactNameQuery.MIDDLE_NAME);
4657                     name.familyName = c.getString(RawContactNameQuery.FAMILY_NAME);
4658                     name.suffix = c.getString(RawContactNameQuery.SUFFIX);
4659                     name.fullNameStyle = c.isNull(RawContactNameQuery.FULL_NAME_STYLE)
4660                             ? FullNameStyle.UNDEFINED
4661                             : c.getInt(RawContactNameQuery.FULL_NAME_STYLE);
4662                     name.phoneticFamilyName = c.getString(RawContactNameQuery.PHONETIC_FAMILY_NAME);
4663                     name.phoneticMiddleName = c.getString(RawContactNameQuery.PHONETIC_MIDDLE_NAME);
4664                     name.phoneticGivenName = c.getString(RawContactNameQuery.PHONETIC_GIVEN_NAME);
4665                     name.phoneticNameStyle = c.isNull(RawContactNameQuery.PHONETIC_NAME_STYLE)
4666                             ? PhoneticNameStyle.UNDEFINED
4667                             : c.getInt(RawContactNameQuery.PHONETIC_NAME_STYLE);
4668                     if (!name.isEmpty()) {
4669                         bestDisplayNameSource = source;
4670                         bestName = name;
4671                     }
4672                 } else if (mimeType == getMimeTypeIdForOrganization()) {
4673                     mCharArrayBuffer.sizeCopied = 0;
4674                     c.copyStringToBuffer(RawContactNameQuery.DATA1, mCharArrayBuffer);
4675                     if (mCharArrayBuffer.sizeCopied != 0) {
4676                         bestDisplayNameSource = source;
4677                         bestDisplayName = new String(mCharArrayBuffer.data, 0,
4678                                 mCharArrayBuffer.sizeCopied);
4679                         bestPhoneticName = c.getString(
4680                                 RawContactNameQuery.ORGANIZATION_PHONETIC_NAME);
4681                         bestPhoneticNameStyle =
4682                                 c.isNull(RawContactNameQuery.ORGANIZATION_PHONETIC_NAME_STYLE)
4683                                    ? PhoneticNameStyle.UNDEFINED
4684                                    : c.getInt(RawContactNameQuery.ORGANIZATION_PHONETIC_NAME_STYLE);
4685                     } else {
4686                         c.copyStringToBuffer(RawContactNameQuery.TITLE, mCharArrayBuffer);
4687                         if (mCharArrayBuffer.sizeCopied != 0) {
4688                             bestDisplayNameSource = source;
4689                             bestDisplayName = new String(mCharArrayBuffer.data, 0,
4690                                     mCharArrayBuffer.sizeCopied);
4691                             bestPhoneticName = null;
4692                             bestPhoneticNameStyle = PhoneticNameStyle.UNDEFINED;
4693                         }
4694                     }
4695                 } else {
4696                     // Display name is at DATA1 in all other types.
4697                     // This is ensured in the constructor.
4698 
4699                     mCharArrayBuffer.sizeCopied = 0;
4700                     c.copyStringToBuffer(RawContactNameQuery.DATA1, mCharArrayBuffer);
4701                     if (mCharArrayBuffer.sizeCopied != 0) {
4702                         bestDisplayNameSource = source;
4703                         bestDisplayName = new String(mCharArrayBuffer.data, 0,
4704                                 mCharArrayBuffer.sizeCopied);
4705                         bestPhoneticName = null;
4706                         bestPhoneticNameStyle = PhoneticNameStyle.UNDEFINED;
4707                     }
4708                 }
4709             }
4710 
4711         } finally {
4712             c.close();
4713         }
4714 
4715         String displayNamePrimary;
4716         String displayNameAlternative;
4717         String sortNamePrimary;
4718         String sortNameAlternative;
4719         String sortKeyPrimary = null;
4720         String sortKeyAlternative = null;
4721         int displayNameStyle = FullNameStyle.UNDEFINED;
4722 
4723         if (bestDisplayNameSource == DisplayNameSources.STRUCTURED_NAME
4724                 || bestDisplayNameSource == DisplayNameSources.STRUCTURED_PHONETIC_NAME) {
4725             displayNameStyle = bestName.fullNameStyle;
4726             if (displayNameStyle == FullNameStyle.CJK
4727                     || displayNameStyle == FullNameStyle.UNDEFINED) {
4728                 displayNameStyle = mNameSplitter.getAdjustedFullNameStyle(displayNameStyle);
4729                 bestName.fullNameStyle = displayNameStyle;
4730             }
4731 
4732             displayNamePrimary = mNameSplitter.join(bestName, true, true);
4733             displayNameAlternative = mNameSplitter.join(bestName, false, true);
4734 
4735             if (TextUtils.isEmpty(bestName.prefix)) {
4736                 sortNamePrimary = displayNamePrimary;
4737                 sortNameAlternative = displayNameAlternative;
4738             } else {
4739                 sortNamePrimary = mNameSplitter.join(bestName, true, false);
4740                 sortNameAlternative = mNameSplitter.join(bestName, false, false);
4741             }
4742 
4743             bestPhoneticName = mNameSplitter.joinPhoneticName(bestName);
4744             bestPhoneticNameStyle = bestName.phoneticNameStyle;
4745         } else {
4746             displayNamePrimary = displayNameAlternative = bestDisplayName;
4747             sortNamePrimary = sortNameAlternative = bestDisplayName;
4748         }
4749 
4750         if (bestPhoneticName != null) {
4751             if (displayNamePrimary == null) {
4752                 displayNamePrimary = bestPhoneticName;
4753             }
4754             if (displayNameAlternative == null) {
4755                 displayNameAlternative = bestPhoneticName;
4756             }
4757             // Phonetic names disregard name order so displayNamePrimary and displayNameAlternative
4758             // are the same.
4759             sortKeyPrimary = sortKeyAlternative = bestPhoneticName;
4760             if (bestPhoneticNameStyle == PhoneticNameStyle.UNDEFINED) {
4761                 bestPhoneticNameStyle = mNameSplitter.guessPhoneticNameStyle(bestPhoneticName);
4762             }
4763         } else {
4764             bestPhoneticNameStyle = PhoneticNameStyle.UNDEFINED;
4765             if (displayNameStyle == FullNameStyle.UNDEFINED) {
4766                 displayNameStyle = mNameSplitter.guessFullNameStyle(bestDisplayName);
4767                 if (displayNameStyle == FullNameStyle.UNDEFINED
4768                         || displayNameStyle == FullNameStyle.CJK) {
4769                     displayNameStyle = mNameSplitter.getAdjustedNameStyleBasedOnPhoneticNameStyle(
4770                             displayNameStyle, bestPhoneticNameStyle);
4771                 }
4772                 displayNameStyle = mNameSplitter.getAdjustedFullNameStyle(displayNameStyle);
4773             }
4774             if (displayNameStyle == FullNameStyle.CHINESE ||
4775                     displayNameStyle == FullNameStyle.CJK) {
4776                 sortKeyPrimary = sortKeyAlternative = sortNamePrimary;
4777             }
4778         }
4779 
4780         if (sortKeyPrimary == null) {
4781             sortKeyPrimary = sortNamePrimary;
4782             sortKeyAlternative = sortNameAlternative;
4783         }
4784 
4785         final ContactLocaleUtils localeUtils = ContactLocaleUtils.getInstance();
4786         int phonebookBucketPrimary = TextUtils.isEmpty(sortKeyPrimary)
4787                 ? localeUtils.getNumberBucketIndex()
4788                 : localeUtils.getBucketIndex(sortKeyPrimary);
4789         String phonebookLabelPrimary = localeUtils.getBucketLabel(phonebookBucketPrimary);
4790 
4791         int phonebookBucketAlternative = TextUtils.isEmpty(sortKeyAlternative)
4792                 ? localeUtils.getNumberBucketIndex()
4793                 : localeUtils.getBucketIndex(sortKeyAlternative);
4794         String phonebookLabelAlternative = localeUtils.getBucketLabel(phonebookBucketAlternative);
4795 
4796         final SQLiteStatement rawContactDisplayNameUpdate = db.compileStatement(
4797                     "UPDATE " + Tables.RAW_CONTACTS +
4798                     " SET " +
4799                             RawContacts.DISPLAY_NAME_SOURCE + "=?," +
4800                             RawContacts.DISPLAY_NAME_PRIMARY + "=?," +
4801                             RawContacts.DISPLAY_NAME_ALTERNATIVE + "=?," +
4802                             RawContacts.PHONETIC_NAME + "=?," +
4803                             RawContacts.PHONETIC_NAME_STYLE + "=?," +
4804                             RawContacts.SORT_KEY_PRIMARY + "=?," +
4805                             RawContactsColumns.PHONEBOOK_LABEL_PRIMARY + "=?," +
4806                             RawContactsColumns.PHONEBOOK_BUCKET_PRIMARY + "=?," +
4807                             RawContacts.SORT_KEY_ALTERNATIVE + "=?," +
4808                             RawContactsColumns.PHONEBOOK_LABEL_ALTERNATIVE + "=?," +
4809                             RawContactsColumns.PHONEBOOK_BUCKET_ALTERNATIVE + "=?" +
4810                     " WHERE " + RawContacts._ID + "=?");
4811 
4812         rawContactDisplayNameUpdate.bindLong(1, bestDisplayNameSource);
4813         bindString(rawContactDisplayNameUpdate, 2, displayNamePrimary);
4814         bindString(rawContactDisplayNameUpdate, 3, displayNameAlternative);
4815         bindString(rawContactDisplayNameUpdate, 4, bestPhoneticName);
4816         rawContactDisplayNameUpdate.bindLong(5, bestPhoneticNameStyle);
4817         bindString(rawContactDisplayNameUpdate, 6, sortKeyPrimary);
4818         bindString(rawContactDisplayNameUpdate, 7, phonebookLabelPrimary);
4819         rawContactDisplayNameUpdate.bindLong(8, phonebookBucketPrimary);
4820         bindString(rawContactDisplayNameUpdate, 9, sortKeyAlternative);
4821         bindString(rawContactDisplayNameUpdate, 10, phonebookLabelAlternative);
4822         rawContactDisplayNameUpdate.bindLong(11, phonebookBucketAlternative);
4823         rawContactDisplayNameUpdate.bindLong(12, rawContactId);
4824         rawContactDisplayNameUpdate.execute();
4825     }
4826 
4827     /**
4828      * Sets the given dataId record in the "data" table to primary, and resets all data records of
4829      * the same mimetype and under the same contact to not be primary.
4830      *
4831      * @param dataId the id of the data record to be set to primary. Pass -1 to clear the primary
4832      * flag of all data items of this raw contacts
4833      */
setIsPrimary(long rawContactId, long dataId, long mimeTypeId)4834     public void setIsPrimary(long rawContactId, long dataId, long mimeTypeId) {
4835         final SQLiteStatement setPrimaryStatement = getWritableDatabase().compileStatement(
4836                     "UPDATE " + Tables.DATA +
4837                     " SET " + Data.IS_PRIMARY + "=(_id=?)" +
4838                     " WHERE " + DataColumns.MIMETYPE_ID + "=?" +
4839                     "   AND " + Data.RAW_CONTACT_ID + "=?");
4840         setPrimaryStatement.bindLong(1, dataId);
4841         setPrimaryStatement.bindLong(2, mimeTypeId);
4842         setPrimaryStatement.bindLong(3, rawContactId);
4843         setPrimaryStatement.execute();
4844     }
4845 
4846     /**
4847      * Clears the super primary of all data items of the given raw contact. does not touch
4848      * other raw contacts of the same joined aggregate
4849      */
clearSuperPrimary(long rawContactId, long mimeTypeId)4850     public void clearSuperPrimary(long rawContactId, long mimeTypeId) {
4851         final SQLiteStatement clearSuperPrimaryStatement = getWritableDatabase().compileStatement(
4852                     "UPDATE " + Tables.DATA +
4853                     " SET " + Data.IS_SUPER_PRIMARY + "=0" +
4854                     " WHERE " + DataColumns.MIMETYPE_ID + "=?" +
4855                     "   AND " + Data.RAW_CONTACT_ID + "=?");
4856         clearSuperPrimaryStatement.bindLong(1, mimeTypeId);
4857         clearSuperPrimaryStatement.bindLong(2, rawContactId);
4858         clearSuperPrimaryStatement.execute();
4859     }
4860 
4861     /**
4862      * Sets the given dataId record in the "data" table to "super primary", and resets all data
4863      * records of the same mimetype and under the same aggregate to not be "super primary".
4864      *
4865      * @param dataId the id of the data record to be set to primary.
4866      */
setIsSuperPrimary(long rawContactId, long dataId, long mimeTypeId)4867     public void setIsSuperPrimary(long rawContactId, long dataId, long mimeTypeId) {
4868         final SQLiteStatement setSuperPrimaryStatement = getWritableDatabase().compileStatement(
4869                     "UPDATE " + Tables.DATA +
4870                     " SET " + Data.IS_SUPER_PRIMARY + "=(" + Data._ID + "=?)" +
4871                     " WHERE " + DataColumns.MIMETYPE_ID + "=?" +
4872                     "   AND " + Data.RAW_CONTACT_ID + " IN (" +
4873                             "SELECT " + RawContacts._ID +
4874                             " FROM " + Tables.RAW_CONTACTS +
4875                             " WHERE " + RawContacts.CONTACT_ID + " =(" +
4876                                     "SELECT " + RawContacts.CONTACT_ID +
4877                                     " FROM " + Tables.RAW_CONTACTS +
4878                                     " WHERE " + RawContacts._ID + "=?))");
4879         setSuperPrimaryStatement.bindLong(1, dataId);
4880         setSuperPrimaryStatement.bindLong(2, mimeTypeId);
4881         setSuperPrimaryStatement.bindLong(3, rawContactId);
4882         setSuperPrimaryStatement.execute();
4883     }
4884 
4885     /**
4886      * Inserts a record in the {@link Tables#NAME_LOOKUP} table.
4887      */
insertNameLookup(long rawContactId, long dataId, int lookupType, String name)4888     public void insertNameLookup(long rawContactId, long dataId, int lookupType, String name) {
4889         if (TextUtils.isEmpty(name)) {
4890             return;
4891         }
4892 
4893         final SQLiteStatement nameLookupInsert = getWritableDatabase().compileStatement(
4894                     "INSERT OR IGNORE INTO " + Tables.NAME_LOOKUP + "("
4895                             + NameLookupColumns.RAW_CONTACT_ID + ","
4896                             + NameLookupColumns.DATA_ID + ","
4897                             + NameLookupColumns.NAME_TYPE + ","
4898                             + NameLookupColumns.NORMALIZED_NAME
4899                     + ") VALUES (?,?,?,?)");
4900         nameLookupInsert.bindLong(1, rawContactId);
4901         nameLookupInsert.bindLong(2, dataId);
4902         nameLookupInsert.bindLong(3, lookupType);
4903         bindString(nameLookupInsert, 4, name);
4904         nameLookupInsert.executeInsert();
4905     }
4906 
4907     /**
4908      * Deletes all {@link Tables#NAME_LOOKUP} table rows associated with the specified data element.
4909      */
deleteNameLookup(long dataId)4910     public void deleteNameLookup(long dataId) {
4911         final SQLiteStatement nameLookupDelete = getWritableDatabase().compileStatement(
4912                     "DELETE FROM " + Tables.NAME_LOOKUP +
4913                     " WHERE " + NameLookupColumns.DATA_ID + "=?");
4914         nameLookupDelete.bindLong(1, dataId);
4915         nameLookupDelete.execute();
4916     }
4917 
insertNameLookupForEmail(long rawContactId, long dataId, String email)4918     public String insertNameLookupForEmail(long rawContactId, long dataId, String email) {
4919         if (TextUtils.isEmpty(email)) {
4920             return null;
4921         }
4922 
4923         String address = extractHandleFromEmailAddress(email);
4924         if (address == null) {
4925             return null;
4926         }
4927 
4928         insertNameLookup(rawContactId, dataId,
4929                 NameLookupType.EMAIL_BASED_NICKNAME, NameNormalizer.normalize(address));
4930         return address;
4931     }
4932 
4933     /**
4934      * Normalizes the nickname and inserts it in the name lookup table.
4935      */
insertNameLookupForNickname(long rawContactId, long dataId, String nickname)4936     public void insertNameLookupForNickname(long rawContactId, long dataId, String nickname) {
4937         if (!TextUtils.isEmpty(nickname)) {
4938             insertNameLookup(rawContactId, dataId,
4939                     NameLookupType.NICKNAME, NameNormalizer.normalize(nickname));
4940         }
4941     }
4942 
4943     /**
4944      * Performs a query and returns true if any Data item of the raw contact with the given
4945      * id and mimetype is marked as super-primary
4946      */
rawContactHasSuperPrimary(long rawContactId, long mimeTypeId)4947     public boolean rawContactHasSuperPrimary(long rawContactId, long mimeTypeId) {
4948         final Cursor existsCursor = getReadableDatabase().rawQuery(
4949                 "SELECT EXISTS(SELECT 1 FROM " + Tables.DATA +
4950                 " WHERE " + Data.RAW_CONTACT_ID + "=?" +
4951                 " AND " + DataColumns.MIMETYPE_ID + "=?" +
4952                 " AND " + Data.IS_SUPER_PRIMARY + "<>0)",
4953                 new String[] {String.valueOf(rawContactId), String.valueOf(mimeTypeId)});
4954         try {
4955             if (!existsCursor.moveToFirst()) throw new IllegalStateException();
4956             return existsCursor.getInt(0) != 0;
4957         } finally {
4958             existsCursor.close();
4959         }
4960     }
4961 
getCurrentCountryIso()4962     public String getCurrentCountryIso() {
4963         return mCountryMonitor.getCountryIso();
4964     }
4965 
4966     @NeededForTesting
setUseStrictPhoneNumberComparisonForTest(boolean useStrict)4967     /* package */ void setUseStrictPhoneNumberComparisonForTest(boolean useStrict) {
4968         mUseStrictPhoneNumberComparison = useStrict;
4969     }
4970 
4971     @NeededForTesting
getUseStrictPhoneNumberComparisonForTest()4972     /* package */ boolean getUseStrictPhoneNumberComparisonForTest() {
4973         return mUseStrictPhoneNumberComparison;
4974     }
4975 
4976     @NeededForTesting
querySearchIndexContentForTest(long contactId)4977     /* package */ String querySearchIndexContentForTest(long contactId) {
4978         return DatabaseUtils.stringForQuery(getReadableDatabase(),
4979                 "SELECT " + SearchIndexColumns.CONTENT +
4980                 " FROM " + Tables.SEARCH_INDEX +
4981                 " WHERE " + SearchIndexColumns.CONTACT_ID + "=CAST(? AS int)",
4982                 new String[] {String.valueOf(contactId)});
4983     }
4984 
4985     @NeededForTesting
querySearchIndexTokensForTest(long contactId)4986     /* package */ String querySearchIndexTokensForTest(long contactId) {
4987         return DatabaseUtils.stringForQuery(getReadableDatabase(),
4988                 "SELECT " + SearchIndexColumns.TOKENS +
4989                 " FROM " + Tables.SEARCH_INDEX +
4990                 " WHERE " + SearchIndexColumns.CONTACT_ID + "=CAST(? AS int)",
4991                 new String[] {String.valueOf(contactId)});
4992     }
4993 
upsertMetadataSync(String backupId, Long accountId, String data, Integer deleted)4994     public long upsertMetadataSync(String backupId, Long accountId, String data, Integer deleted) {
4995         final SQLiteStatement metadataSyncInsert = getWritableDatabase().compileStatement(
4996                     "INSERT OR REPLACE INTO " + Tables.METADATA_SYNC + "("
4997                             + MetadataSync.RAW_CONTACT_BACKUP_ID + ", "
4998                             + MetadataSyncColumns.ACCOUNT_ID + ", "
4999                             + MetadataSync.DATA + ","
5000                             + MetadataSync.DELETED + ")" +
5001                             " VALUES (?,?,?,?)");
5002         metadataSyncInsert.bindString(1, backupId);
5003         metadataSyncInsert.bindLong(2, accountId);
5004         data = (data == null) ? "" : data;
5005         metadataSyncInsert.bindString(3, data);
5006         metadataSyncInsert.bindLong(4, deleted);
5007         return metadataSyncInsert.executeInsert();
5008     }
5009 
notifyProviderStatusChange(Context context)5010     public static void notifyProviderStatusChange(Context context) {
5011         context.getContentResolver().notifyChange(ProviderStatus.CONTENT_URI,
5012                 /* observer= */ null, /* syncToNetwork= */ false);
5013     }
5014 
getDatabaseCreationTime()5015     public long getDatabaseCreationTime() {
5016         return mDatabaseCreationTime;
5017     }
5018 
5019     private SqlChecker mCachedSqlChecker;
5020 
getSqlChecker()5021     private SqlChecker getSqlChecker() {
5022         // No need for synchronization on mCachedSqlChecker, because worst-case we'll just
5023         // initialize it twice.
5024         if (mCachedSqlChecker != null) {
5025             return mCachedSqlChecker;
5026         }
5027         final ArrayList<String> invalidTokens = new ArrayList<>();
5028 
5029         if (DISALLOW_SUB_QUERIES) {
5030             // Disallow referring to tables and views.  However, we exempt tables whose names are
5031             // also used as column names of any tables.  (Right now it's only 'data'.)
5032             invalidTokens.addAll(
5033                     DatabaseAnalyzer.findTableViewsAllowingColumns(getReadableDatabase()));
5034 
5035             // Disallow token "select" to disallow subqueries.
5036             invalidTokens.add("select");
5037 
5038             // Allow the use of "default_directory" for now, as it used to be sort of commonly used...
5039             invalidTokens.remove(Tables.DEFAULT_DIRECTORY.toLowerCase());
5040         }
5041 
5042         mCachedSqlChecker = new SqlChecker(invalidTokens);
5043 
5044         return mCachedSqlChecker;
5045     }
5046 
5047     /**
5048      * Ensure (a piece of) SQL is valid and doesn't contain disallowed tokens.
5049      */
validateSql(String callerPackage, String sqlPiece)5050     public void validateSql(String callerPackage, String sqlPiece) {
5051         // TODO Replace the Runnable with a lambda -- which would crash right now due to an art bug?
5052         runSqlValidation(callerPackage, new Runnable() {
5053             @Override
5054             public void run() {
5055                 ContactsDatabaseHelper.this.getSqlChecker().ensureNoInvalidTokens(sqlPiece);
5056             }
5057         });
5058     }
5059 
5060     /**
5061      * Ensure all keys in {@code values} are valid. (i.e. they're all single token.)
5062      */
validateContentValues(String callerPackage, ContentValues values)5063     public void validateContentValues(String callerPackage, ContentValues values) {
5064         // TODO Replace the Runnable with a lambda -- which would crash right now due to an art bug?
5065         runSqlValidation(callerPackage, new Runnable() {
5066             @Override
5067             public void run() {
5068                 for (String key : values.keySet()) {
5069                     ContactsDatabaseHelper.this.getSqlChecker().ensureSingleTokenOnly(key);
5070                 }
5071             }
5072         });
5073    }
5074 
5075     /**
5076      * Ensure all column names in {@code projection} are valid. (i.e. they're all single token.)
5077      */
validateProjection(String callerPackage, String[] projection)5078     public void validateProjection(String callerPackage, String[] projection) {
5079         // TODO Replace the Runnable with a lambda -- which would crash right now due to an art bug?
5080         if (projection != null) {
5081             runSqlValidation(callerPackage, new Runnable() {
5082                 @Override
5083                 public void run() {
5084                     for (String column : projection) {
5085                         ContactsDatabaseHelper.this.getSqlChecker().ensureSingleTokenOnly(column);
5086                     }
5087                 }
5088             });
5089         }
5090     }
5091 
runSqlValidation(String callerPackage, Runnable r)5092     private void runSqlValidation(String callerPackage, Runnable r) {
5093         try {
5094             r.run();
5095         } catch (InvalidSqlException e) {
5096             reportInvalidSql(callerPackage, e);
5097         }
5098     }
5099 
reportInvalidSql(String callerPackage, InvalidSqlException e)5100     private void reportInvalidSql(String callerPackage, InvalidSqlException e) {
5101         Log.e(TAG, String.format("%s caller=%s", e.getMessage(), callerPackage));
5102         throw e;
5103     }
5104 
5105     /**
5106      * Calls WTF without crashing, so we can collect errors in the wild.  During unit tests, it'll
5107      * log only.
5108      */
logWtf(String message)5109     public void logWtf(String message) {
5110         if (mIsTestInstance) {
5111             Slog.w(TAG, "[Test mode, warning only] " + message);
5112         } else {
5113             Slog.wtfStack(TAG, message);
5114         }
5115     }
5116 }
5117