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