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.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 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 {
     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
     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                     Voicemails._DATA + " TEXT," +
    153                     Voicemails.HAS_CONTENT + " INTEGER," +
    154                     Voicemails.MIME_TYPE + " TEXT," +
    155                     Voicemails.SOURCE_DATA + " TEXT," +
    156                     Voicemails.SOURCE_PACKAGE + " TEXT," +
    157                     Voicemails.TRANSCRIPTION + " TEXT," +
    158                     Voicemails.TRANSCRIPTION_STATE + " INTEGER NOT NULL DEFAULT 0," +
    159                     Voicemails.STATE + " INTEGER," +
    160                     Voicemails.DIRTY + " INTEGER NOT NULL DEFAULT 0," +
    161                     Voicemails.DELETED + " INTEGER NOT NULL DEFAULT 0," +
    162                     Voicemails.BACKED_UP + " INTEGER NOT NULL DEFAULT 0," +
    163                     Voicemails.RESTORED + " INTEGER NOT NULL DEFAULT 0," +
    164                     Voicemails.ARCHIVED + " INTEGER NOT NULL DEFAULT 0," +
    165                     Voicemails.IS_OMTP_VOICEMAIL + " INTEGER NOT NULL DEFAULT 0" +
    166                     ");");
    167 
    168             db.execSQL("CREATE TABLE " + Tables.VOICEMAIL_STATUS + " (" +
    169                     VoicemailContract.Status._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
    170                     VoicemailContract.Status.SOURCE_PACKAGE + " TEXT NOT NULL," +
    171                     VoicemailContract.Status.PHONE_ACCOUNT_COMPONENT_NAME + " TEXT," +
    172                     VoicemailContract.Status.PHONE_ACCOUNT_ID + " TEXT," +
    173                     VoicemailContract.Status.SETTINGS_URI + " TEXT," +
    174                     VoicemailContract.Status.VOICEMAIL_ACCESS_URI + " TEXT," +
    175                     VoicemailContract.Status.CONFIGURATION_STATE + " INTEGER," +
    176                     VoicemailContract.Status.DATA_CHANNEL_STATE + " INTEGER," +
    177                     VoicemailContract.Status.NOTIFICATION_CHANNEL_STATE + " INTEGER," +
    178                     VoicemailContract.Status.QUOTA_OCCUPIED + " INTEGER DEFAULT -1," +
    179                     VoicemailContract.Status.QUOTA_TOTAL + " INTEGER DEFAULT -1," +
    180                     VoicemailContract.Status.SOURCE_TYPE + " TEXT" +
    181                     ");");
    182 
    183             migrateFromLegacyTables(db);
    184         }
    185 
    186         @Override
    187         public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    188             if (DEBUG) {
    189                 Log.d(TAG, "onUpgrade");
    190             }
    191 
    192             if (oldVersion < 2) {
    193                 upgradeToVersion2(db);
    194             }
    195 
    196             if (oldVersion < 3) {
    197                 upgradeToVersion3(db);
    198             }
    199 
    200             if (oldVersion < 4) {
    201                 upgradeToVersion4(db);
    202             }
    203 
    204             if (oldVersion < 5) {
    205                 upgradeToVersion5(db);
    206             }
    207         }
    208     }
    209 
    210     @VisibleForTesting
    211     CallLogDatabaseHelper(Context context, String databaseName) {
    212         mContext = context;
    213         mOpenHelper = new OpenHelper(mContext, databaseName, /* factory=*/ null, DATABASE_VERSION);
    214     }
    215 
    216     public static synchronized CallLogDatabaseHelper getInstance(Context context) {
    217         if (sInstance == null) {
    218             sInstance = new CallLogDatabaseHelper(context, DATABASE_NAME);
    219         }
    220         return sInstance;
    221     }
    222 
    223     public static synchronized CallLogDatabaseHelper getInstanceForShadow(Context context) {
    224         if (sInstanceForShadow == null) {
    225             // Shadow provider is always encryption-aware.
    226             sInstanceForShadow = new CallLogDatabaseHelper(
    227                     context.createDeviceProtectedStorageContext(), SHADOW_DATABASE_NAME);
    228         }
    229         return sInstanceForShadow;
    230     }
    231 
    232     public SQLiteDatabase getReadableDatabase() {
    233         return mOpenHelper.getReadableDatabase();
    234     }
    235 
    236     public SQLiteDatabase getWritableDatabase() {
    237         return mOpenHelper.getWritableDatabase();
    238     }
    239 
    240     public String getProperty(String key, String defaultValue) {
    241         return PropertyUtils.getProperty(getReadableDatabase(), key, defaultValue);
    242     }
    243 
    244     public void setProperty(String key, String value) {
    245         PropertyUtils.setProperty(getWritableDatabase(), key, value);
    246     }
    247 
    248     /**
    249      * Add the {@link Calls.VIA_NUMBER} Column to the CallLog Database.
    250      */
    251     private void upgradeToVersion2(SQLiteDatabase db) {
    252         db.execSQL("ALTER TABLE " + Tables.CALLS + " ADD " + Calls.VIA_NUMBER +
    253                 " TEXT NOT NULL DEFAULT ''");
    254     }
    255 
    256     /**
    257      * Add the {@link Status.SOURCE_TYPE} Column to the VoicemailStatus Database.
    258      */
    259     private void upgradeToVersion3(SQLiteDatabase db) {
    260         db.execSQL("ALTER TABLE " + Tables.VOICEMAIL_STATUS + " ADD " + Status.SOURCE_TYPE +
    261                 " TEXT");
    262     }
    263 
    264     /**
    265      * Add {@link Voicemails.BACKED_UP} {@link Voicemails.ARCHIVE} {@link
    266      * Voicemails.IS_OMTP_VOICEMAIL} column to the CallLog database.
    267      */
    268     private void upgradeToVersion4(SQLiteDatabase db) {
    269         db.execSQL("ALTER TABLE calls ADD backed_up INTEGER NOT NULL DEFAULT 0");
    270         db.execSQL("ALTER TABLE calls ADD restored INTEGER NOT NULL DEFAULT 0");
    271         db.execSQL("ALTER TABLE calls ADD archived INTEGER NOT NULL DEFAULT 0");
    272         db.execSQL("ALTER TABLE calls ADD is_omtp_voicemail INTEGER NOT NULL DEFAULT 0");
    273     }
    274 
    275     /**
    276      * Add {@link Voicemails.TRANSCRIPTION_STATE} column to the CallLog database.
    277      */
    278     private void upgradeToVersion5(SQLiteDatabase db) {
    279         db.execSQL("ALTER TABLE calls ADD transcription_state INTEGER NOT NULL DEFAULT 0");
    280     }
    281 
    282     /**
    283      * Perform the migration from the contacts2.db (of the latest version) to the current calllog/
    284      * voicemail status tables.
    285      */
    286     private void migrateFromLegacyTables(SQLiteDatabase calllog) {
    287         final SQLiteDatabase contacts = getContactsWritableDatabaseForMigration();
    288 
    289         if (contacts == null) {
    290             Log.w(TAG, "Contacts DB == null, skipping migration. (running tests?)");
    291             return;
    292         }
    293         if (DEBUG) {
    294             Log.d(TAG, "migrateFromLegacyTables");
    295         }
    296 
    297         if ("1".equals(PropertyUtils.getProperty(calllog, DbProperties.DATA_MIGRATED, ""))) {
    298             return;
    299         }
    300 
    301         Log.i(TAG, "Migrating from old tables...");
    302 
    303         contacts.beginTransaction();
    304         try {
    305             if (!tableExists(contacts, LegacyConstants.CALLS_LEGACY)
    306                     || !tableExists(contacts, LegacyConstants.VOICEMAIL_STATUS_LEGACY)) {
    307                 // This is fine on new devices. (or after a "clear data".)
    308                 Log.i(TAG, "Source tables don't exist.");
    309                 return;
    310             }
    311             calllog.beginTransaction();
    312             try {
    313 
    314                 final ContentValues cv = new ContentValues();
    315 
    316                 try (Cursor source = contacts.rawQuery(
    317                         "SELECT * FROM " + LegacyConstants.CALLS_LEGACY, null)) {
    318                     while (source.moveToNext()) {
    319                         cv.clear();
    320 
    321                         DatabaseUtils.cursorRowToContentValues(source, cv);
    322 
    323                         calllog.insertOrThrow(Tables.CALLS, null, cv);
    324                     }
    325                 }
    326 
    327                 try (Cursor source = contacts.rawQuery("SELECT * FROM " +
    328                         LegacyConstants.VOICEMAIL_STATUS_LEGACY, null)) {
    329                     while (source.moveToNext()) {
    330                         cv.clear();
    331 
    332                         DatabaseUtils.cursorRowToContentValues(source, cv);
    333 
    334                         calllog.insertOrThrow(Tables.VOICEMAIL_STATUS, null, cv);
    335                     }
    336                 }
    337 
    338                 contacts.execSQL("DROP TABLE " + LegacyConstants.CALLS_LEGACY + ";");
    339                 contacts.execSQL("DROP TABLE " + LegacyConstants.VOICEMAIL_STATUS_LEGACY + ";");
    340 
    341                 // Also copy the last sync time.
    342                 PropertyUtils.setProperty(calllog, DbProperties.CALL_LOG_LAST_SYNCED,
    343                         PropertyUtils.getProperty(contacts,
    344                                 LegacyConstants.CALL_LOG_LAST_SYNCED_LEGACY, null));
    345 
    346                 Log.i(TAG, "Migration completed.");
    347 
    348                 calllog.setTransactionSuccessful();
    349             } finally {
    350                 calllog.endTransaction();
    351             }
    352 
    353             contacts.setTransactionSuccessful();
    354         } catch (RuntimeException e) {
    355             // We don't want to be stuck here, so we just swallow exceptions...
    356             Log.w(TAG, "Exception caught during migration", e);
    357         } finally {
    358             contacts.endTransaction();
    359         }
    360         PropertyUtils.setProperty(calllog, DbProperties.DATA_MIGRATED, "1");
    361     }
    362 
    363     @VisibleForTesting
    364     static boolean tableExists(SQLiteDatabase db, String table) {
    365         return DatabaseUtils.longForQuery(db,
    366                 "select count(*) from sqlite_master where type='table' and name=?",
    367                 new String[] {table}) > 0;
    368     }
    369 
    370     @VisibleForTesting
    371     @Nullable // We return null during tests when migration is not needed.
    372     SQLiteDatabase getContactsWritableDatabaseForMigration() {
    373         return ContactsDatabaseHelper.getInstance(mContext).getWritableDatabase();
    374     }
    375 
    376     public ArraySet<String> selectDistinctColumn(String table, String column) {
    377         final ArraySet<String> ret = new ArraySet<>();
    378         final SQLiteDatabase db = getReadableDatabase();
    379         final Cursor c = db.rawQuery("SELECT DISTINCT "
    380                 + column
    381                 + " FROM " + table, null);
    382         try {
    383             c.moveToPosition(-1);
    384             while (c.moveToNext()) {
    385                 if (c.isNull(0)) {
    386                     continue;
    387                 }
    388                 final String s = c.getString(0);
    389 
    390                 if (!TextUtils.isEmpty(s)) {
    391                     ret.add(s);
    392                 }
    393             }
    394             return ret;
    395         } finally {
    396             c.close();
    397         }
    398     }
    399 
    400     @VisibleForTesting
    401     void closeForTest() {
    402         mOpenHelper.close();
    403     }
    404 
    405     public void wipeForTest() {
    406         getWritableDatabase().execSQL("DELETE FROM " + Tables.CALLS);
    407     }
    408 }
    409