Home | History | Annotate | Download | only in contacts
      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