• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.util.Log;
30 
31 import com.android.internal.annotations.VisibleForTesting;
32 import com.android.providers.contacts.util.PropertyUtils;
33 
34 /**
35  * SQLite database (helper) for {@link CallLogProvider} and {@link VoicemailContentProvider}.
36  */
37 public 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 {
OpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version)85         public OpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory,
86                 int version) {
87             super(context, name, factory, version);
88         }
89 
90         @Override
onCreate(SQLiteDatabase db)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
onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)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
CallLogDatabaseHelper(Context context, String databaseName)192     CallLogDatabaseHelper(Context context, String databaseName) {
193         mContext = context;
194         mOpenHelper = new OpenHelper(mContext, databaseName, /* factory=*/ null, DATABASE_VERSION);
195     }
196 
getInstance(Context context)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 
getInstanceForShadow(Context context)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 
getReadableDatabase()213     public SQLiteDatabase getReadableDatabase() {
214         return mOpenHelper.getReadableDatabase();
215     }
216 
getWritableDatabase()217     public SQLiteDatabase getWritableDatabase() {
218         return mOpenHelper.getWritableDatabase();
219     }
220 
getProperty(String key, String defaultValue)221     public String getProperty(String key, String defaultValue) {
222         return PropertyUtils.getProperty(getReadableDatabase(), key, defaultValue);
223     }
224 
setProperty(String key, String value)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      */
upgradeToVersion2(SQLiteDatabase db)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      */
upgradeToVersion3(SQLiteDatabase db)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      */
migrateFromLegacyTables(SQLiteDatabase calllog)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
tableExists(SQLiteDatabase db, String table)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.
getContactsWritableDatabaseForMigration()335     SQLiteDatabase getContactsWritableDatabaseForMigration() {
336         return ContactsDatabaseHelper.getInstance(mContext).getWritableDatabase();
337     }
338 
339     @VisibleForTesting
closeForTest()340     void closeForTest() {
341         mOpenHelper.close();
342     }
343 
wipeForTest()344     public void wipeForTest() {
345         getWritableDatabase().execSQL("DELETE FROM " + Tables.CALLS);
346     }
347 }
348