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