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