1 /* 2 * Copyright (C) 2015 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License 15 */ 16 package com.android.providers.contacts; 17 18 import android.annotation.Nullable; 19 import android.content.ContentValues; 20 import android.content.Context; 21 import android.database.Cursor; 22 import android.database.DatabaseUtils; 23 import android.database.sqlite.SQLiteDatabase; 24 import android.database.sqlite.SQLiteOpenHelper; 25 import android.provider.CallLog.Calls; 26 import android.provider.VoicemailContract; 27 import android.provider.VoicemailContract.Status; 28 import android.provider.VoicemailContract.Voicemails; 29 import android.text.TextUtils; 30 import android.util.ArraySet; 31 import android.util.Log; 32 33 import com.android.internal.annotations.VisibleForTesting; 34 import com.android.providers.contacts.util.PropertyUtils; 35 36 /** 37 * SQLite database (helper) for {@link CallLogProvider} and {@link VoicemailContentProvider}. 38 */ 39 public class CallLogDatabaseHelper { 40 private static final String TAG = "CallLogDatabaseHelper"; 41 42 private static final int DATABASE_VERSION = 5; 43 44 private static final boolean DEBUG = false; // DON'T SUBMIT WITH TRUE 45 46 private static final String DATABASE_NAME = "calllog.db"; 47 48 private static final String SHADOW_DATABASE_NAME = "calllog_shadow.db"; 49 50 private static final int IDLE_CONNECTION_TIMEOUT_MS = 30000; 51 52 private static CallLogDatabaseHelper sInstance; 53 54 /** Instance for the "shadow" provider. */ 55 private static CallLogDatabaseHelper sInstanceForShadow; 56 57 private final Context mContext; 58 59 private final OpenHelper mOpenHelper; 60 61 public interface Tables { 62 String CALLS = "calls"; 63 String VOICEMAIL_STATUS = "voicemail_status"; 64 } 65 66 public interface DbProperties { 67 String CALL_LOG_LAST_SYNCED = "call_log_last_synced"; 68 String CALL_LOG_LAST_SYNCED_FOR_SHADOW = "call_log_last_synced_for_shadow"; 69 String DATA_MIGRATED = "migrated"; 70 } 71 72 /** 73 * Constants used in the contacts DB helper, which are needed for migration. 74 * 75 * DO NOT CHANCE ANY OF THE CONSTANTS. 76 */ 77 private interface LegacyConstants { 78 /** Table name used in the contacts DB.*/ 79 String CALLS_LEGACY = "calls"; 80 81 /** Table name used in the contacts DB.*/ 82 String VOICEMAIL_STATUS_LEGACY = "voicemail_status"; 83 84 /** Prop name used in the contacts DB.*/ 85 String CALL_LOG_LAST_SYNCED_LEGACY = "call_log_last_synced"; 86 } 87 88 private final class OpenHelper extends SQLiteOpenHelper { OpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version)89 public OpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, 90 int version) { 91 super(context, name, factory, version); 92 // Memory optimization - close idle connections after 30s of inactivity 93 setIdleConnectionTimeout(IDLE_CONNECTION_TIMEOUT_MS); 94 } 95 96 @Override onCreate(SQLiteDatabase db)97 public void onCreate(SQLiteDatabase db) { 98 if (DEBUG) { 99 Log.d(TAG, "onCreate"); 100 } 101 102 PropertyUtils.createPropertiesTable(db); 103 104 // *** NOTE ABOUT CHANGING THE DB SCHEMA *** 105 // 106 // The CALLS and VOICEMAIL_STATUS table used to be in the contacts2.db. So we need to 107 // migrate from these legacy tables, if exist, after creating the calllog DB, which is 108 // done in migrateFromLegacyTables(). 109 // 110 // This migration is slightly different from a regular upgrade step, because it's always 111 // performed from the legacy schema (of the latest version -- because the migration 112 // source is always the latest DB after all the upgrade steps) to the *latest* schema 113 // at once. 114 // 115 // This means certain kind of changes are not doable without changing the 116 // migration logic. For example, if you rename a column in the DB, the migration step 117 // will need to be updated to handle the column name change. 118 119 db.execSQL("CREATE TABLE " + Tables.CALLS + " (" + 120 Calls._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + 121 Calls.NUMBER + " TEXT," + 122 Calls.NUMBER_PRESENTATION + " INTEGER NOT NULL DEFAULT " + 123 Calls.PRESENTATION_ALLOWED + "," + 124 Calls.POST_DIAL_DIGITS + " TEXT NOT NULL DEFAULT ''," + 125 Calls.VIA_NUMBER + " TEXT NOT NULL DEFAULT ''," + 126 Calls.DATE + " INTEGER," + 127 Calls.DURATION + " INTEGER," + 128 Calls.DATA_USAGE + " INTEGER," + 129 Calls.TYPE + " INTEGER," + 130 Calls.FEATURES + " INTEGER NOT NULL DEFAULT 0," + 131 Calls.PHONE_ACCOUNT_COMPONENT_NAME + " TEXT," + 132 Calls.PHONE_ACCOUNT_ID + " TEXT," + 133 Calls.PHONE_ACCOUNT_ADDRESS + " TEXT," + 134 Calls.PHONE_ACCOUNT_HIDDEN + " INTEGER NOT NULL DEFAULT 0," + 135 Calls.SUB_ID + " INTEGER DEFAULT -1," + 136 Calls.NEW + " INTEGER," + 137 Calls.CACHED_NAME + " TEXT," + 138 Calls.CACHED_NUMBER_TYPE + " INTEGER," + 139 Calls.CACHED_NUMBER_LABEL + " TEXT," + 140 Calls.COUNTRY_ISO + " TEXT," + 141 Calls.VOICEMAIL_URI + " TEXT," + 142 Calls.IS_READ + " INTEGER," + 143 Calls.GEOCODED_LOCATION + " TEXT," + 144 Calls.CACHED_LOOKUP_URI + " TEXT," + 145 Calls.CACHED_MATCHED_NUMBER + " TEXT," + 146 Calls.CACHED_NORMALIZED_NUMBER + " TEXT," + 147 Calls.CACHED_PHOTO_ID + " INTEGER NOT NULL DEFAULT 0," + 148 Calls.CACHED_PHOTO_URI + " TEXT," + 149 Calls.CACHED_FORMATTED_NUMBER + " TEXT," + 150 Calls.ADD_FOR_ALL_USERS + " INTEGER NOT NULL DEFAULT 1," + 151 Calls.LAST_MODIFIED + " INTEGER DEFAULT 0," + 152 Voicemails._DATA + " TEXT," + 153 Voicemails.HAS_CONTENT + " INTEGER," + 154 Voicemails.MIME_TYPE + " TEXT," + 155 Voicemails.SOURCE_DATA + " TEXT," + 156 Voicemails.SOURCE_PACKAGE + " TEXT," + 157 Voicemails.TRANSCRIPTION + " TEXT," + 158 Voicemails.TRANSCRIPTION_STATE + " INTEGER NOT NULL DEFAULT 0," + 159 Voicemails.STATE + " INTEGER," + 160 Voicemails.DIRTY + " INTEGER NOT NULL DEFAULT 0," + 161 Voicemails.DELETED + " INTEGER NOT NULL DEFAULT 0," + 162 Voicemails.BACKED_UP + " INTEGER NOT NULL DEFAULT 0," + 163 Voicemails.RESTORED + " INTEGER NOT NULL DEFAULT 0," + 164 Voicemails.ARCHIVED + " INTEGER NOT NULL DEFAULT 0," + 165 Voicemails.IS_OMTP_VOICEMAIL + " INTEGER NOT NULL DEFAULT 0" + 166 ");"); 167 168 db.execSQL("CREATE TABLE " + Tables.VOICEMAIL_STATUS + " (" + 169 VoicemailContract.Status._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + 170 VoicemailContract.Status.SOURCE_PACKAGE + " TEXT NOT NULL," + 171 VoicemailContract.Status.PHONE_ACCOUNT_COMPONENT_NAME + " TEXT," + 172 VoicemailContract.Status.PHONE_ACCOUNT_ID + " TEXT," + 173 VoicemailContract.Status.SETTINGS_URI + " TEXT," + 174 VoicemailContract.Status.VOICEMAIL_ACCESS_URI + " TEXT," + 175 VoicemailContract.Status.CONFIGURATION_STATE + " INTEGER," + 176 VoicemailContract.Status.DATA_CHANNEL_STATE + " INTEGER," + 177 VoicemailContract.Status.NOTIFICATION_CHANNEL_STATE + " INTEGER," + 178 VoicemailContract.Status.QUOTA_OCCUPIED + " INTEGER DEFAULT -1," + 179 VoicemailContract.Status.QUOTA_TOTAL + " INTEGER DEFAULT -1," + 180 VoicemailContract.Status.SOURCE_TYPE + " TEXT" + 181 ");"); 182 183 migrateFromLegacyTables(db); 184 } 185 186 @Override onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)187 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { 188 if (DEBUG) { 189 Log.d(TAG, "onUpgrade"); 190 } 191 192 if (oldVersion < 2) { 193 upgradeToVersion2(db); 194 } 195 196 if (oldVersion < 3) { 197 upgradeToVersion3(db); 198 } 199 200 if (oldVersion < 4) { 201 upgradeToVersion4(db); 202 } 203 204 if (oldVersion < 5) { 205 upgradeToVersion5(db); 206 } 207 } 208 } 209 210 @VisibleForTesting CallLogDatabaseHelper(Context context, String databaseName)211 CallLogDatabaseHelper(Context context, String databaseName) { 212 mContext = context; 213 mOpenHelper = new OpenHelper(mContext, databaseName, /* factory=*/ null, DATABASE_VERSION); 214 } 215 getInstance(Context context)216 public static synchronized CallLogDatabaseHelper getInstance(Context context) { 217 if (sInstance == null) { 218 sInstance = new CallLogDatabaseHelper(context, DATABASE_NAME); 219 } 220 return sInstance; 221 } 222 getInstanceForShadow(Context context)223 public static synchronized CallLogDatabaseHelper getInstanceForShadow(Context context) { 224 if (sInstanceForShadow == null) { 225 // Shadow provider is always encryption-aware. 226 sInstanceForShadow = new CallLogDatabaseHelper( 227 context.createDeviceProtectedStorageContext(), SHADOW_DATABASE_NAME); 228 } 229 return sInstanceForShadow; 230 } 231 getReadableDatabase()232 public SQLiteDatabase getReadableDatabase() { 233 return mOpenHelper.getReadableDatabase(); 234 } 235 getWritableDatabase()236 public SQLiteDatabase getWritableDatabase() { 237 return mOpenHelper.getWritableDatabase(); 238 } 239 getProperty(String key, String defaultValue)240 public String getProperty(String key, String defaultValue) { 241 return PropertyUtils.getProperty(getReadableDatabase(), key, defaultValue); 242 } 243 setProperty(String key, String value)244 public void setProperty(String key, String value) { 245 PropertyUtils.setProperty(getWritableDatabase(), key, value); 246 } 247 248 /** 249 * Add the {@link Calls.VIA_NUMBER} Column to the CallLog Database. 250 */ upgradeToVersion2(SQLiteDatabase db)251 private void upgradeToVersion2(SQLiteDatabase db) { 252 db.execSQL("ALTER TABLE " + Tables.CALLS + " ADD " + Calls.VIA_NUMBER + 253 " TEXT NOT NULL DEFAULT ''"); 254 } 255 256 /** 257 * Add the {@link Status.SOURCE_TYPE} Column to the VoicemailStatus Database. 258 */ upgradeToVersion3(SQLiteDatabase db)259 private void upgradeToVersion3(SQLiteDatabase db) { 260 db.execSQL("ALTER TABLE " + Tables.VOICEMAIL_STATUS + " ADD " + Status.SOURCE_TYPE + 261 " TEXT"); 262 } 263 264 /** 265 * Add {@link Voicemails.BACKED_UP} {@link Voicemails.ARCHIVE} {@link 266 * Voicemails.IS_OMTP_VOICEMAIL} column to the CallLog database. 267 */ upgradeToVersion4(SQLiteDatabase db)268 private void upgradeToVersion4(SQLiteDatabase db) { 269 db.execSQL("ALTER TABLE calls ADD backed_up INTEGER NOT NULL DEFAULT 0"); 270 db.execSQL("ALTER TABLE calls ADD restored INTEGER NOT NULL DEFAULT 0"); 271 db.execSQL("ALTER TABLE calls ADD archived INTEGER NOT NULL DEFAULT 0"); 272 db.execSQL("ALTER TABLE calls ADD is_omtp_voicemail INTEGER NOT NULL DEFAULT 0"); 273 } 274 275 /** 276 * Add {@link Voicemails.TRANSCRIPTION_STATE} column to the CallLog database. 277 */ upgradeToVersion5(SQLiteDatabase db)278 private void upgradeToVersion5(SQLiteDatabase db) { 279 db.execSQL("ALTER TABLE calls ADD transcription_state INTEGER NOT NULL DEFAULT 0"); 280 } 281 282 /** 283 * Perform the migration from the contacts2.db (of the latest version) to the current calllog/ 284 * voicemail status tables. 285 */ migrateFromLegacyTables(SQLiteDatabase calllog)286 private void migrateFromLegacyTables(SQLiteDatabase calllog) { 287 final SQLiteDatabase contacts = getContactsWritableDatabaseForMigration(); 288 289 if (contacts == null) { 290 Log.w(TAG, "Contacts DB == null, skipping migration. (running tests?)"); 291 return; 292 } 293 if (DEBUG) { 294 Log.d(TAG, "migrateFromLegacyTables"); 295 } 296 297 if ("1".equals(PropertyUtils.getProperty(calllog, DbProperties.DATA_MIGRATED, ""))) { 298 return; 299 } 300 301 Log.i(TAG, "Migrating from old tables..."); 302 303 contacts.beginTransaction(); 304 try { 305 if (!tableExists(contacts, LegacyConstants.CALLS_LEGACY) 306 || !tableExists(contacts, LegacyConstants.VOICEMAIL_STATUS_LEGACY)) { 307 // This is fine on new devices. (or after a "clear data".) 308 Log.i(TAG, "Source tables don't exist."); 309 return; 310 } 311 calllog.beginTransaction(); 312 try { 313 314 final ContentValues cv = new ContentValues(); 315 316 try (Cursor source = contacts.rawQuery( 317 "SELECT * FROM " + LegacyConstants.CALLS_LEGACY, null)) { 318 while (source.moveToNext()) { 319 cv.clear(); 320 321 DatabaseUtils.cursorRowToContentValues(source, cv); 322 323 calllog.insertOrThrow(Tables.CALLS, null, cv); 324 } 325 } 326 327 try (Cursor source = contacts.rawQuery("SELECT * FROM " + 328 LegacyConstants.VOICEMAIL_STATUS_LEGACY, null)) { 329 while (source.moveToNext()) { 330 cv.clear(); 331 332 DatabaseUtils.cursorRowToContentValues(source, cv); 333 334 calllog.insertOrThrow(Tables.VOICEMAIL_STATUS, null, cv); 335 } 336 } 337 338 contacts.execSQL("DROP TABLE " + LegacyConstants.CALLS_LEGACY + ";"); 339 contacts.execSQL("DROP TABLE " + LegacyConstants.VOICEMAIL_STATUS_LEGACY + ";"); 340 341 // Also copy the last sync time. 342 PropertyUtils.setProperty(calllog, DbProperties.CALL_LOG_LAST_SYNCED, 343 PropertyUtils.getProperty(contacts, 344 LegacyConstants.CALL_LOG_LAST_SYNCED_LEGACY, null)); 345 346 Log.i(TAG, "Migration completed."); 347 348 calllog.setTransactionSuccessful(); 349 } finally { 350 calllog.endTransaction(); 351 } 352 353 contacts.setTransactionSuccessful(); 354 } catch (RuntimeException e) { 355 // We don't want to be stuck here, so we just swallow exceptions... 356 Log.w(TAG, "Exception caught during migration", e); 357 } finally { 358 contacts.endTransaction(); 359 } 360 PropertyUtils.setProperty(calllog, DbProperties.DATA_MIGRATED, "1"); 361 } 362 363 @VisibleForTesting tableExists(SQLiteDatabase db, String table)364 static boolean tableExists(SQLiteDatabase db, String table) { 365 return DatabaseUtils.longForQuery(db, 366 "select count(*) from sqlite_master where type='table' and name=?", 367 new String[] {table}) > 0; 368 } 369 370 @VisibleForTesting 371 @Nullable // We return null during tests when migration is not needed. getContactsWritableDatabaseForMigration()372 SQLiteDatabase getContactsWritableDatabaseForMigration() { 373 return ContactsDatabaseHelper.getInstance(mContext).getWritableDatabase(); 374 } 375 selectDistinctColumn(String table, String column)376 public ArraySet<String> selectDistinctColumn(String table, String column) { 377 final ArraySet<String> ret = new ArraySet<>(); 378 final SQLiteDatabase db = getReadableDatabase(); 379 final Cursor c = db.rawQuery("SELECT DISTINCT " 380 + column 381 + " FROM " + table, null); 382 try { 383 c.moveToPosition(-1); 384 while (c.moveToNext()) { 385 if (c.isNull(0)) { 386 continue; 387 } 388 final String s = c.getString(0); 389 390 if (!TextUtils.isEmpty(s)) { 391 ret.add(s); 392 } 393 } 394 return ret; 395 } finally { 396 c.close(); 397 } 398 } 399 400 @VisibleForTesting closeForTest()401 void closeForTest() { 402 mOpenHelper.close(); 403 } 404 wipeForTest()405 public void wipeForTest() { 406 getWritableDatabase().execSQL("DELETE FROM " + Tables.CALLS); 407 } 408 } 409