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