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