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