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