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