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