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