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