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