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   KitKat
     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             String value = null;
    445             final Cursor cursor = db.query(Tables.PROPERTIES,
    446                     new String[] {PropertiesColumns.PROPERTY_VALUE},
    447                             PropertiesColumns.PROPERTY_KEY + "=?",
    448                     new String[] {key}, null, null, null);
    449             if (cursor != null) {
    450                 try {
    451                     if (cursor.moveToFirst()) {
    452                         value = cursor.getString(0);
    453                     }
    454                 } finally {
    455                     cursor.close();
    456                 }
    457             }
    458             return value != null ? value : defaultValue;
    459         } catch (SQLiteException e) {
    460             return defaultValue;
    461         }
    462     }
    463 
    464     public int getPropertyAsInt(SQLiteDatabase db, String key, int defaultValue) {
    465         final String stored = getProperty(db, key, "");
    466         try {
    467             return Integer.parseInt(stored);
    468         } catch (NumberFormatException e) {
    469             return defaultValue;
    470         }
    471     }
    472 
    473     private void resetSmartDialLastUpdatedTime() {
    474         final SharedPreferences databaseLastUpdateSharedPref = mContext.getSharedPreferences(
    475                 DATABASE_LAST_CREATED_SHARED_PREF, Context.MODE_PRIVATE);
    476         final SharedPreferences.Editor editor = databaseLastUpdateSharedPref.edit();
    477         editor.putLong(LAST_UPDATED_MILLIS, 0);
    478         editor.commit();
    479     }
    480 
    481     /**
    482      * Starts the database upgrade process in the background.
    483      */
    484     public void startSmartDialUpdateThread() {
    485         new SmartDialUpdateAsyncTask().execute();
    486     }
    487 
    488     private class SmartDialUpdateAsyncTask extends AsyncTask {
    489         @Override
    490         protected Object doInBackground(Object[] objects) {
    491             if (DEBUG) {
    492                 Log.v(TAG, "Updating database");
    493             }
    494             updateSmartDialDatabase();
    495             return null;
    496         }
    497 
    498         @Override
    499         protected void onCancelled() {
    500             if (DEBUG) {
    501                 Log.v(TAG, "Updating Cancelled");
    502             }
    503             super.onCancelled();
    504         }
    505 
    506         @Override
    507         protected void onPostExecute(Object o) {
    508             if (DEBUG) {
    509                 Log.v(TAG, "Updating Finished");
    510             }
    511             super.onPostExecute(o);
    512         }
    513     }
    514     /**
    515      * Removes rows in the smartdial database that matches the contacts that have been deleted
    516      * by other apps since last update.
    517      *
    518      * @param db Database pointer to the dialer database.
    519      * @param last_update_time Time stamp of last update on the smartdial database
    520      */
    521     private void removeDeletedContacts(SQLiteDatabase db, String last_update_time) {
    522         final Cursor deletedContactCursor = mContext.getContentResolver().query(
    523                 DeleteContactQuery.URI,
    524                 DeleteContactQuery.PROJECTION,
    525                 DeleteContactQuery.SELECT_UPDATED_CLAUSE,
    526                 new String[] {last_update_time}, null);
    527         if (deletedContactCursor == null) {
    528             return;
    529         }
    530 
    531         db.beginTransaction();
    532         try {
    533             while (deletedContactCursor.moveToNext()) {
    534                 final Long deleteContactId =
    535                         deletedContactCursor.getLong(DeleteContactQuery.DELETED_CONTACT_ID);
    536                 db.delete(Tables.SMARTDIAL_TABLE,
    537                         SmartDialDbColumns.CONTACT_ID + "=" + deleteContactId, null);
    538                 db.delete(Tables.PREFIX_TABLE,
    539                         PrefixColumns.CONTACT_ID + "=" + deleteContactId, null);
    540             }
    541 
    542             db.setTransactionSuccessful();
    543         } finally {
    544             deletedContactCursor.close();
    545             db.endTransaction();
    546         }
    547     }
    548 
    549     /**
    550      * Removes potentially corrupted entries in the database. These contacts may be added before
    551      * the previous instance of the dialer was destroyed for some reason. For data integrity, we
    552      * delete all of them.
    553 
    554      * @param db Database pointer to the dialer database.
    555      * @param last_update_time Time stamp of last successful update of the dialer database.
    556      */
    557     private void removePotentiallyCorruptedContacts(SQLiteDatabase db, String last_update_time) {
    558         db.delete(Tables.PREFIX_TABLE,
    559                 PrefixColumns.CONTACT_ID + " IN " +
    560                 "(SELECT " + SmartDialDbColumns.CONTACT_ID + " FROM " + Tables.SMARTDIAL_TABLE +
    561                 " WHERE " + SmartDialDbColumns.LAST_SMARTDIAL_UPDATE_TIME + " > " +
    562                 last_update_time + ")",
    563                 null);
    564         db.delete(Tables.SMARTDIAL_TABLE,
    565                 SmartDialDbColumns.LAST_SMARTDIAL_UPDATE_TIME + " > " + last_update_time, null);
    566     }
    567 
    568     /**
    569      * Removes all entries in the smartdial contact database.
    570      */
    571     @VisibleForTesting
    572     void removeAllContacts(SQLiteDatabase db) {
    573         db.delete(Tables.SMARTDIAL_TABLE, null, null);
    574         db.delete(Tables.PREFIX_TABLE, null, null);
    575     }
    576 
    577     /**
    578      * Counts number of rows of the prefix table.
    579      */
    580     @VisibleForTesting
    581     int countPrefixTableRows(SQLiteDatabase db) {
    582         return (int)DatabaseUtils.longForQuery(db, "SELECT COUNT(1) FROM " + Tables.PREFIX_TABLE,
    583                 null);
    584     }
    585 
    586     /**
    587      * Removes rows in the smartdial database that matches updated contacts.
    588      *
    589      * @param db Database pointer to the smartdial database
    590      * @param updatedContactCursor Cursor pointing to the list of recently updated contacts.
    591      */
    592     private void removeUpdatedContacts(SQLiteDatabase db, Cursor updatedContactCursor) {
    593         db.beginTransaction();
    594         try {
    595             while (updatedContactCursor.moveToNext()) {
    596                 final Long contactId = updatedContactCursor.getLong(PhoneQuery.PHONE_CONTACT_ID);
    597 
    598                 db.delete(Tables.SMARTDIAL_TABLE, SmartDialDbColumns.CONTACT_ID + "=" +
    599                         contactId, null);
    600                 db.delete(Tables.PREFIX_TABLE, PrefixColumns.CONTACT_ID + "=" +
    601                         contactId, null);
    602             }
    603 
    604             db.setTransactionSuccessful();
    605         } finally {
    606             db.endTransaction();
    607         }
    608     }
    609 
    610     /**
    611      * Inserts updated contacts as rows to the smartdial table.
    612      *
    613      * @param db Database pointer to the smartdial database.
    614      * @param updatedContactCursor Cursor pointing to the list of recently updated contacts.
    615      * @param currentMillis Current time to be recorded in the smartdial table as update timestamp.
    616      */
    617     @VisibleForTesting
    618     protected void insertUpdatedContactsAndNumberPrefix(SQLiteDatabase db,
    619             Cursor updatedContactCursor, Long currentMillis) {
    620         db.beginTransaction();
    621         try {
    622             final String sqlInsert = "INSERT INTO " + Tables.SMARTDIAL_TABLE + " (" +
    623                     SmartDialDbColumns.DATA_ID + ", " +
    624                     SmartDialDbColumns.NUMBER + ", " +
    625                     SmartDialDbColumns.CONTACT_ID + ", " +
    626                     SmartDialDbColumns.LOOKUP_KEY + ", " +
    627                     SmartDialDbColumns.DISPLAY_NAME_PRIMARY + ", " +
    628                     SmartDialDbColumns.PHOTO_ID + ", " +
    629                     SmartDialDbColumns.LAST_TIME_USED + ", " +
    630                     SmartDialDbColumns.TIMES_USED + ", " +
    631                     SmartDialDbColumns.STARRED + ", " +
    632                     SmartDialDbColumns.IS_SUPER_PRIMARY + ", " +
    633                     SmartDialDbColumns.IN_VISIBLE_GROUP+ ", " +
    634                     SmartDialDbColumns.IS_PRIMARY + ", " +
    635                     SmartDialDbColumns.LAST_SMARTDIAL_UPDATE_TIME + ") " +
    636                     " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
    637             final SQLiteStatement insert = db.compileStatement(sqlInsert);
    638 
    639             final String numberSqlInsert = "INSERT INTO " + Tables.PREFIX_TABLE + " (" +
    640                     PrefixColumns.CONTACT_ID + ", " +
    641                     PrefixColumns.PREFIX  + ") " +
    642                     " VALUES (?, ?)";
    643             final SQLiteStatement numberInsert = db.compileStatement(numberSqlInsert);
    644 
    645             updatedContactCursor.moveToPosition(-1);
    646             while (updatedContactCursor.moveToNext()) {
    647                 insert.clearBindings();
    648 
    649                 // Handle string columns which can possibly be null first. In the case of certain
    650                 // null columns (due to malformed rows possibly inserted by third-party apps
    651                 // or sync adapters), skip the phone number row.
    652                 final String number = updatedContactCursor.getString(PhoneQuery.PHONE_NUMBER);
    653                 if (TextUtils.isEmpty(number)) {
    654                     continue;
    655                 } else {
    656                     insert.bindString(2, number);
    657                 }
    658 
    659                 final String lookupKey = updatedContactCursor.getString(
    660                         PhoneQuery.PHONE_LOOKUP_KEY);
    661                 if (TextUtils.isEmpty(lookupKey)) {
    662                     continue;
    663                 } else {
    664                     insert.bindString(4, lookupKey);
    665                 }
    666 
    667                 final String displayName = updatedContactCursor.getString(
    668                         PhoneQuery.PHONE_DISPLAY_NAME);
    669                 if (displayName == null) {
    670                     insert.bindString(5, mContext.getResources().getString(R.string.missing_name));
    671                 } else {
    672                     insert.bindString(5, displayName);
    673                 }
    674                 insert.bindLong(1, updatedContactCursor.getLong(PhoneQuery.PHONE_ID));
    675                 insert.bindLong(3, updatedContactCursor.getLong(PhoneQuery.PHONE_CONTACT_ID));
    676                 insert.bindLong(6, updatedContactCursor.getLong(PhoneQuery.PHONE_PHOTO_ID));
    677                 insert.bindLong(7, updatedContactCursor.getLong(PhoneQuery.PHONE_LAST_TIME_USED));
    678                 insert.bindLong(8, updatedContactCursor.getInt(PhoneQuery.PHONE_TIMES_USED));
    679                 insert.bindLong(9, updatedContactCursor.getInt(PhoneQuery.PHONE_STARRED));
    680                 insert.bindLong(10, updatedContactCursor.getInt(PhoneQuery.PHONE_IS_SUPER_PRIMARY));
    681                 insert.bindLong(11, updatedContactCursor.getInt(PhoneQuery.PHONE_IN_VISIBLE_GROUP));
    682                 insert.bindLong(12, updatedContactCursor.getInt(PhoneQuery.PHONE_IS_PRIMARY));
    683                 insert.bindLong(13, currentMillis);
    684                 insert.executeInsert();
    685                 final String contactPhoneNumber =
    686                         updatedContactCursor.getString(PhoneQuery.PHONE_NUMBER);
    687                 final ArrayList<String> numberPrefixes =
    688                         SmartDialPrefix.parseToNumberTokens(contactPhoneNumber);
    689 
    690                 for (String numberPrefix : numberPrefixes) {
    691                     numberInsert.bindLong(1, updatedContactCursor.getLong(
    692                             PhoneQuery.PHONE_CONTACT_ID));
    693                     numberInsert.bindString(2, numberPrefix);
    694                     numberInsert.executeInsert();
    695                     numberInsert.clearBindings();
    696                 }
    697             }
    698 
    699             db.setTransactionSuccessful();
    700         } finally {
    701             db.endTransaction();
    702         }
    703     }
    704 
    705     /**
    706      * Inserts prefixes of contact names to the prefix table.
    707      *
    708      * @param db Database pointer to the smartdial database.
    709      * @param nameCursor Cursor pointing to the list of distinct updated contacts.
    710      */
    711     @VisibleForTesting
    712     void insertNamePrefixes(SQLiteDatabase db, Cursor nameCursor) {
    713         final int columnIndexName = nameCursor.getColumnIndex(
    714                 SmartDialDbColumns.DISPLAY_NAME_PRIMARY);
    715         final int columnIndexContactId = nameCursor.getColumnIndex(SmartDialDbColumns.CONTACT_ID);
    716 
    717         db.beginTransaction();
    718         try {
    719             final String sqlInsert = "INSERT INTO " + Tables.PREFIX_TABLE + " (" +
    720                     PrefixColumns.CONTACT_ID + ", " +
    721                     PrefixColumns.PREFIX  + ") " +
    722                     " VALUES (?, ?)";
    723             final SQLiteStatement insert = db.compileStatement(sqlInsert);
    724 
    725             while (nameCursor.moveToNext()) {
    726                 /** Computes a list of prefixes of a given contact name. */
    727                 final ArrayList<String> namePrefixes =
    728                         SmartDialPrefix.generateNamePrefixes(nameCursor.getString(columnIndexName));
    729 
    730                 for (String namePrefix : namePrefixes) {
    731                     insert.bindLong(1, nameCursor.getLong(columnIndexContactId));
    732                     insert.bindString(2, namePrefix);
    733                     insert.executeInsert();
    734                     insert.clearBindings();
    735                 }
    736             }
    737 
    738             db.setTransactionSuccessful();
    739         } finally {
    740             db.endTransaction();
    741         }
    742     }
    743 
    744     /**
    745      * Updates the smart dial and prefix database.
    746      * This method queries the Delta API to get changed contacts since last update, and updates the
    747      * records in smartdial database and prefix database accordingly.
    748      * It also queries the deleted contact database to remove newly deleted contacts since last
    749      * update.
    750      */
    751     public void updateSmartDialDatabase() {
    752         final SQLiteDatabase db = getWritableDatabase();
    753 
    754         synchronized(mLock) {
    755             if (DEBUG) {
    756                 Log.v(TAG, "Starting to update database");
    757             }
    758             final StopWatch stopWatch = DEBUG ? StopWatch.start("Updating databases") : null;
    759 
    760             /** Gets the last update time on the database. */
    761             final SharedPreferences databaseLastUpdateSharedPref = mContext.getSharedPreferences(
    762                     DATABASE_LAST_CREATED_SHARED_PREF, Context.MODE_PRIVATE);
    763             final String lastUpdateMillis = String.valueOf(
    764                     databaseLastUpdateSharedPref.getLong(LAST_UPDATED_MILLIS, 0));
    765 
    766             if (DEBUG) {
    767                 Log.v(TAG, "Last updated at " + lastUpdateMillis);
    768             }
    769             /** Queries the contact database to get contacts that have been updated since the last
    770              * update time.
    771              */
    772             final Cursor updatedContactCursor = mContext.getContentResolver().query(PhoneQuery.URI,
    773                     PhoneQuery.PROJECTION, PhoneQuery.SELECTION,
    774                     new String[]{lastUpdateMillis}, null);
    775             if (updatedContactCursor == null) {
    776                 if (DEBUG) {
    777                     Log.e(TAG, "SmartDial query received null for cursor");
    778                 }
    779                 return;
    780             }
    781 
    782             /** Sets the time after querying the database as the current update time. */
    783             final Long currentMillis = System.currentTimeMillis();
    784 
    785             try {
    786                 if (DEBUG) {
    787                     stopWatch.lap("Queried the Contacts database");
    788                 }
    789 
    790                 /** Prevents the app from reading the dialer database when updating. */
    791                 sInUpdate.getAndSet(true);
    792 
    793                 /** Removes contacts that have been deleted. */
    794                 removeDeletedContacts(db, lastUpdateMillis);
    795                 removePotentiallyCorruptedContacts(db, lastUpdateMillis);
    796 
    797                 if (DEBUG) {
    798                     stopWatch.lap("Finished deleting deleted entries");
    799                 }
    800 
    801                 /** If the database did not exist before, jump through deletion as there is nothing
    802                  * to delete.
    803                  */
    804                 if (!lastUpdateMillis.equals("0")) {
    805                     /** Removes contacts that have been updated. Updated contact information will be
    806                      * inserted later.
    807                      */
    808                     removeUpdatedContacts(db, updatedContactCursor);
    809                     if (DEBUG) {
    810                         stopWatch.lap("Finished deleting updated entries");
    811                     }
    812                 }
    813 
    814                 /** Inserts recently updated contacts to the smartdial database.*/
    815                 insertUpdatedContactsAndNumberPrefix(db, updatedContactCursor, currentMillis);
    816                 if (DEBUG) {
    817                     stopWatch.lap("Finished building the smart dial table");
    818                 }
    819             } finally {
    820                 /** Inserts prefixes of phone numbers into the prefix table.*/
    821                 updatedContactCursor.close();
    822             }
    823 
    824             /** Gets a list of distinct contacts which have been updated, and adds the name prefixes
    825              * of these contacts to the prefix table.
    826              */
    827             final Cursor nameCursor = db.rawQuery(
    828                     "SELECT DISTINCT " +
    829                     SmartDialDbColumns.DISPLAY_NAME_PRIMARY + ", " + SmartDialDbColumns.CONTACT_ID +
    830                     " FROM " + Tables.SMARTDIAL_TABLE +
    831                     " WHERE " + SmartDialDbColumns.LAST_SMARTDIAL_UPDATE_TIME +
    832                     " = " + Long.toString(currentMillis),
    833                     new String[] {});
    834             if (nameCursor != null) {
    835                 try {
    836                     if (DEBUG) {
    837                         stopWatch.lap("Queried the smart dial table for contact names");
    838                     }
    839 
    840                     /** Inserts prefixes of names into the prefix table.*/
    841                     insertNamePrefixes(db, nameCursor);
    842                     if (DEBUG) {
    843                         stopWatch.lap("Finished building the name prefix table");
    844                     }
    845                 } finally {
    846                     nameCursor.close();
    847                 }
    848             }
    849 
    850             /** Creates index on contact_id for fast JOIN operation. */
    851             db.execSQL("CREATE INDEX IF NOT EXISTS smartdial_contact_id_index ON " +
    852                     Tables.SMARTDIAL_TABLE + " (" + SmartDialDbColumns.CONTACT_ID  + ");");
    853             /** Creates index on last_smartdial_update_time for fast SELECT operation. */
    854             db.execSQL("CREATE INDEX IF NOT EXISTS smartdial_last_update_index ON " +
    855                     Tables.SMARTDIAL_TABLE + " (" +
    856                     SmartDialDbColumns.LAST_SMARTDIAL_UPDATE_TIME + ");");
    857             /** Creates index on sorting fields for fast sort operation. */
    858             db.execSQL("CREATE INDEX IF NOT EXISTS smartdial_sort_index ON " +
    859                     Tables.SMARTDIAL_TABLE + " (" +
    860                     SmartDialDbColumns.STARRED + ", " +
    861                     SmartDialDbColumns.IS_SUPER_PRIMARY + ", " +
    862                     SmartDialDbColumns.LAST_TIME_USED + ", " +
    863                     SmartDialDbColumns.TIMES_USED + ", " +
    864                     SmartDialDbColumns.IN_VISIBLE_GROUP +  ", " +
    865                     SmartDialDbColumns.DISPLAY_NAME_PRIMARY + ", " +
    866                     SmartDialDbColumns.CONTACT_ID + ", " +
    867                     SmartDialDbColumns.IS_PRIMARY +
    868                     ");");
    869             /** Creates index on prefix for fast SELECT operation. */
    870             db.execSQL("CREATE INDEX IF NOT EXISTS nameprefix_index ON " +
    871                     Tables.PREFIX_TABLE + " (" + PrefixColumns.PREFIX + ");");
    872             /** Creates index on contact_id for fast JOIN operation. */
    873             db.execSQL("CREATE INDEX IF NOT EXISTS nameprefix_contact_id_index ON " +
    874                     Tables.PREFIX_TABLE + " (" + PrefixColumns.CONTACT_ID + ");");
    875 
    876             if (DEBUG) {
    877                 stopWatch.lap(TAG + "Finished recreating index");
    878             }
    879 
    880             /** Updates the database index statistics.*/
    881             db.execSQL("ANALYZE " + Tables.SMARTDIAL_TABLE);
    882             db.execSQL("ANALYZE " + Tables.PREFIX_TABLE);
    883             db.execSQL("ANALYZE smartdial_contact_id_index");
    884             db.execSQL("ANALYZE smartdial_last_update_index");
    885             db.execSQL("ANALYZE nameprefix_index");
    886             db.execSQL("ANALYZE nameprefix_contact_id_index");
    887             if (DEBUG) {
    888                 stopWatch.stopAndLog(TAG + "Finished updating index stats", 0);
    889             }
    890 
    891             sInUpdate.getAndSet(false);
    892 
    893             final SharedPreferences.Editor editor = databaseLastUpdateSharedPref.edit();
    894             editor.putLong(LAST_UPDATED_MILLIS, currentMillis);
    895             editor.commit();
    896         }
    897     }
    898 
    899     /**
    900      * Returns a list of candidate contacts where the query is a prefix of the dialpad index of
    901      * the contact's name or phone number.
    902      *
    903      * @param query The prefix of a contact's dialpad index.
    904      * @return A list of top candidate contacts that will be suggested to user to match their input.
    905      */
    906     public ArrayList<ContactNumber>  getLooseMatches(String query,
    907             SmartDialNameMatcher nameMatcher) {
    908         final boolean inUpdate = sInUpdate.get();
    909         if (inUpdate) {
    910             return Lists.newArrayList();
    911         }
    912 
    913         final SQLiteDatabase db = getReadableDatabase();
    914 
    915         /** Uses SQL query wildcard '%' to represent prefix matching.*/
    916         final String looseQuery = query + "%";
    917 
    918         final ArrayList<ContactNumber> result = Lists.newArrayList();
    919 
    920         final StopWatch stopWatch = DEBUG ? StopWatch.start(":Name Prefix query") : null;
    921 
    922         final String currentTimeStamp = Long.toString(System.currentTimeMillis());
    923 
    924         /** Queries the database to find contacts that have an index matching the query prefix. */
    925         final Cursor cursor = db.rawQuery("SELECT " +
    926                 SmartDialDbColumns.DATA_ID + ", " +
    927                 SmartDialDbColumns.DISPLAY_NAME_PRIMARY + ", " +
    928                 SmartDialDbColumns.PHOTO_ID + ", " +
    929                 SmartDialDbColumns.NUMBER + ", " +
    930                 SmartDialDbColumns.CONTACT_ID + ", " +
    931                 SmartDialDbColumns.LOOKUP_KEY +
    932                 " FROM " + Tables.SMARTDIAL_TABLE + " WHERE " +
    933                 SmartDialDbColumns.CONTACT_ID + " IN " +
    934                     " (SELECT " + PrefixColumns.CONTACT_ID +
    935                     " FROM " + Tables.PREFIX_TABLE +
    936                     " WHERE " + Tables.PREFIX_TABLE + "." + PrefixColumns.PREFIX +
    937                     " LIKE '" + looseQuery + "')" +
    938                 " ORDER BY " + SmartDialSortingOrder.SORT_ORDER,
    939                 new String[] {currentTimeStamp});
    940         if (cursor == null) {
    941             return result;
    942         }
    943         try {
    944             if (DEBUG) {
    945                 stopWatch.lap("Prefix query completed");
    946             }
    947 
    948             /** Gets the column ID from the cursor.*/
    949             final int columnDataId = 0;
    950             final int columnDisplayNamePrimary = 1;
    951             final int columnPhotoId = 2;
    952             final int columnNumber = 3;
    953             final int columnId = 4;
    954             final int columnLookupKey = 5;
    955             if (DEBUG) {
    956                 stopWatch.lap("Found column IDs");
    957             }
    958 
    959             final Set<ContactMatch> duplicates = new HashSet<ContactMatch>();
    960             int counter = 0;
    961             if (DEBUG) {
    962                 stopWatch.lap("Moved cursor to start");
    963             }
    964             /** Iterates the cursor to find top contact suggestions without duplication.*/
    965             while ((cursor.moveToNext()) && (counter < MAX_ENTRIES)) {
    966                 final long dataID = cursor.getLong(columnDataId);
    967                 final String displayName = cursor.getString(columnDisplayNamePrimary);
    968                 final String phoneNumber = cursor.getString(columnNumber);
    969                 final long id = cursor.getLong(columnId);
    970                 final long photoId = cursor.getLong(columnPhotoId);
    971                 final String lookupKey = cursor.getString(columnLookupKey);
    972 
    973                 /** If a contact already exists and another phone number of the contact is being
    974                  * processed, skip the second instance.
    975                  */
    976                 final ContactMatch contactMatch = new ContactMatch(lookupKey, id);
    977                 if (duplicates.contains(contactMatch)) {
    978                     continue;
    979                 }
    980 
    981                 /**
    982                  * If the contact has either the name or number that matches the query, add to the
    983                  * result.
    984                  */
    985                 final boolean nameMatches = nameMatcher.matches(displayName);
    986                 final boolean numberMatches =
    987                         (nameMatcher.matchesNumber(phoneNumber, query) != null);
    988                 if (nameMatches || numberMatches) {
    989                     /** If a contact has not been added, add it to the result and the hash set.*/
    990                     duplicates.add(contactMatch);
    991                     result.add(new ContactNumber(id, dataID, displayName, phoneNumber, lookupKey,
    992                             photoId));
    993                     counter++;
    994                     if (DEBUG) {
    995                         stopWatch.lap("Added one result: Name: " + displayName);
    996                     }
    997                 }
    998             }
    999 
   1000             if (DEBUG) {
   1001                 stopWatch.stopAndLog(TAG + "Finished loading cursor", 0);
   1002             }
   1003         } finally {
   1004             cursor.close();
   1005         }
   1006         return result;
   1007     }
   1008 }
   1009