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