Have Coalescer return CoalescedRow protos directly.

Bug: 77813585
Test: CoalescerTest, NewCallLogAdapterTest, NewCallLogViewHolderTest
PiperOrigin-RevId: 200086621
Change-Id: I8f79359a5972c578ae439aaf33233900b0606dc8
diff --git a/java/com/android/dialer/calllog/database/Coalescer.java b/java/com/android/dialer/calllog/database/Coalescer.java
index a889b9f..36df87c 100644
--- a/java/com/android/dialer/calllog/database/Coalescer.java
+++ b/java/com/android/dialer/calllog/database/Coalescer.java
@@ -17,9 +17,9 @@
 
 import android.content.ContentValues;
 import android.database.Cursor;
-import android.database.MatrixCursor;
 import android.provider.CallLog.Calls;
 import android.support.annotation.NonNull;
+import android.support.annotation.VisibleForTesting;
 import android.support.annotation.WorkerThread;
 import android.telecom.PhoneAccountHandle;
 import android.text.TextUtils;
@@ -39,12 +39,12 @@
 import com.android.dialer.phonenumberproto.DialerPhoneNumberUtil;
 import com.android.dialer.telecom.TelecomUtil;
 import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.protobuf.InvalidProtocolBufferException;
 import java.util.ArrayList;
 import java.util.List;
-import java.util.Map;
 import java.util.Objects;
 import javax.inject.Inject;
 
@@ -56,24 +56,6 @@
  */
 public class Coalescer {
 
-  // Indexes for CoalescedAnnotatedCallLog.ALL_COLUMNS
-  private static final int ID = 0;
-  private static final int TIMESTAMP = 1;
-  private static final int NUMBER = 2;
-  private static final int FORMATTED_NUMBER = 3;
-  private static final int NUMBER_PRESENTATION = 4;
-  private static final int IS_READ = 5;
-  private static final int NEW = 6;
-  private static final int GEOCODED_LOCATION = 7;
-  private static final int PHONE_ACCOUNT_COMPONENT_NAME = 8;
-  private static final int PHONE_ACCOUNT_ID = 9;
-  private static final int FEATURES = 10;
-  private static final int NUMBER_ATTRIBUTES = 11;
-  private static final int IS_VOICEMAIL_CALL = 12;
-  private static final int VOICEMAIL_CALL_TAG = 13;
-  private static final int CALL_TYPE = 14;
-  private static final int COALESCED_IDS = 15;
-
   private final DataSources dataSources;
   private final FutureTimer futureTimer;
   private final ListeningExecutorService backgroundExecutorService;
@@ -94,12 +76,12 @@
    *
    * @param allAnnotatedCallLogRowsSortedByTimestampDesc {@link AnnotatedCallLog} rows sorted in
    *     descending order of timestamp.
-   * @return a future of a {@link MatrixCursor} containing the {@link CoalescedAnnotatedCallLog}
-   *     rows to display
+   * @return a future of a list of {@link CoalescedRow coalesced rows}, which will be used to
+   *     display call log entries.
    */
-  public ListenableFuture<Cursor> coalesce(
+  public ListenableFuture<ImmutableList<CoalescedRow>> coalesce(
       @NonNull Cursor allAnnotatedCallLogRowsSortedByTimestampDesc) {
-    ListenableFuture<Cursor> coalescingFuture =
+    ListenableFuture<ImmutableList<CoalescedRow>> coalescingFuture =
         backgroundExecutorService.submit(
             () -> coalesceInternal(Assert.isNotNull(allAnnotatedCallLogRowsSortedByTimestampDesc)));
     futureTimer.applyTiming(coalescingFuture, Metrics.NEW_CALL_LOG_COALESCE);
@@ -108,34 +90,34 @@
 
   /**
    * Reads the entire {@link AnnotatedCallLog} into memory from the provided cursor and then builds
-   * and returns a new {@link MatrixCursor} of {@link CoalescedAnnotatedCallLog}, which is the
-   * result of combining adjacent rows which should be collapsed for display purposes.
+   * and returns a list of {@link CoalescedRow coalesced rows}, which is the result of combining
+   * adjacent rows which should be collapsed for display purposes.
    *
    * @param allAnnotatedCallLogRowsSortedByTimestampDesc {@link AnnotatedCallLog} rows sorted in
    *     descending order of timestamp.
-   * @return a new {@link MatrixCursor} containing the {@link CoalescedAnnotatedCallLog} rows to
-   *     display
+   * @return a list of {@link CoalescedRow coalesced rows}, which will be used to display call log
+   *     entries.
    */
   @WorkerThread
   @NonNull
-  private Cursor coalesceInternal(Cursor allAnnotatedCallLogRowsSortedByTimestampDesc) {
+  private ImmutableList<CoalescedRow> coalesceInternal(
+      Cursor allAnnotatedCallLogRowsSortedByTimestampDesc) {
     Assert.isWorkerThread();
 
+    if (!allAnnotatedCallLogRowsSortedByTimestampDesc.moveToFirst()) {
+      return ImmutableList.of();
+    }
+
     // Note: This method relies on rowsShouldBeCombined to determine which rows should be combined,
     // but delegates to data sources to actually aggregate column values.
 
     DialerPhoneNumberUtil dialerPhoneNumberUtil = new DialerPhoneNumberUtil();
 
-    MatrixCursor allCoalescedRowsMatrixCursor =
-        new MatrixCursor(
-            CoalescedAnnotatedCallLog.ALL_COLUMNS,
-            allAnnotatedCallLogRowsSortedByTimestampDesc.getCount());
-
-    if (!allAnnotatedCallLogRowsSortedByTimestampDesc.moveToFirst()) {
-      return allCoalescedRowsMatrixCursor;
-    }
+    ImmutableList.Builder<CoalescedRow> coalescedRowListBuilder = new ImmutableList.Builder<>();
 
     int coalescedRowId = 0;
+
+    // TODO(a bug): Avoid using ContentValues as it doesn't make sense here.
     List<ContentValues> currentRowGroup = new ArrayList<>();
 
     ContentValues firstRow = cursorRowToContentValues(allAnnotatedCallLogRowsSortedByTimestampDesc);
@@ -157,9 +139,10 @@
 
       // Coalesce the group into a single row
       ContentValues coalescedRow = coalesceRowsForAllDataSources(currentRowGroup);
+      coalescedRow.put(CoalescedAnnotatedCallLog._ID, coalescedRowId++);
       coalescedRow.put(
           CoalescedAnnotatedCallLog.COALESCED_IDS, getCoalescedIds(currentRowGroup).toByteArray());
-      addContentValuesToMatrixCursor(coalescedRow, allCoalescedRowsMatrixCursor, coalescedRowId++);
+      coalescedRowListBuilder.add(toCoalescedRowProto(coalescedRow));
 
       // Clear the current group after the rows are coalesced.
       currentRowGroup.clear();
@@ -170,7 +153,7 @@
       }
     }
 
-    return allCoalescedRowsMatrixCursor;
+    return coalescedRowListBuilder.build();
   }
 
   private static ContentValues cursorRowToContentValues(Cursor cursor) {
@@ -293,34 +276,26 @@
   }
 
   /**
-   * @param contentValues a {@link CoalescedAnnotatedCallLog} row
-   * @param matrixCursor represents {@link CoalescedAnnotatedCallLog}
-   */
-  private static void addContentValuesToMatrixCursor(
-      ContentValues contentValues, MatrixCursor matrixCursor, int rowId) {
-    MatrixCursor.RowBuilder rowBuilder = matrixCursor.newRow();
-    rowBuilder.add(CoalescedAnnotatedCallLog._ID, rowId);
-    for (Map.Entry<String, Object> entry : contentValues.valueSet()) {
-      rowBuilder.add(entry.getKey(), entry.getValue());
-    }
-  }
-
-  /**
-   * Creates a new {@link CoalescedRow} based on the data at the provided cursor's current position.
+   * Creates a new {@link CoalescedRow} proto based on the provided {@link ContentValues}.
    *
-   * <p>The provided cursor should be one for {@link CoalescedAnnotatedCallLog}.
+   * <p>The provided {@link ContentValues} should be one for {@link CoalescedAnnotatedCallLog}.
    */
-  public static CoalescedRow toRow(Cursor coalescedAnnotatedCallLogCursor) {
+  @VisibleForTesting
+  static CoalescedRow toCoalescedRowProto(ContentValues coalescedContentValues) {
     DialerPhoneNumber number;
     try {
-      number = DialerPhoneNumber.parseFrom(coalescedAnnotatedCallLogCursor.getBlob(NUMBER));
+      number =
+          DialerPhoneNumber.parseFrom(
+              coalescedContentValues.getAsByteArray(CoalescedAnnotatedCallLog.NUMBER));
     } catch (InvalidProtocolBufferException e) {
       throw new IllegalStateException("Couldn't parse DialerPhoneNumber bytes");
     }
 
     CoalescedIds coalescedIds;
     try {
-      coalescedIds = CoalescedIds.parseFrom(coalescedAnnotatedCallLogCursor.getBlob(COALESCED_IDS));
+      coalescedIds =
+          CoalescedIds.parseFrom(
+              coalescedContentValues.getAsByteArray(CoalescedAnnotatedCallLog.COALESCED_IDS));
     } catch (InvalidProtocolBufferException e) {
       throw new IllegalStateException("Couldn't parse CoalescedIds bytes");
     }
@@ -328,61 +303,64 @@
     NumberAttributes numberAttributes;
     try {
       numberAttributes =
-          NumberAttributes.parseFrom(coalescedAnnotatedCallLogCursor.getBlob(NUMBER_ATTRIBUTES));
+          NumberAttributes.parseFrom(
+              coalescedContentValues.getAsByteArray(CoalescedAnnotatedCallLog.NUMBER_ATTRIBUTES));
     } catch (InvalidProtocolBufferException e) {
       throw new IllegalStateException("Couldn't parse NumberAttributes bytes");
     }
 
     CoalescedRow.Builder coalescedRowBuilder =
         CoalescedRow.newBuilder()
-            .setId(coalescedAnnotatedCallLogCursor.getLong(ID))
-            .setTimestamp(coalescedAnnotatedCallLogCursor.getLong(TIMESTAMP))
+            .setId(coalescedContentValues.getAsLong(CoalescedAnnotatedCallLog._ID))
+            .setTimestamp(coalescedContentValues.getAsLong(CoalescedAnnotatedCallLog.TIMESTAMP))
             .setNumber(number)
-            .setNumberPresentation(coalescedAnnotatedCallLogCursor.getInt(NUMBER_PRESENTATION))
-            .setIsRead(coalescedAnnotatedCallLogCursor.getInt(IS_READ) == 1)
-            .setIsNew(coalescedAnnotatedCallLogCursor.getInt(NEW) == 1)
-            .setFeatures(coalescedAnnotatedCallLogCursor.getInt(FEATURES))
-            .setCallType(coalescedAnnotatedCallLogCursor.getInt(CALL_TYPE))
+            .setNumberPresentation(
+                coalescedContentValues.getAsInteger(CoalescedAnnotatedCallLog.NUMBER_PRESENTATION))
+            .setIsRead(coalescedContentValues.getAsInteger(CoalescedAnnotatedCallLog.IS_READ) == 1)
+            .setIsNew(coalescedContentValues.getAsInteger(CoalescedAnnotatedCallLog.NEW) == 1)
+            .setFeatures(coalescedContentValues.getAsInteger(CoalescedAnnotatedCallLog.FEATURES))
+            .setCallType(coalescedContentValues.getAsInteger(CoalescedAnnotatedCallLog.CALL_TYPE))
             .setNumberAttributes(numberAttributes)
-            .setIsVoicemailCall(coalescedAnnotatedCallLogCursor.getInt(IS_VOICEMAIL_CALL) == 1)
             .setCoalescedIds(coalescedIds);
 
-    String formattedNumber = coalescedAnnotatedCallLogCursor.getString(FORMATTED_NUMBER);
+    // TODO(linyuh): none of the boolean columns in the annotated call log should be null.
+    // This is a bug in VoicemailDataSource, but we should also fix the database constraints.
+    Integer isVoicemailCall =
+        coalescedContentValues.getAsInteger(CoalescedAnnotatedCallLog.IS_VOICEMAIL_CALL);
+    coalescedRowBuilder.setIsVoicemailCall(isVoicemailCall != null && isVoicemailCall == 1);
+
+    String formattedNumber =
+        coalescedContentValues.getAsString(CoalescedAnnotatedCallLog.FORMATTED_NUMBER);
     if (!TextUtils.isEmpty(formattedNumber)) {
       coalescedRowBuilder.setFormattedNumber(formattedNumber);
     }
 
-    String geocodedLocation = coalescedAnnotatedCallLogCursor.getString(GEOCODED_LOCATION);
+    String geocodedLocation =
+        coalescedContentValues.getAsString(CoalescedAnnotatedCallLog.GEOCODED_LOCATION);
     if (!TextUtils.isEmpty(geocodedLocation)) {
       coalescedRowBuilder.setGeocodedLocation(geocodedLocation);
     }
 
     String phoneAccountComponentName =
-        coalescedAnnotatedCallLogCursor.getString(PHONE_ACCOUNT_COMPONENT_NAME);
+        coalescedContentValues.getAsString(CoalescedAnnotatedCallLog.PHONE_ACCOUNT_COMPONENT_NAME);
     if (!TextUtils.isEmpty(phoneAccountComponentName)) {
       coalescedRowBuilder.setPhoneAccountComponentName(
-          coalescedAnnotatedCallLogCursor.getString(PHONE_ACCOUNT_COMPONENT_NAME));
+          coalescedContentValues.getAsString(
+              CoalescedAnnotatedCallLog.PHONE_ACCOUNT_COMPONENT_NAME));
     }
 
-    String phoneAccountId = coalescedAnnotatedCallLogCursor.getString(PHONE_ACCOUNT_ID);
+    String phoneAccountId =
+        coalescedContentValues.getAsString(CoalescedAnnotatedCallLog.PHONE_ACCOUNT_ID);
     if (!TextUtils.isEmpty(phoneAccountId)) {
       coalescedRowBuilder.setPhoneAccountId(phoneAccountId);
     }
 
-    String voicemailCallTag = coalescedAnnotatedCallLogCursor.getString(VOICEMAIL_CALL_TAG);
+    String voicemailCallTag =
+        coalescedContentValues.getAsString(CoalescedAnnotatedCallLog.VOICEMAIL_CALL_TAG);
     if (!TextUtils.isEmpty(voicemailCallTag)) {
       coalescedRowBuilder.setVoicemailCallTag(voicemailCallTag);
     }
 
     return coalescedRowBuilder.build();
   }
-
-  /**
-   * Returns the timestamp at the provided cursor's current position.
-   *
-   * <p>The provided cursor should be one for {@link CoalescedAnnotatedCallLog}.
-   */
-  public static long getTimestamp(Cursor coalescedAnnotatedCallLogCursor) {
-    return coalescedAnnotatedCallLogCursor.getLong(TIMESTAMP);
-  }
 }
diff --git a/java/com/android/dialer/calllog/database/contract/AnnotatedCallLogContract.java b/java/com/android/dialer/calllog/database/contract/AnnotatedCallLogContract.java
index 8ca151c..5aa62c2 100644
--- a/java/com/android/dialer/calllog/database/contract/AnnotatedCallLogContract.java
+++ b/java/com/android/dialer/calllog/database/contract/AnnotatedCallLogContract.java
@@ -21,7 +21,6 @@
 import android.provider.BaseColumns;
 import com.android.dialer.compat.android.provider.VoicemailCompat;
 import com.android.dialer.constants.Constants;
-import java.util.Arrays;
 
 /** Contract for the AnnotatedCallLog content provider. */
 public class AnnotatedCallLogContract {
@@ -29,11 +28,7 @@
 
   public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY);
 
-  /**
-   * Columns shared by {@link AnnotatedCallLog} and {@link CoalescedAnnotatedCallLog}.
-   *
-   * <p>When adding columns be sure to update {@link #ALL_COMMON_COLUMNS}.
-   */
+  /** Columns shared by {@link AnnotatedCallLog} and {@link CoalescedAnnotatedCallLog}. */
   interface CommonColumns extends BaseColumns {
 
     /**
@@ -143,25 +138,6 @@
      * <p>Type: INTEGER (int)
      */
     String CALL_TYPE = "call_type";
-
-    String[] ALL_COMMON_COLUMNS =
-        new String[] {
-          _ID,
-          TIMESTAMP,
-          NUMBER,
-          FORMATTED_NUMBER,
-          NUMBER_PRESENTATION,
-          IS_READ,
-          NEW,
-          GEOCODED_LOCATION,
-          PHONE_ACCOUNT_COMPONENT_NAME,
-          PHONE_ACCOUNT_ID,
-          FEATURES,
-          NUMBER_ATTRIBUTES,
-          IS_VOICEMAIL_CALL,
-          VOICEMAIL_CALL_TAG,
-          CALL_TYPE
-        };
   }
 
   /**
@@ -239,8 +215,6 @@
    *
    * <p>This is an in-memory view of the {@link AnnotatedCallLog} with some adjacent entries
    * collapsed.
-   *
-   * <p>When adding columns be sure to update {@link #COLUMNS_ONLY_IN_COALESCED_CALL_LOG}.
    */
   public static final class CoalescedAnnotatedCallLog implements CommonColumns {
 
@@ -251,21 +225,5 @@
      * <p>Type: BLOB
      */
     public static final String COALESCED_IDS = "coalesced_ids";
-
-    /**
-     * Columns that are only in the {@link CoalescedAnnotatedCallLog} but not the {@link
-     * AnnotatedCallLog}.
-     */
-    private static final String[] COLUMNS_ONLY_IN_COALESCED_CALL_LOG = new String[] {COALESCED_IDS};
-
-    /** All columns in the {@link CoalescedAnnotatedCallLog}. */
-    public static final String[] ALL_COLUMNS =
-        concat(ALL_COMMON_COLUMNS, COLUMNS_ONLY_IN_COALESCED_CALL_LOG);
-  }
-
-  private static String[] concat(String[] first, String[] second) {
-    String[] result = Arrays.copyOf(first, first.length + second.length);
-    System.arraycopy(second, 0, result, first.length, second.length);
-    return result;
   }
 }
diff --git a/java/com/android/dialer/calllog/ui/NewCallLogAdapter.java b/java/com/android/dialer/calllog/ui/NewCallLogAdapter.java
index 58bf8c0..185a93e 100644
--- a/java/com/android/dialer/calllog/ui/NewCallLogAdapter.java
+++ b/java/com/android/dialer/calllog/ui/NewCallLogAdapter.java
@@ -17,19 +17,19 @@
 
 import android.app.Activity;
 import android.content.Context;
-import android.database.Cursor;
 import android.support.annotation.IntDef;
 import android.support.annotation.Nullable;
 import android.support.v7.widget.RecyclerView;
 import android.support.v7.widget.RecyclerView.ViewHolder;
 import android.view.LayoutInflater;
 import android.view.ViewGroup;
-import com.android.dialer.calllog.database.Coalescer;
+import com.android.dialer.calllog.model.CoalescedRow;
 import com.android.dialer.calllogutils.CallLogDates;
 import com.android.dialer.common.Assert;
 import com.android.dialer.logging.Logger;
 import com.android.dialer.promotion.Promotion;
 import com.android.dialer.time.Clock;
+import com.google.common.collect.ImmutableList;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 
@@ -68,7 +68,7 @@
   private final PopCounts popCounts = new PopCounts();
   @Nullable private final Promotion promotion;
 
-  private Cursor cursor;
+  private ImmutableList<CoalescedRow> coalescedRows;
 
   /** Position of the promotion card. Null when it should not be displayed. */
   @Nullable private Integer promotionCardPosition;
@@ -82,9 +82,13 @@
   /** Position of the "Older" header. Null when it should not be displayed. */
   @Nullable private Integer olderHeaderPosition;
 
-  NewCallLogAdapter(Activity activity, Cursor cursor, Clock clock, @Nullable Promotion promotion) {
+  NewCallLogAdapter(
+      Activity activity,
+      ImmutableList<CoalescedRow> coalescedRows,
+      Clock clock,
+      @Nullable Promotion promotion) {
     this.activity = activity;
-    this.cursor = cursor;
+    this.coalescedRows = coalescedRows;
     this.clock = clock;
     this.realtimeRowProcessor = CallLogUiComponent.get(activity).realtimeRowProcessor();
     this.promotion = promotion;
@@ -92,8 +96,8 @@
     setCardAndHeaderPositions();
   }
 
-  void updateCursor(Cursor updatedCursor) {
-    this.cursor = updatedCursor;
+  void updateRows(ImmutableList<CoalescedRow> coalescedRows) {
+    this.coalescedRows = coalescedRows;
     this.realtimeRowProcessor.clearCache();
     this.popCounts.reset();
 
@@ -119,7 +123,7 @@
     }
 
     // If there are no rows to display, set all header positions to null.
-    if (!cursor.moveToFirst()) {
+    if (coalescedRows.isEmpty()) {
       todayHeaderPosition = null;
       yesterdayHeaderPosition = null;
       olderHeaderPosition = null;
@@ -131,17 +135,19 @@
 
     int numItemsInToday = 0;
     int numItemsInYesterday = 0;
-    do {
-      long timestamp = Coalescer.getTimestamp(cursor);
+    int numItemsInOlder = 0;
+    for (CoalescedRow coalescedRow : coalescedRows) {
+      long timestamp = coalescedRow.getTimestamp();
       long dayDifference = CallLogDates.getDayDifference(currentTimeMillis, timestamp);
       if (dayDifference == 0) {
         numItemsInToday++;
       } else if (dayDifference == 1) {
         numItemsInYesterday++;
       } else {
+        numItemsInOlder = coalescedRows.size() - numItemsInToday - numItemsInYesterday;
         break;
       }
-    } while (cursor.moveToNext());
+    }
 
     if (numItemsInToday > 0) {
       numItemsInToday++; // including the "Today" header;
@@ -149,13 +155,16 @@
     if (numItemsInYesterday > 0) {
       numItemsInYesterday++; // including the "Yesterday" header;
     }
+    if (numItemsInOlder > 0) {
+      numItemsInOlder++; // include the "Older" header;
+    }
 
     // Set all header positions.
     // A header position will be null if there is no item to be displayed under that header.
     todayHeaderPosition = numItemsInToday > 0 ? numCards : null;
     yesterdayHeaderPosition = numItemsInYesterday > 0 ? numItemsInToday + numCards : null;
     olderHeaderPosition =
-        !cursor.isAfterLast() ? numItemsInToday + numItemsInYesterday + numCards : null;
+        numItemsInOlder > 0 ? numItemsInToday + numItemsInYesterday + numCards : null;
   }
 
   @Override
@@ -233,8 +242,7 @@
         if (olderHeaderPosition != null && position > olderHeaderPosition) {
           previousCardAndHeaders++;
         }
-        cursor.moveToPosition(position - previousCardAndHeaders);
-        newCallLogViewHolder.bind(cursor);
+        newCallLogViewHolder.bind(coalescedRows.get(position - previousCardAndHeaders));
         break;
       default:
         throw Assert.createIllegalStateFailException(
@@ -277,7 +285,7 @@
     if (olderHeaderPosition != null) {
       numberOfHeaders++;
     }
-    return cursor.getCount() + numberOfHeaders + numberOfCards;
+    return coalescedRows.size() + numberOfHeaders + numberOfCards;
   }
 
   /**
diff --git a/java/com/android/dialer/calllog/ui/NewCallLogFragment.java b/java/com/android/dialer/calllog/ui/NewCallLogFragment.java
index 4b36970..ab0c22b 100644
--- a/java/com/android/dialer/calllog/ui/NewCallLogFragment.java
+++ b/java/com/android/dialer/calllog/ui/NewCallLogFragment.java
@@ -33,6 +33,7 @@
 import com.android.dialer.calllog.CallLogComponent;
 import com.android.dialer.calllog.RefreshAnnotatedCallLogReceiver;
 import com.android.dialer.calllog.database.CallLogDatabaseComponent;
+import com.android.dialer.calllog.model.CoalescedRow;
 import com.android.dialer.common.Assert;
 import com.android.dialer.common.LogUtil;
 import com.android.dialer.common.concurrent.DefaultFutureCallback;
@@ -47,6 +48,7 @@
 import com.android.dialer.util.PermissionsUtil;
 import com.android.dialer.widget.EmptyContentView;
 import com.android.dialer.widget.EmptyContentView.OnEmptyViewActionButtonClickedListener;
+import com.google.common.collect.ImmutableList;
 import com.google.common.util.concurrent.Futures;
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.MoreExecutors;
@@ -65,7 +67,7 @@
   private RecyclerView recyclerView;
   private EmptyContentView emptyContentView;
   private RefreshAnnotatedCallLogReceiver refreshAnnotatedCallLogReceiver;
-  private SupportUiListener<Cursor> coalesingAnnotatedCallLogListener;
+  private SupportUiListener<ImmutableList<CoalescedRow>> coalesingAnnotatedCallLogListener;
 
   private boolean shouldMarkCallsRead = false;
   private final Runnable setShouldMarkCallsReadTrue = () -> shouldMarkCallsRead = true;
@@ -304,13 +306,13 @@
 
     // Start combining adjacent rows which should be collapsed for display purposes.
     // This is a time-consuming process so we will do it in the background.
-    ListenableFuture<Cursor> coalescedCursorFuture =
+    ListenableFuture<ImmutableList<CoalescedRow>> coalescedRowsFuture =
         CallLogDatabaseComponent.get(getContext()).coalescer().coalesce(newCursor);
 
     coalesingAnnotatedCallLogListener.listen(
         getContext(),
-        coalescedCursorFuture,
-        coalescedCursor -> {
+        coalescedRowsFuture,
+        coalescedRows -> {
           LogUtil.i("NewCallLogFragment.onLoadFinished", "coalescing succeeded");
 
           // TODO(zachh): Handle empty cursor by showing empty view.
@@ -323,14 +325,14 @@
             recyclerView.setAdapter(
                 new NewCallLogAdapter(
                     activity,
-                    coalescedCursor,
+                    coalescedRows,
                     System::currentTimeMillis,
                     PromotionComponent.get(getContext())
                         .promotionManager()
                         .getHighestPriorityPromotion(PromotionType.CARD)
                         .orElse(null)));
           } else {
-            ((NewCallLogAdapter) recyclerView.getAdapter()).updateCursor(coalescedCursor);
+            ((NewCallLogAdapter) recyclerView.getAdapter()).updateRows(coalescedRows);
           }
         },
         throwable -> {
diff --git a/java/com/android/dialer/calllog/ui/NewCallLogViewHolder.java b/java/com/android/dialer/calllog/ui/NewCallLogViewHolder.java
index 2377c35..812e7b9 100644
--- a/java/com/android/dialer/calllog/ui/NewCallLogViewHolder.java
+++ b/java/com/android/dialer/calllog/ui/NewCallLogViewHolder.java
@@ -17,7 +17,6 @@
 
 import android.app.Activity;
 import android.content.res.ColorStateList;
-import android.database.Cursor;
 import android.provider.CallLog.Calls;
 import android.support.annotation.ColorInt;
 import android.support.annotation.DrawableRes;
@@ -31,7 +30,6 @@
 import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
 import android.widget.ImageView;
 import android.widget.TextView;
-import com.android.dialer.calllog.database.Coalescer;
 import com.android.dialer.calllog.model.CoalescedRow;
 import com.android.dialer.calllog.ui.NewCallLogAdapter.PopCounts;
 import com.android.dialer.calllog.ui.menu.NewCallLogMenu;
@@ -101,25 +99,21 @@
     uiExecutorService = DialerExecutorComponent.get(activity).uiExecutor();
   }
 
-  /**
-   * @param cursor a cursor for {@link
-   *     com.android.dialer.calllog.database.contract.AnnotatedCallLogContract.CoalescedAnnotatedCallLog}.
-   */
-  void bind(Cursor cursor) {
-    CoalescedRow row = Coalescer.toRow(cursor);
-    currentRowId = row.getId(); // Used to make sure async updates are applied to the correct views
+  void bind(CoalescedRow coalescedRow) {
+    // The row ID is used to make sure async updates are applied to the correct views.
+    currentRowId = coalescedRow.getId();
 
     // Even if there is additional real time processing necessary, we still want to immediately show
     // what information we have, rather than an empty card. For example, if CP2 information needs to
     // be queried on the fly, we can still show the phone number until the contact name loads.
-    displayRow(row);
-    configA11yForRow(row);
+    displayRow(coalescedRow);
+    configA11yForRow(coalescedRow);
 
     // Note: This leaks the view holder via the callback (which is an inner class), but this is OK
     // because we only create ~10 of them (and they'll be collected assuming all jobs finish).
     Futures.addCallback(
-        realtimeRowProcessor.applyRealtimeProcessing(row),
-        new RealtimeRowFutureCallback(row),
+        realtimeRowProcessor.applyRealtimeProcessing(coalescedRow),
+        new RealtimeRowFutureCallback(coalescedRow),
         uiExecutorService);
   }