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 
ContactNumber(long id, long dataID, String displayName, String phoneNumber, String lookupKey, long photoId, int carrierPresence)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
hashCode()291         public int hashCode() {
292             return Objects.hashCode(id, dataId, displayName, phoneNumber, lookupKey, photoId,
293                     carrierPresence);
294         }
295 
296         @Override
equals(Object object)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 
ContactMatch(String lookupKey, long id)322         public ContactMatch(String lookupKey, long id) {
323             this.lookupKey = lookupKey;
324             this.id = id;
325         }
326 
327         @Override
hashCode()328         public int hashCode() {
329             return Objects.hashCode(lookupKey, id);
330         }
331 
332         @Override
equals(Object object)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      */
getInstance(Context context)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
getNewInstanceForTest(Context context)367     static DialerDatabaseHelper getNewInstanceForTest(Context context) {
368         return new DialerDatabaseHelper(context, null, true);
369     }
370 
DialerDatabaseHelper(Context context, String databaseName, boolean isTestInstance)371     protected DialerDatabaseHelper(Context context, String databaseName, boolean isTestInstance) {
372         this(context, databaseName, DATABASE_VERSION);
373         mIsTestInstance = isTestInstance;
374     }
375 
DialerDatabaseHelper(Context context, String databaseName)376     protected DialerDatabaseHelper(Context context, String databaseName) {
377         this(context, databaseName, DATABASE_VERSION);
378     }
379 
DialerDatabaseHelper(Context context, String databaseName, int dbVersion)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
onCreate(SQLiteDatabase db)391     public void onCreate(SQLiteDatabase db) {
392         setupTables(db);
393     }
394 
setupTables(SQLiteDatabase db)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 
dropTables(SQLiteDatabase db)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
onUpgrade(SQLiteDatabase db, int oldNumber, int newNumber)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 
upgradeToVersion8(SQLiteDatabase db)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      */
setProperty(String key, String value)516     public void setProperty(String key, String value) {
517         setProperty(getWritableDatabase(), key, value);
518     }
519 
setProperty(SQLiteDatabase db, String key, String value)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      */
getProperty(String key, String defaultValue)530     public String getProperty(String key, String defaultValue) {
531         return getProperty(getReadableDatabase(), key, defaultValue);
532     }
533 
getProperty(SQLiteDatabase db, String key, String defaultValue)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 
getPropertyAsInt(SQLiteDatabase db, String key, int defaultValue)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 
resetSmartDialLastUpdatedTime()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      */
startSmartDialUpdateThread()576     public void startSmartDialUpdateThread() {
577         if (PermissionsUtil.hasContactsPermissions(mContext)) {
578             new SmartDialUpdateAsyncTask().execute();
579         }
580     }
581 
582     private class SmartDialUpdateAsyncTask extends AsyncTask {
583         @Override
doInBackground(Object[] objects)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
onCancelled()593         protected void onCancelled() {
594             if (DEBUG) {
595                 Log.v(TAG, "Updating Cancelled");
596             }
597             super.onCancelled();
598         }
599 
600         @Override
onPostExecute(Object o)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
removeDeletedContacts(SQLiteDatabase db, Cursor deletedContactCursor)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 
getDeletedContactCursor(String lastUpdateMillis)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      */
removePotentiallyCorruptedContacts(SQLiteDatabase db, String last_update_time)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      */
createVoicemailArchiveTable(SQLiteDatabase db)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
removeAllContacts(SQLiteDatabase db)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
countPrefixTableRows(SQLiteDatabase db)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
removeUpdatedContacts(SQLiteDatabase db, Cursor updatedContactCursor)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
insertUpdatedContactsAndNumberPrefix(SQLiteDatabase db, Cursor updatedContactCursor, Long currentMillis)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
insertNamePrefixes(SQLiteDatabase db, Cursor nameCursor)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      */
updateSmartDialDatabase()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      */
getLooseMatches(String query, SmartDialNameMatcher nameMatcher)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