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