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