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