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