blob: 39c3d5f1c91ef00bde9d7099a3e7b65948715924 [file] [log] [blame]
/*
* Copyright (C) 2015 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.providers.contacts;
import android.annotation.Nullable;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.provider.CallLog.Calls;
import android.provider.VoicemailContract;
import android.provider.VoicemailContract.Status;
import android.provider.VoicemailContract.Voicemails;
import android.text.TextUtils;
import android.util.ArraySet;
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
import com.android.providers.contacts.util.PropertyUtils;
/**
* SQLite database (helper) for {@link CallLogProvider} and {@link VoicemailContentProvider}.
*/
public class CallLogDatabaseHelper {
private static final String TAG = "CallLogDatabaseHelper";
private static final int DATABASE_VERSION = 8;
private static final boolean DEBUG = false; // DON'T SUBMIT WITH TRUE
private static final String DATABASE_NAME = "calllog.db";
private static final String SHADOW_DATABASE_NAME = "calllog_shadow.db";
private static final int IDLE_CONNECTION_TIMEOUT_MS = 30000;
private static CallLogDatabaseHelper sInstance;
/** Instance for the "shadow" provider. */
private static CallLogDatabaseHelper sInstanceForShadow;
private final Context mContext;
private final OpenHelper mOpenHelper;
public interface Tables {
String CALLS = "calls";
String VOICEMAIL_STATUS = "voicemail_status";
}
public interface DbProperties {
String CALL_LOG_LAST_SYNCED = "call_log_last_synced";
String CALL_LOG_LAST_SYNCED_FOR_SHADOW = "call_log_last_synced_for_shadow";
String DATA_MIGRATED = "migrated";
}
/**
* Constants used in the contacts DB helper, which are needed for migration.
*
* DO NOT CHANCE ANY OF THE CONSTANTS.
*/
private interface LegacyConstants {
/** Table name used in the contacts DB.*/
String CALLS_LEGACY = "calls";
/** Table name used in the contacts DB.*/
String VOICEMAIL_STATUS_LEGACY = "voicemail_status";
/** Prop name used in the contacts DB.*/
String CALL_LOG_LAST_SYNCED_LEGACY = "call_log_last_synced";
}
private final class OpenHelper extends SQLiteOpenHelper {
public OpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory,
int version) {
super(context, name, factory, version);
// Memory optimization - close idle connections after 30s of inactivity
setIdleConnectionTimeout(IDLE_CONNECTION_TIMEOUT_MS);
}
@Override
public void onCreate(SQLiteDatabase db) {
if (DEBUG) {
Log.d(TAG, "onCreate");
}
PropertyUtils.createPropertiesTable(db);
// *** NOTE ABOUT CHANGING THE DB SCHEMA ***
//
// The CALLS and VOICEMAIL_STATUS table used to be in the contacts2.db. So we need to
// migrate from these legacy tables, if exist, after creating the calllog DB, which is
// done in migrateFromLegacyTables().
//
// This migration is slightly different from a regular upgrade step, because it's always
// performed from the legacy schema (of the latest version -- because the migration
// source is always the latest DB after all the upgrade steps) to the *latest* schema
// at once.
//
// This means certain kind of changes are not doable without changing the
// migration logic. For example, if you rename a column in the DB, the migration step
// will need to be updated to handle the column name change.
db.execSQL("CREATE TABLE " + Tables.CALLS + " (" +
Calls._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
Calls.NUMBER + " TEXT," +
Calls.NUMBER_PRESENTATION + " INTEGER NOT NULL DEFAULT " +
Calls.PRESENTATION_ALLOWED + "," +
Calls.POST_DIAL_DIGITS + " TEXT NOT NULL DEFAULT ''," +
Calls.VIA_NUMBER + " TEXT NOT NULL DEFAULT ''," +
Calls.DATE + " INTEGER," +
Calls.DURATION + " INTEGER," +
Calls.DATA_USAGE + " INTEGER," +
Calls.TYPE + " INTEGER," +
Calls.FEATURES + " INTEGER NOT NULL DEFAULT 0," +
Calls.PHONE_ACCOUNT_COMPONENT_NAME + " TEXT," +
Calls.PHONE_ACCOUNT_ID + " TEXT," +
Calls.PHONE_ACCOUNT_ADDRESS + " TEXT," +
Calls.PHONE_ACCOUNT_HIDDEN + " INTEGER NOT NULL DEFAULT 0," +
Calls.SUB_ID + " INTEGER DEFAULT -1," +
Calls.NEW + " INTEGER," +
Calls.CACHED_NAME + " TEXT," +
Calls.CACHED_NUMBER_TYPE + " INTEGER," +
Calls.CACHED_NUMBER_LABEL + " TEXT," +
Calls.COUNTRY_ISO + " TEXT," +
Calls.VOICEMAIL_URI + " TEXT," +
Calls.IS_READ + " INTEGER," +
Calls.GEOCODED_LOCATION + " TEXT," +
Calls.CACHED_LOOKUP_URI + " TEXT," +
Calls.CACHED_MATCHED_NUMBER + " TEXT," +
Calls.CACHED_NORMALIZED_NUMBER + " TEXT," +
Calls.CACHED_PHOTO_ID + " INTEGER NOT NULL DEFAULT 0," +
Calls.CACHED_PHOTO_URI + " TEXT," +
Calls.CACHED_FORMATTED_NUMBER + " TEXT," +
Calls.ADD_FOR_ALL_USERS + " INTEGER NOT NULL DEFAULT 1," +
Calls.LAST_MODIFIED + " INTEGER DEFAULT 0," +
Calls.CALL_SCREENING_COMPONENT_NAME + " TEXT," +
Calls.CALL_SCREENING_APP_NAME + " TEXT," +
Calls.BLOCK_REASON + " INTEGER NOT NULL DEFAULT 0," +
Voicemails._DATA + " TEXT," +
Voicemails.HAS_CONTENT + " INTEGER," +
Voicemails.MIME_TYPE + " TEXT," +
Voicemails.SOURCE_DATA + " TEXT," +
Voicemails.SOURCE_PACKAGE + " TEXT," +
Voicemails.TRANSCRIPTION + " TEXT," +
Voicemails.TRANSCRIPTION_STATE + " INTEGER NOT NULL DEFAULT 0," +
Voicemails.STATE + " INTEGER," +
Voicemails.DIRTY + " INTEGER NOT NULL DEFAULT 0," +
Voicemails.DELETED + " INTEGER NOT NULL DEFAULT 0," +
Voicemails.BACKED_UP + " INTEGER NOT NULL DEFAULT 0," +
Voicemails.RESTORED + " INTEGER NOT NULL DEFAULT 0," +
Voicemails.ARCHIVED + " INTEGER NOT NULL DEFAULT 0," +
Voicemails.IS_OMTP_VOICEMAIL + " INTEGER NOT NULL DEFAULT 0" +
");");
db.execSQL("CREATE TABLE " + Tables.VOICEMAIL_STATUS + " (" +
VoicemailContract.Status._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
VoicemailContract.Status.SOURCE_PACKAGE + " TEXT NOT NULL," +
VoicemailContract.Status.PHONE_ACCOUNT_COMPONENT_NAME + " TEXT," +
VoicemailContract.Status.PHONE_ACCOUNT_ID + " TEXT," +
VoicemailContract.Status.SETTINGS_URI + " TEXT," +
VoicemailContract.Status.VOICEMAIL_ACCESS_URI + " TEXT," +
VoicemailContract.Status.CONFIGURATION_STATE + " INTEGER," +
VoicemailContract.Status.DATA_CHANNEL_STATE + " INTEGER," +
VoicemailContract.Status.NOTIFICATION_CHANNEL_STATE + " INTEGER," +
VoicemailContract.Status.QUOTA_OCCUPIED + " INTEGER DEFAULT -1," +
VoicemailContract.Status.QUOTA_TOTAL + " INTEGER DEFAULT -1," +
VoicemailContract.Status.SOURCE_TYPE + " TEXT" +
");");
migrateFromLegacyTables(db);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
if (DEBUG) {
Log.d(TAG, "onUpgrade");
}
if (oldVersion < 2) {
upgradeToVersion2(db);
}
if (oldVersion < 3) {
upgradeToVersion3(db);
}
if (oldVersion < 4) {
upgradeToVersion4(db);
}
if (oldVersion < 5) {
upgradeToVersion5(db);
}
if (oldVersion < 6) {
upgradeToVersion6(db);
}
if (oldVersion < 7) {
upgradeToVersion7(db);
}
if (oldVersion < 8) {
upgradetoVersion8(db);
}
}
}
@VisibleForTesting
CallLogDatabaseHelper(Context context, String databaseName) {
mContext = context;
mOpenHelper = new OpenHelper(mContext, databaseName, /* factory=*/ null, DATABASE_VERSION);
}
public static synchronized CallLogDatabaseHelper getInstance(Context context) {
if (sInstance == null) {
sInstance = new CallLogDatabaseHelper(context, DATABASE_NAME);
}
return sInstance;
}
public static synchronized CallLogDatabaseHelper getInstanceForShadow(Context context) {
if (sInstanceForShadow == null) {
// Shadow provider is always encryption-aware.
sInstanceForShadow = new CallLogDatabaseHelper(
context.createDeviceProtectedStorageContext(), SHADOW_DATABASE_NAME);
}
return sInstanceForShadow;
}
public SQLiteDatabase getReadableDatabase() {
return mOpenHelper.getReadableDatabase();
}
public SQLiteDatabase getWritableDatabase() {
return mOpenHelper.getWritableDatabase();
}
public String getProperty(String key, String defaultValue) {
return PropertyUtils.getProperty(getReadableDatabase(), key, defaultValue);
}
public void setProperty(String key, String value) {
PropertyUtils.setProperty(getWritableDatabase(), key, value);
}
/**
* Add the {@link Calls.VIA_NUMBER} Column to the CallLog Database.
*/
private void upgradeToVersion2(SQLiteDatabase db) {
db.execSQL("ALTER TABLE " + Tables.CALLS + " ADD " + Calls.VIA_NUMBER +
" TEXT NOT NULL DEFAULT ''");
}
/**
* Add the {@link Status.SOURCE_TYPE} Column to the VoicemailStatus Database.
*/
private void upgradeToVersion3(SQLiteDatabase db) {
db.execSQL("ALTER TABLE " + Tables.VOICEMAIL_STATUS + " ADD " + Status.SOURCE_TYPE +
" TEXT");
}
/**
* Add {@link Voicemails.BACKED_UP} {@link Voicemails.ARCHIVE} {@link
* Voicemails.IS_OMTP_VOICEMAIL} column to the CallLog database.
*/
private void upgradeToVersion4(SQLiteDatabase db) {
db.execSQL("ALTER TABLE calls ADD backed_up INTEGER NOT NULL DEFAULT 0");
db.execSQL("ALTER TABLE calls ADD restored INTEGER NOT NULL DEFAULT 0");
db.execSQL("ALTER TABLE calls ADD archived INTEGER NOT NULL DEFAULT 0");
db.execSQL("ALTER TABLE calls ADD is_omtp_voicemail INTEGER NOT NULL DEFAULT 0");
}
/**
* Add {@link Voicemails.TRANSCRIPTION_STATE} column to the CallLog database.
*/
private void upgradeToVersion5(SQLiteDatabase db) {
db.execSQL("ALTER TABLE calls ADD transcription_state INTEGER NOT NULL DEFAULT 0");
}
/**
* Add {@link CallLog.Calls#CALL_SCREENING_COMPONENT_NAME}
* {@link CallLog.Calls#CALL_SCREENING_APP_NAME}
* {@link CallLog.Calls#BLOCK_REASON} column to the CallLog database.
*/
private void upgradeToVersion6(SQLiteDatabase db) {
db.execSQL("ALTER TABLE calls ADD call_screening_component_name TEXT");
db.execSQL("ALTER TABLE calls ADD call_screening_app_name TEXT");
db.execSQL("ALTER TABLE calls ADD block_reason INTEGER NOT NULL DEFAULT 0");
}
/**
* Add {@code android.telecom.CallIdentification} columns; these are destined to be removed
* in {@link #upgradetoVersion8(SQLiteDatabase)}.
* @param db DB to upgrade
*/
private void upgradeToVersion7(SQLiteDatabase db) {
db.execSQL("ALTER TABLE calls ADD call_id_package_name TEXT NULL");
db.execSQL("ALTER TABLE calls ADD call_id_app_name TEXT NULL");
db.execSQL("ALTER TABLE calls ADD call_id_name TEXT NULL");
db.execSQL("ALTER TABLE calls ADD call_id_description TEXT NULL");
db.execSQL("ALTER TABLE calls ADD call_id_details TEXT NULL");
db.execSQL("ALTER TABLE calls ADD call_id_nuisance_confidence INTEGER NULL");
}
/**
* Remove the {@code android.telecom.CallIdentification} column.
* @param db DB to upgrade
*/
private void upgradetoVersion8(SQLiteDatabase db) {
db.beginTransaction();
try {
String oldTable = Tables.CALLS + "_old";
// SQLite3 doesn't support altering a column name, so we'll rename the old calls table..
db.execSQL("ALTER TABLE calls RENAME TO " + oldTable);
// ... create a new one (yes, this seems similar to what is in onCreate, but we can't
// assume that one won't change in the future) ...
db.execSQL("CREATE TABLE " + Tables.CALLS + " (" +
Calls._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
Calls.NUMBER + " TEXT," +
Calls.NUMBER_PRESENTATION + " INTEGER NOT NULL DEFAULT " +
Calls.PRESENTATION_ALLOWED + "," +
Calls.POST_DIAL_DIGITS + " TEXT NOT NULL DEFAULT ''," +
Calls.VIA_NUMBER + " TEXT NOT NULL DEFAULT ''," +
Calls.DATE + " INTEGER," +
Calls.DURATION + " INTEGER," +
Calls.DATA_USAGE + " INTEGER," +
Calls.TYPE + " INTEGER," +
Calls.FEATURES + " INTEGER NOT NULL DEFAULT 0," +
Calls.PHONE_ACCOUNT_COMPONENT_NAME + " TEXT," +
Calls.PHONE_ACCOUNT_ID + " TEXT," +
Calls.PHONE_ACCOUNT_ADDRESS + " TEXT," +
Calls.PHONE_ACCOUNT_HIDDEN + " INTEGER NOT NULL DEFAULT 0," +
Calls.SUB_ID + " INTEGER DEFAULT -1," +
Calls.NEW + " INTEGER," +
Calls.CACHED_NAME + " TEXT," +
Calls.CACHED_NUMBER_TYPE + " INTEGER," +
Calls.CACHED_NUMBER_LABEL + " TEXT," +
Calls.COUNTRY_ISO + " TEXT," +
Calls.VOICEMAIL_URI + " TEXT," +
Calls.IS_READ + " INTEGER," +
Calls.GEOCODED_LOCATION + " TEXT," +
Calls.CACHED_LOOKUP_URI + " TEXT," +
Calls.CACHED_MATCHED_NUMBER + " TEXT," +
Calls.CACHED_NORMALIZED_NUMBER + " TEXT," +
Calls.CACHED_PHOTO_ID + " INTEGER NOT NULL DEFAULT 0," +
Calls.CACHED_PHOTO_URI + " TEXT," +
Calls.CACHED_FORMATTED_NUMBER + " TEXT," +
Calls.ADD_FOR_ALL_USERS + " INTEGER NOT NULL DEFAULT 1," +
Calls.LAST_MODIFIED + " INTEGER DEFAULT 0," +
Calls.CALL_SCREENING_COMPONENT_NAME + " TEXT," +
Calls.CALL_SCREENING_APP_NAME + " TEXT," +
Calls.BLOCK_REASON + " INTEGER NOT NULL DEFAULT 0," +
Voicemails._DATA + " TEXT," +
Voicemails.HAS_CONTENT + " INTEGER," +
Voicemails.MIME_TYPE + " TEXT," +
Voicemails.SOURCE_DATA + " TEXT," +
Voicemails.SOURCE_PACKAGE + " TEXT," +
Voicemails.TRANSCRIPTION + " TEXT," +
Voicemails.TRANSCRIPTION_STATE + " INTEGER NOT NULL DEFAULT 0," +
Voicemails.STATE + " INTEGER," +
Voicemails.DIRTY + " INTEGER NOT NULL DEFAULT 0," +
Voicemails.DELETED + " INTEGER NOT NULL DEFAULT 0," +
Voicemails.BACKED_UP + " INTEGER NOT NULL DEFAULT 0," +
Voicemails.RESTORED + " INTEGER NOT NULL DEFAULT 0," +
Voicemails.ARCHIVED + " INTEGER NOT NULL DEFAULT 0," +
Voicemails.IS_OMTP_VOICEMAIL + " INTEGER NOT NULL DEFAULT 0" +
");");
String allTheColumns = Calls._ID + ", " +
Calls.NUMBER + ", " +
Calls.NUMBER_PRESENTATION + ", " +
Calls.POST_DIAL_DIGITS + ", " +
Calls.VIA_NUMBER + ", " +
Calls.DATE + ", " +
Calls.DURATION + ", " +
Calls.DATA_USAGE + ", " +
Calls.TYPE + ", " +
Calls.FEATURES + ", " +
Calls.PHONE_ACCOUNT_COMPONENT_NAME + ", " +
Calls.PHONE_ACCOUNT_ID + ", " +
Calls.PHONE_ACCOUNT_ADDRESS + ", " +
Calls.PHONE_ACCOUNT_HIDDEN + ", " +
Calls.SUB_ID + ", " +
Calls.NEW + ", " +
Calls.CACHED_NAME + ", " +
Calls.CACHED_NUMBER_TYPE + ", " +
Calls.CACHED_NUMBER_LABEL + ", " +
Calls.COUNTRY_ISO + ", " +
Calls.VOICEMAIL_URI + ", " +
Calls.IS_READ + ", " +
Calls.GEOCODED_LOCATION + ", " +
Calls.CACHED_LOOKUP_URI + ", " +
Calls.CACHED_MATCHED_NUMBER + ", " +
Calls.CACHED_NORMALIZED_NUMBER + ", " +
Calls.CACHED_PHOTO_ID + ", " +
Calls.CACHED_PHOTO_URI + ", " +
Calls.CACHED_FORMATTED_NUMBER + ", " +
Calls.ADD_FOR_ALL_USERS + ", " +
Calls.LAST_MODIFIED + ", " +
Calls.CALL_SCREENING_COMPONENT_NAME + ", " +
Calls.CALL_SCREENING_APP_NAME + ", " +
Calls.BLOCK_REASON + ", " +
Voicemails._DATA + ", " +
Voicemails.HAS_CONTENT + ", " +
Voicemails.MIME_TYPE + ", " +
Voicemails.SOURCE_DATA + ", " +
Voicemails.SOURCE_PACKAGE + ", " +
Voicemails.TRANSCRIPTION + ", " +
Voicemails.TRANSCRIPTION_STATE + ", " +
Voicemails.STATE + ", " +
Voicemails.DIRTY + ", " +
Voicemails.DELETED + ", " +
Voicemails.BACKED_UP + ", " +
Voicemails.RESTORED + ", " +
Voicemails.ARCHIVED + ", " +
Voicemails.IS_OMTP_VOICEMAIL;
// .. so we insert into the new table all the values from the old table ...
db.execSQL("INSERT INTO " + Tables.CALLS + " (" +
allTheColumns + ") SELECT " +
allTheColumns + " FROM " + oldTable);
// .. and drop the old table we renamed.
db.execSQL("DROP TABLE " + oldTable);
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
}
/**
* Perform the migration from the contacts2.db (of the latest version) to the current calllog/
* voicemail status tables.
*/
private void migrateFromLegacyTables(SQLiteDatabase calllog) {
final SQLiteDatabase contacts = getContactsWritableDatabaseForMigration();
if (contacts == null) {
Log.w(TAG, "Contacts DB == null, skipping migration. (running tests?)");
return;
}
if (DEBUG) {
Log.d(TAG, "migrateFromLegacyTables");
}
if ("1".equals(PropertyUtils.getProperty(calllog, DbProperties.DATA_MIGRATED, ""))) {
return;
}
Log.i(TAG, "Migrating from old tables...");
contacts.beginTransaction();
try {
if (!tableExists(contacts, LegacyConstants.CALLS_LEGACY)
|| !tableExists(contacts, LegacyConstants.VOICEMAIL_STATUS_LEGACY)) {
// This is fine on new devices. (or after a "clear data".)
Log.i(TAG, "Source tables don't exist.");
return;
}
calllog.beginTransaction();
try {
final ContentValues cv = new ContentValues();
try (Cursor source = contacts.rawQuery(
"SELECT * FROM " + LegacyConstants.CALLS_LEGACY, null)) {
while (source.moveToNext()) {
cv.clear();
DatabaseUtils.cursorRowToContentValues(source, cv);
calllog.insertOrThrow(Tables.CALLS, null, cv);
}
}
try (Cursor source = contacts.rawQuery("SELECT * FROM " +
LegacyConstants.VOICEMAIL_STATUS_LEGACY, null)) {
while (source.moveToNext()) {
cv.clear();
DatabaseUtils.cursorRowToContentValues(source, cv);
calllog.insertOrThrow(Tables.VOICEMAIL_STATUS, null, cv);
}
}
contacts.execSQL("DROP TABLE " + LegacyConstants.CALLS_LEGACY + ";");
contacts.execSQL("DROP TABLE " + LegacyConstants.VOICEMAIL_STATUS_LEGACY + ";");
// Also copy the last sync time.
PropertyUtils.setProperty(calllog, DbProperties.CALL_LOG_LAST_SYNCED,
PropertyUtils.getProperty(contacts,
LegacyConstants.CALL_LOG_LAST_SYNCED_LEGACY, null));
Log.i(TAG, "Migration completed.");
calllog.setTransactionSuccessful();
} finally {
calllog.endTransaction();
}
contacts.setTransactionSuccessful();
} catch (RuntimeException e) {
// We don't want to be stuck here, so we just swallow exceptions...
Log.w(TAG, "Exception caught during migration", e);
} finally {
contacts.endTransaction();
}
PropertyUtils.setProperty(calllog, DbProperties.DATA_MIGRATED, "1");
}
@VisibleForTesting
static boolean tableExists(SQLiteDatabase db, String table) {
return DatabaseUtils.longForQuery(db,
"select count(*) from sqlite_master where type='table' and name=?",
new String[] {table}) > 0;
}
@VisibleForTesting
@Nullable // We return null during tests when migration is not needed.
SQLiteDatabase getContactsWritableDatabaseForMigration() {
return ContactsDatabaseHelper.getInstance(mContext).getWritableDatabase();
}
public ArraySet<String> selectDistinctColumn(String table, String column) {
final ArraySet<String> ret = new ArraySet<>();
final SQLiteDatabase db = getReadableDatabase();
final Cursor c = db.rawQuery("SELECT DISTINCT "
+ column
+ " FROM " + table, null);
try {
c.moveToPosition(-1);
while (c.moveToNext()) {
if (c.isNull(0)) {
continue;
}
final String s = c.getString(0);
if (!TextUtils.isEmpty(s)) {
ret.add(s);
}
}
return ret;
} finally {
c.close();
}
}
@VisibleForTesting
void closeForTest() {
mOpenHelper.close();
}
public void wipeForTest() {
getWritableDatabase().execSQL("DELETE FROM " + Tables.CALLS);
}
}