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