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