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