blob: b9e4b9a900b8ae6f6934919f6655be0ef2276071 [file] [log] [blame]
/*
* 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.SharedPreferences;
import android.database.Cursor;
import android.database.DatabaseUtils;
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.text.TextUtils;
import android.util.Log;
import com.android.contacts.common.util.StopWatch;
import com.android.dialer.R;
import com.android.dialer.dialpad.SmartDialNameMatcher;
import com.android.dialer.dialpad.SmartDialPrefix;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Objects;
import com.google.common.base.Preconditions;
import com.google.common.collect.Lists;
import java.util.ArrayList;
import java.util.HashSet;
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 {
private static final String TAG = "DialerDatabaseHelper";
private static final boolean DEBUG = false;
private static DialerDatabaseHelper sSingleton = null;
private static final Object mLock = new Object();
private static final AtomicBoolean sInUpdate = new AtomicBoolean(false);
private final Context mContext;
/**
* SmartDial DB version ranges:
* <pre>
* 0-98 KeyLimePie
* </pre>
*/
public static final int DATABASE_VERSION = 4;
public static final String DATABASE_NAME = "dialer.db";
/**
* 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;
public interface Tables {
/** Saves the necessary smart dial information of all contacts. */
static final String SMARTDIAL_TABLE = "smartdial_table";
/** Saves all possible prefixes to refer to a contacts.*/
static final String PREFIX_TABLE = "prefix_table";
/** Database properties for internal use */
static final String PROPERTIES = "properties";
}
public interface SmartDialDbColumns {
static final String _ID = "id";
static final String DATA_ID = "data_id";
static final String NUMBER = "phone_number";
static final String CONTACT_ID = "contact_id";
static final String LOOKUP_KEY = "lookup_key";
static final String DISPLAY_NAME_PRIMARY = "display_name";
static final String PHOTO_ID = "photo_id";
static final String LAST_TIME_USED = "last_time_used";
static final String TIMES_USED = "times_used";
static final String STARRED = "starred";
static final String IS_SUPER_PRIMARY = "is_super_primary";
static final String IN_VISIBLE_GROUP = "in_visible_group";
static final String IS_PRIMARY = "is_primary";
static final String LAST_SMARTDIAL_UPDATE_TIME = "last_smartdial_update_time";
}
public static interface PrefixColumns extends BaseColumns {
static final String PREFIX = "prefix";
static final 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 static interface PhoneQuery {
static final Uri URI = Phone.CONTENT_URI.buildUpon().
appendQueryParameter(ContactsContract.DIRECTORY_PARAM_KEY,
String.valueOf(Directory.DEFAULT)).
appendQueryParameter(ContactsContract.REMOVE_DUPLICATE_ENTRIES, "true").
build();
static final 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
};
static final int PHONE_ID = 0;
static final int PHONE_TYPE = 1;
static final int PHONE_LABEL = 2;
static final int PHONE_NUMBER = 3;
static final int PHONE_CONTACT_ID = 4;
static final int PHONE_LOOKUP_KEY = 5;
static final int PHONE_DISPLAY_NAME = 6;
static final int PHONE_PHOTO_ID = 7;
static final int PHONE_LAST_TIME_USED = 8;
static final int PHONE_TIMES_USED = 9;
static final int PHONE_STARRED = 10;
static final int PHONE_IS_SUPER_PRIMARY = 11;
static final int PHONE_IN_VISIBLE_GROUP = 12;
static final int PHONE_IS_PRIMARY = 13;
/** Selects only rows that have been updated after a certain time stamp.*/
static final String SELECT_UPDATED_CLAUSE =
Phone.CONTACT_LAST_UPDATED_TIMESTAMP + " > ?";
}
/** Query options for querying the deleted contact database.*/
public static interface DeleteContactQuery {
static final Uri URI = ContactsContract.DeletedContacts.CONTENT_URI;
static final String[] PROJECTION = new String[] {
ContactsContract.DeletedContacts.CONTACT_ID, // 0
ContactsContract.DeletedContacts.CONTACT_DELETED_TIMESTAMP, // 1
};
static final int DELETED_CONTACT_ID = 0;
static final int DELECTED_TIMESTAMP = 1;
/** Selects only rows that have been deleted after a certain time stamp.*/
public static final 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 static interface SmartDialSortingOrder {
/** Current contacts - those contacted within the last 3 days (in milliseconds) */
static final long LAST_TIME_USED_CURRENT_MS = 3L * 24 * 60 * 60 * 1000;
/** Recent contacts - those contacted within the last 30 days (in milliseconds) */
static final long LAST_TIME_USED_RECENT_MS = 30L * 24 * 60 * 60 * 1000;
/** Time since last contact. */
static final 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.
*/
static final 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.
*/
static final 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 ContactNumber(long id, long dataID, String displayName, String phoneNumber,
String lookupKey, long photoId) {
this.dataId = dataID;
this.id = id;
this.displayName = displayName;
this.phoneNumber = phoneNumber;
this.lookupKey = lookupKey;
this.photoId = photoId;
}
@Override
public int hashCode() {
return Objects.hashCode(id, dataId, displayName, phoneNumber, lookupKey, photoId);
}
@Override
public boolean equals(Object object) {
if (this == object) {
return true;
}
if (object instanceof ContactNumber) {
final ContactNumber that = (ContactNumber) object;
return Objects.equal(this.id, that.id)
&& Objects.equal(this.dataId, that.dataId)
&& Objects.equal(this.displayName, that.displayName)
&& Objects.equal(this.phoneNumber, that.phoneNumber)
&& Objects.equal(this.lookupKey, that.lookupKey)
&& Objects.equal(this.photoId, that.photoId);
}
return false;
}
}
/**
* Data format for finding duplicated contacts.
*/
private 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.hashCode(lookupKey, id);
}
@Override
public boolean equals(Object object) {
if (this == object) {
return true;
}
if (object instanceof ContactMatch) {
final ContactMatch that = (ContactMatch) object;
return Objects.equal(this.lookupKey, that.lookupKey)
&& Objects.equal(this.id, that.id);
}
return false;
}
}
/**
* Access function to get the singleton instance of DialerDatabaseHelper.
*/
public static synchronized DialerDatabaseHelper getInstance(Context context) {
if (DEBUG) {
Log.v(TAG, "Getting Instance");
}
if (sSingleton == null) {
// Use application context instead of activity context because this is a singleton,
// and we don't want to leak the activity if the activity is not running but the
// dialer database helper is still doing work.
sSingleton = new DialerDatabaseHelper(context.getApplicationContext(),
DATABASE_NAME);
}
return sSingleton;
}
/**
* Returns a new instance for unit tests. The database will be created in memory.
*/
@VisibleForTesting
static DialerDatabaseHelper getNewInstanceForTest(Context context) {
return new DialerDatabaseHelper(context, null);
}
protected DialerDatabaseHelper(Context context, String databaseName) {
this(context, databaseName, DATABASE_VERSION);
}
protected DialerDatabaseHelper(Context context, String databaseName, int dbVersion) {
super(context, databaseName, null, dbVersion);
mContext = Preconditions.checkNotNull(context, "Context must not be null");
}
/**
* 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" +
");");
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 " +
");");
setProperty(db, DATABASE_VERSION_PROPERTY, String.valueOf(DATABASE_VERSION));
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);
}
@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) {
Log.e(TAG, "Malformed database version..recreating database");
}
if (oldVersion < 4) {
setupTables(db);
return;
}
if (oldVersion != DATABASE_VERSION) {
throw new IllegalStateException(
"error upgrading the database to version " + DATABASE_VERSION);
}
setProperty(db, DATABASE_VERSION_PROPERTY, String.valueOf(DATABASE_VERSION));
}
/**
* 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 {
final Cursor cursor = db.query(Tables.PROPERTIES,
new String[] {PropertiesColumns.PROPERTY_VALUE},
PropertiesColumns.PROPERTY_KEY + "=?",
new String[] {key}, null, null, null);
String value = 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.commit();
}
/**
* Starts the database upgrade process in the background.
*/
public void startSmartDialUpdateThread() {
new SmartDialUpdateAsyncTask().execute();
}
private class SmartDialUpdateAsyncTask extends AsyncTask {
@Override
protected Object doInBackground(Object[] objects) {
if (DEBUG) {
Log.v(TAG, "Updating database");
}
updateSmartDialDatabase();
return null;
}
@Override
protected void onCancelled() {
if (DEBUG) {
Log.v(TAG, "Updating Cancelled");
}
super.onCancelled();
}
@Override
protected void onPostExecute(Object o) {
if (DEBUG) {
Log.v(TAG, "Updating Finished");
}
super.onPostExecute(o);
}
}
/**
* Removes rows in the smartdial database that matches the contacts that have been deleted
* by other apps since last update.
*
* @param db Database pointer to the dialer database.
* @param last_update_time Time stamp of last update on the smartdial database
*/
private void removeDeletedContacts(SQLiteDatabase db, String last_update_time) {
final Cursor deletedContactCursor = mContext.getContentResolver().query(
DeleteContactQuery.URI,
DeleteContactQuery.PROJECTION,
DeleteContactQuery.SELECT_UPDATED_CLAUSE,
new String[] {last_update_time}, null);
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();
}
}
/**
* 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 all entries in the smartdial contact database.
*/
@VisibleForTesting
void removeAllContacts(SQLiteDatabase db) {
db.delete(Tables.SMARTDIAL_TABLE, null, null);
db.delete(Tables.PREFIX_TABLE, null, null);
}
/**
* Counts number of rows of the prefix table.
*/
@VisibleForTesting
int countPrefixTableRows(SQLiteDatabase db) {
return (int)DatabaseUtils.longForQuery(db, "SELECT COUNT(1) FROM " + Tables.PREFIX_TABLE,
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.
*/
private void removeUpdatedContacts(SQLiteDatabase db, Cursor updatedContactCursor) {
db.beginTransaction();
try {
while (updatedContactCursor.moveToNext()) {
final Long contactId = updatedContactCursor.getLong(PhoneQuery.PHONE_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.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.bindLong(1, updatedContactCursor.getLong(PhoneQuery.PHONE_ID));
insert.bindString(2, updatedContactCursor.getString(PhoneQuery.PHONE_NUMBER));
insert.bindLong(3, updatedContactCursor.getLong(PhoneQuery.PHONE_CONTACT_ID));
insert.bindString(4, updatedContactCursor.getString(PhoneQuery.PHONE_LOOKUP_KEY));
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(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, currentMillis);
insert.executeInsert();
insert.clearBindings();
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() {
final SQLiteDatabase db = getWritableDatabase();
synchronized(mLock) {
if (DEBUG) {
Log.v(TAG, "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));
if (DEBUG) {
Log.v(TAG, "Last updated at " + lastUpdateMillis);
}
/** Queries the contact database to get contacts that have been updated since the last
* update time.
*/
final Cursor updatedContactCursor = mContext.getContentResolver().query(PhoneQuery.URI,
PhoneQuery.PROJECTION, PhoneQuery.SELECT_UPDATED_CLAUSE,
new String[]{lastUpdateMillis}, null);
/** 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");
}
if (updatedContactCursor == null) {
if (DEBUG) {
Log.e(TAG, "SmartDial query received null for cursor");
}
return;
}
/** Prevents the app from reading the dialer database when updating. */
sInUpdate.getAndSet(true);
/** Removes contacts that have been deleted. */
removeDeletedContacts(db, lastUpdateMillis);
removePotentiallyCorruptedContacts(db, lastUpdateMillis);
if (DEBUG) {
stopWatch.lap("Finished deleting deleted entries");
}
try {
/** 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.
*/
removeUpdatedContacts(db, updatedContactCursor);
if (DEBUG) {
stopWatch.lap("Finished deleting updated entries");
}
}
/** Inserts recently updated contacts to the smartdial database.*/
insertUpdatedContactsAndNumberPrefix(db, updatedContactCursor, currentMillis);
if (DEBUG) {
stopWatch.lap("Finished building the smart dial table");
}
} finally {
/** Inserts prefixes of phone numbers into the prefix table.*/
updatedContactCursor.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 (DEBUG) {
stopWatch.lap("Queried the smart dial table for contact names");
}
if (nameCursor != null) {
try {
/** 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);
}
sInUpdate.getAndSet(false);
final SharedPreferences.Editor editor = databaseLastUpdateSharedPref.edit();
editor.putLong(LAST_UPDATED_MILLIS, currentMillis);
editor.commit();
}
}
/**
* 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 = sInUpdate.get();
if (inUpdate) {
return Lists.newArrayList();
}
final SQLiteDatabase db = getReadableDatabase();
/** Uses SQL query wildcard '%' to represent prefix matching.*/
final String looseQuery = query + "%";
final ArrayList<ContactNumber> result = Lists.newArrayList();
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 +
" 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 (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;
if (DEBUG) {
stopWatch.lap("Found column IDs");
}
final Set<ContactMatch> duplicates = new HashSet<ContactMatch>();
int counter = 0;
try {
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);
/** 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));
counter++;
if (DEBUG) {
stopWatch.lap("Added one result");
}
}
}
if (DEBUG) {
stopWatch.stopAndLog(TAG + "Finished loading cursor", 0);
}
} finally {
cursor.close();
}
return result;
}
}