| /* |
| * 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; |
| } |
| } |