Implemented SpeedDialUiItemLoader.

SpeedDialUiItemLoader builds a listenable future for returning
a list of SpeedDialUiItems which are the POJO representation of
each speed dial list element.

Bug: 36841782
Test: SpeedDialContentObserverTest
PiperOrigin-RevId: 192186376
Change-Id: I70f3abbeac14117ff4a68355e3a07b395b72386b
diff --git a/java/com/android/dialer/speeddial/SpeedDialUiItem.java b/java/com/android/dialer/speeddial/SpeedDialUiItem.java
new file mode 100644
index 0000000..17552ad
--- /dev/null
+++ b/java/com/android/dialer/speeddial/SpeedDialUiItem.java
@@ -0,0 +1,159 @@
+/*
+ * 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;
+
+import android.database.Cursor;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.support.annotation.Nullable;
+import android.text.TextUtils;
+import com.android.dialer.common.Assert;
+import com.android.dialer.speeddial.database.SpeedDialEntry;
+import com.android.dialer.speeddial.database.SpeedDialEntry.Channel;
+import com.google.auto.value.AutoValue;
+import com.google.common.collect.ImmutableList;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * POJO representation of each speed dial list element.
+ *
+ * <p>Contains all data needed for the UI so that the UI never needs do additional contact queries.
+ *
+ * <p>Differs from {@link SpeedDialEntry} in that entries are specific to favorited/starred contacts
+ * and {@link SpeedDialUiItem}s can be both favorites and suggested contacts.
+ */
+@AutoValue
+public abstract class SpeedDialUiItem {
+
+  public static final int LOOKUP_KEY = 0;
+  public static final int CONTACT_ID = 1;
+  public static final int DISPLAY_NAME = 2;
+  public static final int STARRED = 3;
+  public static final int NUMBER = 4;
+  public static final int LABEL = 5;
+  public static final int PHOTO_ID = 6;
+  public static final int PHOTO_URI = 7;
+
+  public static final String[] PHONE_PROJECTION = {
+    Phone.LOOKUP_KEY,
+    Phone.CONTACT_ID,
+    Phone.DISPLAY_NAME,
+    Phone.STARRED,
+    Phone.NUMBER,
+    Phone.LABEL,
+    Phone.PHOTO_ID,
+    Phone.PHOTO_URI
+  };
+
+  public static Builder builder() {
+    return new AutoValue_SpeedDialUiItem.Builder().setChannels(ImmutableList.of());
+  }
+
+  /** Convert a cursor with projection {@link #PHONE_PROJECTION} into a {@link SpeedDialUiItem}. */
+  public static SpeedDialUiItem fromCursor(Cursor cursor) {
+    Assert.checkArgument(cursor != null);
+    Assert.checkArgument(cursor.getCount() != 0);
+    String lookupKey = cursor.getString(LOOKUP_KEY);
+    SpeedDialUiItem.Builder builder =
+        SpeedDialUiItem.builder()
+            .setLookupKey(lookupKey)
+            .setContactId(cursor.getLong(CONTACT_ID))
+            // TODO(a bug): handle last name first preference
+            .setName(cursor.getString(DISPLAY_NAME))
+            .setIsStarred(cursor.getInt(STARRED) == 1)
+            .setPhotoId(cursor.getLong(PHOTO_ID))
+            .setPhotoUri(
+                TextUtils.isEmpty(cursor.getString(PHOTO_URI)) ? "" : cursor.getString(PHOTO_URI));
+
+    // While there are more rows and the lookup keys are the same, add a channel for each of the
+    // contact's phone numbers.
+    List<Channel> channels = new ArrayList<>();
+    do {
+      channels.add(
+          Channel.builder()
+              .setNumber(cursor.getString(NUMBER))
+              .setLabel(TextUtils.isEmpty(cursor.getString(LABEL)) ? "" : cursor.getString(LABEL))
+              // TODO(a bug): add another channel for each technology (Duo, ViLTE, ect.)
+              .setTechnology(Channel.VOICE)
+              .build());
+    } while (cursor.moveToNext() && Objects.equals(lookupKey, cursor.getString(LOOKUP_KEY)));
+
+    builder.setChannels(ImmutableList.copyOf(channels));
+    return builder.build();
+  }
+
+  /** @see android.provider.ContactsContract.Contacts#DISPLAY_NAME */
+  public abstract String name();
+
+  /** @see android.provider.ContactsContract.Contacts#_ID */
+  public abstract long contactId();
+
+  /** @see android.provider.ContactsContract.Contacts#LOOKUP_KEY */
+  public abstract String lookupKey();
+
+  /** @see android.provider.ContactsContract.Contacts#STARRED */
+  public abstract boolean isStarred();
+
+  /** @see Phone#PHOTO_ID */
+  public abstract long photoId();
+
+  /** @see Phone#PHOTO_URI */
+  public abstract String photoUri();
+
+  /**
+   * Since a contact can have multiple phone numbers and each number can have multiple technologies,
+   * enumerate each one here so that the user can choose the correct one. Each channel here
+   * represents a row in the {@link com.android.dialer.speeddial.DisambigDialog}.
+   *
+   * @see com.android.dialer.speeddial.database.SpeedDialEntry.Channel
+   */
+  public abstract ImmutableList<Channel> channels();
+
+  /**
+   * Will be null when the user hasn't chosen a default yet.
+   *
+   * @see com.android.dialer.speeddial.database.SpeedDialEntry#defaultChannel()
+   */
+  public abstract @Nullable Channel defaultChannel();
+
+  public abstract Builder toBuilder();
+
+  /** Builder class for speed dial contact. */
+  @AutoValue.Builder
+  public abstract static class Builder {
+
+    public abstract Builder setName(String name);
+
+    public abstract Builder setContactId(long contactId);
+
+    public abstract Builder setLookupKey(String lookupKey);
+
+    public abstract Builder setIsStarred(boolean isStarred);
+
+    public abstract Builder setPhotoId(long photoId);
+
+    public abstract Builder setPhotoUri(String photoUri);
+
+    public abstract Builder setChannels(ImmutableList<Channel> channels);
+
+    /** Set to null if the user hasn't chosen a default or the channel no longer exists. */
+    public abstract Builder setDefaultChannel(@Nullable Channel defaultChannel);
+
+    public abstract SpeedDialUiItem build();
+  }
+}
diff --git a/java/com/android/dialer/speeddial/SpeedDialUiItemLoader.java b/java/com/android/dialer/speeddial/SpeedDialUiItemLoader.java
new file mode 100644
index 0000000..257c74f
--- /dev/null
+++ b/java/com/android/dialer/speeddial/SpeedDialUiItemLoader.java
@@ -0,0 +1,225 @@
+/*
+ * 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;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Build.VERSION_CODES;
+import android.provider.ContactsContract;
+import android.provider.ContactsContract.CommonDataKinds.Phone;
+import android.provider.ContactsContract.Contacts;
+import android.support.annotation.WorkerThread;
+import com.android.dialer.common.Assert;
+import com.android.dialer.common.LogUtil;
+import com.android.dialer.common.concurrent.Annotations.BackgroundExecutor;
+import com.android.dialer.common.concurrent.DialerExecutor.SuccessListener;
+import com.android.dialer.inject.ApplicationContext;
+import com.android.dialer.speeddial.database.SpeedDialEntry;
+import com.android.dialer.speeddial.database.SpeedDialEntry.Channel;
+import com.android.dialer.speeddial.database.SpeedDialEntryDao;
+import com.android.dialer.speeddial.database.SpeedDialEntryDatabaseHelper;
+import com.google.common.collect.ImmutableList;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import java.util.ArrayList;
+import java.util.List;
+import javax.inject.Inject;
+
+/**
+ * Loads a list of {@link SpeedDialUiItem SpeedDialUiItems}.
+ *
+ * @see #loadSpeedDialUiItems()
+ *     <ol>
+ *       <li>Retrieve the list of {@link SpeedDialEntry} from {@link SpeedDialEntryDatabaseHelper}.
+ *       <li>Build a list of {@link SpeedDialUiItem} based on {@link SpeedDialEntry#lookupKey()} in
+ *           {@link Phone#CONTENT_URI}.
+ *       <li>Remove any {@link SpeedDialEntry} that is no longer starred or whose contact was
+ *           deleted.
+ *       <li>Update each {@link SpeedDialEntry} contact id, lookup key and channel.
+ *       <li>Build a list of {@link SpeedDialUiItem} from {@link Contacts#STREQUENT_PHONE_ONLY}.
+ *       <li>If any starred contacts in that list aren't in the {@link
+ *           SpeedDialEntryDatabaseHelper}, insert them now.
+ *       <li>Notify the {@link SuccessListener} of the complete list of {@link SpeedDialUiItem
+ *           SpeedDialContacts} composed from {@link SpeedDialEntry SpeedDialEntries} and
+ *           non-starred {@link Contacts#STREQUENT_PHONE_ONLY}.
+ *     </ol>
+ */
+@SuppressWarnings("AndroidApiChecker")
+@TargetApi(VERSION_CODES.N)
+public final class SpeedDialUiItemLoader {
+
+  private final Context appContext;
+  private final ListeningExecutorService backgroundExecutor;
+
+  @Inject
+  public SpeedDialUiItemLoader(
+      @ApplicationContext Context appContext,
+      @BackgroundExecutor ListeningExecutorService backgroundExecutor) {
+    this.appContext = appContext;
+    this.backgroundExecutor = backgroundExecutor;
+  }
+
+  /**
+   * Returns a {@link ListenableFuture} for a list of {@link SpeedDialUiItem SpeedDialUiItems}. This
+   * list is composed of starred contacts from {@link SpeedDialEntryDatabaseHelper} and suggestions
+   * from {@link Contacts#STREQUENT_PHONE_ONLY}.
+   */
+  public ListenableFuture<ImmutableList<SpeedDialUiItem>> loadSpeedDialUiItems() {
+    return backgroundExecutor.submit(this::doInBackground);
+  }
+
+  @WorkerThread
+  private ImmutableList<SpeedDialUiItem> doInBackground() {
+    Assert.isWorkerThread();
+    SpeedDialEntryDao db = new SpeedDialEntryDatabaseHelper(appContext);
+
+    // This is the list of contacts that we will display to the user
+    List<SpeedDialUiItem> speedDialUiItems = new ArrayList<>();
+
+    // We'll use these lists to update the SpeedDialEntry database
+    List<SpeedDialEntry> entriesToInsert = new ArrayList<>();
+    List<SpeedDialEntry> entriesToUpdate = new ArrayList<>();
+    List<Long> entriesToDelete = new ArrayList<>();
+
+    // Track the highest entry ID
+    // TODO(a bug): use auto-generated IDs
+    long maxId = 0L;
+
+    // Get all SpeedDialEntries and mark them to be updated or deleted
+    List<SpeedDialEntry> entries = db.getAllEntries();
+    for (SpeedDialEntry entry : entries) {
+      maxId = Math.max(entry.id(), maxId);
+
+      SpeedDialUiItem contact = getSpeedDialContact(entry);
+      // Remove contacts that no longer exist or are no longer starred
+      if (contact == null || !contact.isStarred()) {
+        entriesToDelete.add(entry.id());
+        continue;
+      }
+
+      // Contact exists, so update its entry in SpeedDialEntry Database
+      entriesToUpdate.add(
+          entry
+              .toBuilder()
+              .setLookupKey(contact.lookupKey())
+              .setContactId(contact.contactId())
+              .setDefaultChannel(contact.defaultChannel())
+              .build());
+
+      // These are our existing starred entries
+      speedDialUiItems.add(contact);
+    }
+
+    // Get all Strequent Contacts
+    List<SpeedDialUiItem> strequentContacts = getStrequentContacts();
+
+    // For each contact, if it isn't starred, add it as a suggestion.
+    // If it is starred and not already accounted for above, then insert into the SpeedDialEntry DB.
+    for (SpeedDialUiItem contact : strequentContacts) {
+      if (!contact.isStarred()) {
+        // Add this contact as a suggestion
+        // TODO(calderwoodra): set the defaults of these automatically
+        speedDialUiItems.add(contact);
+
+      } else if (speedDialUiItems.stream().noneMatch(c -> c.contactId() == contact.contactId())) {
+        // Increment the ID so there aren't any collisions
+        maxId += 1;
+        entriesToInsert.add(
+            SpeedDialEntry.builder()
+                .setId(maxId)
+                .setLookupKey(contact.lookupKey())
+                .setContactId(contact.contactId())
+                .setDefaultChannel(contact.defaultChannel())
+                .build());
+
+        // These are our newly starred contacts
+        speedDialUiItems.add(contact);
+      }
+    }
+
+    // TODO(a bug): use a single db transaction
+    db.delete(entriesToDelete);
+    db.update(entriesToUpdate);
+    db.insert(entriesToInsert);
+    return ImmutableList.copyOf(speedDialUiItems);
+  }
+
+  @WorkerThread
+  private SpeedDialUiItem getSpeedDialContact(SpeedDialEntry entry) {
+    Assert.isWorkerThread();
+    // TODO(b77725860): Might need to use the lookup uri to get the contact id first, then query
+    // based on that.
+    SpeedDialUiItem contact;
+    try (Cursor cursor =
+        appContext
+            .getContentResolver()
+            .query(
+                Phone.CONTENT_URI,
+                SpeedDialUiItem.PHONE_PROJECTION,
+                Phone.NUMBER + " IS NOT NULL AND " + Phone.LOOKUP_KEY + "=?",
+                new String[] {entry.lookupKey()},
+                null)) {
+
+      if (cursor == null || cursor.getCount() == 0) {
+        // Contact not found, potentially deleted
+        LogUtil.e("SpeedDialUiItemLoader.getSpeedDialContact", "Contact not found.");
+        return null;
+      }
+
+      cursor.moveToFirst();
+      contact = SpeedDialUiItem.fromCursor(cursor);
+    }
+
+    // Preserve the default channel if it didn't change/still exists
+    Channel defaultChannel = entry.defaultChannel();
+    if (defaultChannel != null) {
+      if (contact.channels().contains(defaultChannel)) {
+        contact = contact.toBuilder().setDefaultChannel(defaultChannel).build();
+      }
+    }
+
+    // TODO(calderwoodra): Consider setting the default channel if there is only one channel
+    return contact;
+  }
+
+  @WorkerThread
+  private List<SpeedDialUiItem> getStrequentContacts() {
+    Assert.isWorkerThread();
+    Uri uri =
+        Contacts.CONTENT_STREQUENT_URI
+            .buildUpon()
+            .appendQueryParameter(ContactsContract.STREQUENT_PHONE_ONLY, "true")
+            .build();
+    try (Cursor cursor =
+        appContext
+            .getContentResolver()
+            .query(uri, SpeedDialUiItem.PHONE_PROJECTION, null, null, null)) {
+      List<SpeedDialUiItem> contacts = new ArrayList<>();
+      if (cursor == null || cursor.getCount() == 0) {
+        return contacts;
+      }
+
+      cursor.moveToPosition(-1);
+      while (cursor.moveToNext()) {
+        contacts.add(SpeedDialUiItem.fromCursor(cursor));
+      }
+      return contacts;
+    }
+  }
+}
diff --git a/java/com/android/dialer/speeddial/database/SpeedDialEntry.java b/java/com/android/dialer/speeddial/database/SpeedDialEntry.java
index aa90909..f636194 100644
--- a/java/com/android/dialer/speeddial/database/SpeedDialEntry.java
+++ b/java/com/android/dialer/speeddial/database/SpeedDialEntry.java
@@ -72,13 +72,17 @@
 
     public static final int UNKNOWN = 0;
     public static final int VOICE = 1;
-    public static final int VIDEO = 2;
+    public static final int IMS_VIDEO = 2;
+    public static final int DUO = 3;
 
     /** Whether the Channel is for an audio or video call. */
     @Retention(RetentionPolicy.SOURCE)
-    @IntDef({UNKNOWN, VOICE, VIDEO})
+    @IntDef({UNKNOWN, VOICE, IMS_VIDEO, DUO})
     public @interface Technology {}
 
+    public boolean isVideoTechnology() {
+      return technology() == IMS_VIDEO || technology() == DUO;
+    }
     /**
      * Raw phone number as the user entered it.
      *
@@ -96,6 +100,8 @@
 
     public abstract @Technology int technology();
 
+    public abstract Builder toBuilder();
+
     public static Builder builder() {
       return new AutoValue_SpeedDialEntry_Channel.Builder();
     }
diff --git a/java/com/android/dialer/speeddial/database/SpeedDialEntryDao.java b/java/com/android/dialer/speeddial/database/SpeedDialEntryDao.java
index 39cb115..0efc110 100644
--- a/java/com/android/dialer/speeddial/database/SpeedDialEntryDao.java
+++ b/java/com/android/dialer/speeddial/database/SpeedDialEntryDao.java
@@ -18,7 +18,11 @@
 
 import java.util.List;
 
-/** Interface that databases support speed dial entries should implement. */
+/**
+ * Interface that databases support speed dial entries should implement.
+ *
+ * <p>This database is only used for favorite/starred contacts.
+ */
 public interface SpeedDialEntryDao {
 
   /** Return all entries in the database */
diff --git a/java/com/android/dialer/speeddial/database/SpeedDialEntryDatabaseHelper.java b/java/com/android/dialer/speeddial/database/SpeedDialEntryDatabaseHelper.java
index 1812dbd..01d49c3 100644
--- a/java/com/android/dialer/speeddial/database/SpeedDialEntryDatabaseHelper.java
+++ b/java/com/android/dialer/speeddial/database/SpeedDialEntryDatabaseHelper.java
@@ -28,7 +28,11 @@
 import java.util.ArrayList;
 import java.util.List;
 
-/** {@link SpeedDialEntryDao} implemented as an SQLite database. */
+/**
+ * {@link SpeedDialEntryDao} implemented as an SQLite database.
+ *
+ * @see SpeedDialEntryDao
+ */
 public final class SpeedDialEntryDatabaseHelper extends SQLiteOpenHelper
     implements SpeedDialEntryDao {
 
@@ -42,7 +46,7 @@
   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";
+  private static final String PHONE_TECHNOLOGY = "phone_technology";
 
   // Column positions
   private static final int POSITION_ID = 0;
@@ -50,7 +54,7 @@
   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;
+  private static final int POSITION_PHONE_TECHNOLOGY = 5;
 
   // Create Table Query
   private static final String CREATE_TABLE_SQL =
@@ -62,7 +66,7 @@
           + (LOOKUP_KEY + " text, ")
           + (PHONE_NUMBER + " text, ")
           + (PHONE_LABEL + " text, ")
-          + (PHONE_TYPE + " integer ")
+          + (PHONE_TECHNOLOGY + " integer ")
           + ");";
 
   private static final String DELETE_TABLE_SQL = "drop table if exists " + TABLE_NAME;
@@ -98,15 +102,17 @@
         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;
+        String number = cursor.getString(POSITION_PHONE_NUMBER);
+        Channel channel = null;
+        if (!TextUtils.isEmpty(number)) {
+          channel =
+              Channel.builder()
+                  .setNumber(number)
+                  .setLabel(cursor.getString(POSITION_PHONE_LABEL))
+                  .setTechnology(cursor.getInt(POSITION_PHONE_TECHNOLOGY))
+                  .build();
         }
+
         SpeedDialEntry entry =
             SpeedDialEntry.builder()
                 .setDefaultChannel(channel)
@@ -183,7 +189,7 @@
     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());
+      values.put(PHONE_TECHNOLOGY, entry.defaultChannel().technology());
     }
     return values;
   }