Wire up SpeedDial fragment with SpeedDialUiItemLoader.

This change is mostly just a migration from a cursor loader and
cursor to a listenable future and list of POJOs.

Bug: 36841782
Test: tap
PiperOrigin-RevId: 192349724
Change-Id: I37140dcc2e5e03bc5745573c0d777e18c4f1a880
diff --git a/java/com/android/dialer/binary/basecomponent/BaseDialerRootComponent.java b/java/com/android/dialer/binary/basecomponent/BaseDialerRootComponent.java
index b668d91..2d3ef19 100644
--- a/java/com/android/dialer/binary/basecomponent/BaseDialerRootComponent.java
+++ b/java/com/android/dialer/binary/basecomponent/BaseDialerRootComponent.java
@@ -36,6 +36,7 @@
 import com.android.dialer.preferredsim.suggestion.SimSuggestionComponent;
 import com.android.dialer.simulator.SimulatorComponent;
 import com.android.dialer.spam.SpamComponent;
+import com.android.dialer.speeddial.loader.UiItemLoaderComponent;
 import com.android.dialer.storage.StorageComponent;
 import com.android.dialer.strictmode.StrictModeComponent;
 import com.android.incallui.calllocation.CallLocationComponent;
@@ -67,6 +68,7 @@
         PhoneLookupDatabaseComponent.HasComponent,
         PhoneNumberGeoUtilComponent.HasComponent,
         PreCallComponent.HasComponent,
+        UiItemLoaderComponent.HasComponent,
         SimSuggestionComponent.HasComponent,
         SimulatorComponent.HasComponent,
         SpamComponent.HasComponent,
diff --git a/java/com/android/dialer/speeddial/FavoritesViewHolder.java b/java/com/android/dialer/speeddial/FavoritesViewHolder.java
index c25b05e..92ffb0a 100644
--- a/java/com/android/dialer/speeddial/FavoritesViewHolder.java
+++ b/java/com/android/dialer/speeddial/FavoritesViewHolder.java
@@ -17,13 +17,8 @@
 package com.android.dialer.speeddial;
 
 import android.content.Context;
-import android.content.res.Resources;
-import android.database.Cursor;
-import android.net.Uri;
-import android.provider.ContactsContract.CommonDataKinds.Phone;
 import android.provider.ContactsContract.Contacts;
 import android.support.v7.widget.RecyclerView;
-import android.text.TextUtils;
 import android.view.View;
 import android.view.View.OnClickListener;
 import android.view.View.OnLongClickListener;
@@ -31,8 +26,12 @@
 import android.widget.QuickContactBadge;
 import android.widget.TextView;
 import com.android.dialer.common.Assert;
-import com.android.dialer.contactphoto.ContactPhotoManager;
-import com.android.dialer.lettertile.LetterTileDrawable;
+import com.android.dialer.glidephotomanager.GlidePhotoManagerComponent;
+import com.android.dialer.glidephotomanager.PhotoInfo;
+import com.android.dialer.speeddial.database.SpeedDialEntry.Channel;
+import com.android.dialer.speeddial.loader.SpeedDialUiItem;
+import java.util.ArrayList;
+import java.util.List;
 
 /** ViewHolder for starred/favorite contacts in {@link SpeedDialFragment}. */
 public class FavoritesViewHolder extends RecyclerView.ViewHolder
@@ -48,7 +47,7 @@
   private boolean hasDefaultNumber;
   private boolean isVideoCall;
   private String number;
-  private String lookupKey;
+  private List<Channel> channels;
 
   public FavoritesViewHolder(View view, FavoriteContactsListener listener) {
     super(view);
@@ -62,44 +61,37 @@
     this.listener = listener;
   }
 
-  public void bind(Context context, Cursor cursor) {
-    Assert.checkArgument(cursor.getInt(StrequentContactsCursorLoader.PHONE_STARRED) == 1);
-    isVideoCall = false; // TODO(calderwoodra): get from disambig data
-    number = cursor.getString(StrequentContactsCursorLoader.PHONE_NUMBER);
+  public void bind(Context context, SpeedDialUiItem speedDialUiItem) {
+    Assert.checkArgument(speedDialUiItem.isStarred());
 
-    String name = cursor.getString(StrequentContactsCursorLoader.PHONE_DISPLAY_NAME);
-    long contactId = cursor.getLong(StrequentContactsCursorLoader.PHONE_ID);
-    lookupKey = cursor.getString(StrequentContactsCursorLoader.PHONE_LOOKUP_KEY);
-    Uri contactUri = Contacts.getLookupUri(contactId, lookupKey);
-
-    String photoUri = cursor.getString(StrequentContactsCursorLoader.PHONE_PHOTO_URI);
-    ContactPhotoManager.getInstance(context)
-        .loadDialerThumbnailOrPhoto(
-            photoView,
-            contactUri,
-            cursor.getLong(StrequentContactsCursorLoader.PHONE_PHOTO_ID),
-            photoUri == null ? null : Uri.parse(photoUri),
-            name,
-            LetterTileDrawable.TYPE_DEFAULT);
-    nameView.setText(name);
-    phoneType.setText(getLabel(context.getResources(), cursor));
-    videoCallIcon.setVisibility(isVideoCall ? View.VISIBLE : View.GONE);
-
-    // TODO(calderwoodra): Update this to include communication avenues also
-    hasDefaultNumber = cursor.getInt(StrequentContactsCursorLoader.PHONE_IS_SUPER_PRIMARY) != 0;
-  }
-
-  // TODO(calderwoodra): handle CNAP and cequint types.
-  // TODO(calderwoodra): unify this into a utility method with CallLogAdapter#getNumberType
-  private static String getLabel(Resources resources, Cursor cursor) {
-    int numberType = cursor.getInt(StrequentContactsCursorLoader.PHONE_TYPE);
-    String numberLabel = cursor.getString(StrequentContactsCursorLoader.PHONE_LABEL);
-
-    // Returns empty label instead of "custom" if the custom label is empty.
-    if (numberType == Phone.TYPE_CUSTOM && TextUtils.isEmpty(numberLabel)) {
-      return "";
+    nameView.setText(speedDialUiItem.name());
+    hasDefaultNumber = speedDialUiItem.defaultChannel() != null;
+    if (hasDefaultNumber) {
+      channels = new ArrayList<>();
+      isVideoCall = speedDialUiItem.defaultChannel().isVideoTechnology();
+      number = speedDialUiItem.defaultChannel().number();
+      phoneType.setText(speedDialUiItem.defaultChannel().label());
+      videoCallIcon.setVisibility(isVideoCall ? View.VISIBLE : View.GONE);
+    } else {
+      channels = speedDialUiItem.channels();
+      isVideoCall = false;
+      number = null;
+      phoneType.setText("");
+      videoCallIcon.setVisibility(View.GONE);
     }
-    return (String) Phone.getTypeLabel(resources, numberType, numberLabel);
+
+    GlidePhotoManagerComponent.get(context)
+        .glidePhotoManager()
+        .loadQuickContactBadge(
+            photoView,
+            PhotoInfo.newBuilder()
+                .setPhotoId(speedDialUiItem.photoId())
+                .setPhotoUri(speedDialUiItem.photoUri())
+                .setName(speedDialUiItem.name())
+                .setLookupUri(
+                    Contacts.getLookupUri(speedDialUiItem.contactId(), speedDialUiItem.lookupKey())
+                        .toString())
+                .build());
   }
 
   @Override
@@ -107,7 +99,7 @@
     if (hasDefaultNumber) {
       listener.onClick(number, isVideoCall);
     } else {
-      listener.onAmbiguousContactClicked(lookupKey);
+      listener.onAmbiguousContactClicked(channels);
     }
   }
 
@@ -122,7 +114,7 @@
   public interface FavoriteContactsListener {
 
     /** Called when the user clicks on a favorite contact that doesn't have a default number. */
-    void onAmbiguousContactClicked(String contactId);
+    void onAmbiguousContactClicked(List<Channel> channels);
 
     /** Called when the user clicks on a favorite contact. */
     void onClick(String number, boolean isVideoCall);
diff --git a/java/com/android/dialer/speeddial/SpeedDialAdapter.java b/java/com/android/dialer/speeddial/SpeedDialAdapter.java
index 5f7b68e..3312397 100644
--- a/java/com/android/dialer/speeddial/SpeedDialAdapter.java
+++ b/java/com/android/dialer/speeddial/SpeedDialAdapter.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2017 The Android Open Source Project
+ * 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.
@@ -16,19 +16,30 @@
 
 package com.android.dialer.speeddial;
 
+import android.annotation.TargetApi;
 import android.content.Context;
+import android.os.Build.VERSION_CODES;
+import android.support.annotation.IntDef;
+import android.support.annotation.NonNull;
 import android.support.annotation.VisibleForTesting;
 import android.support.v7.widget.GridLayoutManager;
 import android.support.v7.widget.GridLayoutManager.SpanSizeLookup;
 import android.support.v7.widget.RecyclerView;
 import android.support.v7.widget.RecyclerView.LayoutManager;
+import android.support.v7.widget.RecyclerView.ViewHolder;
+import android.util.ArrayMap;
 import android.view.LayoutInflater;
 import android.view.ViewGroup;
 import com.android.dialer.common.Assert;
 import com.android.dialer.speeddial.FavoritesViewHolder.FavoriteContactsListener;
 import com.android.dialer.speeddial.HeaderViewHolder.SpeedDialHeaderListener;
-import com.android.dialer.speeddial.SpeedDialCursor.RowType;
 import com.android.dialer.speeddial.SuggestionViewHolder.SuggestedContactsListener;
+import com.android.dialer.speeddial.loader.SpeedDialUiItem;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
 
 /**
  * RecyclerView adapter for {@link SpeedDialFragment}.
@@ -42,14 +53,26 @@
  *   <li>Suggested contacts
  * </ol>
  */
-final class SpeedDialAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
+@SuppressWarnings("AndroidApiChecker")
+@TargetApi(VERSION_CODES.N)
+public final class SpeedDialAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
+
+  @Retention(RetentionPolicy.SOURCE)
+  @IntDef({RowType.STARRED_HEADER, RowType.SUGGESTION_HEADER, RowType.STARRED, RowType.SUGGESTION})
+  @interface RowType {
+    int STARRED_HEADER = 0;
+    int SUGGESTION_HEADER = 1;
+    int STARRED = 2;
+    int SUGGESTION = 3;
+  }
 
   private final Context context;
   private final FavoriteContactsListener favoritesListener;
   private final SuggestedContactsListener suggestedListener;
   private final SpeedDialHeaderListener headerListener;
 
-  private SpeedDialCursor cursor;
+  private final Map<Integer, Integer> positionToRowTypeMap = new ArrayMap<>();
+  private List<SpeedDialUiItem> speedDialUiItems;
 
   public SpeedDialAdapter(
       Context context,
@@ -64,39 +87,45 @@
 
   @Override
   public int getItemViewType(int position) {
-    return cursor.getRowType(position);
+    return positionToRowTypeMap.get(position);
   }
 
+  @NonNull
   @Override
-  public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+  public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
     LayoutInflater inflater = LayoutInflater.from(context);
-    if (viewType == RowType.STARRED) {
-      return new FavoritesViewHolder(
-          inflater.inflate(R.layout.favorite_item_layout, parent, false), favoritesListener);
-    } else if (viewType == RowType.SUGGESTION) {
-      return new SuggestionViewHolder(
-          inflater.inflate(R.layout.suggestion_row_layout, parent, false), suggestedListener);
-    } else if (viewType == RowType.HEADER) {
-      return new HeaderViewHolder(
-          inflater.inflate(R.layout.speed_dial_header_layout, parent, false), headerListener);
-    } else {
-      throw Assert.createIllegalStateFailException("Invalid viewType: " + viewType);
+    switch (viewType) {
+      case RowType.STARRED:
+        return new FavoritesViewHolder(
+            inflater.inflate(R.layout.favorite_item_layout, parent, false), favoritesListener);
+      case RowType.SUGGESTION:
+        return new SuggestionViewHolder(
+            inflater.inflate(R.layout.suggestion_row_layout, parent, false), suggestedListener);
+      case RowType.STARRED_HEADER:
+      case RowType.SUGGESTION_HEADER:
+        return new HeaderViewHolder(
+            inflater.inflate(R.layout.speed_dial_header_layout, parent, false), headerListener);
+      default:
+        throw Assert.createIllegalStateFailException("Invalid viewType: " + viewType);
     }
   }
 
   @Override
-  public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
-    cursor.moveToPosition(position);
-    switch (cursor.getRowType(position)) {
-      case RowType.HEADER:
-        ((HeaderViewHolder) holder).setHeaderText(cursor.getHeader());
-        ((HeaderViewHolder) holder).showAddButton(cursor.hasFavorites() && position == 0);
-        break;
+  public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
+    switch (getItemViewType(position)) {
+      case RowType.STARRED_HEADER:
+        ((HeaderViewHolder) holder).setHeaderText(R.string.favorites_header);
+        ((HeaderViewHolder) holder).showAddButton(true);
+        return;
+      case RowType.SUGGESTION_HEADER:
+        ((HeaderViewHolder) holder).setHeaderText(R.string.suggestions_header);
+        ((HeaderViewHolder) holder).showAddButton(false);
+        return;
       case RowType.STARRED:
-        ((FavoritesViewHolder) holder).bind(context, cursor);
+        ((FavoritesViewHolder) holder).bind(context, speedDialUiItems.get(position - 1));
         break;
       case RowType.SUGGESTION:
-        ((SuggestionViewHolder) holder).bind(context, cursor);
+        ((SuggestionViewHolder) holder).bind(context, speedDialUiItems.get(position - 2));
         break;
       default:
         throw Assert.createIllegalStateFailException("Invalid view holder: " + holder);
@@ -105,15 +134,35 @@
 
   @Override
   public int getItemCount() {
-    return cursor == null || cursor.isClosed() ? 0 : cursor.getCount();
+    return positionToRowTypeMap.size();
   }
 
-  public void setCursor(SpeedDialCursor cursor) {
-    this.cursor = cursor;
-    notifyDataSetChanged();
+  public void setSpeedDialUiItems(List<SpeedDialUiItem> immutableSpeedDialUiItems) {
+    speedDialUiItems = new ArrayList<>();
+    speedDialUiItems.addAll(immutableSpeedDialUiItems);
+    speedDialUiItems.sort((o1, o2) -> Boolean.compare(o2.isStarred(), o1.isStarred()));
+    positionToRowTypeMap.clear();
+    if (speedDialUiItems.isEmpty()) {
+      return;
+    }
+
+    // Show the add favorites even if there are no favorite contacts
+    positionToRowTypeMap.put(0, RowType.STARRED_HEADER);
+    int positionOfSuggestionHeader = 1;
+    for (int i = 0; i < speedDialUiItems.size(); i++) {
+      if (speedDialUiItems.get(i).isStarred()) {
+        positionToRowTypeMap.put(i + 1, RowType.STARRED); // +1 for the header
+        positionOfSuggestionHeader++;
+      } else {
+        positionToRowTypeMap.put(i + 2, RowType.SUGGESTION); // +2 for both headers
+      }
+    }
+    if (!speedDialUiItems.get(speedDialUiItems.size() - 1).isStarred()) {
+      positionToRowTypeMap.put(positionOfSuggestionHeader, RowType.SUGGESTION_HEADER);
+    }
   }
 
-  LayoutManager getLayoutManager(Context context) {
+  /* package-private */ LayoutManager getLayoutManager(Context context) {
     GridLayoutManager layoutManager = new GridLayoutManager(context, 3 /* spanCount */);
     layoutManager.setSpanSizeLookup(
         new SpanSizeLookup() {
@@ -127,15 +176,16 @@
 
   @VisibleForTesting
   int getSpanSize(int position) {
-    switch (cursor.getRowType(position)) {
+    switch (getItemViewType(position)) {
       case RowType.SUGGESTION:
-      case RowType.HEADER:
+      case RowType.STARRED_HEADER:
+      case RowType.SUGGESTION_HEADER:
         return 3; // span the whole screen
       case RowType.STARRED:
         return 1; // span 1/3 of the screen
       default:
         throw Assert.createIllegalStateFailException(
-            "Invalid row type: " + cursor.getRowType(position));
+            "Invalid row type: " + positionToRowTypeMap.get(position));
     }
   }
 }
diff --git a/java/com/android/dialer/speeddial/SpeedDialCursor.java b/java/com/android/dialer/speeddial/SpeedDialCursor.java
deleted file mode 100644
index 1208dae..0000000
--- a/java/com/android/dialer/speeddial/SpeedDialCursor.java
+++ /dev/null
@@ -1,148 +0,0 @@
-/*
- * Copyright (C) 2017 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.SuppressLint;
-import android.database.Cursor;
-import android.database.MatrixCursor;
-import android.database.MergeCursor;
-import android.support.annotation.IntDef;
-import android.support.annotation.StringRes;
-import com.android.dialer.common.Assert;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.util.ArrayList;
-import java.util.List;
-
-/** Cursor for favorites contacts. */
-final class SpeedDialCursor extends MergeCursor {
-
-  /**
-   * Caps the speed dial list to contain at most 20 contacts, including favorites and suggestions.
-   * It is only a soft limit though, for the case that there are more than 20 favorite contacts.
-   */
-  private static final int SPEED_DIAL_CONTACT_LIST_SOFT_LIMIT = 20;
-
-  private static final String[] HEADER_CURSOR_PROJECTION = {"header"};
-  private static final int HEADER_COLUMN_POSITION = 0;
-  private boolean hasFavorites;
-
-  @Retention(RetentionPolicy.SOURCE)
-  @IntDef({RowType.HEADER, RowType.STARRED, RowType.SUGGESTION})
-  @interface RowType {
-    int HEADER = 0;
-    int STARRED = 1;
-    int SUGGESTION = 2;
-  }
-
-  public static SpeedDialCursor newInstance(Cursor strequentCursor) {
-    if (strequentCursor == null || strequentCursor.getCount() == 0) {
-      return null;
-    }
-    SpeedDialCursor cursor = new SpeedDialCursor(buildCursors(strequentCursor));
-    strequentCursor.close();
-    return cursor;
-  }
-
-  private static Cursor[] buildCursors(Cursor strequentCursor) {
-    MatrixCursor starred = new MatrixCursor(StrequentContactsCursorLoader.PHONE_PROJECTION);
-    MatrixCursor suggestions = new MatrixCursor(StrequentContactsCursorLoader.PHONE_PROJECTION);
-
-    strequentCursor.moveToPosition(-1);
-    while (strequentCursor.moveToNext()) {
-      if (strequentCursor.getPosition() != 0) {
-        long contactId = strequentCursor.getLong(StrequentContactsCursorLoader.PHONE_CONTACT_ID);
-        int position = strequentCursor.getPosition();
-        boolean duplicate = false;
-        // Iterate backwards through the cursor to check that this isn't a duplicate contact
-        // TODO(calderwoodra): improve this algorithm (currently O(n^2)).
-        while (strequentCursor.moveToPrevious() && !duplicate) {
-          duplicate |=
-              strequentCursor.getLong(StrequentContactsCursorLoader.PHONE_CONTACT_ID) == contactId;
-        }
-        strequentCursor.moveToPosition(position);
-        if (duplicate) {
-          continue;
-        }
-      }
-
-      if (strequentCursor.getInt(StrequentContactsCursorLoader.PHONE_STARRED) == 1) {
-        StrequentContactsCursorLoader.addToCursor(starred, strequentCursor);
-      } else if (starred.getCount() + suggestions.getCount() < SPEED_DIAL_CONTACT_LIST_SOFT_LIMIT) {
-        // Since all starred contacts come before each non-starred contact, it's safe to assume that
-        // this list will never exceed the soft limit unless there are more starred contacts than
-        // the limit permits.
-        StrequentContactsCursorLoader.addToCursor(suggestions, strequentCursor);
-      }
-    }
-
-    List<Cursor> cursorList = new ArrayList<>();
-    if (starred.getCount() > 0) {
-      cursorList.add(createHeaderCursor(R.string.favorites_header));
-      cursorList.add(starred);
-    }
-    if (suggestions.getCount() > 0) {
-      cursorList.add(createHeaderCursor(R.string.suggestions_header));
-      cursorList.add(suggestions);
-    }
-    return cursorList.toArray(new Cursor[cursorList.size()]);
-  }
-
-  private static Cursor createHeaderCursor(@StringRes int header) {
-    MatrixCursor cursor = new MatrixCursor(HEADER_CURSOR_PROJECTION);
-    cursor.newRow().add(HEADER_CURSOR_PROJECTION[HEADER_COLUMN_POSITION], header);
-    return cursor;
-  }
-
-  @RowType
-  int getRowType(int position) {
-    moveToPosition(position);
-    if (getColumnCount() == 1) {
-      return RowType.HEADER;
-    } else if (getInt(StrequentContactsCursorLoader.PHONE_STARRED) == 1) {
-      return RowType.STARRED;
-    } else {
-      return RowType.SUGGESTION;
-    }
-  }
-
-  @SuppressLint("DefaultLocale")
-  @StringRes
-  int getHeader() {
-    if (getRowType(getPosition()) != RowType.HEADER) {
-      throw Assert.createIllegalStateFailException(
-          String.format("Current position (%d) is not a header.", getPosition()));
-    }
-    return getInt(HEADER_COLUMN_POSITION);
-  }
-
-  public boolean hasFavorites() {
-    return hasFavorites;
-  }
-
-  private SpeedDialCursor(Cursor[] cursors) {
-    super(cursors);
-    for (Cursor cursor : cursors) {
-      cursor.moveToFirst();
-      if (cursor.getColumnCount() != 1
-          && cursor.getInt(StrequentContactsCursorLoader.PHONE_STARRED) == 1) {
-        hasFavorites = true;
-        break;
-      }
-    }
-  }
-}
diff --git a/java/com/android/dialer/speeddial/SpeedDialFragment.java b/java/com/android/dialer/speeddial/SpeedDialFragment.java
index 03a3c75..d1f195b 100644
--- a/java/com/android/dialer/speeddial/SpeedDialFragment.java
+++ b/java/com/android/dialer/speeddial/SpeedDialFragment.java
@@ -17,23 +17,28 @@
 package com.android.dialer.speeddial;
 
 import android.content.Intent;
-import android.database.Cursor;
 import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
 import android.support.annotation.Nullable;
 import android.support.v4.app.Fragment;
-import android.support.v4.app.LoaderManager.LoaderCallbacks;
-import android.support.v4.content.Loader;
 import android.support.v7.widget.RecyclerView;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.ViewGroup;
 import com.android.dialer.callintent.CallInitiationType;
 import com.android.dialer.callintent.CallIntentBuilder;
-import com.android.dialer.common.Assert;
 import com.android.dialer.precall.PreCall;
 import com.android.dialer.speeddial.FavoritesViewHolder.FavoriteContactsListener;
 import com.android.dialer.speeddial.HeaderViewHolder.SpeedDialHeaderListener;
 import com.android.dialer.speeddial.SuggestionViewHolder.SuggestedContactsListener;
+import com.android.dialer.speeddial.database.SpeedDialEntry.Channel;
+import com.android.dialer.speeddial.loader.SpeedDialUiItem;
+import com.android.dialer.speeddial.loader.UiItemLoaderComponent;
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.MoreExecutors;
+import java.util.List;
 
 /**
  * Fragment for displaying:
@@ -47,13 +52,9 @@
  */
 public class SpeedDialFragment extends Fragment {
 
-  private static final int STREQUENT_CONTACTS_LOADER_ID = 1;
-
   private final SpeedDialHeaderListener headerListener = new SpeedDialFragmentHeaderListener();
   private final FavoriteContactsListener favoritesListener = new SpeedDialFavoritesListener();
   private final SuggestedContactsListener suggestedListener = new SpeedDialSuggestedListener();
-  private final SpeedDialFragmentLoaderCallback loaderCallback =
-      new SpeedDialFragmentLoaderCallback();
 
   private SpeedDialAdapter adapter;
 
@@ -72,7 +73,6 @@
         new SpeedDialAdapter(getContext(), favoritesListener, suggestedListener, headerListener);
     recyclerView.setLayoutManager(adapter.getLayoutManager(getContext()));
     recyclerView.setAdapter(adapter);
-    getLoaderManager().initLoader(STREQUENT_CONTACTS_LOADER_ID, null /* args */, loaderCallback);
     return view;
   }
 
@@ -84,7 +84,28 @@
   @Override
   public void onResume() {
     super.onResume();
-    getLoaderManager().restartLoader(STREQUENT_CONTACTS_LOADER_ID, null, loaderCallback);
+    Futures.addCallback(
+        UiItemLoaderComponent.get(getContext().getApplicationContext())
+            .speedDialUiItemLoader()
+            .loadSpeedDialUiItems(),
+        new FutureCallback<List<SpeedDialUiItem>>() {
+          @Override
+          public void onSuccess(List<SpeedDialUiItem> speedDialUiItems) {
+            // TODO(calderwoodra): this is bad
+            new Handler(Looper.getMainLooper())
+                .post(
+                    () -> {
+                      adapter.setSpeedDialUiItems(speedDialUiItems);
+                      adapter.notifyDataSetChanged();
+                    });
+          }
+
+          @Override
+          public void onFailure(Throwable throwable) {
+            throw new RuntimeException(throwable);
+          }
+        },
+        MoreExecutors.directExecutor());
   }
 
   private class SpeedDialFragmentHeaderListener implements SpeedDialHeaderListener {
@@ -98,8 +119,8 @@
   private class SpeedDialFavoritesListener implements FavoriteContactsListener {
 
     @Override
-    public void onAmbiguousContactClicked(String lookupKey) {
-      DisambigDialog.show(lookupKey, getFragmentManager());
+    public void onAmbiguousContactClicked(List<Channel> channels) {
+      // TODO(calderwoodra): implement the disambig dialog with channels
     }
 
     @Override
@@ -130,29 +151,4 @@
           getContext(), new CallIntentBuilder(number, CallInitiationType.Type.SPEED_DIAL));
     }
   }
-
-  /**
-   * Loader callback that registers a content observer. {@link #unregisterContentObserver()} needs
-   * to be called during tear down of the fragment.
-   */
-  private class SpeedDialFragmentLoaderCallback implements LoaderCallbacks<Cursor> {
-
-    @Override
-    public Loader<Cursor> onCreateLoader(int id, Bundle args) {
-      if (id == STREQUENT_CONTACTS_LOADER_ID) {
-        return new StrequentContactsCursorLoader(getContext());
-      }
-      throw Assert.createIllegalStateFailException("Invalid loader id: " + id);
-    }
-
-    @Override
-    public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
-      adapter.setCursor((SpeedDialCursor) data);
-    }
-
-    @Override
-    public void onLoaderReset(Loader<Cursor> loader) {
-      adapter.setCursor(null);
-    }
-  }
 }
diff --git a/java/com/android/dialer/speeddial/StrequentContactsCursorLoader.java b/java/com/android/dialer/speeddial/StrequentContactsCursorLoader.java
deleted file mode 100644
index a2dcfdc..0000000
--- a/java/com/android/dialer/speeddial/StrequentContactsCursorLoader.java
+++ /dev/null
@@ -1,97 +0,0 @@
-/*
- * Copyright (C) 2017 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.content.Context;
-import android.database.Cursor;
-import android.database.MatrixCursor;
-import android.net.Uri;
-import android.provider.ContactsContract;
-import android.provider.ContactsContract.CommonDataKinds.Phone;
-import android.provider.ContactsContract.Contacts;
-import android.support.v4.content.CursorLoader;
-
-/** Cursor Loader for strequent contacts. */
-public final class StrequentContactsCursorLoader extends CursorLoader {
-
-  static final int PHONE_ID = 0;
-  static final int PHONE_DISPLAY_NAME = 1;
-  static final int PHONE_STARRED = 2;
-  static final int PHONE_PHOTO_URI = 3;
-  static final int PHONE_LOOKUP_KEY = 4;
-  static final int PHONE_PHOTO_ID = 5;
-  static final int PHONE_NUMBER = 6;
-  static final int PHONE_TYPE = 7;
-  static final int PHONE_LABEL = 8;
-  static final int PHONE_IS_SUPER_PRIMARY = 9;
-  static final int PHONE_PINNED = 10;
-  static final int PHONE_CONTACT_ID = 11;
-
-  public static final String[] PHONE_PROJECTION =
-      new String[] {
-        Phone._ID, // 0
-        Phone.DISPLAY_NAME, // 1
-        Phone.STARRED, // 2
-        Phone.PHOTO_URI, // 3
-        Phone.LOOKUP_KEY, // 4
-        Phone.PHOTO_ID, // 5
-        Phone.NUMBER, // 6
-        Phone.TYPE, // 7
-        Phone.LABEL, // 8
-        Phone.IS_SUPER_PRIMARY, // 9
-        Phone.PINNED, // 10
-        Phone.CONTACT_ID, // 11
-      };
-
-  StrequentContactsCursorLoader(Context context) {
-    super(
-        context,
-        buildUri(),
-        PHONE_PROJECTION,
-        null /* selection */,
-        null /* selectionArgs */,
-        null /* sortOrder */);
-    // TODO(calderwoodra): implement alternative display names
-  }
-
-  static void addToCursor(MatrixCursor dest, Cursor source) {
-    dest.newRow()
-        .add(PHONE_PROJECTION[PHONE_ID], source.getLong(PHONE_ID))
-        .add(PHONE_PROJECTION[PHONE_DISPLAY_NAME], source.getString(PHONE_DISPLAY_NAME))
-        .add(PHONE_PROJECTION[PHONE_STARRED], source.getInt(PHONE_STARRED))
-        .add(PHONE_PROJECTION[PHONE_PHOTO_URI], source.getString(PHONE_PHOTO_URI))
-        .add(PHONE_PROJECTION[PHONE_LOOKUP_KEY], source.getString(PHONE_LOOKUP_KEY))
-        .add(PHONE_PROJECTION[PHONE_NUMBER], source.getString(PHONE_NUMBER))
-        .add(PHONE_PROJECTION[PHONE_TYPE], source.getInt(PHONE_TYPE))
-        .add(PHONE_PROJECTION[PHONE_LABEL], source.getString(PHONE_LABEL))
-        .add(PHONE_PROJECTION[PHONE_IS_SUPER_PRIMARY], source.getInt(PHONE_IS_SUPER_PRIMARY))
-        .add(PHONE_PROJECTION[PHONE_PINNED], source.getInt(PHONE_PINNED))
-        .add(PHONE_PROJECTION[PHONE_CONTACT_ID], source.getLong(PHONE_CONTACT_ID));
-  }
-
-  private static Uri buildUri() {
-    return Contacts.CONTENT_STREQUENT_URI
-        .buildUpon()
-        .appendQueryParameter(ContactsContract.STREQUENT_PHONE_ONLY, "true")
-        .build();
-  }
-
-  @Override
-  public Cursor loadInBackground() {
-    return SpeedDialCursor.newInstance(super.loadInBackground());
-  }
-}
diff --git a/java/com/android/dialer/speeddial/SuggestionViewHolder.java b/java/com/android/dialer/speeddial/SuggestionViewHolder.java
index 213a54f..9e4c81d 100644
--- a/java/com/android/dialer/speeddial/SuggestionViewHolder.java
+++ b/java/com/android/dialer/speeddial/SuggestionViewHolder.java
@@ -17,10 +17,6 @@
 package com.android.dialer.speeddial;
 
 import android.content.Context;
-import android.content.res.Resources;
-import android.database.Cursor;
-import android.net.Uri;
-import android.provider.ContactsContract.CommonDataKinds.Phone;
 import android.provider.ContactsContract.Contacts;
 import android.support.v7.widget.RecyclerView;
 import android.text.TextUtils;
@@ -28,10 +24,12 @@
 import android.view.View.OnClickListener;
 import android.widget.QuickContactBadge;
 import android.widget.TextView;
-import com.android.dialer.contactphoto.ContactPhotoManager;
-import com.android.dialer.lettertile.LetterTileDrawable;
+import com.android.dialer.common.Assert;
+import com.android.dialer.glidephotomanager.GlidePhotoManagerComponent;
+import com.android.dialer.glidephotomanager.PhotoInfo;
 import com.android.dialer.location.GeoUtil;
 import com.android.dialer.phonenumberutil.PhoneNumberHelper;
+import com.android.dialer.speeddial.loader.SpeedDialUiItem;
 
 /** ViewHolder for displaying suggested contacts in {@link SpeedDialFragment}. */
 public class SuggestionViewHolder extends RecyclerView.ViewHolder implements OnClickListener {
@@ -54,46 +52,35 @@
     this.listener = listener;
   }
 
-  public void bind(Context context, Cursor cursor) {
-    number = cursor.getString(StrequentContactsCursorLoader.PHONE_NUMBER);
-    number = PhoneNumberHelper.formatNumber(context, number, GeoUtil.getCurrentCountryIso(context));
+  public void bind(Context context, SpeedDialUiItem speedDialUiItem) {
+    Assert.isNotNull(speedDialUiItem.defaultChannel());
+    number =
+        PhoneNumberHelper.formatNumber(
+            context,
+            speedDialUiItem.defaultChannel().number(),
+            GeoUtil.getCurrentCountryIso(context));
 
-    String name = cursor.getString(StrequentContactsCursorLoader.PHONE_DISPLAY_NAME);
-    String label = getLabel(context.getResources(), cursor);
+    String label = speedDialUiItem.defaultChannel().label();
     String secondaryInfo =
         TextUtils.isEmpty(label)
             ? number
             : context.getString(R.string.call_subject_type_and_number, label, number);
 
-    nameOrNumberView.setText(name);
+    nameOrNumberView.setText(speedDialUiItem.name());
     numberView.setText(secondaryInfo);
 
-    long contactId = cursor.getLong(StrequentContactsCursorLoader.PHONE_ID);
-    String lookupKey = cursor.getString(StrequentContactsCursorLoader.PHONE_LOOKUP_KEY);
-    Uri contactUri = Contacts.getLookupUri(contactId, lookupKey);
-
-    String photoUri = cursor.getString(StrequentContactsCursorLoader.PHONE_PHOTO_URI);
-    ContactPhotoManager.getInstance(context)
-        .loadDialerThumbnailOrPhoto(
+    GlidePhotoManagerComponent.get(context)
+        .glidePhotoManager()
+        .loadQuickContactBadge(
             photoView,
-            contactUri,
-            cursor.getLong(StrequentContactsCursorLoader.PHONE_PHOTO_ID),
-            photoUri == null ? null : Uri.parse(photoUri),
-            name,
-            LetterTileDrawable.TYPE_DEFAULT);
-  }
-
-  // TODO(calderwoodra): handle CNAP and cequint types.
-  // TODO(calderwoodra): unify this into a utility method with CallLogAdapter#getNumberType
-  private static String getLabel(Resources resources, Cursor cursor) {
-    int numberType = cursor.getInt(StrequentContactsCursorLoader.PHONE_TYPE);
-    String numberLabel = cursor.getString(StrequentContactsCursorLoader.PHONE_LABEL);
-
-    // Returns empty label instead of "custom" if the custom label is empty.
-    if (numberType == Phone.TYPE_CUSTOM && TextUtils.isEmpty(numberLabel)) {
-      return "";
-    }
-    return (String) Phone.getTypeLabel(resources, numberType, numberLabel);
+            PhotoInfo.newBuilder()
+                .setPhotoId(speedDialUiItem.photoId())
+                .setPhotoUri(speedDialUiItem.photoUri())
+                .setName(speedDialUiItem.name())
+                .setLookupUri(
+                    Contacts.getLookupUri(speedDialUiItem.contactId(), speedDialUiItem.lookupKey())
+                        .toString())
+                .build());
   }
 
   @Override
diff --git a/java/com/android/dialer/speeddial/database/SpeedDialEntry.java b/java/com/android/dialer/speeddial/database/SpeedDialEntry.java
index 5b54b79..13ef4e3 100644
--- a/java/com/android/dialer/speeddial/database/SpeedDialEntry.java
+++ b/java/com/android/dialer/speeddial/database/SpeedDialEntry.java
@@ -88,6 +88,7 @@
     public boolean isVideoTechnology() {
       return technology() == IMS_VIDEO || technology() == DUO;
     }
+
     /**
      * Raw phone number as the user entered it.
      *
diff --git a/java/com/android/dialer/speeddial/SpeedDialUiItem.java b/java/com/android/dialer/speeddial/loader/SpeedDialUiItem.java
similarity index 98%
rename from java/com/android/dialer/speeddial/SpeedDialUiItem.java
rename to java/com/android/dialer/speeddial/loader/SpeedDialUiItem.java
index 17552ad..3381bf8 100644
--- a/java/com/android/dialer/speeddial/SpeedDialUiItem.java
+++ b/java/com/android/dialer/speeddial/loader/SpeedDialUiItem.java
@@ -14,7 +14,7 @@
  * limitations under the License
  */
 
-package com.android.dialer.speeddial;
+package com.android.dialer.speeddial.loader;
 
 import android.database.Cursor;
 import android.provider.ContactsContract.CommonDataKinds.Phone;
diff --git a/java/com/android/dialer/speeddial/SpeedDialUiItemLoader.java b/java/com/android/dialer/speeddial/loader/SpeedDialUiItemLoader.java
similarity index 93%
rename from java/com/android/dialer/speeddial/SpeedDialUiItemLoader.java
rename to java/com/android/dialer/speeddial/loader/SpeedDialUiItemLoader.java
index 13e5f87..c23b67d 100644
--- a/java/com/android/dialer/speeddial/SpeedDialUiItemLoader.java
+++ b/java/com/android/dialer/speeddial/loader/SpeedDialUiItemLoader.java
@@ -14,7 +14,7 @@
  * limitations under the License
  */
 
-package com.android.dialer.speeddial;
+package com.android.dialer.speeddial.loader;
 
 import android.annotation.TargetApi;
 import android.content.Context;
@@ -29,6 +29,7 @@
 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.common.concurrent.DialerFutureSerializer;
 import com.android.dialer.inject.ApplicationContext;
 import com.android.dialer.speeddial.database.SpeedDialEntry;
 import com.android.dialer.speeddial.database.SpeedDialEntry.Channel;
@@ -40,6 +41,7 @@
 import java.util.ArrayList;
 import java.util.List;
 import javax.inject.Inject;
+import javax.inject.Singleton;
 
 /**
  * Loads a list of {@link SpeedDialUiItem SpeedDialUiItems}.
@@ -62,10 +64,13 @@
  */
 @SuppressWarnings("AndroidApiChecker")
 @TargetApi(VERSION_CODES.N)
-public final class SpeedDialUiItemLoader {
+@Singleton
+public final class SpeedDialUiItemLoader implements UiItemLoader {
 
   private final Context appContext;
   private final ListeningExecutorService backgroundExecutor;
+  // Used to ensure that only one refresh flow runs at a time.
+  private final DialerFutureSerializer dialerFutureSerializer = new DialerFutureSerializer();
 
   @Inject
   public SpeedDialUiItemLoader(
@@ -80,8 +85,10 @@
    * list is composed of starred contacts from {@link SpeedDialEntryDatabaseHelper} and suggestions
    * from {@link Contacts#STREQUENT_PHONE_ONLY}.
    */
+  @Override
   public ListenableFuture<ImmutableList<SpeedDialUiItem>> loadSpeedDialUiItems() {
-    return backgroundExecutor.submit(this::doInBackground);
+    return dialerFutureSerializer.submitAsync(
+        () -> backgroundExecutor.submit(this::doInBackground), backgroundExecutor);
   }
 
   @WorkerThread
@@ -128,7 +135,6 @@
     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())) {
diff --git a/java/com/android/dialer/speeddial/loader/UiItemLoader.java b/java/com/android/dialer/speeddial/loader/UiItemLoader.java
new file mode 100644
index 0000000..4b9a731
--- /dev/null
+++ b/java/com/android/dialer/speeddial/loader/UiItemLoader.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2017 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.loader;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.util.concurrent.ListenableFuture;
+
+/** Provides operation for loading {@link SpeedDialUiItem SpeedDialUiItems} */
+public interface UiItemLoader {
+
+  /**
+   * Returns a {@link ListenableFuture} for a list of {@link SpeedDialUiItem SpeedDialUiItems}. This
+   * list is composed of starred contacts from {@link
+   * com.android.dialer.speeddial.database.SpeedDialEntryDatabaseHelper} and suggestions from {@link
+   * android.provider.ContactsContract.Contacts#STREQUENT_PHONE_ONLY}.
+   */
+  ListenableFuture<ImmutableList<SpeedDialUiItem>> loadSpeedDialUiItems();
+}
diff --git a/java/com/android/dialer/speeddial/loader/UiItemLoaderComponent.java b/java/com/android/dialer/speeddial/loader/UiItemLoaderComponent.java
new file mode 100644
index 0000000..7d01b43
--- /dev/null
+++ b/java/com/android/dialer/speeddial/loader/UiItemLoaderComponent.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2017 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.loader;
+
+import android.content.Context;
+import com.android.dialer.inject.HasRootComponent;
+import dagger.Subcomponent;
+
+/** Dagger component for the speeddial/loader package. */
+@Subcomponent
+public abstract class UiItemLoaderComponent {
+
+  public abstract SpeedDialUiItemLoader speedDialUiItemLoader();
+
+  public static UiItemLoaderComponent get(Context context) {
+    return ((UiItemLoaderComponent.HasComponent)
+            ((HasRootComponent) context.getApplicationContext()).component())
+        .uiItemLoaderComponent();
+  }
+
+  /** Used to refer to the root application component. */
+  public interface HasComponent {
+    UiItemLoaderComponent uiItemLoaderComponent();
+  }
+}