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