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