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