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