1 /* 2 * Copyright (C) 2013 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.dialer.database; 18 19 import android.content.ContentValues; 20 import android.content.Context; 21 import android.content.Intent; 22 import android.content.SharedPreferences; 23 import android.database.Cursor; 24 import android.database.sqlite.SQLiteDatabase; 25 import android.database.sqlite.SQLiteException; 26 import android.database.sqlite.SQLiteOpenHelper; 27 import android.database.sqlite.SQLiteStatement; 28 import android.net.Uri; 29 import android.provider.BaseColumns; 30 import android.provider.ContactsContract; 31 import android.provider.ContactsContract.CommonDataKinds.Phone; 32 import android.provider.ContactsContract.Contacts; 33 import android.provider.ContactsContract.Data; 34 import android.provider.ContactsContract.Directory; 35 import android.support.annotation.Nullable; 36 import android.support.annotation.VisibleForTesting; 37 import android.support.annotation.WorkerThread; 38 import android.text.TextUtils; 39 import com.android.contacts.common.R; 40 import com.android.contacts.common.util.StopWatch; 41 import com.android.dialer.common.LogUtil; 42 import com.android.dialer.common.concurrent.DialerExecutor.Worker; 43 import com.android.dialer.common.concurrent.DialerExecutorComponent; 44 import com.android.dialer.common.database.Selection; 45 import com.android.dialer.configprovider.ConfigProviderBindings; 46 import com.android.dialer.database.FilteredNumberContract.FilteredNumberColumns; 47 import com.android.dialer.smartdial.util.SmartDialNameMatcher; 48 import com.android.dialer.smartdial.util.SmartDialPrefix; 49 import com.android.dialer.util.PermissionsUtil; 50 import java.util.ArrayList; 51 import java.util.HashSet; 52 import java.util.Objects; 53 import java.util.Set; 54 55 /** 56 * Database helper for smart dial. Designed as a singleton to make sure there is only one access 57 * point to the database. Provides methods to maintain, update, and query the database. 58 */ 59 public class DialerDatabaseHelper extends SQLiteOpenHelper { 60 61 /** 62 * SmartDial DB version ranges: 63 * 64 * <pre> 65 * 0-98 KitKat 66 * </pre> 67 */ 68 public static final int DATABASE_VERSION = 10; 69 70 public static final String DATABASE_NAME = "dialer.db"; 71 72 public static final String ACTION_SMART_DIAL_UPDATED = 73 "com.android.dialer.database.ACTION_SMART_DIAL_UPDATED"; 74 private static final String TAG = "DialerDatabaseHelper"; 75 private static final boolean DEBUG = false; 76 /** Saves the last update time of smart dial databases to shared preferences. */ 77 private static final String DATABASE_LAST_CREATED_SHARED_PREF = "com.android.dialer"; 78 79 private static final String LAST_UPDATED_MILLIS = "last_updated_millis"; 80 81 @VisibleForTesting 82 static final String DEFAULT_LAST_UPDATED_CONFIG_KEY = "smart_dial_default_last_update_millis"; 83 84 private static final String DATABASE_VERSION_PROPERTY = "database_version"; 85 private static final int MAX_ENTRIES = 20; 86 87 private final Context context; 88 private boolean isTestInstance = false; 89 90 protected DialerDatabaseHelper(Context context, String databaseName, int dbVersion) { 91 super(context, databaseName, null, dbVersion); 92 this.context = Objects.requireNonNull(context, "Context must not be null"); 93 } 94 95 public void setIsTestInstance(boolean isTestInstance) { 96 this.isTestInstance = isTestInstance; 97 } 98 99 /** 100 * Creates tables in the database when database is created for the first time. 101 * 102 * @param db The database. 103 */ 104 @Override 105 public void onCreate(SQLiteDatabase db) { 106 setupTables(db); 107 } 108 109 private void setupTables(SQLiteDatabase db) { 110 dropTables(db); 111 db.execSQL( 112 "CREATE TABLE " 113 + Tables.SMARTDIAL_TABLE 114 + " (" 115 + SmartDialDbColumns._ID 116 + " INTEGER PRIMARY KEY AUTOINCREMENT," 117 + SmartDialDbColumns.DATA_ID 118 + " INTEGER, " 119 + SmartDialDbColumns.NUMBER 120 + " TEXT," 121 + SmartDialDbColumns.CONTACT_ID 122 + " INTEGER," 123 + SmartDialDbColumns.LOOKUP_KEY 124 + " TEXT," 125 + SmartDialDbColumns.DISPLAY_NAME_PRIMARY 126 + " TEXT, " 127 + SmartDialDbColumns.PHOTO_ID 128 + " INTEGER, " 129 + SmartDialDbColumns.LAST_SMARTDIAL_UPDATE_TIME 130 + " LONG, " 131 + SmartDialDbColumns.LAST_TIME_USED 132 + " LONG, " 133 + SmartDialDbColumns.TIMES_USED 134 + " INTEGER, " 135 + SmartDialDbColumns.STARRED 136 + " INTEGER, " 137 + SmartDialDbColumns.IS_SUPER_PRIMARY 138 + " INTEGER, " 139 + SmartDialDbColumns.IN_VISIBLE_GROUP 140 + " INTEGER, " 141 + SmartDialDbColumns.IS_PRIMARY 142 + " INTEGER, " 143 + SmartDialDbColumns.CARRIER_PRESENCE 144 + " INTEGER NOT NULL DEFAULT 0" 145 + ");"); 146 147 db.execSQL( 148 "CREATE TABLE " 149 + Tables.PREFIX_TABLE 150 + " (" 151 + PrefixColumns._ID 152 + " INTEGER PRIMARY KEY AUTOINCREMENT," 153 + PrefixColumns.PREFIX 154 + " TEXT COLLATE NOCASE, " 155 + PrefixColumns.CONTACT_ID 156 + " INTEGER" 157 + ");"); 158 159 db.execSQL( 160 "CREATE TABLE " 161 + Tables.PROPERTIES 162 + " (" 163 + PropertiesColumns.PROPERTY_KEY 164 + " TEXT PRIMARY KEY, " 165 + PropertiesColumns.PROPERTY_VALUE 166 + " TEXT " 167 + ");"); 168 169 // This will need to also be updated in setupTablesForFilteredNumberTest and onUpgrade. 170 // Hardcoded so we know on glance what columns are updated in setupTables, 171 // and to be able to guarantee the state of the DB at each upgrade step. 172 db.execSQL( 173 "CREATE TABLE " 174 + Tables.FILTERED_NUMBER_TABLE 175 + " (" 176 + FilteredNumberColumns._ID 177 + " INTEGER PRIMARY KEY AUTOINCREMENT," 178 + FilteredNumberColumns.NORMALIZED_NUMBER 179 + " TEXT UNIQUE," 180 + FilteredNumberColumns.NUMBER 181 + " TEXT," 182 + FilteredNumberColumns.COUNTRY_ISO 183 + " TEXT," 184 + FilteredNumberColumns.TIMES_FILTERED 185 + " INTEGER," 186 + FilteredNumberColumns.LAST_TIME_FILTERED 187 + " LONG," 188 + FilteredNumberColumns.CREATION_TIME 189 + " LONG," 190 + FilteredNumberColumns.TYPE 191 + " INTEGER," 192 + FilteredNumberColumns.SOURCE 193 + " INTEGER" 194 + ");"); 195 196 setProperty(db, DATABASE_VERSION_PROPERTY, String.valueOf(DATABASE_VERSION)); 197 if (!isTestInstance) { 198 resetSmartDialLastUpdatedTime(); 199 } 200 } 201 202 public void dropTables(SQLiteDatabase db) { 203 db.execSQL("DROP TABLE IF EXISTS " + Tables.PREFIX_TABLE); 204 db.execSQL("DROP TABLE IF EXISTS " + Tables.SMARTDIAL_TABLE); 205 db.execSQL("DROP TABLE IF EXISTS " + Tables.PROPERTIES); 206 db.execSQL("DROP TABLE IF EXISTS " + Tables.FILTERED_NUMBER_TABLE); 207 db.execSQL("DROP TABLE IF EXISTS " + Tables.VOICEMAIL_ARCHIVE_TABLE); 208 } 209 210 @Override 211 public void onUpgrade(SQLiteDatabase db, int oldNumber, int newNumber) { 212 // Disregard the old version and new versions provided by SQLiteOpenHelper, we will read 213 // our own from the database. 214 215 int oldVersion; 216 217 oldVersion = getPropertyAsInt(db, DATABASE_VERSION_PROPERTY, 0); 218 219 if (oldVersion == 0) { 220 LogUtil.e( 221 "DialerDatabaseHelper.onUpgrade", "malformed database version..recreating database"); 222 } 223 224 if (oldVersion < 4) { 225 setupTables(db); 226 return; 227 } 228 229 if (oldVersion < 7) { 230 db.execSQL("DROP TABLE IF EXISTS " + Tables.FILTERED_NUMBER_TABLE); 231 db.execSQL( 232 "CREATE TABLE " 233 + Tables.FILTERED_NUMBER_TABLE 234 + " (" 235 + FilteredNumberColumns._ID 236 + " INTEGER PRIMARY KEY AUTOINCREMENT," 237 + FilteredNumberColumns.NORMALIZED_NUMBER 238 + " TEXT UNIQUE," 239 + FilteredNumberColumns.NUMBER 240 + " TEXT," 241 + FilteredNumberColumns.COUNTRY_ISO 242 + " TEXT," 243 + FilteredNumberColumns.TIMES_FILTERED 244 + " INTEGER," 245 + FilteredNumberColumns.LAST_TIME_FILTERED 246 + " LONG," 247 + FilteredNumberColumns.CREATION_TIME 248 + " LONG," 249 + FilteredNumberColumns.TYPE 250 + " INTEGER," 251 + FilteredNumberColumns.SOURCE 252 + " INTEGER" 253 + ");"); 254 oldVersion = 7; 255 } 256 257 if (oldVersion < 8) { 258 upgradeToVersion8(db); 259 oldVersion = 8; 260 } 261 262 if (oldVersion < 10) { 263 db.execSQL("DROP TABLE IF EXISTS " + Tables.VOICEMAIL_ARCHIVE_TABLE); 264 oldVersion = 10; 265 } 266 267 if (oldVersion != DATABASE_VERSION) { 268 throw new IllegalStateException( 269 "error upgrading the database to version " + DATABASE_VERSION); 270 } 271 272 setProperty(db, DATABASE_VERSION_PROPERTY, String.valueOf(DATABASE_VERSION)); 273 } 274 275 public void upgradeToVersion8(SQLiteDatabase db) { 276 db.execSQL("ALTER TABLE smartdial_table ADD carrier_presence INTEGER NOT NULL DEFAULT 0"); 277 } 278 279 /** Stores a key-value pair in the {@link Tables#PROPERTIES} table. */ 280 public void setProperty(String key, String value) { 281 setProperty(getWritableDatabase(), key, value); 282 } 283 284 public void setProperty(SQLiteDatabase db, String key, String value) { 285 final ContentValues values = new ContentValues(); 286 values.put(PropertiesColumns.PROPERTY_KEY, key); 287 values.put(PropertiesColumns.PROPERTY_VALUE, value); 288 db.replace(Tables.PROPERTIES, null, values); 289 } 290 291 /** Returns the value from the {@link Tables#PROPERTIES} table. */ 292 public String getProperty(String key, String defaultValue) { 293 return getProperty(getReadableDatabase(), key, defaultValue); 294 } 295 296 public String getProperty(SQLiteDatabase db, String key, String defaultValue) { 297 try { 298 String value = null; 299 final Cursor cursor = 300 db.query( 301 Tables.PROPERTIES, 302 new String[] {PropertiesColumns.PROPERTY_VALUE}, 303 PropertiesColumns.PROPERTY_KEY + "=?", 304 new String[] {key}, 305 null, 306 null, 307 null); 308 if (cursor != null) { 309 try { 310 if (cursor.moveToFirst()) { 311 value = cursor.getString(0); 312 } 313 } finally { 314 cursor.close(); 315 } 316 } 317 return value != null ? value : defaultValue; 318 } catch (SQLiteException e) { 319 return defaultValue; 320 } 321 } 322 323 public int getPropertyAsInt(SQLiteDatabase db, String key, int defaultValue) { 324 final String stored = getProperty(db, key, ""); 325 try { 326 return Integer.parseInt(stored); 327 } catch (NumberFormatException e) { 328 return defaultValue; 329 } 330 } 331 332 private void resetSmartDialLastUpdatedTime() { 333 final SharedPreferences databaseLastUpdateSharedPref = 334 context.getSharedPreferences(DATABASE_LAST_CREATED_SHARED_PREF, Context.MODE_PRIVATE); 335 final SharedPreferences.Editor editor = databaseLastUpdateSharedPref.edit(); 336 editor.putLong(LAST_UPDATED_MILLIS, 0); 337 editor.apply(); 338 } 339 340 /** 341 * Starts the database upgrade process in the background. 342 * 343 * @see #updateSmartDialDatabase(boolean) for the usage of {@code forceUpdate}. 344 */ 345 public void startSmartDialUpdateThread(boolean forceUpdate) { 346 if (PermissionsUtil.hasContactsReadPermissions(context)) { 347 DialerExecutorComponent.get(context) 348 .dialerExecutorFactory() 349 .createNonUiTaskBuilder(new UpdateSmartDialWorker()) 350 .build() 351 .executeParallel(forceUpdate); 352 } 353 } 354 355 /** 356 * Removes rows in the smartdial database that matches the contacts that have been deleted by 357 * other apps since last update. 358 * 359 * @param db Database to operate on. 360 * @param lastUpdatedTimeMillis the last time at which an update to the smart dial database was 361 * run. 362 */ 363 private void removeDeletedContacts(SQLiteDatabase db, String lastUpdatedTimeMillis) { 364 Cursor deletedContactCursor = getDeletedContactCursor(lastUpdatedTimeMillis); 365 366 if (deletedContactCursor == null) { 367 return; 368 } 369 370 db.beginTransaction(); 371 try { 372 if (!deletedContactCursor.moveToFirst()) { 373 return; 374 } 375 376 do { 377 if (deletedContactCursor.isNull(DeleteContactQuery.DELETED_CONTACT_ID)) { 378 LogUtil.i( 379 "DialerDatabaseHelper.removeDeletedContacts", 380 "contact_id column null. Row was deleted during iteration, skipping"); 381 continue; 382 } 383 384 long deleteContactId = deletedContactCursor.getLong(DeleteContactQuery.DELETED_CONTACT_ID); 385 386 Selection smartDialSelection = 387 Selection.column(SmartDialDbColumns.CONTACT_ID).is("=", deleteContactId); 388 db.delete( 389 Tables.SMARTDIAL_TABLE, 390 smartDialSelection.getSelection(), 391 smartDialSelection.getSelectionArgs()); 392 393 Selection prefixSelection = 394 Selection.column(PrefixColumns.CONTACT_ID).is("=", deleteContactId); 395 db.delete( 396 Tables.PREFIX_TABLE, 397 prefixSelection.getSelection(), 398 prefixSelection.getSelectionArgs()); 399 } while (deletedContactCursor.moveToNext()); 400 401 db.setTransactionSuccessful(); 402 } finally { 403 deletedContactCursor.close(); 404 db.endTransaction(); 405 } 406 } 407 408 private Cursor getDeletedContactCursor(String lastUpdateMillis) { 409 return context 410 .getContentResolver() 411 .query( 412 DeleteContactQuery.URI, 413 DeleteContactQuery.PROJECTION, 414 DeleteContactQuery.SELECT_UPDATED_CLAUSE, 415 new String[] {lastUpdateMillis}, 416 null); 417 } 418 419 /** 420 * Removes potentially corrupted entries in the database. These contacts may be added before the 421 * previous instance of the dialer was destroyed for some reason. For data integrity, we delete 422 * all of them. 423 * 424 * @param db Database pointer to the dialer database. 425 * @param last_update_time Time stamp of last successful update of the dialer database. 426 */ 427 private void removePotentiallyCorruptedContacts(SQLiteDatabase db, String last_update_time) { 428 db.delete( 429 Tables.PREFIX_TABLE, 430 PrefixColumns.CONTACT_ID 431 + " IN " 432 + "(SELECT " 433 + SmartDialDbColumns.CONTACT_ID 434 + " FROM " 435 + Tables.SMARTDIAL_TABLE 436 + " WHERE " 437 + SmartDialDbColumns.LAST_SMARTDIAL_UPDATE_TIME 438 + " > " 439 + last_update_time 440 + ")", 441 null); 442 db.delete( 443 Tables.SMARTDIAL_TABLE, 444 SmartDialDbColumns.LAST_SMARTDIAL_UPDATE_TIME + " > " + last_update_time, 445 null); 446 } 447 448 /** 449 * Removes rows in the smartdial database that matches updated contacts. 450 * 451 * @param db Database pointer to the smartdial database 452 * @param updatedContactCursor Cursor pointing to the list of recently updated contacts. 453 */ 454 @VisibleForTesting 455 void removeUpdatedContacts(SQLiteDatabase db, Cursor updatedContactCursor) { 456 db.beginTransaction(); 457 try { 458 updatedContactCursor.moveToPosition(-1); 459 while (updatedContactCursor.moveToNext()) { 460 if (updatedContactCursor.isNull(UpdatedContactQuery.UPDATED_CONTACT_ID)) { 461 LogUtil.i( 462 "DialerDatabaseHelper.removeUpdatedContacts", 463 "contact_id column null. Row was deleted during iteration, skipping"); 464 continue; 465 } 466 467 final Long contactId = updatedContactCursor.getLong(UpdatedContactQuery.UPDATED_CONTACT_ID); 468 469 db.delete(Tables.SMARTDIAL_TABLE, SmartDialDbColumns.CONTACT_ID + "=" + contactId, null); 470 db.delete(Tables.PREFIX_TABLE, PrefixColumns.CONTACT_ID + "=" + contactId, null); 471 } 472 473 db.setTransactionSuccessful(); 474 } finally { 475 db.endTransaction(); 476 } 477 } 478 479 /** 480 * Inserts updated contacts as rows to the smartdial table. 481 * 482 * @param db Database pointer to the smartdial database. 483 * @param updatedContactCursor Cursor pointing to the list of recently updated contacts. 484 * @param currentMillis Current time to be recorded in the smartdial table as update timestamp. 485 */ 486 @VisibleForTesting 487 protected void insertUpdatedContactsAndNumberPrefix( 488 SQLiteDatabase db, Cursor updatedContactCursor, Long currentMillis) { 489 db.beginTransaction(); 490 try { 491 final String sqlInsert = 492 "INSERT INTO " 493 + Tables.SMARTDIAL_TABLE 494 + " (" 495 + SmartDialDbColumns.DATA_ID 496 + ", " 497 + SmartDialDbColumns.NUMBER 498 + ", " 499 + SmartDialDbColumns.CONTACT_ID 500 + ", " 501 + SmartDialDbColumns.LOOKUP_KEY 502 + ", " 503 + SmartDialDbColumns.DISPLAY_NAME_PRIMARY 504 + ", " 505 + SmartDialDbColumns.PHOTO_ID 506 + ", " 507 + SmartDialDbColumns.LAST_TIME_USED 508 + ", " 509 + SmartDialDbColumns.TIMES_USED 510 + ", " 511 + SmartDialDbColumns.STARRED 512 + ", " 513 + SmartDialDbColumns.IS_SUPER_PRIMARY 514 + ", " 515 + SmartDialDbColumns.IN_VISIBLE_GROUP 516 + ", " 517 + SmartDialDbColumns.IS_PRIMARY 518 + ", " 519 + SmartDialDbColumns.CARRIER_PRESENCE 520 + ", " 521 + SmartDialDbColumns.LAST_SMARTDIAL_UPDATE_TIME 522 + ") " 523 + " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"; 524 final SQLiteStatement insert = db.compileStatement(sqlInsert); 525 526 final String numberSqlInsert = 527 "INSERT INTO " 528 + Tables.PREFIX_TABLE 529 + " (" 530 + PrefixColumns.CONTACT_ID 531 + ", " 532 + PrefixColumns.PREFIX 533 + ") " 534 + " VALUES (?, ?)"; 535 final SQLiteStatement numberInsert = db.compileStatement(numberSqlInsert); 536 537 updatedContactCursor.moveToPosition(-1); 538 while (updatedContactCursor.moveToNext()) { 539 insert.clearBindings(); 540 541 if (updatedContactCursor.isNull(PhoneQuery.PHONE_ID)) { 542 LogUtil.i( 543 "DialerDatabaseHelper.insertUpdatedContactsAndNumberPrefix", 544 "_id column null. Row was deleted during iteration, skipping"); 545 continue; 546 } 547 548 // Handle string columns which can possibly be null first. In the case of certain 549 // null columns (due to malformed rows possibly inserted by third-party apps 550 // or sync adapters), skip the phone number row. 551 final String number = updatedContactCursor.getString(PhoneQuery.PHONE_NUMBER); 552 if (TextUtils.isEmpty(number)) { 553 continue; 554 } else { 555 insert.bindString(2, number); 556 } 557 558 final String lookupKey = updatedContactCursor.getString(PhoneQuery.PHONE_LOOKUP_KEY); 559 if (TextUtils.isEmpty(lookupKey)) { 560 continue; 561 } else { 562 insert.bindString(4, lookupKey); 563 } 564 565 final String displayName = updatedContactCursor.getString(PhoneQuery.PHONE_DISPLAY_NAME); 566 if (displayName == null) { 567 insert.bindString(5, context.getResources().getString(R.string.missing_name)); 568 } else { 569 insert.bindString(5, displayName); 570 } 571 insert.bindLong(1, updatedContactCursor.getLong(PhoneQuery.PHONE_ID)); 572 insert.bindLong(3, updatedContactCursor.getLong(PhoneQuery.PHONE_CONTACT_ID)); 573 insert.bindLong(6, updatedContactCursor.getLong(PhoneQuery.PHONE_PHOTO_ID)); 574 insert.bindLong(7, updatedContactCursor.getLong(PhoneQuery.PHONE_LAST_TIME_USED)); 575 insert.bindLong(8, updatedContactCursor.getInt(PhoneQuery.PHONE_TIMES_USED)); 576 insert.bindLong(9, updatedContactCursor.getInt(PhoneQuery.PHONE_STARRED)); 577 insert.bindLong(10, updatedContactCursor.getInt(PhoneQuery.PHONE_IS_SUPER_PRIMARY)); 578 insert.bindLong(11, updatedContactCursor.getInt(PhoneQuery.PHONE_IN_VISIBLE_GROUP)); 579 insert.bindLong(12, updatedContactCursor.getInt(PhoneQuery.PHONE_IS_PRIMARY)); 580 insert.bindLong(13, updatedContactCursor.getInt(PhoneQuery.PHONE_CARRIER_PRESENCE)); 581 insert.bindLong(14, currentMillis); 582 insert.executeInsert(); 583 final String contactPhoneNumber = updatedContactCursor.getString(PhoneQuery.PHONE_NUMBER); 584 final ArrayList<String> numberPrefixes = 585 SmartDialPrefix.parseToNumberTokens(context, contactPhoneNumber); 586 587 for (String numberPrefix : numberPrefixes) { 588 numberInsert.bindLong(1, updatedContactCursor.getLong(PhoneQuery.PHONE_CONTACT_ID)); 589 numberInsert.bindString(2, numberPrefix); 590 numberInsert.executeInsert(); 591 numberInsert.clearBindings(); 592 } 593 } 594 595 db.setTransactionSuccessful(); 596 } finally { 597 db.endTransaction(); 598 } 599 } 600 601 /** 602 * Inserts prefixes of contact names to the prefix table. 603 * 604 * @param db Database pointer to the smartdial database. 605 * @param nameCursor Cursor pointing to the list of distinct updated contacts. 606 */ 607 @VisibleForTesting 608 void insertNamePrefixes(SQLiteDatabase db, Cursor nameCursor) { 609 final int columnIndexName = nameCursor.getColumnIndex(SmartDialDbColumns.DISPLAY_NAME_PRIMARY); 610 final int columnIndexContactId = nameCursor.getColumnIndex(SmartDialDbColumns.CONTACT_ID); 611 612 db.beginTransaction(); 613 try { 614 final String sqlInsert = 615 "INSERT INTO " 616 + Tables.PREFIX_TABLE 617 + " (" 618 + PrefixColumns.CONTACT_ID 619 + ", " 620 + PrefixColumns.PREFIX 621 + ") " 622 + " VALUES (?, ?)"; 623 final SQLiteStatement insert = db.compileStatement(sqlInsert); 624 625 while (nameCursor.moveToNext()) { 626 if (nameCursor.isNull(columnIndexContactId)) { 627 LogUtil.i( 628 "DialerDatabaseHelper.insertNamePrefixes", 629 "contact_id column null. Row was deleted during iteration, skipping"); 630 continue; 631 } 632 633 /** Computes a list of prefixes of a given contact name. */ 634 final ArrayList<String> namePrefixes = 635 SmartDialPrefix.generateNamePrefixes(context, nameCursor.getString(columnIndexName)); 636 637 for (String namePrefix : namePrefixes) { 638 insert.bindLong(1, nameCursor.getLong(columnIndexContactId)); 639 insert.bindString(2, namePrefix); 640 insert.executeInsert(); 641 insert.clearBindings(); 642 } 643 } 644 645 db.setTransactionSuccessful(); 646 } finally { 647 db.endTransaction(); 648 } 649 } 650 651 /** 652 * Updates the smart dial and prefix database. This method queries the Delta API to get changed 653 * contacts since last update, and updates the records in smartdial database and prefix database 654 * accordingly. It also queries the deleted contact database to remove newly deleted contacts 655 * since last update. 656 * 657 * @param forceUpdate If set to true, update the database by reloading all contacts. 658 */ 659 @WorkerThread 660 public synchronized void updateSmartDialDatabase(boolean forceUpdate) { 661 LogUtil.enterBlock("DialerDatabaseHelper.updateSmartDialDatabase"); 662 663 final SQLiteDatabase db = getWritableDatabase(); 664 665 LogUtil.v("DialerDatabaseHelper.updateSmartDialDatabase", "starting to update database"); 666 final StopWatch stopWatch = DEBUG ? StopWatch.start("Updating databases") : null; 667 668 /** Gets the last update time on the database. */ 669 final SharedPreferences databaseLastUpdateSharedPref = 670 context.getSharedPreferences(DATABASE_LAST_CREATED_SHARED_PREF, Context.MODE_PRIVATE); 671 672 long defaultLastUpdateMillis = 673 ConfigProviderBindings.get(context).getLong(DEFAULT_LAST_UPDATED_CONFIG_KEY, 0); 674 675 long sharedPrefLastUpdateMillis = 676 databaseLastUpdateSharedPref.getLong(LAST_UPDATED_MILLIS, defaultLastUpdateMillis); 677 678 final String lastUpdateMillis = String.valueOf(forceUpdate ? 0 : sharedPrefLastUpdateMillis); 679 680 LogUtil.i( 681 "DialerDatabaseHelper.updateSmartDialDatabase", "last updated at %s", lastUpdateMillis); 682 683 /** Sets the time after querying the database as the current update time. */ 684 final Long currentMillis = System.currentTimeMillis(); 685 686 if (DEBUG) { 687 stopWatch.lap("Queried the Contacts database"); 688 } 689 690 /** Removes contacts that have been deleted. */ 691 removeDeletedContacts(db, lastUpdateMillis); 692 removePotentiallyCorruptedContacts(db, lastUpdateMillis); 693 694 if (DEBUG) { 695 stopWatch.lap("Finished deleting deleted entries"); 696 } 697 698 /** 699 * If the database did not exist before, jump through deletion as there is nothing to delete. 700 */ 701 if (!lastUpdateMillis.equals("0")) { 702 /** 703 * Removes contacts that have been updated. Updated contact information will be inserted 704 * later. Note that this has to use a separate result set from updatePhoneCursor, since it is 705 * possible for a contact to be updated (e.g. phone number deleted), but have no results show 706 * up in updatedPhoneCursor (since all of its phone numbers have been deleted). 707 */ 708 final Cursor updatedContactCursor = 709 context 710 .getContentResolver() 711 .query( 712 UpdatedContactQuery.URI, 713 UpdatedContactQuery.PROJECTION, 714 UpdatedContactQuery.SELECT_UPDATED_CLAUSE, 715 new String[] {lastUpdateMillis}, 716 null); 717 if (updatedContactCursor == null) { 718 LogUtil.e( 719 "DialerDatabaseHelper.updateSmartDialDatabase", 720 "smartDial query received null for cursor"); 721 return; 722 } 723 try { 724 removeUpdatedContacts(db, updatedContactCursor); 725 } finally { 726 updatedContactCursor.close(); 727 } 728 if (DEBUG) { 729 stopWatch.lap("Finished deleting entries belonging to updated contacts"); 730 } 731 } 732 733 /** 734 * Queries the contact database to get all phone numbers that have been updated since the last 735 * update time. 736 */ 737 final Cursor updatedPhoneCursor = 738 context 739 .getContentResolver() 740 .query( 741 PhoneQuery.URI, 742 PhoneQuery.PROJECTION, 743 PhoneQuery.SELECTION, 744 new String[] {lastUpdateMillis}, 745 null); 746 if (updatedPhoneCursor == null) { 747 LogUtil.e( 748 "DialerDatabaseHelper.updateSmartDialDatabase", 749 "smartDial query received null for cursor"); 750 return; 751 } 752 753 try { 754 /** Inserts recently updated phone numbers to the smartdial database. */ 755 insertUpdatedContactsAndNumberPrefix(db, updatedPhoneCursor, currentMillis); 756 if (DEBUG) { 757 stopWatch.lap("Finished building the smart dial table"); 758 } 759 } finally { 760 updatedPhoneCursor.close(); 761 } 762 763 /** 764 * Gets a list of distinct contacts which have been updated, and adds the name prefixes of these 765 * contacts to the prefix table. 766 */ 767 final Cursor nameCursor = 768 db.rawQuery( 769 "SELECT DISTINCT " 770 + SmartDialDbColumns.DISPLAY_NAME_PRIMARY 771 + ", " 772 + SmartDialDbColumns.CONTACT_ID 773 + " FROM " 774 + Tables.SMARTDIAL_TABLE 775 + " WHERE " 776 + SmartDialDbColumns.LAST_SMARTDIAL_UPDATE_TIME 777 + " = " 778 + currentMillis, 779 new String[] {}); 780 if (nameCursor != null) { 781 try { 782 if (DEBUG) { 783 stopWatch.lap("Queried the smart dial table for contact names"); 784 } 785 786 /** Inserts prefixes of names into the prefix table. */ 787 insertNamePrefixes(db, nameCursor); 788 if (DEBUG) { 789 stopWatch.lap("Finished building the name prefix table"); 790 } 791 } finally { 792 nameCursor.close(); 793 } 794 } 795 796 /** Creates index on contact_id for fast JOIN operation. */ 797 db.execSQL( 798 "CREATE INDEX IF NOT EXISTS smartdial_contact_id_index ON " 799 + Tables.SMARTDIAL_TABLE 800 + " (" 801 + SmartDialDbColumns.CONTACT_ID 802 + ");"); 803 /** Creates index on last_smartdial_update_time for fast SELECT operation. */ 804 db.execSQL( 805 "CREATE INDEX IF NOT EXISTS smartdial_last_update_index ON " 806 + Tables.SMARTDIAL_TABLE 807 + " (" 808 + SmartDialDbColumns.LAST_SMARTDIAL_UPDATE_TIME 809 + ");"); 810 /** Creates index on sorting fields for fast sort operation. */ 811 db.execSQL( 812 "CREATE INDEX IF NOT EXISTS smartdial_sort_index ON " 813 + Tables.SMARTDIAL_TABLE 814 + " (" 815 + SmartDialDbColumns.STARRED 816 + ", " 817 + SmartDialDbColumns.IS_SUPER_PRIMARY 818 + ", " 819 + SmartDialDbColumns.LAST_TIME_USED 820 + ", " 821 + SmartDialDbColumns.TIMES_USED 822 + ", " 823 + SmartDialDbColumns.IN_VISIBLE_GROUP 824 + ", " 825 + SmartDialDbColumns.DISPLAY_NAME_PRIMARY 826 + ", " 827 + SmartDialDbColumns.CONTACT_ID 828 + ", " 829 + SmartDialDbColumns.IS_PRIMARY 830 + ");"); 831 /** Creates index on prefix for fast SELECT operation. */ 832 db.execSQL( 833 "CREATE INDEX IF NOT EXISTS nameprefix_index ON " 834 + Tables.PREFIX_TABLE 835 + " (" 836 + PrefixColumns.PREFIX 837 + ");"); 838 /** Creates index on contact_id for fast JOIN operation. */ 839 db.execSQL( 840 "CREATE INDEX IF NOT EXISTS nameprefix_contact_id_index ON " 841 + Tables.PREFIX_TABLE 842 + " (" 843 + PrefixColumns.CONTACT_ID 844 + ");"); 845 846 if (DEBUG) { 847 stopWatch.lap(TAG + "Finished recreating index"); 848 } 849 850 /** Updates the database index statistics. */ 851 db.execSQL("ANALYZE " + Tables.SMARTDIAL_TABLE); 852 db.execSQL("ANALYZE " + Tables.PREFIX_TABLE); 853 db.execSQL("ANALYZE smartdial_contact_id_index"); 854 db.execSQL("ANALYZE smartdial_last_update_index"); 855 db.execSQL("ANALYZE nameprefix_index"); 856 db.execSQL("ANALYZE nameprefix_contact_id_index"); 857 if (DEBUG) { 858 stopWatch.stopAndLog(TAG + "Finished updating index stats", 0); 859 } 860 861 final SharedPreferences.Editor editor = databaseLastUpdateSharedPref.edit(); 862 editor.putLong(LAST_UPDATED_MILLIS, currentMillis); 863 editor.apply(); 864 865 LogUtil.i("DialerDatabaseHelper.updateSmartDialDatabase", "broadcasting smart dial update"); 866 867 // Notify content observers that smart dial database has been updated. 868 Intent intent = new Intent(ACTION_SMART_DIAL_UPDATED); 869 intent.setPackage(context.getPackageName()); 870 context.sendBroadcast(intent); 871 } 872 873 /** 874 * Returns a list of candidate contacts where the query is a prefix of the dialpad index of the 875 * contact's name or phone number. 876 * 877 * @param query The prefix of a contact's dialpad index. 878 * @return A list of top candidate contacts that will be suggested to user to match their input. 879 */ 880 @WorkerThread 881 public synchronized ArrayList<ContactNumber> getLooseMatches( 882 String query, SmartDialNameMatcher nameMatcher) { 883 final SQLiteDatabase db = getReadableDatabase(); 884 885 /** Uses SQL query wildcard '%' to represent prefix matching. */ 886 final String looseQuery = query + "%"; 887 888 final ArrayList<ContactNumber> result = new ArrayList<>(); 889 890 final StopWatch stopWatch = DEBUG ? StopWatch.start(":Name Prefix query") : null; 891 892 final String currentTimeStamp = Long.toString(System.currentTimeMillis()); 893 894 /** Queries the database to find contacts that have an index matching the query prefix. */ 895 final Cursor cursor = 896 db.rawQuery( 897 "SELECT " 898 + SmartDialDbColumns.DATA_ID 899 + ", " 900 + SmartDialDbColumns.DISPLAY_NAME_PRIMARY 901 + ", " 902 + SmartDialDbColumns.PHOTO_ID 903 + ", " 904 + SmartDialDbColumns.NUMBER 905 + ", " 906 + SmartDialDbColumns.CONTACT_ID 907 + ", " 908 + SmartDialDbColumns.LOOKUP_KEY 909 + ", " 910 + SmartDialDbColumns.CARRIER_PRESENCE 911 + " FROM " 912 + Tables.SMARTDIAL_TABLE 913 + " WHERE " 914 + SmartDialDbColumns.CONTACT_ID 915 + " IN " 916 + " (SELECT " 917 + PrefixColumns.CONTACT_ID 918 + " FROM " 919 + Tables.PREFIX_TABLE 920 + " WHERE " 921 + Tables.PREFIX_TABLE 922 + "." 923 + PrefixColumns.PREFIX 924 + " LIKE '" 925 + looseQuery 926 + "')" 927 + " ORDER BY " 928 + SmartDialSortingOrder.SORT_ORDER, 929 new String[] {currentTimeStamp}); 930 if (cursor == null) { 931 return result; 932 } 933 try { 934 if (DEBUG) { 935 stopWatch.lap("Prefix query completed"); 936 } 937 938 /** Gets the column ID from the cursor. */ 939 final int columnDataId = 0; 940 final int columnDisplayNamePrimary = 1; 941 final int columnPhotoId = 2; 942 final int columnNumber = 3; 943 final int columnId = 4; 944 final int columnLookupKey = 5; 945 final int columnCarrierPresence = 6; 946 if (DEBUG) { 947 stopWatch.lap("Found column IDs"); 948 } 949 950 final Set<ContactMatch> duplicates = new HashSet<>(); 951 int counter = 0; 952 if (DEBUG) { 953 stopWatch.lap("Moved cursor to start"); 954 } 955 /** Iterates the cursor to find top contact suggestions without duplication. */ 956 while ((cursor.moveToNext()) && (counter < MAX_ENTRIES)) { 957 final long dataID = cursor.getLong(columnDataId); 958 final String displayName = cursor.getString(columnDisplayNamePrimary); 959 final String phoneNumber = cursor.getString(columnNumber); 960 final long id = cursor.getLong(columnId); 961 final long photoId = cursor.getLong(columnPhotoId); 962 final String lookupKey = cursor.getString(columnLookupKey); 963 final int carrierPresence = cursor.getInt(columnCarrierPresence); 964 965 /** 966 * If a contact already exists and another phone number of the contact is being processed, 967 * skip the second instance. 968 */ 969 final ContactMatch contactMatch = new ContactMatch(lookupKey, id); 970 if (duplicates.contains(contactMatch)) { 971 continue; 972 } 973 974 /** 975 * If the contact has either the name or number that matches the query, add to the result. 976 */ 977 final boolean nameMatches = nameMatcher.matches(context, displayName); 978 final boolean numberMatches = 979 (nameMatcher.matchesNumber(context, phoneNumber, query) != null); 980 if (nameMatches || numberMatches) { 981 /** If a contact has not been added, add it to the result and the hash set. */ 982 duplicates.add(contactMatch); 983 result.add( 984 new ContactNumber( 985 id, dataID, displayName, phoneNumber, lookupKey, photoId, carrierPresence)); 986 counter++; 987 if (DEBUG) { 988 stopWatch.lap("Added one result: Name: " + displayName); 989 } 990 } 991 } 992 993 if (DEBUG) { 994 stopWatch.stopAndLog(TAG + "Finished loading cursor", 0); 995 } 996 } finally { 997 cursor.close(); 998 } 999 return result; 1000 } 1001 1002 public interface Tables { 1003 1004 /** Saves a list of numbers to be blocked. */ 1005 String FILTERED_NUMBER_TABLE = "filtered_numbers_table"; 1006 /** Saves the necessary smart dial information of all contacts. */ 1007 String SMARTDIAL_TABLE = "smartdial_table"; 1008 /** Saves all possible prefixes to refer to a contacts. */ 1009 String PREFIX_TABLE = "prefix_table"; 1010 /** Saves all archived voicemail information. */ 1011 String VOICEMAIL_ARCHIVE_TABLE = "voicemail_archive_table"; 1012 /** Database properties for internal use */ 1013 String PROPERTIES = "properties"; 1014 } 1015 1016 public interface SmartDialDbColumns { 1017 1018 String _ID = "id"; 1019 String DATA_ID = "data_id"; 1020 String NUMBER = "phone_number"; 1021 String CONTACT_ID = "contact_id"; 1022 String LOOKUP_KEY = "lookup_key"; 1023 String DISPLAY_NAME_PRIMARY = "display_name"; 1024 String PHOTO_ID = "photo_id"; 1025 String LAST_TIME_USED = "last_time_used"; 1026 String TIMES_USED = "times_used"; 1027 String STARRED = "starred"; 1028 String IS_SUPER_PRIMARY = "is_super_primary"; 1029 String IN_VISIBLE_GROUP = "in_visible_group"; 1030 String IS_PRIMARY = "is_primary"; 1031 String CARRIER_PRESENCE = "carrier_presence"; 1032 String LAST_SMARTDIAL_UPDATE_TIME = "last_smartdial_update_time"; 1033 } 1034 1035 public interface PrefixColumns extends BaseColumns { 1036 1037 String PREFIX = "prefix"; 1038 String CONTACT_ID = "contact_id"; 1039 } 1040 1041 public interface PropertiesColumns { 1042 1043 String PROPERTY_KEY = "property_key"; 1044 String PROPERTY_VALUE = "property_value"; 1045 } 1046 1047 /** Query options for querying the contact database. */ 1048 public interface PhoneQuery { 1049 1050 Uri URI = 1051 Phone.CONTENT_URI 1052 .buildUpon() 1053 .appendQueryParameter( 1054 ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Directory.DEFAULT)) 1055 .appendQueryParameter(ContactsContract.REMOVE_DUPLICATE_ENTRIES, "true") 1056 .build(); 1057 1058 String[] PROJECTION = 1059 new String[] { 1060 Phone._ID, // 0 1061 Phone.TYPE, // 1 1062 Phone.LABEL, // 2 1063 Phone.NUMBER, // 3 1064 Phone.CONTACT_ID, // 4 1065 Phone.LOOKUP_KEY, // 5 1066 Phone.DISPLAY_NAME_PRIMARY, // 6 1067 Phone.PHOTO_ID, // 7 1068 Data.LAST_TIME_USED, // 8 1069 Data.TIMES_USED, // 9 1070 Contacts.STARRED, // 10 1071 Data.IS_SUPER_PRIMARY, // 11 1072 Contacts.IN_VISIBLE_GROUP, // 12 1073 Data.IS_PRIMARY, // 13 1074 Data.CARRIER_PRESENCE, // 14 1075 }; 1076 1077 int PHONE_ID = 0; 1078 int PHONE_TYPE = 1; 1079 int PHONE_LABEL = 2; 1080 int PHONE_NUMBER = 3; 1081 int PHONE_CONTACT_ID = 4; 1082 int PHONE_LOOKUP_KEY = 5; 1083 int PHONE_DISPLAY_NAME = 6; 1084 int PHONE_PHOTO_ID = 7; 1085 int PHONE_LAST_TIME_USED = 8; 1086 int PHONE_TIMES_USED = 9; 1087 int PHONE_STARRED = 10; 1088 int PHONE_IS_SUPER_PRIMARY = 11; 1089 int PHONE_IN_VISIBLE_GROUP = 12; 1090 int PHONE_IS_PRIMARY = 13; 1091 int PHONE_CARRIER_PRESENCE = 14; 1092 1093 /** Selects only rows that have been updated after a certain time stamp. */ 1094 String SELECT_UPDATED_CLAUSE = Phone.CONTACT_LAST_UPDATED_TIMESTAMP + " > ?"; 1095 1096 /** 1097 * Ignores contacts that have an unreasonably long lookup key. These are likely to be the result 1098 * of multiple (> 50) merged raw contacts, and are likely to cause OutOfMemoryExceptions within 1099 * SQLite, or cause memory allocation problems later on when iterating through the cursor set 1100 * (see a bug) 1101 */ 1102 String SELECT_IGNORE_LOOKUP_KEY_TOO_LONG_CLAUSE = "length(" + Phone.LOOKUP_KEY + ") < 1000"; 1103 1104 String SELECTION = SELECT_UPDATED_CLAUSE + " AND " + SELECT_IGNORE_LOOKUP_KEY_TOO_LONG_CLAUSE; 1105 } 1106 1107 /** 1108 * Query for all contacts that have been updated since the last time the smart dial database was 1109 * updated. 1110 */ 1111 public interface UpdatedContactQuery { 1112 1113 Uri URI = ContactsContract.Contacts.CONTENT_URI; 1114 1115 String[] PROJECTION = 1116 new String[] { 1117 ContactsContract.Contacts._ID // 0 1118 }; 1119 1120 int UPDATED_CONTACT_ID = 0; 1121 1122 String SELECT_UPDATED_CLAUSE = 1123 ContactsContract.Contacts.CONTACT_LAST_UPDATED_TIMESTAMP + " > ?"; 1124 } 1125 1126 /** Query options for querying the deleted contact database. */ 1127 public interface DeleteContactQuery { 1128 1129 Uri URI = ContactsContract.DeletedContacts.CONTENT_URI; 1130 1131 String[] PROJECTION = 1132 new String[] { 1133 ContactsContract.DeletedContacts.CONTACT_ID, // 0 1134 ContactsContract.DeletedContacts.CONTACT_DELETED_TIMESTAMP, // 1 1135 }; 1136 1137 int DELETED_CONTACT_ID = 0; 1138 int DELETED_TIMESTAMP = 1; 1139 1140 /** Selects only rows that have been deleted after a certain time stamp. */ 1141 String SELECT_UPDATED_CLAUSE = 1142 ContactsContract.DeletedContacts.CONTACT_DELETED_TIMESTAMP + " > ?"; 1143 } 1144 1145 /** 1146 * Gets the sorting order for the smartdial table. This computes a SQL "ORDER BY" argument by 1147 * composing contact status and recent contact details together. 1148 */ 1149 private interface SmartDialSortingOrder { 1150 1151 /** Current contacts - those contacted within the last 3 days (in milliseconds) */ 1152 long LAST_TIME_USED_CURRENT_MS = 3L * 24 * 60 * 60 * 1000; 1153 /** Recent contacts - those contacted within the last 30 days (in milliseconds) */ 1154 long LAST_TIME_USED_RECENT_MS = 30L * 24 * 60 * 60 * 1000; 1155 1156 /** Time since last contact. */ 1157 String TIME_SINCE_LAST_USED_MS = 1158 "( ?1 - " + Tables.SMARTDIAL_TABLE + "." + SmartDialDbColumns.LAST_TIME_USED + ")"; 1159 1160 /** 1161 * Contacts that have been used in the past 3 days rank higher than contacts that have been used 1162 * in the past 30 days, which rank higher than contacts that have not been used in recent 30 1163 * days. 1164 */ 1165 String SORT_BY_DATA_USAGE = 1166 "(CASE WHEN " 1167 + TIME_SINCE_LAST_USED_MS 1168 + " < " 1169 + LAST_TIME_USED_CURRENT_MS 1170 + " THEN 0 " 1171 + " WHEN " 1172 + TIME_SINCE_LAST_USED_MS 1173 + " < " 1174 + LAST_TIME_USED_RECENT_MS 1175 + " THEN 1 " 1176 + " ELSE 2 END)"; 1177 1178 /** 1179 * This sort order is similar to that used by the ContactsProvider when returning a list of 1180 * frequently called contacts. 1181 */ 1182 String SORT_ORDER = 1183 Tables.SMARTDIAL_TABLE 1184 + "." 1185 + SmartDialDbColumns.STARRED 1186 + " DESC, " 1187 + Tables.SMARTDIAL_TABLE 1188 + "." 1189 + SmartDialDbColumns.IS_SUPER_PRIMARY 1190 + " DESC, " 1191 + SORT_BY_DATA_USAGE 1192 + ", " 1193 + Tables.SMARTDIAL_TABLE 1194 + "." 1195 + SmartDialDbColumns.TIMES_USED 1196 + " DESC, " 1197 + Tables.SMARTDIAL_TABLE 1198 + "." 1199 + SmartDialDbColumns.IN_VISIBLE_GROUP 1200 + " DESC, " 1201 + Tables.SMARTDIAL_TABLE 1202 + "." 1203 + SmartDialDbColumns.DISPLAY_NAME_PRIMARY 1204 + ", " 1205 + Tables.SMARTDIAL_TABLE 1206 + "." 1207 + SmartDialDbColumns.CONTACT_ID 1208 + ", " 1209 + Tables.SMARTDIAL_TABLE 1210 + "." 1211 + SmartDialDbColumns.IS_PRIMARY 1212 + " DESC"; 1213 } 1214 1215 /** 1216 * Simple data format for a contact, containing only information needed for showing up in smart 1217 * dial interface. 1218 */ 1219 public static class ContactNumber { 1220 1221 public final long id; 1222 public final long dataId; 1223 public final String displayName; 1224 public final String phoneNumber; 1225 public final String lookupKey; 1226 public final long photoId; 1227 public final int carrierPresence; 1228 1229 public ContactNumber( 1230 long id, 1231 long dataID, 1232 String displayName, 1233 String phoneNumber, 1234 String lookupKey, 1235 long photoId, 1236 int carrierPresence) { 1237 this.dataId = dataID; 1238 this.id = id; 1239 this.displayName = displayName; 1240 this.phoneNumber = phoneNumber; 1241 this.lookupKey = lookupKey; 1242 this.photoId = photoId; 1243 this.carrierPresence = carrierPresence; 1244 } 1245 1246 @Override 1247 public int hashCode() { 1248 return Objects.hash( 1249 id, dataId, displayName, phoneNumber, lookupKey, photoId, carrierPresence); 1250 } 1251 1252 @Override 1253 public boolean equals(Object object) { 1254 if (this == object) { 1255 return true; 1256 } 1257 if (object instanceof ContactNumber) { 1258 final ContactNumber that = (ContactNumber) object; 1259 return Objects.equals(this.id, that.id) 1260 && Objects.equals(this.dataId, that.dataId) 1261 && Objects.equals(this.displayName, that.displayName) 1262 && Objects.equals(this.phoneNumber, that.phoneNumber) 1263 && Objects.equals(this.lookupKey, that.lookupKey) 1264 && Objects.equals(this.photoId, that.photoId) 1265 && Objects.equals(this.carrierPresence, that.carrierPresence); 1266 } 1267 return false; 1268 } 1269 } 1270 1271 /** Data format for finding duplicated contacts. */ 1272 private static class ContactMatch { 1273 1274 private final String lookupKey; 1275 private final long id; 1276 1277 public ContactMatch(String lookupKey, long id) { 1278 this.lookupKey = lookupKey; 1279 this.id = id; 1280 } 1281 1282 @Override 1283 public int hashCode() { 1284 return Objects.hash(lookupKey, id); 1285 } 1286 1287 @Override 1288 public boolean equals(Object object) { 1289 if (this == object) { 1290 return true; 1291 } 1292 if (object instanceof ContactMatch) { 1293 final ContactMatch that = (ContactMatch) object; 1294 return Objects.equals(this.lookupKey, that.lookupKey) && Objects.equals(this.id, that.id); 1295 } 1296 return false; 1297 } 1298 } 1299 1300 private class UpdateSmartDialWorker implements Worker<Boolean, Void> { 1301 1302 @Nullable 1303 @Override 1304 public Void doInBackground(Boolean forceUpdate) throws Throwable { 1305 updateSmartDialDatabase(forceUpdate); 1306 return null; 1307 } 1308 } 1309 } 1310