1320 lines
45 KiB
Java
1320 lines
45 KiB
Java
/*
|
|
* 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.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.support.annotation.WorkerThread;
|
|
import android.text.TextUtils;
|
|
import com.android.contacts.common.util.StopWatch;
|
|
import com.android.dialer.common.LogUtil;
|
|
import com.android.dialer.common.concurrent.DefaultFutureCallback;
|
|
import com.android.dialer.common.concurrent.DialerExecutorComponent;
|
|
import com.android.dialer.common.concurrent.DialerFutureSerializer;
|
|
import com.android.dialer.common.database.Selection;
|
|
import com.android.dialer.configprovider.ConfigProviderComponent;
|
|
import com.android.dialer.contacts.resources.R;
|
|
import com.android.dialer.database.FilteredNumberContract.FilteredNumberColumns;
|
|
import com.android.dialer.smartdial.util.SmartDialNameMatcher;
|
|
import com.android.dialer.smartdial.util.SmartDialPrefix;
|
|
import com.android.dialer.util.PermissionsUtil;
|
|
import com.google.common.util.concurrent.Futures;
|
|
import com.google.common.util.concurrent.MoreExecutors;
|
|
import java.util.ArrayList;
|
|
import java.util.HashSet;
|
|
import java.util.Objects;
|
|
import java.util.Set;
|
|
|
|
/**
|
|
* 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";
|
|
|
|
@VisibleForTesting
|
|
static final String DEFAULT_LAST_UPDATED_CONFIG_KEY = "smart_dial_default_last_update_millis";
|
|
|
|
private static final String DATABASE_VERSION_PROPERTY = "database_version";
|
|
private static final int MAX_ENTRIES = 20;
|
|
|
|
private final Context context;
|
|
private final DialerFutureSerializer dialerFutureSerializer = new DialerFutureSerializer();
|
|
|
|
private boolean isTestInstance = false;
|
|
|
|
protected DialerDatabaseHelper(Context context, String databaseName, int dbVersion) {
|
|
super(context, databaseName, null, dbVersion);
|
|
this.context = Objects.requireNonNull(context, "Context must not be null");
|
|
}
|
|
|
|
public void setIsTestInstance(boolean isTestInstance) {
|
|
this.isTestInstance = 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 (!isTestInstance) {
|
|
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 =
|
|
context.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.
|
|
*
|
|
* @see #updateSmartDialDatabase(boolean) for the usage of {@code forceUpdate}.
|
|
*/
|
|
public void startSmartDialUpdateThread(boolean forceUpdate) {
|
|
if (PermissionsUtil.hasContactsReadPermissions(context)) {
|
|
Futures.addCallback(
|
|
// Serialize calls to updateSmartDialDatabase. Use FutureSerializer instead of
|
|
// synchronizing on the method to prevent deadlocking thread pool. FutureSerializer
|
|
// provides the guarantee that the next AsyncCallable won't even be submitted until the
|
|
// ListenableFuture returned by the previous one completes. See a bug.
|
|
dialerFutureSerializer.submit(
|
|
() -> {
|
|
updateSmartDialDatabase(forceUpdate);
|
|
return null;
|
|
},
|
|
DialerExecutorComponent.get(context).backgroundExecutor()),
|
|
new DefaultFutureCallback<>(),
|
|
MoreExecutors.directExecutor());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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 lastUpdatedTimeMillis the last time at which an update to the smart dial database was
|
|
* run.
|
|
*/
|
|
private void removeDeletedContacts(SQLiteDatabase db, String lastUpdatedTimeMillis) {
|
|
Cursor deletedContactCursor = getDeletedContactCursor(lastUpdatedTimeMillis);
|
|
|
|
if (deletedContactCursor == null) {
|
|
return;
|
|
}
|
|
|
|
db.beginTransaction();
|
|
try {
|
|
if (!deletedContactCursor.moveToFirst()) {
|
|
return;
|
|
}
|
|
|
|
do {
|
|
if (deletedContactCursor.isNull(DeleteContactQuery.DELETED_CONTACT_ID)) {
|
|
LogUtil.i(
|
|
"DialerDatabaseHelper.removeDeletedContacts",
|
|
"contact_id column null. Row was deleted during iteration, skipping");
|
|
continue;
|
|
}
|
|
|
|
long deleteContactId = deletedContactCursor.getLong(DeleteContactQuery.DELETED_CONTACT_ID);
|
|
|
|
Selection smartDialSelection =
|
|
Selection.column(SmartDialDbColumns.CONTACT_ID).is("=", deleteContactId);
|
|
db.delete(
|
|
Tables.SMARTDIAL_TABLE,
|
|
smartDialSelection.getSelection(),
|
|
smartDialSelection.getSelectionArgs());
|
|
|
|
Selection prefixSelection =
|
|
Selection.column(PrefixColumns.CONTACT_ID).is("=", deleteContactId);
|
|
db.delete(
|
|
Tables.PREFIX_TABLE,
|
|
prefixSelection.getSelection(),
|
|
prefixSelection.getSelectionArgs());
|
|
} while (deletedContactCursor.moveToNext());
|
|
|
|
db.setTransactionSuccessful();
|
|
} finally {
|
|
deletedContactCursor.close();
|
|
db.endTransaction();
|
|
}
|
|
}
|
|
|
|
private Cursor getDeletedContactCursor(String lastUpdateMillis) {
|
|
return context
|
|
.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()) {
|
|
if (updatedContactCursor.isNull(UpdatedContactQuery.UPDATED_CONTACT_ID)) {
|
|
LogUtil.i(
|
|
"DialerDatabaseHelper.removeUpdatedContacts",
|
|
"contact_id column null. Row was deleted during iteration, skipping");
|
|
continue;
|
|
}
|
|
|
|
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();
|
|
|
|
if (updatedContactCursor.isNull(PhoneQuery.PHONE_ID)) {
|
|
LogUtil.i(
|
|
"DialerDatabaseHelper.insertUpdatedContactsAndNumberPrefix",
|
|
"_id column null. Row was deleted during iteration, skipping");
|
|
continue;
|
|
}
|
|
|
|
// 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, context.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(context, 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()) {
|
|
if (nameCursor.isNull(columnIndexContactId)) {
|
|
LogUtil.i(
|
|
"DialerDatabaseHelper.insertNamePrefixes",
|
|
"contact_id column null. Row was deleted during iteration, skipping");
|
|
continue;
|
|
}
|
|
|
|
/** Computes a list of prefixes of a given contact name. */
|
|
final ArrayList<String> namePrefixes =
|
|
SmartDialPrefix.generateNamePrefixes(context, 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.
|
|
*
|
|
* @param forceUpdate If set to true, update the database by reloading all contacts.
|
|
*/
|
|
@WorkerThread
|
|
public void updateSmartDialDatabase(boolean forceUpdate) {
|
|
LogUtil.enterBlock("DialerDatabaseHelper.updateSmartDialDatabase");
|
|
|
|
final SQLiteDatabase db = getWritableDatabase();
|
|
|
|
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 =
|
|
context.getSharedPreferences(DATABASE_LAST_CREATED_SHARED_PREF, Context.MODE_PRIVATE);
|
|
|
|
long defaultLastUpdateMillis =
|
|
ConfigProviderComponent.get(context)
|
|
.getConfigProvider()
|
|
.getLong(DEFAULT_LAST_UPDATED_CONFIG_KEY, 0);
|
|
|
|
long sharedPrefLastUpdateMillis =
|
|
databaseLastUpdateSharedPref.getLong(LAST_UPDATED_MILLIS, defaultLastUpdateMillis);
|
|
|
|
final String lastUpdateMillis = String.valueOf(forceUpdate ? 0 : sharedPrefLastUpdateMillis);
|
|
|
|
LogUtil.i(
|
|
"DialerDatabaseHelper.updateSmartDialDatabase", "last updated at %s", 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");
|
|
}
|
|
|
|
/** Removes contacts that have been deleted. */
|
|
removeDeletedContacts(db, 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 =
|
|
context
|
|
.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 =
|
|
context
|
|
.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
|
|
+ " = "
|
|
+ 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);
|
|
}
|
|
|
|
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(context.getPackageName());
|
|
context.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.
|
|
*/
|
|
@WorkerThread
|
|
public synchronized ArrayList<ContactNumber> getLooseMatches(
|
|
String query, SmartDialNameMatcher nameMatcher) {
|
|
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)) {
|
|
if (cursor.isNull(columnDataId)) {
|
|
LogUtil.i(
|
|
"DialerDatabaseHelper.getLooseMatches",
|
|
"_id column null. Row was deleted during iteration, skipping");
|
|
continue;
|
|
}
|
|
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(context, displayName);
|
|
final boolean numberMatches =
|
|
(nameMatcher.matchesNumber(context, 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 a bug)
|
|
*/
|
|
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;
|
|
}
|
|
}
|
|
}
|