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