1 /* 2 * Copyright (C) 2018 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.speeddial.database; 18 19 import android.content.ContentValues; 20 import android.content.Context; 21 import android.database.Cursor; 22 import android.database.sqlite.SQLiteDatabase; 23 import android.database.sqlite.SQLiteOpenHelper; 24 import android.text.TextUtils; 25 import com.android.dialer.common.Assert; 26 import com.android.dialer.common.database.Selection; 27 import com.android.dialer.speeddial.database.SpeedDialEntry.Channel; 28 import com.google.common.base.Optional; 29 import com.google.common.collect.ImmutableList; 30 import com.google.common.collect.ImmutableMap; 31 import java.util.ArrayList; 32 import java.util.List; 33 34 /** 35 * {@link SpeedDialEntryDao} implemented as an SQLite database. 36 * 37 * @see SpeedDialEntryDao 38 */ 39 public final class SpeedDialEntryDatabaseHelper extends SQLiteOpenHelper 40 implements SpeedDialEntryDao { 41 42 /** 43 * If the pinned position is absent, then we need to write an impossible value in the table like 44 * -1 so that it doesn't default to 0. When we read this value from the table, we'll translate it 45 * to Optional.absent() in the resulting {@link SpeedDialEntry}. 46 */ 47 private static final int PINNED_POSITION_ABSENT = -1; 48 49 private static final int DATABASE_VERSION = 2; 50 private static final String DATABASE_NAME = "CPSpeedDialEntry"; 51 52 // Column names 53 private static final String TABLE_NAME = "speed_dial_entries"; 54 private static final String ID = "id"; 55 private static final String PINNED_POSITION = "pinned_position"; 56 private static final String CONTACT_ID = "contact_id"; 57 private static final String LOOKUP_KEY = "lookup_key"; 58 private static final String PHONE_NUMBER = "phone_number"; 59 private static final String PHONE_TYPE = "phone_type"; 60 private static final String PHONE_LABEL = "phone_label"; 61 private static final String PHONE_TECHNOLOGY = "phone_technology"; 62 63 // Column positions 64 private static final int POSITION_ID = 0; 65 private static final int POSITION_PINNED_POSITION = 1; 66 private static final int POSITION_CONTACT_ID = 2; 67 private static final int POSITION_LOOKUP_KEY = 3; 68 private static final int POSITION_PHONE_NUMBER = 4; 69 private static final int POSITION_PHONE_TYPE = 5; 70 private static final int POSITION_PHONE_LABEL = 6; 71 private static final int POSITION_PHONE_TECHNOLOGY = 7; 72 73 // Create Table Query 74 private static final String CREATE_TABLE_SQL = 75 "create table if not exists " 76 + TABLE_NAME 77 + " (" 78 + (ID + " integer primary key, ") 79 + (PINNED_POSITION + " integer, ") 80 + (CONTACT_ID + " integer, ") 81 + (LOOKUP_KEY + " text, ") 82 + (PHONE_NUMBER + " text, ") 83 + (PHONE_TYPE + " integer, ") 84 + (PHONE_LABEL + " text, ") 85 + (PHONE_TECHNOLOGY + " integer ") 86 + ");"; 87 88 private static final String DELETE_TABLE_SQL = "drop table if exists " + TABLE_NAME; 89 SpeedDialEntryDatabaseHelper(Context context)90 public SpeedDialEntryDatabaseHelper(Context context) { 91 super(context, DATABASE_NAME, null, DATABASE_VERSION); 92 } 93 94 @Override onCreate(SQLiteDatabase db)95 public void onCreate(SQLiteDatabase db) { 96 db.execSQL(CREATE_TABLE_SQL); 97 } 98 99 @Override onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion)100 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { 101 // TODO(calderwoodra): handle upgrades more elegantly 102 db.execSQL(DELETE_TABLE_SQL); 103 this.onCreate(db); 104 } 105 106 @Override onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion)107 public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { 108 // TODO(calderwoodra): handle upgrades more elegantly 109 this.onUpgrade(db, oldVersion, newVersion); 110 } 111 112 @Override getAllEntries()113 public ImmutableList<SpeedDialEntry> getAllEntries() { 114 List<SpeedDialEntry> entries = new ArrayList<>(); 115 116 String query = "SELECT * FROM " + TABLE_NAME; 117 try (SQLiteDatabase db = getReadableDatabase(); 118 Cursor cursor = db.rawQuery(query, null)) { 119 cursor.moveToPosition(-1); 120 while (cursor.moveToNext()) { 121 String number = cursor.getString(POSITION_PHONE_NUMBER); 122 Channel channel = null; 123 if (!TextUtils.isEmpty(number)) { 124 channel = 125 Channel.builder() 126 .setNumber(number) 127 .setPhoneType(cursor.getInt(POSITION_PHONE_TYPE)) 128 .setLabel(Optional.of(cursor.getString(POSITION_PHONE_LABEL)).or("")) 129 .setTechnology(cursor.getInt(POSITION_PHONE_TECHNOLOGY)) 130 .build(); 131 } 132 133 Optional<Integer> pinnedPosition = Optional.of(cursor.getInt(POSITION_PINNED_POSITION)); 134 if (pinnedPosition.or(PINNED_POSITION_ABSENT) == PINNED_POSITION_ABSENT) { 135 pinnedPosition = Optional.absent(); 136 } 137 138 SpeedDialEntry entry = 139 SpeedDialEntry.builder() 140 .setDefaultChannel(channel) 141 .setContactId(cursor.getLong(POSITION_CONTACT_ID)) 142 .setLookupKey(cursor.getString(POSITION_LOOKUP_KEY)) 143 .setPinnedPosition(pinnedPosition) 144 .setId(cursor.getLong(POSITION_ID)) 145 .build(); 146 entries.add(entry); 147 } 148 } 149 return ImmutableList.copyOf(entries); 150 } 151 152 @Override insert(ImmutableList<SpeedDialEntry> entries)153 public ImmutableMap<SpeedDialEntry, Long> insert(ImmutableList<SpeedDialEntry> entries) { 154 if (entries.isEmpty()) { 155 return ImmutableMap.of(); 156 } 157 158 SQLiteDatabase db = getWritableDatabase(); 159 db.beginTransaction(); 160 try { 161 ImmutableMap<SpeedDialEntry, Long> insertedEntriesToIdsMap = insert(db, entries); 162 db.setTransactionSuccessful(); 163 return insertedEntriesToIdsMap; 164 } finally { 165 db.endTransaction(); 166 db.close(); 167 } 168 } 169 insert( SQLiteDatabase writeableDatabase, ImmutableList<SpeedDialEntry> entries)170 private ImmutableMap<SpeedDialEntry, Long> insert( 171 SQLiteDatabase writeableDatabase, ImmutableList<SpeedDialEntry> entries) { 172 ImmutableMap.Builder<SpeedDialEntry, Long> insertedEntriesToIdsMap = ImmutableMap.builder(); 173 for (SpeedDialEntry entry : entries) { 174 Assert.checkArgument(entry.id() == null); 175 long id = writeableDatabase.insert(TABLE_NAME, null, buildContentValuesWithoutId(entry)); 176 if (id == -1L) { 177 throw Assert.createUnsupportedOperationFailException( 178 "Attempted to insert a row that already exists."); 179 } 180 // It's impossible to insert two identical entries but this is an important assumption we need 181 // to verify because there's an assumption that each entry will correspond to exactly one id. 182 // ImmutableMap#put verifies this check for us. 183 insertedEntriesToIdsMap.put(entry, id); 184 } 185 return insertedEntriesToIdsMap.build(); 186 } 187 188 @Override insert(SpeedDialEntry entry)189 public long insert(SpeedDialEntry entry) { 190 long updateRowId; 191 try (SQLiteDatabase db = getWritableDatabase()) { 192 updateRowId = db.insert(TABLE_NAME, null, buildContentValuesWithoutId(entry)); 193 } 194 if (updateRowId == -1) { 195 throw Assert.createUnsupportedOperationFailException( 196 "Attempted to insert a row that already exists."); 197 } 198 return updateRowId; 199 } 200 201 @Override update(ImmutableList<SpeedDialEntry> entries)202 public void update(ImmutableList<SpeedDialEntry> entries) { 203 if (entries.isEmpty()) { 204 return; 205 } 206 207 SQLiteDatabase db = getWritableDatabase(); 208 db.beginTransaction(); 209 try { 210 update(db, entries); 211 db.setTransactionSuccessful(); 212 } finally { 213 db.endTransaction(); 214 db.close(); 215 } 216 } 217 update(SQLiteDatabase writeableDatabase, ImmutableList<SpeedDialEntry> entries)218 private void update(SQLiteDatabase writeableDatabase, ImmutableList<SpeedDialEntry> entries) { 219 for (SpeedDialEntry entry : entries) { 220 int count = 221 writeableDatabase.update( 222 TABLE_NAME, 223 buildContentValuesWithId(entry), 224 ID + " = ?", 225 new String[] {Long.toString(entry.id())}); 226 if (count != 1) { 227 throw Assert.createUnsupportedOperationFailException( 228 "Attempted to update an undetermined number of rows: " + count); 229 } 230 } 231 } 232 buildContentValuesWithId(SpeedDialEntry entry)233 private ContentValues buildContentValuesWithId(SpeedDialEntry entry) { 234 return buildContentValues(entry, true); 235 } 236 buildContentValuesWithoutId(SpeedDialEntry entry)237 private ContentValues buildContentValuesWithoutId(SpeedDialEntry entry) { 238 return buildContentValues(entry, false); 239 } 240 buildContentValues(SpeedDialEntry entry, boolean includeId)241 private ContentValues buildContentValues(SpeedDialEntry entry, boolean includeId) { 242 ContentValues values = new ContentValues(); 243 if (includeId) { 244 values.put(ID, entry.id()); 245 } 246 values.put(PINNED_POSITION, entry.pinnedPosition().or(PINNED_POSITION_ABSENT)); 247 values.put(CONTACT_ID, entry.contactId()); 248 values.put(LOOKUP_KEY, entry.lookupKey()); 249 if (entry.defaultChannel() != null) { 250 values.put(PHONE_NUMBER, entry.defaultChannel().number()); 251 values.put(PHONE_TYPE, entry.defaultChannel().phoneType()); 252 values.put(PHONE_LABEL, entry.defaultChannel().label()); 253 values.put(PHONE_TECHNOLOGY, entry.defaultChannel().technology()); 254 } 255 return values; 256 } 257 258 @Override delete(ImmutableList<Long> ids)259 public void delete(ImmutableList<Long> ids) { 260 if (ids.isEmpty()) { 261 return; 262 } 263 264 try (SQLiteDatabase db = getWritableDatabase()) { 265 delete(db, ids); 266 } 267 } 268 delete(SQLiteDatabase writeableDatabase, ImmutableList<Long> ids)269 private void delete(SQLiteDatabase writeableDatabase, ImmutableList<Long> ids) { 270 List<String> idStrings = new ArrayList<>(); 271 for (Long id : ids) { 272 idStrings.add(Long.toString(id)); 273 } 274 275 Selection selection = Selection.builder().and(Selection.column(ID).in(idStrings)).build(); 276 int count = 277 writeableDatabase.delete( 278 TABLE_NAME, selection.getSelection(), selection.getSelectionArgs()); 279 if (count != ids.size()) { 280 throw Assert.createUnsupportedOperationFailException( 281 "Attempted to delete an undetermined number of rows: " + count); 282 } 283 } 284 285 @Override insertUpdateAndDelete( ImmutableList<SpeedDialEntry> entriesToInsert, ImmutableList<SpeedDialEntry> entriesToUpdate, ImmutableList<Long> entriesToDelete)286 public ImmutableMap<SpeedDialEntry, Long> insertUpdateAndDelete( 287 ImmutableList<SpeedDialEntry> entriesToInsert, 288 ImmutableList<SpeedDialEntry> entriesToUpdate, 289 ImmutableList<Long> entriesToDelete) { 290 if (entriesToInsert.isEmpty() && entriesToUpdate.isEmpty() && entriesToDelete.isEmpty()) { 291 return ImmutableMap.of(); 292 } 293 SQLiteDatabase db = getWritableDatabase(); 294 db.beginTransaction(); 295 try { 296 ImmutableMap<SpeedDialEntry, Long> insertedEntriesToIdsMap = insert(db, entriesToInsert); 297 update(db, entriesToUpdate); 298 delete(db, entriesToDelete); 299 db.setTransactionSuccessful(); 300 return insertedEntriesToIdsMap; 301 } finally { 302 db.endTransaction(); 303 db.close(); 304 } 305 } 306 307 @Override deleteAll()308 public void deleteAll() { 309 SQLiteDatabase db = getWritableDatabase(); 310 db.beginTransaction(); 311 try { 312 // Passing null into where clause will delete all rows 313 db.delete(TABLE_NAME, /* whereClause=*/ null, null); 314 db.setTransactionSuccessful(); 315 } finally { 316 db.endTransaction(); 317 db.close(); 318 } 319 } 320 } 321