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