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 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 291 public int hashCode() { 292 return Objects.hashCode(id, dataId, displayName, phoneNumber, lookupKey, photoId, 293 carrierPresence); 294 } 295 296 @Override 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 322 public ContactMatch(String lookupKey, long id) { 323 this.lookupKey = lookupKey; 324 this.id = id; 325 } 326 327 @Override 328 public int hashCode() { 329 return Objects.hashCode(lookupKey, id); 330 } 331 332 @Override 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 */ 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 367 static DialerDatabaseHelper getNewInstanceForTest(Context context) { 368 return new DialerDatabaseHelper(context, null, true); 369 } 370 371 protected DialerDatabaseHelper(Context context, String databaseName, boolean isTestInstance) { 372 this(context, databaseName, DATABASE_VERSION); 373 mIsTestInstance = isTestInstance; 374 } 375 376 protected DialerDatabaseHelper(Context context, String databaseName) { 377 this(context, databaseName, DATABASE_VERSION); 378 } 379 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 391 public void onCreate(SQLiteDatabase db) { 392 setupTables(db); 393 } 394 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 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 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 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 */ 516 public void setProperty(String key, String value) { 517 setProperty(getWritableDatabase(), key, value); 518 } 519 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 */ 530 public String getProperty(String key, String defaultValue) { 531 return getProperty(getReadableDatabase(), key, defaultValue); 532 } 533 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 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 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 */ 576 public void startSmartDialUpdateThread() { 577 if (PermissionsUtil.hasContactsPermissions(mContext)) { 578 new SmartDialUpdateAsyncTask().execute(); 579 } 580 } 581 582 private class SmartDialUpdateAsyncTask extends AsyncTask { 583 @Override 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 593 protected void onCancelled() { 594 if (DEBUG) { 595 Log.v(TAG, "Updating Cancelled"); 596 } 597 super.onCancelled(); 598 } 599 600 @Override 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 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 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 */ 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 */ 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 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 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 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 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 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 */ 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 */ 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