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