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