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