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