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