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