1 /* 2 * Copyright (C) 2017 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 17 package com.android.dialer.calllog.database; 18 19 import android.content.Context; 20 import android.database.sqlite.SQLiteDatabase; 21 import android.database.sqlite.SQLiteOpenHelper; 22 import android.provider.CallLog.Calls; 23 import android.support.annotation.VisibleForTesting; 24 import com.android.dialer.calllog.database.contract.AnnotatedCallLogContract.AnnotatedCallLog; 25 import com.android.dialer.common.LogUtil; 26 import com.android.dialer.common.concurrent.Annotations.BackgroundExecutor; 27 import com.android.dialer.inject.ApplicationContext; 28 import com.google.common.util.concurrent.ListenableFuture; 29 import com.google.common.util.concurrent.ListeningExecutorService; 30 import java.util.Locale; 31 import javax.inject.Inject; 32 import javax.inject.Singleton; 33 34 /** {@link SQLiteOpenHelper} for the AnnotatedCallLog database. */ 35 @Singleton 36 public class AnnotatedCallLogDatabaseHelper extends SQLiteOpenHelper { 37 38 @VisibleForTesting static final int VERSION = 4; 39 40 private static final String FILENAME = "annotated_call_log.db"; 41 42 private final Context appContext; 43 private final int maxRows; 44 private final ListeningExecutorService backgroundExecutor; 45 46 @Inject AnnotatedCallLogDatabaseHelper( @pplicationContext Context appContext, @AnnotatedCallLogMaxRows int maxRows, @BackgroundExecutor ListeningExecutorService backgroundExecutor)47 public AnnotatedCallLogDatabaseHelper( 48 @ApplicationContext Context appContext, 49 @AnnotatedCallLogMaxRows int maxRows, 50 @BackgroundExecutor ListeningExecutorService backgroundExecutor) { 51 super(appContext, FILENAME, null, VERSION); 52 53 this.appContext = appContext; 54 this.maxRows = maxRows; 55 this.backgroundExecutor = backgroundExecutor; 56 } 57 58 /** 59 * Important note: 60 * 61 * <p>Do NOT modify/delete columns (e.g., adding constraints, changing column type, etc). 62 * 63 * <p>As SQLite's "ALTER TABLE" statement doesn't support such operations, doing so requires 64 * complex, expensive, and error-prone operations to upgrade the database (see 65 * https://www.sqlite.org/lang_altertable.html "Making Other Kinds Of Table Schema Changes"). 66 * 67 * <p>All column constraints are enforced when data are inserted/updated via 68 * AnnotatedCallLogContentProvider. See AnnotatedCallLogConstraints for details. 69 */ 70 private static final String CREATE_TABLE_SQL = 71 "create table if not exists " 72 + AnnotatedCallLog.TABLE 73 + " (" 74 + (AnnotatedCallLog._ID + " integer primary key, ") 75 + (AnnotatedCallLog.TIMESTAMP + " integer, ") 76 + (AnnotatedCallLog.NUMBER + " blob, ") 77 + (AnnotatedCallLog.FORMATTED_NUMBER + " text, ") 78 + (AnnotatedCallLog.NUMBER_PRESENTATION + " integer, ") 79 + (AnnotatedCallLog.DURATION + " integer, ") 80 + (AnnotatedCallLog.DATA_USAGE + " integer, ") 81 + (AnnotatedCallLog.IS_READ + " integer, ") 82 + (AnnotatedCallLog.NEW + " integer, ") 83 + (AnnotatedCallLog.GEOCODED_LOCATION + " text, ") 84 + (AnnotatedCallLog.PHONE_ACCOUNT_COMPONENT_NAME + " text, ") 85 + (AnnotatedCallLog.PHONE_ACCOUNT_ID + " text, ") 86 + (AnnotatedCallLog.FEATURES + " integer, ") 87 + (AnnotatedCallLog.TRANSCRIPTION + " integer, ") 88 + (AnnotatedCallLog.VOICEMAIL_URI + " text, ") 89 + (AnnotatedCallLog.CALL_TYPE + " integer, ") 90 + (AnnotatedCallLog.NUMBER_ATTRIBUTES + " blob, ") 91 + (AnnotatedCallLog.IS_VOICEMAIL_CALL + " integer, ") 92 + (AnnotatedCallLog.VOICEMAIL_CALL_TAG + " text, ") 93 + (AnnotatedCallLog.TRANSCRIPTION_STATE + " integer, ") 94 + (AnnotatedCallLog.CALL_MAPPING_ID + " text") 95 + ");"; 96 97 /** 98 * Deletes all but the first maxRows rows (by timestamp, excluding voicemails) to keep the table a 99 * manageable size. 100 */ 101 private static final String CREATE_TRIGGER_SQL = 102 "create trigger delete_old_rows after insert on " 103 + AnnotatedCallLog.TABLE 104 + " when (select count(*) from " 105 + AnnotatedCallLog.TABLE 106 + " where " 107 + AnnotatedCallLog.CALL_TYPE 108 + " != " 109 + Calls.VOICEMAIL_TYPE 110 + ") > %d" 111 + " begin delete from " 112 + AnnotatedCallLog.TABLE 113 + " where " 114 + AnnotatedCallLog._ID 115 + " in (select " 116 + AnnotatedCallLog._ID 117 + " from " 118 + AnnotatedCallLog.TABLE 119 + " where " 120 + AnnotatedCallLog.CALL_TYPE 121 + " != " 122 + Calls.VOICEMAIL_TYPE 123 + " order by timestamp limit (select count(*)-%d" 124 + " from " 125 + AnnotatedCallLog.TABLE 126 + " where " 127 + AnnotatedCallLog.CALL_TYPE 128 + " != " 129 + Calls.VOICEMAIL_TYPE 130 + ")); end;"; 131 132 private static final String CREATE_INDEX_ON_CALL_TYPE_SQL = 133 "create index call_type_index on " 134 + AnnotatedCallLog.TABLE 135 + " (" 136 + AnnotatedCallLog.CALL_TYPE 137 + ");"; 138 139 private static final String CREATE_INDEX_ON_NUMBER_SQL = 140 "create index number_index on " 141 + AnnotatedCallLog.TABLE 142 + " (" 143 + AnnotatedCallLog.NUMBER 144 + ");"; 145 146 @Override onCreate(SQLiteDatabase db)147 public void onCreate(SQLiteDatabase db) { 148 LogUtil.enterBlock("AnnotatedCallLogDatabaseHelper.onCreate"); 149 long startTime = System.currentTimeMillis(); 150 db.execSQL(CREATE_TABLE_SQL); 151 db.execSQL(String.format(Locale.US, CREATE_TRIGGER_SQL, maxRows, maxRows)); 152 db.execSQL(CREATE_INDEX_ON_CALL_TYPE_SQL); 153 db.execSQL(CREATE_INDEX_ON_NUMBER_SQL); 154 // TODO(zachh): Consider logging impression. 155 LogUtil.i( 156 "AnnotatedCallLogDatabaseHelper.onCreate", 157 "took: %dms", 158 System.currentTimeMillis() - startTime); 159 } 160 161 @Override onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)162 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { 163 if (oldVersion < 2) { 164 upgradeToV2(db); 165 } 166 167 // Version 3 upgrade was buggy and didn't make any schema changes. 168 // So we go directly to version 4. 169 if (oldVersion < 4) { 170 upgradeToV4(db); 171 } 172 } 173 upgradeToV2(SQLiteDatabase db)174 private static void upgradeToV2(SQLiteDatabase db) { 175 db.execSQL( 176 "alter table " 177 + AnnotatedCallLog.TABLE 178 + " add column " 179 + AnnotatedCallLog.CALL_MAPPING_ID 180 + " text;"); 181 db.execSQL( 182 "update " 183 + AnnotatedCallLog.TABLE 184 + " set " 185 + AnnotatedCallLog.CALL_MAPPING_ID 186 + " = " 187 + AnnotatedCallLog.TIMESTAMP); 188 } 189 upgradeToV4(SQLiteDatabase db)190 private static void upgradeToV4(SQLiteDatabase db) { 191 // Starting from v4, we will enforce column constraints in the AnnotatedCallLogContentProvider 192 // instead of on the database level. 193 // The constraints are as follows (see AnnotatedCallLogConstraints for details). 194 // IS_READ: not null, must be 0 or 1; 195 // NEW: not null, must be 0 or 1; 196 // IS_VOICEMAIL_CALL: not null, must be 0 or 1; and 197 // CALL_TYPE: not null, must be one of android.provider.CallLog.Calls#TYPE. 198 // 199 // There is no need to update the old schema as the constraints above are more strict than 200 // those in the old schema. 201 // 202 // Version 3 schema defaulted column IS_VOICEMAIL_CALL to 0 but we didn't update the schema in 203 // onUpgrade. As a result, null values can still be inserted if the user has an older version of 204 // the database. For version 4, we need to set all null values to 0. 205 db.execSQL( 206 "update " 207 + AnnotatedCallLog.TABLE 208 + " set " 209 + AnnotatedCallLog.IS_VOICEMAIL_CALL 210 + " = 0 " 211 + "where " 212 + AnnotatedCallLog.IS_VOICEMAIL_CALL 213 + " is null"); 214 } 215 216 /** Closes the database and deletes it. */ delete()217 public ListenableFuture<Void> delete() { 218 return backgroundExecutor.submit( 219 () -> { 220 close(); 221 appContext.deleteDatabase(FILENAME); 222 return null; 223 }); 224 } 225 } 226