| /* |
| * Copyright (C) 2018 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License |
| */ |
| |
| package com.android.dialer.speeddial.database; |
| |
| import android.content.ContentValues; |
| import android.content.Context; |
| import android.database.Cursor; |
| import android.database.sqlite.SQLiteDatabase; |
| import android.database.sqlite.SQLiteOpenHelper; |
| import android.text.TextUtils; |
| import com.android.dialer.common.Assert; |
| import com.android.dialer.common.database.Selection; |
| import com.android.dialer.speeddial.database.SpeedDialEntry.Channel; |
| import com.google.common.base.Optional; |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableMap; |
| import java.util.ArrayList; |
| import java.util.List; |
| |
| /** |
| * {@link SpeedDialEntryDao} implemented as an SQLite database. |
| * |
| * @see SpeedDialEntryDao |
| */ |
| public final class SpeedDialEntryDatabaseHelper extends SQLiteOpenHelper |
| implements SpeedDialEntryDao { |
| |
| /** |
| * If the pinned position is absent, then we need to write an impossible value in the table like |
| * -1 so that it doesn't default to 0. When we read this value from the table, we'll translate it |
| * to Optional.absent() in the resulting {@link SpeedDialEntry}. |
| */ |
| private static final int PINNED_POSITION_ABSENT = -1; |
| |
| private static final int DATABASE_VERSION = 2; |
| private static final String DATABASE_NAME = "CPSpeedDialEntry"; |
| |
| // Column names |
| private static final String TABLE_NAME = "speed_dial_entries"; |
| private static final String ID = "id"; |
| private static final String PINNED_POSITION = "pinned_position"; |
| private static final String CONTACT_ID = "contact_id"; |
| private static final String LOOKUP_KEY = "lookup_key"; |
| private static final String PHONE_NUMBER = "phone_number"; |
| private static final String PHONE_TYPE = "phone_type"; |
| private static final String PHONE_LABEL = "phone_label"; |
| private static final String PHONE_TECHNOLOGY = "phone_technology"; |
| |
| // Column positions |
| private static final int POSITION_ID = 0; |
| private static final int POSITION_PINNED_POSITION = 1; |
| private static final int POSITION_CONTACT_ID = 2; |
| private static final int POSITION_LOOKUP_KEY = 3; |
| private static final int POSITION_PHONE_NUMBER = 4; |
| private static final int POSITION_PHONE_TYPE = 5; |
| private static final int POSITION_PHONE_LABEL = 6; |
| private static final int POSITION_PHONE_TECHNOLOGY = 7; |
| |
| // Create Table Query |
| private static final String CREATE_TABLE_SQL = |
| "create table if not exists " |
| + TABLE_NAME |
| + " (" |
| + (ID + " integer primary key, ") |
| + (PINNED_POSITION + " integer, ") |
| + (CONTACT_ID + " integer, ") |
| + (LOOKUP_KEY + " text, ") |
| + (PHONE_NUMBER + " text, ") |
| + (PHONE_TYPE + " integer, ") |
| + (PHONE_LABEL + " text, ") |
| + (PHONE_TECHNOLOGY + " integer ") |
| + ");"; |
| |
| private static final String DELETE_TABLE_SQL = "drop table if exists " + TABLE_NAME; |
| |
| public SpeedDialEntryDatabaseHelper(Context context) { |
| super(context, DATABASE_NAME, null, DATABASE_VERSION); |
| } |
| |
| @Override |
| public void onCreate(SQLiteDatabase db) { |
| db.execSQL(CREATE_TABLE_SQL); |
| } |
| |
| @Override |
| public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { |
| // TODO(calderwoodra): handle upgrades more elegantly |
| db.execSQL(DELETE_TABLE_SQL); |
| this.onCreate(db); |
| } |
| |
| @Override |
| public void onDowngrade(SQLiteDatabase db, int oldVersion, int newVersion) { |
| // TODO(calderwoodra): handle upgrades more elegantly |
| this.onUpgrade(db, oldVersion, newVersion); |
| } |
| |
| @Override |
| public ImmutableList<SpeedDialEntry> getAllEntries() { |
| List<SpeedDialEntry> entries = new ArrayList<>(); |
| |
| String query = "SELECT * FROM " + TABLE_NAME; |
| try (SQLiteDatabase db = getReadableDatabase(); |
| Cursor cursor = db.rawQuery(query, null)) { |
| cursor.moveToPosition(-1); |
| while (cursor.moveToNext()) { |
| String number = cursor.getString(POSITION_PHONE_NUMBER); |
| Channel channel = null; |
| if (!TextUtils.isEmpty(number)) { |
| channel = |
| Channel.builder() |
| .setNumber(number) |
| .setPhoneType(cursor.getInt(POSITION_PHONE_TYPE)) |
| .setLabel(Optional.fromNullable(cursor.getString(POSITION_PHONE_LABEL)).or("")) |
| .setTechnology(cursor.getInt(POSITION_PHONE_TECHNOLOGY)) |
| .build(); |
| } |
| |
| Optional<Integer> pinnedPosition = Optional.of(cursor.getInt(POSITION_PINNED_POSITION)); |
| if (pinnedPosition.or(PINNED_POSITION_ABSENT) == PINNED_POSITION_ABSENT) { |
| pinnedPosition = Optional.absent(); |
| } |
| |
| SpeedDialEntry entry = |
| SpeedDialEntry.builder() |
| .setDefaultChannel(channel) |
| .setContactId(cursor.getLong(POSITION_CONTACT_ID)) |
| .setLookupKey(cursor.getString(POSITION_LOOKUP_KEY)) |
| .setPinnedPosition(pinnedPosition) |
| .setId(cursor.getLong(POSITION_ID)) |
| .build(); |
| entries.add(entry); |
| } |
| } |
| return ImmutableList.copyOf(entries); |
| } |
| |
| @Override |
| public ImmutableMap<SpeedDialEntry, Long> insert(ImmutableList<SpeedDialEntry> entries) { |
| if (entries.isEmpty()) { |
| return ImmutableMap.of(); |
| } |
| |
| SQLiteDatabase db = getWritableDatabase(); |
| db.beginTransaction(); |
| try { |
| ImmutableMap<SpeedDialEntry, Long> insertedEntriesToIdsMap = insert(db, entries); |
| db.setTransactionSuccessful(); |
| return insertedEntriesToIdsMap; |
| } finally { |
| db.endTransaction(); |
| db.close(); |
| } |
| } |
| |
| private ImmutableMap<SpeedDialEntry, Long> insert( |
| SQLiteDatabase writeableDatabase, ImmutableList<SpeedDialEntry> entries) { |
| ImmutableMap.Builder<SpeedDialEntry, Long> insertedEntriesToIdsMap = ImmutableMap.builder(); |
| for (SpeedDialEntry entry : entries) { |
| Assert.checkArgument(entry.id() == null); |
| long id = writeableDatabase.insert(TABLE_NAME, null, buildContentValuesWithoutId(entry)); |
| if (id == -1L) { |
| throw Assert.createUnsupportedOperationFailException( |
| "Attempted to insert a row that already exists."); |
| } |
| // It's impossible to insert two identical entries but this is an important assumption we need |
| // to verify because there's an assumption that each entry will correspond to exactly one id. |
| // ImmutableMap#put verifies this check for us. |
| insertedEntriesToIdsMap.put(entry, id); |
| } |
| return insertedEntriesToIdsMap.build(); |
| } |
| |
| @Override |
| public long insert(SpeedDialEntry entry) { |
| long updateRowId; |
| try (SQLiteDatabase db = getWritableDatabase()) { |
| updateRowId = db.insert(TABLE_NAME, null, buildContentValuesWithoutId(entry)); |
| } |
| if (updateRowId == -1) { |
| throw Assert.createUnsupportedOperationFailException( |
| "Attempted to insert a row that already exists."); |
| } |
| return updateRowId; |
| } |
| |
| @Override |
| public void update(ImmutableList<SpeedDialEntry> entries) { |
| if (entries.isEmpty()) { |
| return; |
| } |
| |
| SQLiteDatabase db = getWritableDatabase(); |
| db.beginTransaction(); |
| try { |
| update(db, entries); |
| db.setTransactionSuccessful(); |
| } finally { |
| db.endTransaction(); |
| db.close(); |
| } |
| } |
| |
| private void update(SQLiteDatabase writeableDatabase, ImmutableList<SpeedDialEntry> entries) { |
| for (SpeedDialEntry entry : entries) { |
| int count = |
| writeableDatabase.update( |
| TABLE_NAME, |
| buildContentValuesWithId(entry), |
| ID + " = ?", |
| new String[] {Long.toString(entry.id())}); |
| if (count != 1) { |
| throw Assert.createUnsupportedOperationFailException( |
| "Attempted to update an undetermined number of rows: " + count); |
| } |
| } |
| } |
| |
| private ContentValues buildContentValuesWithId(SpeedDialEntry entry) { |
| return buildContentValues(entry, true); |
| } |
| |
| private ContentValues buildContentValuesWithoutId(SpeedDialEntry entry) { |
| return buildContentValues(entry, false); |
| } |
| |
| private ContentValues buildContentValues(SpeedDialEntry entry, boolean includeId) { |
| ContentValues values = new ContentValues(); |
| if (includeId) { |
| values.put(ID, entry.id()); |
| } |
| values.put(PINNED_POSITION, entry.pinnedPosition().or(PINNED_POSITION_ABSENT)); |
| values.put(CONTACT_ID, entry.contactId()); |
| values.put(LOOKUP_KEY, entry.lookupKey()); |
| if (entry.defaultChannel() != null) { |
| values.put(PHONE_NUMBER, entry.defaultChannel().number()); |
| values.put(PHONE_TYPE, entry.defaultChannel().phoneType()); |
| values.put(PHONE_LABEL, entry.defaultChannel().label()); |
| values.put(PHONE_TECHNOLOGY, entry.defaultChannel().technology()); |
| } |
| return values; |
| } |
| |
| @Override |
| public void delete(ImmutableList<Long> ids) { |
| if (ids.isEmpty()) { |
| return; |
| } |
| |
| try (SQLiteDatabase db = getWritableDatabase()) { |
| delete(db, ids); |
| } |
| } |
| |
| private void delete(SQLiteDatabase writeableDatabase, ImmutableList<Long> ids) { |
| List<String> idStrings = new ArrayList<>(); |
| for (Long id : ids) { |
| idStrings.add(Long.toString(id)); |
| } |
| |
| Selection selection = Selection.builder().and(Selection.column(ID).in(idStrings)).build(); |
| int count = |
| writeableDatabase.delete( |
| TABLE_NAME, selection.getSelection(), selection.getSelectionArgs()); |
| if (count != ids.size()) { |
| throw Assert.createUnsupportedOperationFailException( |
| "Attempted to delete an undetermined number of rows: " + count); |
| } |
| } |
| |
| @Override |
| public ImmutableMap<SpeedDialEntry, Long> insertUpdateAndDelete( |
| ImmutableList<SpeedDialEntry> entriesToInsert, |
| ImmutableList<SpeedDialEntry> entriesToUpdate, |
| ImmutableList<Long> entriesToDelete) { |
| if (entriesToInsert.isEmpty() && entriesToUpdate.isEmpty() && entriesToDelete.isEmpty()) { |
| return ImmutableMap.of(); |
| } |
| SQLiteDatabase db = getWritableDatabase(); |
| db.beginTransaction(); |
| try { |
| ImmutableMap<SpeedDialEntry, Long> insertedEntriesToIdsMap = insert(db, entriesToInsert); |
| update(db, entriesToUpdate); |
| delete(db, entriesToDelete); |
| db.setTransactionSuccessful(); |
| return insertedEntriesToIdsMap; |
| } finally { |
| db.endTransaction(); |
| db.close(); |
| } |
| } |
| |
| @Override |
| public void deleteAll() { |
| SQLiteDatabase db = getWritableDatabase(); |
| db.beginTransaction(); |
| try { |
| // Passing null into where clause will delete all rows |
| db.delete(TABLE_NAME, /* whereClause=*/ null, null); |
| db.setTransactionSuccessful(); |
| } finally { |
| db.endTransaction(); |
| db.close(); |
| } |
| } |
| } |