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