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