Merge "Add SpeedDialEntry database helper."
diff --git a/java/com/android/dialer/speeddial/database/SpeedDialEntry.java b/java/com/android/dialer/speeddial/database/SpeedDialEntry.java
new file mode 100644
index 0000000..aa90909
--- /dev/null
+++ b/java/com/android/dialer/speeddial/database/SpeedDialEntry.java
@@ -0,0 +1,116 @@
+/*
+ * 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.provider.ContactsContract.CommonDataKinds.Phone;
+import android.support.annotation.IntDef;
+import android.support.annotation.Nullable;
+import com.google.auto.value.AutoValue;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/** POJO representation of database rows returned by {@link SpeedDialEntryDao}. */
+@AutoValue
+public abstract class SpeedDialEntry {
+
+  /** Unique ID */
+  public abstract long id();
+
+  /** @see {@link Contacts#_ID} */
+  public abstract long contactId();
+
+  /** @see {@link Contacts#LOOKUP_KEY} */
+  public abstract String lookupKey();
+
+  /**
+   * {@link Channel} that is associated with this entry.
+   *
+   * <p>Contacts with multiple channels do not have a default until specified by the user. Once the
+   * default channel is determined, all calls should be placed to this channel.
+   */
+  @Nullable
+  public abstract Channel defaultChannel();
+
+  public abstract Builder toBuilder();
+
+  public static Builder builder() {
+    return new AutoValue_SpeedDialEntry.Builder();
+  }
+
+  /** Builder class for speed dial entry. */
+  @AutoValue.Builder
+  public abstract static class Builder {
+
+    public abstract Builder setId(long id);
+
+    public abstract Builder setContactId(long contactId);
+
+    public abstract Builder setLookupKey(String lookupKey);
+
+    public abstract Builder setDefaultChannel(@Nullable Channel defaultChannel);
+
+    public abstract SpeedDialEntry build();
+  }
+
+  /** POJO representation of a relevant phone number columns in {@link SpeedDialEntryDao}. */
+  @AutoValue
+  public abstract static class Channel {
+
+    public static final int UNKNOWN = 0;
+    public static final int VOICE = 1;
+    public static final int VIDEO = 2;
+
+    /** Whether the Channel is for an audio or video call. */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({UNKNOWN, VOICE, VIDEO})
+    public @interface Technology {}
+
+    /**
+     * Raw phone number as the user entered it.
+     *
+     * @see {@link Phone#NUMBER}
+     */
+    public abstract String number();
+
+    /**
+     * Label that the user associated with this number like {@link Phone#TYPE_WORK}, {@link
+     * Phone#TYPE_HOME}, ect.
+     *
+     * @see {@link Phone#LABEL}
+     */
+    public abstract String label();
+
+    public abstract @Technology int technology();
+
+    public static Builder builder() {
+      return new AutoValue_SpeedDialEntry_Channel.Builder();
+    }
+
+    /** Builder class for {@link Channel}. */
+    @AutoValue.Builder
+    public abstract static class Builder {
+
+      public abstract Builder setNumber(String number);
+
+      public abstract Builder setLabel(String label);
+
+      public abstract Builder setTechnology(@Technology int technology);
+
+      public abstract Channel build();
+    }
+  }
+}
diff --git a/java/com/android/dialer/speeddial/database/SpeedDialEntryDao.java b/java/com/android/dialer/speeddial/database/SpeedDialEntryDao.java
new file mode 100644
index 0000000..39cb115
--- /dev/null
+++ b/java/com/android/dialer/speeddial/database/SpeedDialEntryDao.java
@@ -0,0 +1,57 @@
+/*
+ * 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 java.util.List;
+
+/** Interface that databases support speed dial entries should implement. */
+public interface SpeedDialEntryDao {
+
+  /** Return all entries in the database */
+  List<SpeedDialEntry> getAllEntries();
+
+  /**
+   * Insert new entries.
+   *
+   * <p>Fails if any of the {@link SpeedDialEntry#id()} already exist.
+   */
+  void insert(List<SpeedDialEntry> entries);
+
+  /**
+   * Insert a new entry.
+   *
+   * <p>Fails if the {@link SpeedDialEntry#id()} already exists.
+   */
+  long insert(SpeedDialEntry entry);
+
+  /**
+   * Updates existing entries based on {@link SpeedDialEntry#id}.
+   *
+   * <p>Fails if the {@link SpeedDialEntry#id()} doesn't exist.
+   */
+  void update(List<SpeedDialEntry> entries);
+
+  /**
+   * Delete the passed in entries based on {@link SpeedDialEntry#id}.
+   *
+   * <p>Fails if the {@link SpeedDialEntry#id()} doesn't exist.
+   */
+  void delete(List<Long> entries);
+
+  /** Delete all entries in the database. */
+  void deleteAll();
+}
diff --git a/java/com/android/dialer/speeddial/database/SpeedDialEntryDatabaseHelper.java b/java/com/android/dialer/speeddial/database/SpeedDialEntryDatabaseHelper.java
new file mode 100644
index 0000000..1812dbd
--- /dev/null
+++ b/java/com/android/dialer/speeddial/database/SpeedDialEntryDatabaseHelper.java
@@ -0,0 +1,221 @@
+/*
+ * 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 java.util.ArrayList;
+import java.util.List;
+
+/** {@link SpeedDialEntryDao} implemented as an SQLite database. */
+public final class SpeedDialEntryDatabaseHelper extends SQLiteOpenHelper
+    implements SpeedDialEntryDao {
+
+  private static final int DATABASE_VERSION = 1;
+  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 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_LABEL = "phone_label";
+  private static final String PHONE_TYPE = "phone_type";
+
+  // Column positions
+  private static final int POSITION_ID = 0;
+  private static final int POSITION_CONTACT_ID = 1;
+  private static final int POSITION_LOOKUP_KEY = 2;
+  private static final int POSITION_PHONE_NUMBER = 3;
+  private static final int POSITION_PHONE_LABEL = 4;
+  private static final int POSITION_PHONE_TYPE = 5;
+
+  // Create Table Query
+  private static final String CREATE_TABLE_SQL =
+      "create table if not exists "
+          + TABLE_NAME
+          + " ("
+          + (ID + " integer primary key, ")
+          + (CONTACT_ID + " integer, ")
+          + (LOOKUP_KEY + " text, ")
+          + (PHONE_NUMBER + " text, ")
+          + (PHONE_LABEL + " text, ")
+          + (PHONE_TYPE + " 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 List<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()) {
+        Channel channel =
+            Channel.builder()
+                .setNumber(cursor.getString(POSITION_PHONE_NUMBER))
+                .setLabel(cursor.getString(POSITION_PHONE_LABEL))
+                .setTechnology(cursor.getInt(POSITION_PHONE_TYPE))
+                .build();
+        if (TextUtils.isEmpty(channel.number())) {
+          channel = null;
+        }
+        SpeedDialEntry entry =
+            SpeedDialEntry.builder()
+                .setDefaultChannel(channel)
+                .setContactId(cursor.getLong(POSITION_CONTACT_ID))
+                .setLookupKey(cursor.getString(POSITION_LOOKUP_KEY))
+                .setId(cursor.getInt(POSITION_ID))
+                .build();
+        entries.add(entry);
+      }
+    }
+    return entries;
+  }
+
+  @Override
+  public void insert(List<SpeedDialEntry> entries) {
+    SQLiteDatabase db = getWritableDatabase();
+    db.beginTransaction();
+    try {
+      for (SpeedDialEntry entry : entries) {
+        if (db.insert(TABLE_NAME, null, buildContentValues(entry)) == -1L) {
+          throw Assert.createUnsupportedOperationFailException(
+              "Attempted to insert a row that already exists.");
+        }
+      }
+      db.setTransactionSuccessful();
+    } finally {
+      db.endTransaction();
+      db.close();
+    }
+  }
+
+  @Override
+  public long insert(SpeedDialEntry entry) {
+    long updateRowId;
+    try (SQLiteDatabase db = getWritableDatabase()) {
+      updateRowId = db.insert(TABLE_NAME, null, buildContentValues(entry));
+    }
+    if (updateRowId == -1) {
+      throw Assert.createUnsupportedOperationFailException(
+          "Attempted to insert a row that already exists.");
+    }
+    return updateRowId;
+  }
+
+  @Override
+  public void update(List<SpeedDialEntry> entries) {
+    SQLiteDatabase db = getWritableDatabase();
+    db.beginTransaction();
+    try {
+      for (SpeedDialEntry entry : entries) {
+        int count =
+            db.update(
+                TABLE_NAME,
+                buildContentValues(entry),
+                ID + " = ?",
+                new String[] {Long.toString(entry.id())});
+        if (count != 1) {
+          throw Assert.createUnsupportedOperationFailException(
+              "Attempted to update an undetermined number of rows: " + count);
+        }
+      }
+      db.setTransactionSuccessful();
+    } finally {
+      db.endTransaction();
+      db.close();
+    }
+  }
+
+  private ContentValues buildContentValues(SpeedDialEntry entry) {
+    ContentValues values = new ContentValues();
+    values.put(ID, entry.id());
+    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_LABEL, entry.defaultChannel().label());
+      values.put(PHONE_TYPE, entry.defaultChannel().technology());
+    }
+    return values;
+  }
+
+  @Override
+  public void delete(List<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();
+    try (SQLiteDatabase db = getWritableDatabase()) {
+      int count = db.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 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();
+    }
+  }
+}