summaryrefslogtreecommitdiff
path: root/java/com/android/dialer/database/DialerDatabaseHelper.java
diff options
context:
space:
mode:
Diffstat (limited to 'java/com/android/dialer/database/DialerDatabaseHelper.java')
-rw-r--r--java/com/android/dialer/database/DialerDatabaseHelper.java1250
1 files changed, 1250 insertions, 0 deletions
diff --git a/java/com/android/dialer/database/DialerDatabaseHelper.java b/java/com/android/dialer/database/DialerDatabaseHelper.java
new file mode 100644
index 000000000..348814281
--- /dev/null
+++ b/java/com/android/dialer/database/DialerDatabaseHelper.java
@@ -0,0 +1,1250 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.dialer.database;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.database.Cursor;
+import android.database.sqlite.SQLiteDatabase;
+import android.database.sqlite.SQLiteException;
+import android.database.sqlite.SQLiteOpenHelper;
+import android.database.sqlite.SQLiteStatement;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.provider.BaseColumns;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Data;
+import android.provider.ContactsContract.Directory;
+import android.support.annotation.VisibleForTesting;
+import android.text.TextUtils;
+import com.android.contacts.common.R;
+import com.android.contacts.common.util.StopWatch;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.database.FilteredNumberContract.FilteredNumberColumns;
+import com.android.dialer.smartdial.SmartDialNameMatcher;
+import com.android.dialer.smartdial.SmartDialPrefix;
+import com.android.dialer.util.PermissionsUtil;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * Database helper for smart dial. Designed as a singleton to make sure there is only one access
+ * point to the database. Provides methods to maintain, update, and query the database.
+ */
+public class DialerDatabaseHelper extends SQLiteOpenHelper {
+
+ /**
+ * SmartDial DB version ranges:
+ *
+ * <pre>
+ * 0-98 KitKat
+ * </pre>
+ */
+ public static final int DATABASE_VERSION = 10;
+
+ public static final String DATABASE_NAME = "dialer.db";
+
+ public static final String ACTION_SMART_DIAL_UPDATED =
+ "com.android.dialer.database.ACTION_SMART_DIAL_UPDATED";
+ private static final String TAG = "DialerDatabaseHelper";
+ private static final boolean DEBUG = false;
+ /** Saves the last update time of smart dial databases to shared preferences. */
+ private static final String DATABASE_LAST_CREATED_SHARED_PREF = "com.android.dialer";
+
+ private static final String LAST_UPDATED_MILLIS = "last_updated_millis";
+ private static final String DATABASE_VERSION_PROPERTY = "database_version";
+ private static final int MAX_ENTRIES = 20;
+
+ private final Context mContext;
+ private final Object mLock = new Object();
+ private final AtomicBoolean mInUpdate = new AtomicBoolean(false);
+ private boolean mIsTestInstance = false;
+
+ protected DialerDatabaseHelper(Context context, String databaseName, int dbVersion) {
+ super(context, databaseName, null, dbVersion);
+ mContext = Objects.requireNonNull(context, "Context must not be null");
+ }
+
+ public void setIsTestInstance(boolean isTestInstance) {
+ mIsTestInstance = isTestInstance;
+ }
+
+ /**
+ * Creates tables in the database when database is created for the first time.
+ *
+ * @param db The database.
+ */
+ @Override
+ public void onCreate(SQLiteDatabase db) {
+ setupTables(db);
+ }
+
+ private void setupTables(SQLiteDatabase db) {
+ dropTables(db);
+ db.execSQL(
+ "CREATE TABLE "
+ + Tables.SMARTDIAL_TABLE
+ + " ("
+ + SmartDialDbColumns._ID
+ + " INTEGER PRIMARY KEY AUTOINCREMENT,"
+ + SmartDialDbColumns.DATA_ID
+ + " INTEGER, "
+ + SmartDialDbColumns.NUMBER
+ + " TEXT,"
+ + SmartDialDbColumns.CONTACT_ID
+ + " INTEGER,"
+ + SmartDialDbColumns.LOOKUP_KEY
+ + " TEXT,"
+ + SmartDialDbColumns.DISPLAY_NAME_PRIMARY
+ + " TEXT, "
+ + SmartDialDbColumns.PHOTO_ID
+ + " INTEGER, "
+ + SmartDialDbColumns.LAST_SMARTDIAL_UPDATE_TIME
+ + " LONG, "
+ + SmartDialDbColumns.LAST_TIME_USED
+ + " LONG, "
+ + SmartDialDbColumns.TIMES_USED
+ + " INTEGER, "
+ + SmartDialDbColumns.STARRED
+ + " INTEGER, "
+ + SmartDialDbColumns.IS_SUPER_PRIMARY
+ + " INTEGER, "
+ + SmartDialDbColumns.IN_VISIBLE_GROUP
+ + " INTEGER, "
+ + SmartDialDbColumns.IS_PRIMARY
+ + " INTEGER, "
+ + SmartDialDbColumns.CARRIER_PRESENCE
+ + " INTEGER NOT NULL DEFAULT 0"
+ + ");");
+
+ db.execSQL(
+ "CREATE TABLE "
+ + Tables.PREFIX_TABLE
+ + " ("
+ + PrefixColumns._ID
+ + " INTEGER PRIMARY KEY AUTOINCREMENT,"
+ + PrefixColumns.PREFIX
+ + " TEXT COLLATE NOCASE, "
+ + PrefixColumns.CONTACT_ID
+ + " INTEGER"
+ + ");");
+
+ db.execSQL(
+ "CREATE TABLE "
+ + Tables.PROPERTIES
+ + " ("
+ + PropertiesColumns.PROPERTY_KEY
+ + " TEXT PRIMARY KEY, "
+ + PropertiesColumns.PROPERTY_VALUE
+ + " TEXT "
+ + ");");
+
+ // This will need to also be updated in setupTablesForFilteredNumberTest and onUpgrade.
+ // Hardcoded so we know on glance what columns are updated in setupTables,
+ // and to be able to guarantee the state of the DB at each upgrade step.
+ db.execSQL(
+ "CREATE TABLE "
+ + Tables.FILTERED_NUMBER_TABLE
+ + " ("
+ + FilteredNumberColumns._ID
+ + " INTEGER PRIMARY KEY AUTOINCREMENT,"
+ + FilteredNumberColumns.NORMALIZED_NUMBER
+ + " TEXT UNIQUE,"
+ + FilteredNumberColumns.NUMBER
+ + " TEXT,"
+ + FilteredNumberColumns.COUNTRY_ISO
+ + " TEXT,"
+ + FilteredNumberColumns.TIMES_FILTERED
+ + " INTEGER,"
+ + FilteredNumberColumns.LAST_TIME_FILTERED
+ + " LONG,"
+ + FilteredNumberColumns.CREATION_TIME
+ + " LONG,"
+ + FilteredNumberColumns.TYPE
+ + " INTEGER,"
+ + FilteredNumberColumns.SOURCE
+ + " INTEGER"
+ + ");");
+
+ setProperty(db, DATABASE_VERSION_PROPERTY, String.valueOf(DATABASE_VERSION));
+ if (!mIsTestInstance) {
+ resetSmartDialLastUpdatedTime();
+ }
+ }
+
+ public void dropTables(SQLiteDatabase db) {
+ db.execSQL("DROP TABLE IF EXISTS " + Tables.PREFIX_TABLE);
+ db.execSQL("DROP TABLE IF EXISTS " + Tables.SMARTDIAL_TABLE);
+ db.execSQL("DROP TABLE IF EXISTS " + Tables.PROPERTIES);
+ db.execSQL("DROP TABLE IF EXISTS " + Tables.FILTERED_NUMBER_TABLE);
+ db.execSQL("DROP TABLE IF EXISTS " + Tables.VOICEMAIL_ARCHIVE_TABLE);
+ }
+
+ @Override
+ public void onUpgrade(SQLiteDatabase db, int oldNumber, int newNumber) {
+ // Disregard the old version and new versions provided by SQLiteOpenHelper, we will read
+ // our own from the database.
+
+ int oldVersion;
+
+ oldVersion = getPropertyAsInt(db, DATABASE_VERSION_PROPERTY, 0);
+
+ if (oldVersion == 0) {
+ LogUtil.e(
+ "DialerDatabaseHelper.onUpgrade", "malformed database version..recreating database");
+ }
+
+ if (oldVersion < 4) {
+ setupTables(db);
+ return;
+ }
+
+ if (oldVersion < 7) {
+ db.execSQL("DROP TABLE IF EXISTS " + Tables.FILTERED_NUMBER_TABLE);
+ db.execSQL(
+ "CREATE TABLE "
+ + Tables.FILTERED_NUMBER_TABLE
+ + " ("
+ + FilteredNumberColumns._ID
+ + " INTEGER PRIMARY KEY AUTOINCREMENT,"
+ + FilteredNumberColumns.NORMALIZED_NUMBER
+ + " TEXT UNIQUE,"
+ + FilteredNumberColumns.NUMBER
+ + " TEXT,"
+ + FilteredNumberColumns.COUNTRY_ISO
+ + " TEXT,"
+ + FilteredNumberColumns.TIMES_FILTERED
+ + " INTEGER,"
+ + FilteredNumberColumns.LAST_TIME_FILTERED
+ + " LONG,"
+ + FilteredNumberColumns.CREATION_TIME
+ + " LONG,"
+ + FilteredNumberColumns.TYPE
+ + " INTEGER,"
+ + FilteredNumberColumns.SOURCE
+ + " INTEGER"
+ + ");");
+ oldVersion = 7;
+ }
+
+ if (oldVersion < 8) {
+ upgradeToVersion8(db);
+ oldVersion = 8;
+ }
+
+ if (oldVersion < 10) {
+ db.execSQL("DROP TABLE IF EXISTS " + Tables.VOICEMAIL_ARCHIVE_TABLE);
+ oldVersion = 10;
+ }
+
+ if (oldVersion != DATABASE_VERSION) {
+ throw new IllegalStateException(
+ "error upgrading the database to version " + DATABASE_VERSION);
+ }
+
+ setProperty(db, DATABASE_VERSION_PROPERTY, String.valueOf(DATABASE_VERSION));
+ }
+
+ public void upgradeToVersion8(SQLiteDatabase db) {
+ db.execSQL("ALTER TABLE smartdial_table ADD carrier_presence INTEGER NOT NULL DEFAULT 0");
+ }
+
+ /** Stores a key-value pair in the {@link Tables#PROPERTIES} table. */
+ public void setProperty(String key, String value) {
+ setProperty(getWritableDatabase(), key, value);
+ }
+
+ public void setProperty(SQLiteDatabase db, String key, String value) {
+ final ContentValues values = new ContentValues();
+ values.put(PropertiesColumns.PROPERTY_KEY, key);
+ values.put(PropertiesColumns.PROPERTY_VALUE, value);
+ db.replace(Tables.PROPERTIES, null, values);
+ }
+
+ /** Returns the value from the {@link Tables#PROPERTIES} table. */
+ public String getProperty(String key, String defaultValue) {
+ return getProperty(getReadableDatabase(), key, defaultValue);
+ }
+
+ public String getProperty(SQLiteDatabase db, String key, String defaultValue) {
+ try {
+ String value = null;
+ final Cursor cursor =
+ db.query(
+ Tables.PROPERTIES,
+ new String[] {PropertiesColumns.PROPERTY_VALUE},
+ PropertiesColumns.PROPERTY_KEY + "=?",
+ new String[] {key},
+ null,
+ null,
+ null);
+ if (cursor != null) {
+ try {
+ if (cursor.moveToFirst()) {
+ value = cursor.getString(0);
+ }
+ } finally {
+ cursor.close();
+ }
+ }
+ return value != null ? value : defaultValue;
+ } catch (SQLiteException e) {
+ return defaultValue;
+ }
+ }
+
+ public int getPropertyAsInt(SQLiteDatabase db, String key, int defaultValue) {
+ final String stored = getProperty(db, key, "");
+ try {
+ return Integer.parseInt(stored);
+ } catch (NumberFormatException e) {
+ return defaultValue;
+ }
+ }
+
+ private void resetSmartDialLastUpdatedTime() {
+ final SharedPreferences databaseLastUpdateSharedPref =
+ mContext.getSharedPreferences(DATABASE_LAST_CREATED_SHARED_PREF, Context.MODE_PRIVATE);
+ final SharedPreferences.Editor editor = databaseLastUpdateSharedPref.edit();
+ editor.putLong(LAST_UPDATED_MILLIS, 0);
+ editor.apply();
+ }
+
+ /** Starts the database upgrade process in the background. */
+ public void startSmartDialUpdateThread() {
+ if (PermissionsUtil.hasContactsReadPermissions(mContext)) {
+ new SmartDialUpdateAsyncTask().execute();
+ }
+ }
+
+ /**
+ * Removes rows in the smartdial database that matches the contacts that have been deleted by
+ * other apps since last update.
+ *
+ * @param db Database to operate on.
+ * @param deletedContactCursor Cursor containing rows of deleted contacts
+ */
+ @VisibleForTesting
+ void removeDeletedContacts(SQLiteDatabase db, Cursor deletedContactCursor) {
+ if (deletedContactCursor == null) {
+ return;
+ }
+
+ db.beginTransaction();
+ try {
+ while (deletedContactCursor.moveToNext()) {
+ final Long deleteContactId =
+ deletedContactCursor.getLong(DeleteContactQuery.DELETED_CONTACT_ID);
+ db.delete(
+ Tables.SMARTDIAL_TABLE, SmartDialDbColumns.CONTACT_ID + "=" + deleteContactId, null);
+ db.delete(Tables.PREFIX_TABLE, PrefixColumns.CONTACT_ID + "=" + deleteContactId, null);
+ }
+
+ db.setTransactionSuccessful();
+ } finally {
+ deletedContactCursor.close();
+ db.endTransaction();
+ }
+ }
+
+ private Cursor getDeletedContactCursor(String lastUpdateMillis) {
+ return mContext
+ .getContentResolver()
+ .query(
+ DeleteContactQuery.URI,
+ DeleteContactQuery.PROJECTION,
+ DeleteContactQuery.SELECT_UPDATED_CLAUSE,
+ new String[] {lastUpdateMillis},
+ null);
+ }
+
+ /**
+ * Removes potentially corrupted entries in the database. These contacts may be added before the
+ * previous instance of the dialer was destroyed for some reason. For data integrity, we delete
+ * all of them.
+ *
+ * @param db Database pointer to the dialer database.
+ * @param last_update_time Time stamp of last successful update of the dialer database.
+ */
+ private void removePotentiallyCorruptedContacts(SQLiteDatabase db, String last_update_time) {
+ db.delete(
+ Tables.PREFIX_TABLE,
+ PrefixColumns.CONTACT_ID
+ + " IN "
+ + "(SELECT "
+ + SmartDialDbColumns.CONTACT_ID
+ + " FROM "
+ + Tables.SMARTDIAL_TABLE
+ + " WHERE "
+ + SmartDialDbColumns.LAST_SMARTDIAL_UPDATE_TIME
+ + " > "
+ + last_update_time
+ + ")",
+ null);
+ db.delete(
+ Tables.SMARTDIAL_TABLE,
+ SmartDialDbColumns.LAST_SMARTDIAL_UPDATE_TIME + " > " + last_update_time,
+ null);
+ }
+
+ /**
+ * Removes rows in the smartdial database that matches updated contacts.
+ *
+ * @param db Database pointer to the smartdial database
+ * @param updatedContactCursor Cursor pointing to the list of recently updated contacts.
+ */
+ @VisibleForTesting
+ void removeUpdatedContacts(SQLiteDatabase db, Cursor updatedContactCursor) {
+ db.beginTransaction();
+ try {
+ updatedContactCursor.moveToPosition(-1);
+ while (updatedContactCursor.moveToNext()) {
+ final Long contactId = updatedContactCursor.getLong(UpdatedContactQuery.UPDATED_CONTACT_ID);
+
+ db.delete(Tables.SMARTDIAL_TABLE, SmartDialDbColumns.CONTACT_ID + "=" + contactId, null);
+ db.delete(Tables.PREFIX_TABLE, PrefixColumns.CONTACT_ID + "=" + contactId, null);
+ }
+
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ /**
+ * Inserts updated contacts as rows to the smartdial table.
+ *
+ * @param db Database pointer to the smartdial database.
+ * @param updatedContactCursor Cursor pointing to the list of recently updated contacts.
+ * @param currentMillis Current time to be recorded in the smartdial table as update timestamp.
+ */
+ @VisibleForTesting
+ protected void insertUpdatedContactsAndNumberPrefix(
+ SQLiteDatabase db, Cursor updatedContactCursor, Long currentMillis) {
+ db.beginTransaction();
+ try {
+ final String sqlInsert =
+ "INSERT INTO "
+ + Tables.SMARTDIAL_TABLE
+ + " ("
+ + SmartDialDbColumns.DATA_ID
+ + ", "
+ + SmartDialDbColumns.NUMBER
+ + ", "
+ + SmartDialDbColumns.CONTACT_ID
+ + ", "
+ + SmartDialDbColumns.LOOKUP_KEY
+ + ", "
+ + SmartDialDbColumns.DISPLAY_NAME_PRIMARY
+ + ", "
+ + SmartDialDbColumns.PHOTO_ID
+ + ", "
+ + SmartDialDbColumns.LAST_TIME_USED
+ + ", "
+ + SmartDialDbColumns.TIMES_USED
+ + ", "
+ + SmartDialDbColumns.STARRED
+ + ", "
+ + SmartDialDbColumns.IS_SUPER_PRIMARY
+ + ", "
+ + SmartDialDbColumns.IN_VISIBLE_GROUP
+ + ", "
+ + SmartDialDbColumns.IS_PRIMARY
+ + ", "
+ + SmartDialDbColumns.CARRIER_PRESENCE
+ + ", "
+ + SmartDialDbColumns.LAST_SMARTDIAL_UPDATE_TIME
+ + ") "
+ + " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)";
+ final SQLiteStatement insert = db.compileStatement(sqlInsert);
+
+ final String numberSqlInsert =
+ "INSERT INTO "
+ + Tables.PREFIX_TABLE
+ + " ("
+ + PrefixColumns.CONTACT_ID
+ + ", "
+ + PrefixColumns.PREFIX
+ + ") "
+ + " VALUES (?, ?)";
+ final SQLiteStatement numberInsert = db.compileStatement(numberSqlInsert);
+
+ updatedContactCursor.moveToPosition(-1);
+ while (updatedContactCursor.moveToNext()) {
+ insert.clearBindings();
+
+ // Handle string columns which can possibly be null first. In the case of certain
+ // null columns (due to malformed rows possibly inserted by third-party apps
+ // or sync adapters), skip the phone number row.
+ final String number = updatedContactCursor.getString(PhoneQuery.PHONE_NUMBER);
+ if (TextUtils.isEmpty(number)) {
+ continue;
+ } else {
+ insert.bindString(2, number);
+ }
+
+ final String lookupKey = updatedContactCursor.getString(PhoneQuery.PHONE_LOOKUP_KEY);
+ if (TextUtils.isEmpty(lookupKey)) {
+ continue;
+ } else {
+ insert.bindString(4, lookupKey);
+ }
+
+ final String displayName = updatedContactCursor.getString(PhoneQuery.PHONE_DISPLAY_NAME);
+ if (displayName == null) {
+ insert.bindString(5, mContext.getResources().getString(R.string.missing_name));
+ } else {
+ insert.bindString(5, displayName);
+ }
+ insert.bindLong(1, updatedContactCursor.getLong(PhoneQuery.PHONE_ID));
+ insert.bindLong(3, updatedContactCursor.getLong(PhoneQuery.PHONE_CONTACT_ID));
+ insert.bindLong(6, updatedContactCursor.getLong(PhoneQuery.PHONE_PHOTO_ID));
+ insert.bindLong(7, updatedContactCursor.getLong(PhoneQuery.PHONE_LAST_TIME_USED));
+ insert.bindLong(8, updatedContactCursor.getInt(PhoneQuery.PHONE_TIMES_USED));
+ insert.bindLong(9, updatedContactCursor.getInt(PhoneQuery.PHONE_STARRED));
+ insert.bindLong(10, updatedContactCursor.getInt(PhoneQuery.PHONE_IS_SUPER_PRIMARY));
+ insert.bindLong(11, updatedContactCursor.getInt(PhoneQuery.PHONE_IN_VISIBLE_GROUP));
+ insert.bindLong(12, updatedContactCursor.getInt(PhoneQuery.PHONE_IS_PRIMARY));
+ insert.bindLong(13, updatedContactCursor.getInt(PhoneQuery.PHONE_CARRIER_PRESENCE));
+ insert.bindLong(14, currentMillis);
+ insert.executeInsert();
+ final String contactPhoneNumber = updatedContactCursor.getString(PhoneQuery.PHONE_NUMBER);
+ final ArrayList<String> numberPrefixes =
+ SmartDialPrefix.parseToNumberTokens(contactPhoneNumber);
+
+ for (String numberPrefix : numberPrefixes) {
+ numberInsert.bindLong(1, updatedContactCursor.getLong(PhoneQuery.PHONE_CONTACT_ID));
+ numberInsert.bindString(2, numberPrefix);
+ numberInsert.executeInsert();
+ numberInsert.clearBindings();
+ }
+ }
+
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ /**
+ * Inserts prefixes of contact names to the prefix table.
+ *
+ * @param db Database pointer to the smartdial database.
+ * @param nameCursor Cursor pointing to the list of distinct updated contacts.
+ */
+ @VisibleForTesting
+ void insertNamePrefixes(SQLiteDatabase db, Cursor nameCursor) {
+ final int columnIndexName = nameCursor.getColumnIndex(SmartDialDbColumns.DISPLAY_NAME_PRIMARY);
+ final int columnIndexContactId = nameCursor.getColumnIndex(SmartDialDbColumns.CONTACT_ID);
+
+ db.beginTransaction();
+ try {
+ final String sqlInsert =
+ "INSERT INTO "
+ + Tables.PREFIX_TABLE
+ + " ("
+ + PrefixColumns.CONTACT_ID
+ + ", "
+ + PrefixColumns.PREFIX
+ + ") "
+ + " VALUES (?, ?)";
+ final SQLiteStatement insert = db.compileStatement(sqlInsert);
+
+ while (nameCursor.moveToNext()) {
+ /** Computes a list of prefixes of a given contact name. */
+ final ArrayList<String> namePrefixes =
+ SmartDialPrefix.generateNamePrefixes(nameCursor.getString(columnIndexName));
+
+ for (String namePrefix : namePrefixes) {
+ insert.bindLong(1, nameCursor.getLong(columnIndexContactId));
+ insert.bindString(2, namePrefix);
+ insert.executeInsert();
+ insert.clearBindings();
+ }
+ }
+
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+ }
+
+ /**
+ * Updates the smart dial and prefix database. This method queries the Delta API to get changed
+ * contacts since last update, and updates the records in smartdial database and prefix database
+ * accordingly. It also queries the deleted contact database to remove newly deleted contacts
+ * since last update.
+ */
+ public void updateSmartDialDatabase() {
+ LogUtil.enterBlock("DialerDatabaseHelper.updateSmartDialDatabase");
+
+ final SQLiteDatabase db = getWritableDatabase();
+
+ synchronized (mLock) {
+ LogUtil.v("DialerDatabaseHelper.updateSmartDialDatabase", "starting to update database");
+ final StopWatch stopWatch = DEBUG ? StopWatch.start("Updating databases") : null;
+
+ /** Gets the last update time on the database. */
+ final SharedPreferences databaseLastUpdateSharedPref =
+ mContext.getSharedPreferences(DATABASE_LAST_CREATED_SHARED_PREF, Context.MODE_PRIVATE);
+ final String lastUpdateMillis =
+ String.valueOf(databaseLastUpdateSharedPref.getLong(LAST_UPDATED_MILLIS, 0));
+
+ LogUtil.v(
+ "DialerDatabaseHelper.updateSmartDialDatabase", "last updated at " + lastUpdateMillis);
+
+ /** Sets the time after querying the database as the current update time. */
+ final Long currentMillis = System.currentTimeMillis();
+
+ if (DEBUG) {
+ stopWatch.lap("Queried the Contacts database");
+ }
+
+ /** Prevents the app from reading the dialer database when updating. */
+ mInUpdate.getAndSet(true);
+
+ /** Removes contacts that have been deleted. */
+ removeDeletedContacts(db, getDeletedContactCursor(lastUpdateMillis));
+ removePotentiallyCorruptedContacts(db, lastUpdateMillis);
+
+ if (DEBUG) {
+ stopWatch.lap("Finished deleting deleted entries");
+ }
+
+ /**
+ * If the database did not exist before, jump through deletion as there is nothing to delete.
+ */
+ if (!lastUpdateMillis.equals("0")) {
+ /**
+ * Removes contacts that have been updated. Updated contact information will be inserted
+ * later. Note that this has to use a separate result set from updatePhoneCursor, since it
+ * is possible for a contact to be updated (e.g. phone number deleted), but have no results
+ * show up in updatedPhoneCursor (since all of its phone numbers have been deleted).
+ */
+ final Cursor updatedContactCursor =
+ mContext
+ .getContentResolver()
+ .query(
+ UpdatedContactQuery.URI,
+ UpdatedContactQuery.PROJECTION,
+ UpdatedContactQuery.SELECT_UPDATED_CLAUSE,
+ new String[] {lastUpdateMillis},
+ null);
+ if (updatedContactCursor == null) {
+ LogUtil.e(
+ "DialerDatabaseHelper.updateSmartDialDatabase",
+ "smartDial query received null for cursor");
+ return;
+ }
+ try {
+ removeUpdatedContacts(db, updatedContactCursor);
+ } finally {
+ updatedContactCursor.close();
+ }
+ if (DEBUG) {
+ stopWatch.lap("Finished deleting entries belonging to updated contacts");
+ }
+ }
+
+ /**
+ * Queries the contact database to get all phone numbers that have been updated since the last
+ * update time.
+ */
+ final Cursor updatedPhoneCursor =
+ mContext
+ .getContentResolver()
+ .query(
+ PhoneQuery.URI,
+ PhoneQuery.PROJECTION,
+ PhoneQuery.SELECTION,
+ new String[] {lastUpdateMillis},
+ null);
+ if (updatedPhoneCursor == null) {
+ LogUtil.e(
+ "DialerDatabaseHelper.updateSmartDialDatabase",
+ "smartDial query received null for cursor");
+ return;
+ }
+
+ try {
+ /** Inserts recently updated phone numbers to the smartdial database. */
+ insertUpdatedContactsAndNumberPrefix(db, updatedPhoneCursor, currentMillis);
+ if (DEBUG) {
+ stopWatch.lap("Finished building the smart dial table");
+ }
+ } finally {
+ updatedPhoneCursor.close();
+ }
+
+ /**
+ * Gets a list of distinct contacts which have been updated, and adds the name prefixes of
+ * these contacts to the prefix table.
+ */
+ final Cursor nameCursor =
+ db.rawQuery(
+ "SELECT DISTINCT "
+ + SmartDialDbColumns.DISPLAY_NAME_PRIMARY
+ + ", "
+ + SmartDialDbColumns.CONTACT_ID
+ + " FROM "
+ + Tables.SMARTDIAL_TABLE
+ + " WHERE "
+ + SmartDialDbColumns.LAST_SMARTDIAL_UPDATE_TIME
+ + " = "
+ + Long.toString(currentMillis),
+ new String[] {});
+ if (nameCursor != null) {
+ try {
+ if (DEBUG) {
+ stopWatch.lap("Queried the smart dial table for contact names");
+ }
+
+ /** Inserts prefixes of names into the prefix table. */
+ insertNamePrefixes(db, nameCursor);
+ if (DEBUG) {
+ stopWatch.lap("Finished building the name prefix table");
+ }
+ } finally {
+ nameCursor.close();
+ }
+ }
+
+ /** Creates index on contact_id for fast JOIN operation. */
+ db.execSQL(
+ "CREATE INDEX IF NOT EXISTS smartdial_contact_id_index ON "
+ + Tables.SMARTDIAL_TABLE
+ + " ("
+ + SmartDialDbColumns.CONTACT_ID
+ + ");");
+ /** Creates index on last_smartdial_update_time for fast SELECT operation. */
+ db.execSQL(
+ "CREATE INDEX IF NOT EXISTS smartdial_last_update_index ON "
+ + Tables.SMARTDIAL_TABLE
+ + " ("
+ + SmartDialDbColumns.LAST_SMARTDIAL_UPDATE_TIME
+ + ");");
+ /** Creates index on sorting fields for fast sort operation. */
+ db.execSQL(
+ "CREATE INDEX IF NOT EXISTS smartdial_sort_index ON "
+ + Tables.SMARTDIAL_TABLE
+ + " ("
+ + SmartDialDbColumns.STARRED
+ + ", "
+ + SmartDialDbColumns.IS_SUPER_PRIMARY
+ + ", "
+ + SmartDialDbColumns.LAST_TIME_USED
+ + ", "
+ + SmartDialDbColumns.TIMES_USED
+ + ", "
+ + SmartDialDbColumns.IN_VISIBLE_GROUP
+ + ", "
+ + SmartDialDbColumns.DISPLAY_NAME_PRIMARY
+ + ", "
+ + SmartDialDbColumns.CONTACT_ID
+ + ", "
+ + SmartDialDbColumns.IS_PRIMARY
+ + ");");
+ /** Creates index on prefix for fast SELECT operation. */
+ db.execSQL(
+ "CREATE INDEX IF NOT EXISTS nameprefix_index ON "
+ + Tables.PREFIX_TABLE
+ + " ("
+ + PrefixColumns.PREFIX
+ + ");");
+ /** Creates index on contact_id for fast JOIN operation. */
+ db.execSQL(
+ "CREATE INDEX IF NOT EXISTS nameprefix_contact_id_index ON "
+ + Tables.PREFIX_TABLE
+ + " ("
+ + PrefixColumns.CONTACT_ID
+ + ");");
+
+ if (DEBUG) {
+ stopWatch.lap(TAG + "Finished recreating index");
+ }
+
+ /** Updates the database index statistics. */
+ db.execSQL("ANALYZE " + Tables.SMARTDIAL_TABLE);
+ db.execSQL("ANALYZE " + Tables.PREFIX_TABLE);
+ db.execSQL("ANALYZE smartdial_contact_id_index");
+ db.execSQL("ANALYZE smartdial_last_update_index");
+ db.execSQL("ANALYZE nameprefix_index");
+ db.execSQL("ANALYZE nameprefix_contact_id_index");
+ if (DEBUG) {
+ stopWatch.stopAndLog(TAG + "Finished updating index stats", 0);
+ }
+
+ mInUpdate.getAndSet(false);
+
+ final SharedPreferences.Editor editor = databaseLastUpdateSharedPref.edit();
+ editor.putLong(LAST_UPDATED_MILLIS, currentMillis);
+ editor.apply();
+
+ LogUtil.i("DialerDatabaseHelper.updateSmartDialDatabase", "broadcasting smart dial update");
+
+ // Notify content observers that smart dial database has been updated.
+ Intent intent = new Intent(ACTION_SMART_DIAL_UPDATED);
+ intent.setPackage(mContext.getPackageName());
+ mContext.sendBroadcast(intent);
+ }
+ }
+
+ /**
+ * Returns a list of candidate contacts where the query is a prefix of the dialpad index of the
+ * contact's name or phone number.
+ *
+ * @param query The prefix of a contact's dialpad index.
+ * @return A list of top candidate contacts that will be suggested to user to match their input.
+ */
+ public ArrayList<ContactNumber> getLooseMatches(String query, SmartDialNameMatcher nameMatcher) {
+ final boolean inUpdate = mInUpdate.get();
+ if (inUpdate) {
+ return new ArrayList<>();
+ }
+
+ final SQLiteDatabase db = getReadableDatabase();
+
+ /** Uses SQL query wildcard '%' to represent prefix matching. */
+ final String looseQuery = query + "%";
+
+ final ArrayList<ContactNumber> result = new ArrayList<>();
+
+ final StopWatch stopWatch = DEBUG ? StopWatch.start(":Name Prefix query") : null;
+
+ final String currentTimeStamp = Long.toString(System.currentTimeMillis());
+
+ /** Queries the database to find contacts that have an index matching the query prefix. */
+ final Cursor cursor =
+ db.rawQuery(
+ "SELECT "
+ + SmartDialDbColumns.DATA_ID
+ + ", "
+ + SmartDialDbColumns.DISPLAY_NAME_PRIMARY
+ + ", "
+ + SmartDialDbColumns.PHOTO_ID
+ + ", "
+ + SmartDialDbColumns.NUMBER
+ + ", "
+ + SmartDialDbColumns.CONTACT_ID
+ + ", "
+ + SmartDialDbColumns.LOOKUP_KEY
+ + ", "
+ + SmartDialDbColumns.CARRIER_PRESENCE
+ + " FROM "
+ + Tables.SMARTDIAL_TABLE
+ + " WHERE "
+ + SmartDialDbColumns.CONTACT_ID
+ + " IN "
+ + " (SELECT "
+ + PrefixColumns.CONTACT_ID
+ + " FROM "
+ + Tables.PREFIX_TABLE
+ + " WHERE "
+ + Tables.PREFIX_TABLE
+ + "."
+ + PrefixColumns.PREFIX
+ + " LIKE '"
+ + looseQuery
+ + "')"
+ + " ORDER BY "
+ + SmartDialSortingOrder.SORT_ORDER,
+ new String[] {currentTimeStamp});
+ if (cursor == null) {
+ return result;
+ }
+ try {
+ if (DEBUG) {
+ stopWatch.lap("Prefix query completed");
+ }
+
+ /** Gets the column ID from the cursor. */
+ final int columnDataId = 0;
+ final int columnDisplayNamePrimary = 1;
+ final int columnPhotoId = 2;
+ final int columnNumber = 3;
+ final int columnId = 4;
+ final int columnLookupKey = 5;
+ final int columnCarrierPresence = 6;
+ if (DEBUG) {
+ stopWatch.lap("Found column IDs");
+ }
+
+ final Set<ContactMatch> duplicates = new HashSet<>();
+ int counter = 0;
+ if (DEBUG) {
+ stopWatch.lap("Moved cursor to start");
+ }
+ /** Iterates the cursor to find top contact suggestions without duplication. */
+ while ((cursor.moveToNext()) && (counter < MAX_ENTRIES)) {
+ final long dataID = cursor.getLong(columnDataId);
+ final String displayName = cursor.getString(columnDisplayNamePrimary);
+ final String phoneNumber = cursor.getString(columnNumber);
+ final long id = cursor.getLong(columnId);
+ final long photoId = cursor.getLong(columnPhotoId);
+ final String lookupKey = cursor.getString(columnLookupKey);
+ final int carrierPresence = cursor.getInt(columnCarrierPresence);
+
+ /**
+ * If a contact already exists and another phone number of the contact is being processed,
+ * skip the second instance.
+ */
+ final ContactMatch contactMatch = new ContactMatch(lookupKey, id);
+ if (duplicates.contains(contactMatch)) {
+ continue;
+ }
+
+ /**
+ * If the contact has either the name or number that matches the query, add to the result.
+ */
+ final boolean nameMatches = nameMatcher.matches(displayName);
+ final boolean numberMatches = (nameMatcher.matchesNumber(phoneNumber, query) != null);
+ if (nameMatches || numberMatches) {
+ /** If a contact has not been added, add it to the result and the hash set. */
+ duplicates.add(contactMatch);
+ result.add(
+ new ContactNumber(
+ id, dataID, displayName, phoneNumber, lookupKey, photoId, carrierPresence));
+ counter++;
+ if (DEBUG) {
+ stopWatch.lap("Added one result: Name: " + displayName);
+ }
+ }
+ }
+
+ if (DEBUG) {
+ stopWatch.stopAndLog(TAG + "Finished loading cursor", 0);
+ }
+ } finally {
+ cursor.close();
+ }
+ return result;
+ }
+
+ public interface Tables {
+
+ /** Saves a list of numbers to be blocked. */
+ String FILTERED_NUMBER_TABLE = "filtered_numbers_table";
+ /** Saves the necessary smart dial information of all contacts. */
+ String SMARTDIAL_TABLE = "smartdial_table";
+ /** Saves all possible prefixes to refer to a contacts. */
+ String PREFIX_TABLE = "prefix_table";
+ /** Saves all archived voicemail information. */
+ String VOICEMAIL_ARCHIVE_TABLE = "voicemail_archive_table";
+ /** Database properties for internal use */
+ String PROPERTIES = "properties";
+ }
+
+ public interface SmartDialDbColumns {
+
+ String _ID = "id";
+ String DATA_ID = "data_id";
+ String NUMBER = "phone_number";
+ String CONTACT_ID = "contact_id";
+ String LOOKUP_KEY = "lookup_key";
+ String DISPLAY_NAME_PRIMARY = "display_name";
+ String PHOTO_ID = "photo_id";
+ String LAST_TIME_USED = "last_time_used";
+ String TIMES_USED = "times_used";
+ String STARRED = "starred";
+ String IS_SUPER_PRIMARY = "is_super_primary";
+ String IN_VISIBLE_GROUP = "in_visible_group";
+ String IS_PRIMARY = "is_primary";
+ String CARRIER_PRESENCE = "carrier_presence";
+ String LAST_SMARTDIAL_UPDATE_TIME = "last_smartdial_update_time";
+ }
+
+ public interface PrefixColumns extends BaseColumns {
+
+ String PREFIX = "prefix";
+ String CONTACT_ID = "contact_id";
+ }
+
+ public interface PropertiesColumns {
+
+ String PROPERTY_KEY = "property_key";
+ String PROPERTY_VALUE = "property_value";
+ }
+
+ /** Query options for querying the contact database. */
+ public interface PhoneQuery {
+
+ Uri URI =
+ Phone.CONTENT_URI
+ .buildUpon()
+ .appendQueryParameter(
+ ContactsContract.DIRECTORY_PARAM_KEY, String.valueOf(Directory.DEFAULT))
+ .appendQueryParameter(ContactsContract.REMOVE_DUPLICATE_ENTRIES, "true")
+ .build();
+
+ String[] PROJECTION =
+ new String[] {
+ Phone._ID, // 0
+ Phone.TYPE, // 1
+ Phone.LABEL, // 2
+ Phone.NUMBER, // 3
+ Phone.CONTACT_ID, // 4
+ Phone.LOOKUP_KEY, // 5
+ Phone.DISPLAY_NAME_PRIMARY, // 6
+ Phone.PHOTO_ID, // 7
+ Data.LAST_TIME_USED, // 8
+ Data.TIMES_USED, // 9
+ Contacts.STARRED, // 10
+ Data.IS_SUPER_PRIMARY, // 11
+ Contacts.IN_VISIBLE_GROUP, // 12
+ Data.IS_PRIMARY, // 13
+ Data.CARRIER_PRESENCE, // 14
+ };
+
+ int PHONE_ID = 0;
+ int PHONE_TYPE = 1;
+ int PHONE_LABEL = 2;
+ int PHONE_NUMBER = 3;
+ int PHONE_CONTACT_ID = 4;
+ int PHONE_LOOKUP_KEY = 5;
+ int PHONE_DISPLAY_NAME = 6;
+ int PHONE_PHOTO_ID = 7;
+ int PHONE_LAST_TIME_USED = 8;
+ int PHONE_TIMES_USED = 9;
+ int PHONE_STARRED = 10;
+ int PHONE_IS_SUPER_PRIMARY = 11;
+ int PHONE_IN_VISIBLE_GROUP = 12;
+ int PHONE_IS_PRIMARY = 13;
+ int PHONE_CARRIER_PRESENCE = 14;
+
+ /** Selects only rows that have been updated after a certain time stamp. */
+ String SELECT_UPDATED_CLAUSE = Phone.CONTACT_LAST_UPDATED_TIMESTAMP + " > ?";
+
+ /**
+ * Ignores contacts that have an unreasonably long lookup key. These are likely to be the result
+ * of multiple (> 50) merged raw contacts, and are likely to cause OutOfMemoryExceptions within
+ * SQLite, or cause memory allocation problems later on when iterating through the cursor set
+ * (see b/13133579)
+ */
+ String SELECT_IGNORE_LOOKUP_KEY_TOO_LONG_CLAUSE = "length(" + Phone.LOOKUP_KEY + ") < 1000";
+
+ String SELECTION = SELECT_UPDATED_CLAUSE + " AND " + SELECT_IGNORE_LOOKUP_KEY_TOO_LONG_CLAUSE;
+ }
+
+ /**
+ * Query for all contacts that have been updated since the last time the smart dial database was
+ * updated.
+ */
+ public interface UpdatedContactQuery {
+
+ Uri URI = ContactsContract.Contacts.CONTENT_URI;
+
+ String[] PROJECTION =
+ new String[] {
+ ContactsContract.Contacts._ID // 0
+ };
+
+ int UPDATED_CONTACT_ID = 0;
+
+ String SELECT_UPDATED_CLAUSE =
+ ContactsContract.Contacts.CONTACT_LAST_UPDATED_TIMESTAMP + " > ?";
+ }
+
+ /** Query options for querying the deleted contact database. */
+ public interface DeleteContactQuery {
+
+ Uri URI = ContactsContract.DeletedContacts.CONTENT_URI;
+
+ String[] PROJECTION =
+ new String[] {
+ ContactsContract.DeletedContacts.CONTACT_ID, // 0
+ ContactsContract.DeletedContacts.CONTACT_DELETED_TIMESTAMP, // 1
+ };
+
+ int DELETED_CONTACT_ID = 0;
+ int DELETED_TIMESTAMP = 1;
+
+ /** Selects only rows that have been deleted after a certain time stamp. */
+ String SELECT_UPDATED_CLAUSE =
+ ContactsContract.DeletedContacts.CONTACT_DELETED_TIMESTAMP + " > ?";
+ }
+
+ /**
+ * Gets the sorting order for the smartdial table. This computes a SQL "ORDER BY" argument by
+ * composing contact status and recent contact details together.
+ */
+ private interface SmartDialSortingOrder {
+
+ /** Current contacts - those contacted within the last 3 days (in milliseconds) */
+ long LAST_TIME_USED_CURRENT_MS = 3L * 24 * 60 * 60 * 1000;
+ /** Recent contacts - those contacted within the last 30 days (in milliseconds) */
+ long LAST_TIME_USED_RECENT_MS = 30L * 24 * 60 * 60 * 1000;
+
+ /** Time since last contact. */
+ String TIME_SINCE_LAST_USED_MS =
+ "( ?1 - " + Tables.SMARTDIAL_TABLE + "." + SmartDialDbColumns.LAST_TIME_USED + ")";
+
+ /**
+ * Contacts that have been used in the past 3 days rank higher than contacts that have been used
+ * in the past 30 days, which rank higher than contacts that have not been used in recent 30
+ * days.
+ */
+ String SORT_BY_DATA_USAGE =
+ "(CASE WHEN "
+ + TIME_SINCE_LAST_USED_MS
+ + " < "
+ + LAST_TIME_USED_CURRENT_MS
+ + " THEN 0 "
+ + " WHEN "
+ + TIME_SINCE_LAST_USED_MS
+ + " < "
+ + LAST_TIME_USED_RECENT_MS
+ + " THEN 1 "
+ + " ELSE 2 END)";
+
+ /**
+ * This sort order is similar to that used by the ContactsProvider when returning a list of
+ * frequently called contacts.
+ */
+ String SORT_ORDER =
+ Tables.SMARTDIAL_TABLE
+ + "."
+ + SmartDialDbColumns.STARRED
+ + " DESC, "
+ + Tables.SMARTDIAL_TABLE
+ + "."
+ + SmartDialDbColumns.IS_SUPER_PRIMARY
+ + " DESC, "
+ + SORT_BY_DATA_USAGE
+ + ", "
+ + Tables.SMARTDIAL_TABLE
+ + "."
+ + SmartDialDbColumns.TIMES_USED
+ + " DESC, "
+ + Tables.SMARTDIAL_TABLE
+ + "."
+ + SmartDialDbColumns.IN_VISIBLE_GROUP
+ + " DESC, "
+ + Tables.SMARTDIAL_TABLE
+ + "."
+ + SmartDialDbColumns.DISPLAY_NAME_PRIMARY
+ + ", "
+ + Tables.SMARTDIAL_TABLE
+ + "."
+ + SmartDialDbColumns.CONTACT_ID
+ + ", "
+ + Tables.SMARTDIAL_TABLE
+ + "."
+ + SmartDialDbColumns.IS_PRIMARY
+ + " DESC";
+ }
+
+ /**
+ * Simple data format for a contact, containing only information needed for showing up in smart
+ * dial interface.
+ */
+ public static class ContactNumber {
+
+ public final long id;
+ public final long dataId;
+ public final String displayName;
+ public final String phoneNumber;
+ public final String lookupKey;
+ public final long photoId;
+ public final int carrierPresence;
+
+ public ContactNumber(
+ long id,
+ long dataID,
+ String displayName,
+ String phoneNumber,
+ String lookupKey,
+ long photoId,
+ int carrierPresence) {
+ this.dataId = dataID;
+ this.id = id;
+ this.displayName = displayName;
+ this.phoneNumber = phoneNumber;
+ this.lookupKey = lookupKey;
+ this.photoId = photoId;
+ this.carrierPresence = carrierPresence;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(
+ id, dataId, displayName, phoneNumber, lookupKey, photoId, carrierPresence);
+ }
+
+ @Override
+ public boolean equals(Object object) {
+ if (this == object) {
+ return true;
+ }
+ if (object instanceof ContactNumber) {
+ final ContactNumber that = (ContactNumber) object;
+ return Objects.equals(this.id, that.id)
+ && Objects.equals(this.dataId, that.dataId)
+ && Objects.equals(this.displayName, that.displayName)
+ && Objects.equals(this.phoneNumber, that.phoneNumber)
+ && Objects.equals(this.lookupKey, that.lookupKey)
+ && Objects.equals(this.photoId, that.photoId)
+ && Objects.equals(this.carrierPresence, that.carrierPresence);
+ }
+ return false;
+ }
+ }
+
+ /** Data format for finding duplicated contacts. */
+ private static class ContactMatch {
+
+ private final String lookupKey;
+ private final long id;
+
+ public ContactMatch(String lookupKey, long id) {
+ this.lookupKey = lookupKey;
+ this.id = id;
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(lookupKey, id);
+ }
+
+ @Override
+ public boolean equals(Object object) {
+ if (this == object) {
+ return true;
+ }
+ if (object instanceof ContactMatch) {
+ final ContactMatch that = (ContactMatch) object;
+ return Objects.equals(this.lookupKey, that.lookupKey) && Objects.equals(this.id, that.id);
+ }
+ return false;
+ }
+ }
+
+ private class SmartDialUpdateAsyncTask extends AsyncTask<Object, Object, Object> {
+
+ @Override
+ protected Object doInBackground(Object... objects) {
+ updateSmartDialDatabase();
+ return null;
+ }
+ }
+}