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