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 = 8; 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 Calls.CALL_SCREENING_COMPONENT_NAME + " TEXT," + 153 Calls.CALL_SCREENING_APP_NAME + " TEXT," + 154 Calls.BLOCK_REASON + " INTEGER NOT NULL DEFAULT 0," + 155 156 Voicemails._DATA + " TEXT," + 157 Voicemails.HAS_CONTENT + " INTEGER," + 158 Voicemails.MIME_TYPE + " TEXT," + 159 Voicemails.SOURCE_DATA + " TEXT," + 160 Voicemails.SOURCE_PACKAGE + " TEXT," + 161 Voicemails.TRANSCRIPTION + " TEXT," + 162 Voicemails.TRANSCRIPTION_STATE + " INTEGER NOT NULL DEFAULT 0," + 163 Voicemails.STATE + " INTEGER," + 164 Voicemails.DIRTY + " INTEGER NOT NULL DEFAULT 0," + 165 Voicemails.DELETED + " INTEGER NOT NULL DEFAULT 0," + 166 Voicemails.BACKED_UP + " INTEGER NOT NULL DEFAULT 0," + 167 Voicemails.RESTORED + " INTEGER NOT NULL DEFAULT 0," + 168 Voicemails.ARCHIVED + " INTEGER NOT NULL DEFAULT 0," + 169 Voicemails.IS_OMTP_VOICEMAIL + " INTEGER NOT NULL DEFAULT 0" + 170 ");"); 171 172 db.execSQL("CREATE TABLE " + Tables.VOICEMAIL_STATUS + " (" + 173 VoicemailContract.Status._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + 174 VoicemailContract.Status.SOURCE_PACKAGE + " TEXT NOT NULL," + 175 VoicemailContract.Status.PHONE_ACCOUNT_COMPONENT_NAME + " TEXT," + 176 VoicemailContract.Status.PHONE_ACCOUNT_ID + " TEXT," + 177 VoicemailContract.Status.SETTINGS_URI + " TEXT," + 178 VoicemailContract.Status.VOICEMAIL_ACCESS_URI + " TEXT," + 179 VoicemailContract.Status.CONFIGURATION_STATE + " INTEGER," + 180 VoicemailContract.Status.DATA_CHANNEL_STATE + " INTEGER," + 181 VoicemailContract.Status.NOTIFICATION_CHANNEL_STATE + " INTEGER," + 182 VoicemailContract.Status.QUOTA_OCCUPIED + " INTEGER DEFAULT -1," + 183 VoicemailContract.Status.QUOTA_TOTAL + " INTEGER DEFAULT -1," + 184 VoicemailContract.Status.SOURCE_TYPE + " TEXT" + 185 ");"); 186 187 migrateFromLegacyTables(db); 188 } 189 190 @Override onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)191 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { 192 if (DEBUG) { 193 Log.d(TAG, "onUpgrade"); 194 } 195 196 if (oldVersion < 2) { 197 upgradeToVersion2(db); 198 } 199 200 if (oldVersion < 3) { 201 upgradeToVersion3(db); 202 } 203 204 if (oldVersion < 4) { 205 upgradeToVersion4(db); 206 } 207 208 if (oldVersion < 5) { 209 upgradeToVersion5(db); 210 } 211 212 if (oldVersion < 6) { 213 upgradeToVersion6(db); 214 } 215 216 if (oldVersion < 7) { 217 upgradeToVersion7(db); 218 } 219 220 if (oldVersion < 8) { 221 upgradetoVersion8(db); 222 } 223 } 224 } 225 226 @VisibleForTesting CallLogDatabaseHelper(Context context, String databaseName)227 CallLogDatabaseHelper(Context context, String databaseName) { 228 mContext = context; 229 mOpenHelper = new OpenHelper(mContext, databaseName, /* factory=*/ null, DATABASE_VERSION); 230 } 231 getInstance(Context context)232 public static synchronized CallLogDatabaseHelper getInstance(Context context) { 233 if (sInstance == null) { 234 sInstance = new CallLogDatabaseHelper(context, DATABASE_NAME); 235 } 236 return sInstance; 237 } 238 getInstanceForShadow(Context context)239 public static synchronized CallLogDatabaseHelper getInstanceForShadow(Context context) { 240 if (sInstanceForShadow == null) { 241 // Shadow provider is always encryption-aware. 242 sInstanceForShadow = new CallLogDatabaseHelper( 243 context.createDeviceProtectedStorageContext(), SHADOW_DATABASE_NAME); 244 } 245 return sInstanceForShadow; 246 } 247 getReadableDatabase()248 public SQLiteDatabase getReadableDatabase() { 249 return mOpenHelper.getReadableDatabase(); 250 } 251 getWritableDatabase()252 public SQLiteDatabase getWritableDatabase() { 253 return mOpenHelper.getWritableDatabase(); 254 } 255 getProperty(String key, String defaultValue)256 public String getProperty(String key, String defaultValue) { 257 return PropertyUtils.getProperty(getReadableDatabase(), key, defaultValue); 258 } 259 setProperty(String key, String value)260 public void setProperty(String key, String value) { 261 PropertyUtils.setProperty(getWritableDatabase(), key, value); 262 } 263 264 /** 265 * Add the {@link Calls.VIA_NUMBER} Column to the CallLog Database. 266 */ upgradeToVersion2(SQLiteDatabase db)267 private void upgradeToVersion2(SQLiteDatabase db) { 268 db.execSQL("ALTER TABLE " + Tables.CALLS + " ADD " + Calls.VIA_NUMBER + 269 " TEXT NOT NULL DEFAULT ''"); 270 } 271 272 /** 273 * Add the {@link Status.SOURCE_TYPE} Column to the VoicemailStatus Database. 274 */ upgradeToVersion3(SQLiteDatabase db)275 private void upgradeToVersion3(SQLiteDatabase db) { 276 db.execSQL("ALTER TABLE " + Tables.VOICEMAIL_STATUS + " ADD " + Status.SOURCE_TYPE + 277 " TEXT"); 278 } 279 280 /** 281 * Add {@link Voicemails.BACKED_UP} {@link Voicemails.ARCHIVE} {@link 282 * Voicemails.IS_OMTP_VOICEMAIL} column to the CallLog database. 283 */ upgradeToVersion4(SQLiteDatabase db)284 private void upgradeToVersion4(SQLiteDatabase db) { 285 db.execSQL("ALTER TABLE calls ADD backed_up INTEGER NOT NULL DEFAULT 0"); 286 db.execSQL("ALTER TABLE calls ADD restored INTEGER NOT NULL DEFAULT 0"); 287 db.execSQL("ALTER TABLE calls ADD archived INTEGER NOT NULL DEFAULT 0"); 288 db.execSQL("ALTER TABLE calls ADD is_omtp_voicemail INTEGER NOT NULL DEFAULT 0"); 289 } 290 291 /** 292 * Add {@link Voicemails.TRANSCRIPTION_STATE} column to the CallLog database. 293 */ upgradeToVersion5(SQLiteDatabase db)294 private void upgradeToVersion5(SQLiteDatabase db) { 295 db.execSQL("ALTER TABLE calls ADD transcription_state INTEGER NOT NULL DEFAULT 0"); 296 } 297 298 /** 299 * Add {@link CallLog.Calls#CALL_SCREENING_COMPONENT_NAME} 300 * {@link CallLog.Calls#CALL_SCREENING_APP_NAME} 301 * {@link CallLog.Calls#BLOCK_REASON} column to the CallLog database. 302 */ upgradeToVersion6(SQLiteDatabase db)303 private void upgradeToVersion6(SQLiteDatabase db) { 304 db.execSQL("ALTER TABLE calls ADD call_screening_component_name TEXT"); 305 db.execSQL("ALTER TABLE calls ADD call_screening_app_name TEXT"); 306 db.execSQL("ALTER TABLE calls ADD block_reason INTEGER NOT NULL DEFAULT 0"); 307 } 308 309 /** 310 * Add {@code android.telecom.CallIdentification} columns; these are destined to be removed 311 * in {@link #upgradetoVersion8(SQLiteDatabase)}. 312 * @param db DB to upgrade 313 */ upgradeToVersion7(SQLiteDatabase db)314 private void upgradeToVersion7(SQLiteDatabase db) { 315 db.execSQL("ALTER TABLE calls ADD call_id_package_name TEXT NULL"); 316 db.execSQL("ALTER TABLE calls ADD call_id_app_name TEXT NULL"); 317 db.execSQL("ALTER TABLE calls ADD call_id_name TEXT NULL"); 318 db.execSQL("ALTER TABLE calls ADD call_id_description TEXT NULL"); 319 db.execSQL("ALTER TABLE calls ADD call_id_details TEXT NULL"); 320 db.execSQL("ALTER TABLE calls ADD call_id_nuisance_confidence INTEGER NULL"); 321 } 322 323 /** 324 * Remove the {@code android.telecom.CallIdentification} column. 325 * @param db DB to upgrade 326 */ upgradetoVersion8(SQLiteDatabase db)327 private void upgradetoVersion8(SQLiteDatabase db) { 328 db.beginTransaction(); 329 try { 330 String oldTable = Tables.CALLS + "_old"; 331 // SQLite3 doesn't support altering a column name, so we'll rename the old calls table.. 332 db.execSQL("ALTER TABLE calls RENAME TO " + oldTable); 333 334 // ... create a new one (yes, this seems similar to what is in onCreate, but we can't 335 // assume that one won't change in the future) ... 336 db.execSQL("CREATE TABLE " + Tables.CALLS + " (" + 337 Calls._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + 338 Calls.NUMBER + " TEXT," + 339 Calls.NUMBER_PRESENTATION + " INTEGER NOT NULL DEFAULT " + 340 Calls.PRESENTATION_ALLOWED + "," + 341 Calls.POST_DIAL_DIGITS + " TEXT NOT NULL DEFAULT ''," + 342 Calls.VIA_NUMBER + " TEXT NOT NULL DEFAULT ''," + 343 Calls.DATE + " INTEGER," + 344 Calls.DURATION + " INTEGER," + 345 Calls.DATA_USAGE + " INTEGER," + 346 Calls.TYPE + " INTEGER," + 347 Calls.FEATURES + " INTEGER NOT NULL DEFAULT 0," + 348 Calls.PHONE_ACCOUNT_COMPONENT_NAME + " TEXT," + 349 Calls.PHONE_ACCOUNT_ID + " TEXT," + 350 Calls.PHONE_ACCOUNT_ADDRESS + " TEXT," + 351 Calls.PHONE_ACCOUNT_HIDDEN + " INTEGER NOT NULL DEFAULT 0," + 352 Calls.SUB_ID + " INTEGER DEFAULT -1," + 353 Calls.NEW + " INTEGER," + 354 Calls.CACHED_NAME + " TEXT," + 355 Calls.CACHED_NUMBER_TYPE + " INTEGER," + 356 Calls.CACHED_NUMBER_LABEL + " TEXT," + 357 Calls.COUNTRY_ISO + " TEXT," + 358 Calls.VOICEMAIL_URI + " TEXT," + 359 Calls.IS_READ + " INTEGER," + 360 Calls.GEOCODED_LOCATION + " TEXT," + 361 Calls.CACHED_LOOKUP_URI + " TEXT," + 362 Calls.CACHED_MATCHED_NUMBER + " TEXT," + 363 Calls.CACHED_NORMALIZED_NUMBER + " TEXT," + 364 Calls.CACHED_PHOTO_ID + " INTEGER NOT NULL DEFAULT 0," + 365 Calls.CACHED_PHOTO_URI + " TEXT," + 366 Calls.CACHED_FORMATTED_NUMBER + " TEXT," + 367 Calls.ADD_FOR_ALL_USERS + " INTEGER NOT NULL DEFAULT 1," + 368 Calls.LAST_MODIFIED + " INTEGER DEFAULT 0," + 369 Calls.CALL_SCREENING_COMPONENT_NAME + " TEXT," + 370 Calls.CALL_SCREENING_APP_NAME + " TEXT," + 371 Calls.BLOCK_REASON + " INTEGER NOT NULL DEFAULT 0," + 372 373 Voicemails._DATA + " TEXT," + 374 Voicemails.HAS_CONTENT + " INTEGER," + 375 Voicemails.MIME_TYPE + " TEXT," + 376 Voicemails.SOURCE_DATA + " TEXT," + 377 Voicemails.SOURCE_PACKAGE + " TEXT," + 378 Voicemails.TRANSCRIPTION + " TEXT," + 379 Voicemails.TRANSCRIPTION_STATE + " INTEGER NOT NULL DEFAULT 0," + 380 Voicemails.STATE + " INTEGER," + 381 Voicemails.DIRTY + " INTEGER NOT NULL DEFAULT 0," + 382 Voicemails.DELETED + " INTEGER NOT NULL DEFAULT 0," + 383 Voicemails.BACKED_UP + " INTEGER NOT NULL DEFAULT 0," + 384 Voicemails.RESTORED + " INTEGER NOT NULL DEFAULT 0," + 385 Voicemails.ARCHIVED + " INTEGER NOT NULL DEFAULT 0," + 386 Voicemails.IS_OMTP_VOICEMAIL + " INTEGER NOT NULL DEFAULT 0" + 387 ");"); 388 389 String allTheColumns = Calls._ID + ", " + 390 Calls.NUMBER + ", " + 391 Calls.NUMBER_PRESENTATION + ", " + 392 Calls.POST_DIAL_DIGITS + ", " + 393 Calls.VIA_NUMBER + ", " + 394 Calls.DATE + ", " + 395 Calls.DURATION + ", " + 396 Calls.DATA_USAGE + ", " + 397 Calls.TYPE + ", " + 398 Calls.FEATURES + ", " + 399 Calls.PHONE_ACCOUNT_COMPONENT_NAME + ", " + 400 Calls.PHONE_ACCOUNT_ID + ", " + 401 Calls.PHONE_ACCOUNT_ADDRESS + ", " + 402 Calls.PHONE_ACCOUNT_HIDDEN + ", " + 403 Calls.SUB_ID + ", " + 404 Calls.NEW + ", " + 405 Calls.CACHED_NAME + ", " + 406 Calls.CACHED_NUMBER_TYPE + ", " + 407 Calls.CACHED_NUMBER_LABEL + ", " + 408 Calls.COUNTRY_ISO + ", " + 409 Calls.VOICEMAIL_URI + ", " + 410 Calls.IS_READ + ", " + 411 Calls.GEOCODED_LOCATION + ", " + 412 Calls.CACHED_LOOKUP_URI + ", " + 413 Calls.CACHED_MATCHED_NUMBER + ", " + 414 Calls.CACHED_NORMALIZED_NUMBER + ", " + 415 Calls.CACHED_PHOTO_ID + ", " + 416 Calls.CACHED_PHOTO_URI + ", " + 417 Calls.CACHED_FORMATTED_NUMBER + ", " + 418 Calls.ADD_FOR_ALL_USERS + ", " + 419 Calls.LAST_MODIFIED + ", " + 420 Calls.CALL_SCREENING_COMPONENT_NAME + ", " + 421 Calls.CALL_SCREENING_APP_NAME + ", " + 422 Calls.BLOCK_REASON + ", " + 423 424 Voicemails._DATA + ", " + 425 Voicemails.HAS_CONTENT + ", " + 426 Voicemails.MIME_TYPE + ", " + 427 Voicemails.SOURCE_DATA + ", " + 428 Voicemails.SOURCE_PACKAGE + ", " + 429 Voicemails.TRANSCRIPTION + ", " + 430 Voicemails.TRANSCRIPTION_STATE + ", " + 431 Voicemails.STATE + ", " + 432 Voicemails.DIRTY + ", " + 433 Voicemails.DELETED + ", " + 434 Voicemails.BACKED_UP + ", " + 435 Voicemails.RESTORED + ", " + 436 Voicemails.ARCHIVED + ", " + 437 Voicemails.IS_OMTP_VOICEMAIL; 438 439 // .. so we insert into the new table all the values from the old table ... 440 db.execSQL("INSERT INTO " + Tables.CALLS + " (" + 441 allTheColumns + ") SELECT " + 442 allTheColumns + " FROM " + oldTable); 443 444 // .. and drop the old table we renamed. 445 db.execSQL("DROP TABLE " + oldTable); 446 447 db.setTransactionSuccessful(); 448 } finally { 449 db.endTransaction(); 450 } 451 } 452 453 /** 454 * Perform the migration from the contacts2.db (of the latest version) to the current calllog/ 455 * voicemail status tables. 456 */ migrateFromLegacyTables(SQLiteDatabase calllog)457 private void migrateFromLegacyTables(SQLiteDatabase calllog) { 458 final SQLiteDatabase contacts = getContactsWritableDatabaseForMigration(); 459 460 if (contacts == null) { 461 Log.w(TAG, "Contacts DB == null, skipping migration. (running tests?)"); 462 return; 463 } 464 if (DEBUG) { 465 Log.d(TAG, "migrateFromLegacyTables"); 466 } 467 468 if ("1".equals(PropertyUtils.getProperty(calllog, DbProperties.DATA_MIGRATED, ""))) { 469 return; 470 } 471 472 Log.i(TAG, "Migrating from old tables..."); 473 474 contacts.beginTransaction(); 475 try { 476 if (!tableExists(contacts, LegacyConstants.CALLS_LEGACY) 477 || !tableExists(contacts, LegacyConstants.VOICEMAIL_STATUS_LEGACY)) { 478 // This is fine on new devices. (or after a "clear data".) 479 Log.i(TAG, "Source tables don't exist."); 480 return; 481 } 482 calllog.beginTransaction(); 483 try { 484 485 final ContentValues cv = new ContentValues(); 486 487 try (Cursor source = contacts.rawQuery( 488 "SELECT * FROM " + LegacyConstants.CALLS_LEGACY, null)) { 489 while (source.moveToNext()) { 490 cv.clear(); 491 492 DatabaseUtils.cursorRowToContentValues(source, cv); 493 494 calllog.insertOrThrow(Tables.CALLS, null, cv); 495 } 496 } 497 498 try (Cursor source = contacts.rawQuery("SELECT * FROM " + 499 LegacyConstants.VOICEMAIL_STATUS_LEGACY, null)) { 500 while (source.moveToNext()) { 501 cv.clear(); 502 503 DatabaseUtils.cursorRowToContentValues(source, cv); 504 505 calllog.insertOrThrow(Tables.VOICEMAIL_STATUS, null, cv); 506 } 507 } 508 509 contacts.execSQL("DROP TABLE " + LegacyConstants.CALLS_LEGACY + ";"); 510 contacts.execSQL("DROP TABLE " + LegacyConstants.VOICEMAIL_STATUS_LEGACY + ";"); 511 512 // Also copy the last sync time. 513 PropertyUtils.setProperty(calllog, DbProperties.CALL_LOG_LAST_SYNCED, 514 PropertyUtils.getProperty(contacts, 515 LegacyConstants.CALL_LOG_LAST_SYNCED_LEGACY, null)); 516 517 Log.i(TAG, "Migration completed."); 518 519 calllog.setTransactionSuccessful(); 520 } finally { 521 calllog.endTransaction(); 522 } 523 524 contacts.setTransactionSuccessful(); 525 } catch (RuntimeException e) { 526 // We don't want to be stuck here, so we just swallow exceptions... 527 Log.w(TAG, "Exception caught during migration", e); 528 } finally { 529 contacts.endTransaction(); 530 } 531 PropertyUtils.setProperty(calllog, DbProperties.DATA_MIGRATED, "1"); 532 } 533 534 @VisibleForTesting tableExists(SQLiteDatabase db, String table)535 static boolean tableExists(SQLiteDatabase db, String table) { 536 return DatabaseUtils.longForQuery(db, 537 "select count(*) from sqlite_master where type='table' and name=?", 538 new String[] {table}) > 0; 539 } 540 541 @VisibleForTesting 542 @Nullable // We return null during tests when migration is not needed. getContactsWritableDatabaseForMigration()543 SQLiteDatabase getContactsWritableDatabaseForMigration() { 544 return ContactsDatabaseHelper.getInstance(mContext).getWritableDatabase(); 545 } 546 selectDistinctColumn(String table, String column)547 public ArraySet<String> selectDistinctColumn(String table, String column) { 548 final ArraySet<String> ret = new ArraySet<>(); 549 final SQLiteDatabase db = getReadableDatabase(); 550 final Cursor c = db.rawQuery("SELECT DISTINCT " 551 + column 552 + " FROM " + table, null); 553 try { 554 c.moveToPosition(-1); 555 while (c.moveToNext()) { 556 if (c.isNull(0)) { 557 continue; 558 } 559 final String s = c.getString(0); 560 561 if (!TextUtils.isEmpty(s)) { 562 ret.add(s); 563 } 564 } 565 return ret; 566 } finally { 567 c.close(); 568 } 569 } 570 571 @VisibleForTesting closeForTest()572 void closeForTest() { 573 mOpenHelper.close(); 574 } 575 wipeForTest()576 public void wipeForTest() { 577 getWritableDatabase().execSQL("DELETE FROM " + Tables.CALLS); 578 } 579 } 580