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 DialerDatabaseHelper(Context context, String databaseName, int dbVersion)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 setIsTestInstance(boolean isTestInstance)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 onCreate(SQLiteDatabase db)105 public void onCreate(SQLiteDatabase db) { 106 setupTables(db); 107 } 108 setupTables(SQLiteDatabase db)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 dropTables(SQLiteDatabase db)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 onUpgrade(SQLiteDatabase db, int oldNumber, int newNumber)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 upgradeToVersion8(SQLiteDatabase db)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. */ setProperty(String key, String value)280 public void setProperty(String key, String value) { 281 setProperty(getWritableDatabase(), key, value); 282 } 283 setProperty(SQLiteDatabase db, String key, String value)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. */ getProperty(String key, String defaultValue)292 public String getProperty(String key, String defaultValue) { 293 return getProperty(getReadableDatabase(), key, defaultValue); 294 } 295 getProperty(SQLiteDatabase db, String key, String defaultValue)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 getPropertyAsInt(SQLiteDatabase db, String key, int defaultValue)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 resetSmartDialLastUpdatedTime()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 */ startSmartDialUpdateThread(boolean forceUpdate)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 */ removeDeletedContacts(SQLiteDatabase db, String lastUpdatedTimeMillis)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 getDeletedContactCursor(String lastUpdateMillis)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 */ removePotentiallyCorruptedContacts(SQLiteDatabase db, String last_update_time)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 removeUpdatedContacts(SQLiteDatabase db, Cursor updatedContactCursor)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 insertUpdatedContactsAndNumberPrefix( SQLiteDatabase db, Cursor updatedContactCursor, Long currentMillis)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 insertNamePrefixes(SQLiteDatabase db, Cursor nameCursor)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 updateSmartDialDatabase(boolean forceUpdate)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 getLooseMatches( String query, SmartDialNameMatcher nameMatcher)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 ContactNumber( long id, long dataID, String displayName, String phoneNumber, String lookupKey, long photoId, int carrierPresence)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 hashCode()1247 public int hashCode() { 1248 return Objects.hash( 1249 id, dataId, displayName, phoneNumber, lookupKey, photoId, carrierPresence); 1250 } 1251 1252 @Override equals(Object object)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 ContactMatch(String lookupKey, long id)1277 public ContactMatch(String lookupKey, long id) { 1278 this.lookupKey = lookupKey; 1279 this.id = id; 1280 } 1281 1282 @Override hashCode()1283 public int hashCode() { 1284 return Objects.hash(lookupKey, id); 1285 } 1286 1287 @Override equals(Object object)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 doInBackground(Boolean forceUpdate)1304 public Void doInBackground(Boolean forceUpdate) throws Throwable { 1305 updateSmartDialDatabase(forceUpdate); 1306 return null; 1307 } 1308 } 1309 } 1310