Improve Coalescer performance

Bug: 77813585
Test: CoalescerTest
PiperOrigin-RevId: 200764878
Change-Id: I7e3d9c3b4eab1e5de12a108b82c04704550c8c5e
diff --git a/java/com/android/dialer/calllog/database/Coalescer.java b/java/com/android/dialer/calllog/database/Coalescer.java
index 903b7e2..2ad9f9a 100644
--- a/java/com/android/dialer/calllog/database/Coalescer.java
+++ b/java/com/android/dialer/calllog/database/Coalescer.java
@@ -15,11 +15,9 @@
  */
 package com.android.dialer.calllog.database;
 
-import android.content.ContentValues;
 import android.database.Cursor;
 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;
@@ -27,9 +25,6 @@
 import com.android.dialer.DialerPhoneNumber;
 import com.android.dialer.NumberAttributes;
 import com.android.dialer.calllog.database.contract.AnnotatedCallLogContract.AnnotatedCallLog;
-import com.android.dialer.calllog.database.contract.AnnotatedCallLogContract.CoalescedAnnotatedCallLog;
-import com.android.dialer.calllog.datasources.CallLogDataSource;
-import com.android.dialer.calllog.datasources.DataSources;
 import com.android.dialer.calllog.model.CoalescedRow;
 import com.android.dialer.common.Assert;
 import com.android.dialer.common.concurrent.Annotations.BackgroundExecutor;
@@ -38,35 +33,24 @@
 import com.android.dialer.metrics.Metrics;
 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.Objects;
 import javax.inject.Inject;
 
-/**
- * Coalesces rows in {@link AnnotatedCallLog} by combining adjacent rows.
- *
- * <p>Applies the logic that determines which adjacent rows should be coalesced, and then delegates
- * to each data source to determine how individual columns should be aggregated.
- */
+/** Combines adjacent rows in {@link AnnotatedCallLog}. */
 public class Coalescer {
 
-  private final DataSources dataSources;
   private final FutureTimer futureTimer;
   private final ListeningExecutorService backgroundExecutorService;
 
   @Inject
   Coalescer(
       @BackgroundExecutor ListeningExecutorService backgroundExecutorService,
-      DataSources dataSources,
       FutureTimer futureTimer) {
     this.backgroundExecutorService = backgroundExecutorService;
-    this.dataSources = dataSources;
     this.futureTimer = futureTimer;
   }
 
@@ -108,256 +92,249 @@
       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();
-
     ImmutableList.Builder<CoalescedRow> coalescedRowListBuilder = new ImmutableList.Builder<>();
 
-    int coalescedRowId = 0;
+    RowCombiner rowCombiner = new RowCombiner(allAnnotatedCallLogRowsSortedByTimestampDesc);
+    rowCombiner.startNewGroup();
 
-    // TODO(a bug): Avoid using ContentValues as it doesn't make sense here.
-    List<ContentValues> currentRowGroup = new ArrayList<>();
+    long coalescedRowId = 0;
+    do {
+      boolean isRowMerged = rowCombiner.mergeRow(allAnnotatedCallLogRowsSortedByTimestampDesc);
 
-    ContentValues firstRow = cursorRowToContentValues(allAnnotatedCallLogRowsSortedByTimestampDesc);
-    currentRowGroup.add(firstRow);
-
-    while (!currentRowGroup.isEmpty()) {
-      // Group consecutive rows
-      ContentValues firstRowInGroup = currentRowGroup.get(0);
-      ContentValues currentRow = null;
-      while (allAnnotatedCallLogRowsSortedByTimestampDesc.moveToNext()) {
-        currentRow = cursorRowToContentValues(allAnnotatedCallLogRowsSortedByTimestampDesc);
-
-        if (!rowsShouldBeCombined(dialerPhoneNumberUtil, firstRowInGroup, currentRow)) {
-          break;
-        }
-
-        currentRowGroup.add(currentRow);
+      if (isRowMerged) {
+        allAnnotatedCallLogRowsSortedByTimestampDesc.moveToNext();
       }
 
-      // Coalesce the group into a single row
-      ContentValues coalescedRow = coalesceRowsForAllDataSources(currentRowGroup);
-      coalescedRow.put(CoalescedAnnotatedCallLog._ID, coalescedRowId++);
-      coalescedRow.put(
-          CoalescedAnnotatedCallLog.COALESCED_IDS, getCoalescedIds(currentRowGroup).toByteArray());
-      coalescedRowListBuilder.add(toCoalescedRowProto(coalescedRow));
-
-      // Clear the current group after the rows are coalesced.
-      currentRowGroup.clear();
-
-      // Add the first of the remaining rows to the current group.
-      if (!allAnnotatedCallLogRowsSortedByTimestampDesc.isAfterLast()) {
-        currentRowGroup.add(currentRow);
+      if (!isRowMerged || allAnnotatedCallLogRowsSortedByTimestampDesc.isAfterLast()) {
+        coalescedRowListBuilder.add(
+            rowCombiner.combine().toBuilder().setId(coalescedRowId++).build());
+        rowCombiner.startNewGroup();
       }
-    }
+    } while (!allAnnotatedCallLogRowsSortedByTimestampDesc.isAfterLast());
 
     return coalescedRowListBuilder.build();
   }
 
-  private static ContentValues cursorRowToContentValues(Cursor cursor) {
-    ContentValues values = new ContentValues();
-    String[] columns = cursor.getColumnNames();
-    int length = columns.length;
-    for (int i = 0; i < length; i++) {
-      if (cursor.getType(i) == Cursor.FIELD_TYPE_BLOB) {
-        values.put(columns[i], cursor.getBlob(i));
-      } else {
-        values.put(columns[i], cursor.getString(i));
-      }
-    }
-    return values;
-  }
+  /** Combines rows from {@link AnnotatedCallLog} into a {@link CoalescedRow}. */
+  private static final class RowCombiner {
+    private final CoalescedRow.Builder coalescedRowBuilder = CoalescedRow.newBuilder();
+    private final CoalescedIds.Builder coalescedIdsBuilder = CoalescedIds.newBuilder();
 
-  /**
-   * @param row1 a row from {@link AnnotatedCallLog}
-   * @param row2 a row from {@link AnnotatedCallLog}
-   */
-  private static boolean rowsShouldBeCombined(
-      DialerPhoneNumberUtil dialerPhoneNumberUtil, ContentValues row1, ContentValues row2) {
-    // Don't combine rows which don't use the same phone account.
-    PhoneAccountHandle phoneAccount1 =
-        TelecomUtil.composePhoneAccountHandle(
-            row1.getAsString(AnnotatedCallLog.PHONE_ACCOUNT_COMPONENT_NAME),
-            row1.getAsString(AnnotatedCallLog.PHONE_ACCOUNT_ID));
-    PhoneAccountHandle phoneAccount2 =
-        TelecomUtil.composePhoneAccountHandle(
-            row2.getAsString(AnnotatedCallLog.PHONE_ACCOUNT_COMPONENT_NAME),
-            row2.getAsString(AnnotatedCallLog.PHONE_ACCOUNT_ID));
-    if (!Objects.equals(phoneAccount1, phoneAccount2)) {
-      return false;
+    // Indexes for columns in AnnotatedCallLog
+    private final int idColumn;
+    private final int timestampColumn;
+    private final int numberColumn;
+    private final int formattedNumberColumn;
+    private final int numberPresentationColumn;
+    private final int isReadColumn;
+    private final int isNewColumn;
+    private final int geocodedLocationColumn;
+    private final int phoneAccountComponentNameColumn;
+    private final int phoneAccountIdColumn;
+    private final int featuresColumn;
+    private final int numberAttributesColumn;
+    private final int isVoicemailCallColumn;
+    private final int voicemailCallTagColumn;
+    private final int callTypeColumn;
+
+    // DialerPhoneNumberUtil will be created lazily as its instantiation is expensive.
+    private DialerPhoneNumberUtil dialerPhoneNumberUtil = null;
+
+    RowCombiner(Cursor annotatedCallLogRow) {
+      idColumn = annotatedCallLogRow.getColumnIndexOrThrow(AnnotatedCallLog._ID);
+      timestampColumn = annotatedCallLogRow.getColumnIndexOrThrow(AnnotatedCallLog.TIMESTAMP);
+      numberColumn = annotatedCallLogRow.getColumnIndexOrThrow(AnnotatedCallLog.NUMBER);
+      formattedNumberColumn =
+          annotatedCallLogRow.getColumnIndexOrThrow(AnnotatedCallLog.FORMATTED_NUMBER);
+      numberPresentationColumn =
+          annotatedCallLogRow.getColumnIndexOrThrow(AnnotatedCallLog.NUMBER_PRESENTATION);
+      isReadColumn = annotatedCallLogRow.getColumnIndexOrThrow(AnnotatedCallLog.IS_READ);
+      isNewColumn = annotatedCallLogRow.getColumnIndexOrThrow(AnnotatedCallLog.NEW);
+      geocodedLocationColumn =
+          annotatedCallLogRow.getColumnIndexOrThrow(AnnotatedCallLog.GEOCODED_LOCATION);
+      phoneAccountComponentNameColumn =
+          annotatedCallLogRow.getColumnIndexOrThrow(AnnotatedCallLog.PHONE_ACCOUNT_COMPONENT_NAME);
+      phoneAccountIdColumn =
+          annotatedCallLogRow.getColumnIndexOrThrow(AnnotatedCallLog.PHONE_ACCOUNT_ID);
+      featuresColumn = annotatedCallLogRow.getColumnIndexOrThrow(AnnotatedCallLog.FEATURES);
+      numberAttributesColumn =
+          annotatedCallLogRow.getColumnIndexOrThrow(AnnotatedCallLog.NUMBER_ATTRIBUTES);
+      isVoicemailCallColumn =
+          annotatedCallLogRow.getColumnIndexOrThrow(AnnotatedCallLog.IS_VOICEMAIL_CALL);
+      voicemailCallTagColumn =
+          annotatedCallLogRow.getColumnIndexOrThrow(AnnotatedCallLog.VOICEMAIL_CALL_TAG);
+      callTypeColumn = annotatedCallLogRow.getColumnIndexOrThrow(AnnotatedCallLog.CALL_TYPE);
     }
 
-    if (!row1.getAsInteger(AnnotatedCallLog.NUMBER_PRESENTATION)
-        .equals(row2.getAsInteger(AnnotatedCallLog.NUMBER_PRESENTATION))) {
-      return false;
+    /**
+     * Prepares {@link RowCombiner} for building a new group of rows by clearing information on all
+     * previously merged rows.
+     */
+    void startNewGroup() {
+      coalescedRowBuilder.clear();
+      coalescedIdsBuilder.clear();
     }
 
-    if (!meetsCallFeatureCriteria(row1, row2)) {
-      return false;
-    }
+    /**
+     * Merge the given {@link AnnotatedCallLog} row into the current group.
+     *
+     * @return true if the given row is merged.
+     */
+    boolean mergeRow(Cursor annotatedCallLogRow) {
+      Assert.checkArgument(annotatedCallLogRow.getInt(callTypeColumn) != Calls.VOICEMAIL_TYPE);
 
-    DialerPhoneNumber number1;
-    DialerPhoneNumber number2;
-    try {
-      byte[] number1Bytes = row1.getAsByteArray(AnnotatedCallLog.NUMBER);
-      byte[] number2Bytes = row2.getAsByteArray(AnnotatedCallLog.NUMBER);
-
-      if (number1Bytes == null || number2Bytes == null) {
-        // Empty numbers should not be combined.
+      if (!canMergeRow(annotatedCallLogRow)) {
         return false;
       }
 
-      number1 = DialerPhoneNumber.parseFrom(number1Bytes);
-      number2 = DialerPhoneNumber.parseFrom(number2Bytes);
-    } catch (InvalidProtocolBufferException e) {
-      throw Assert.createAssertionFailException("error parsing DialerPhoneNumber proto", e);
-    }
-    return dialerPhoneNumberUtil.isMatch(number1, number2);
-  }
+      // Set fields that don't use the most recent value.
+      //
+      // Currently there is only one such field: "features".
+      // If any call in a group includes a feature (like Wifi/HD), consider the group to have
+      // the feature.
+      coalescedRowBuilder.setFeatures(
+          coalescedRowBuilder.getFeatures() | annotatedCallLogRow.getInt(featuresColumn));
 
-  /**
-   * Returns true if column {@link AnnotatedCallLog#FEATURES} of the two given rows indicate that
-   * they can be coalesced.
-   */
-  private static boolean meetsCallFeatureCriteria(ContentValues row1, ContentValues row2) {
-    int row1Features = row1.getAsInteger(AnnotatedCallLog.FEATURES);
-    int row2Features = row2.getAsInteger(AnnotatedCallLog.FEATURES);
+      // Set fields that use the most recent value.
+      // Rows passed to Coalescer are already sorted in descending order of timestamp. If the
+      // coalesced ID list is not empty, it means RowCombiner has merged the most recent row in a
+      // group and there is no need to continue as we only set fields that use the most recent value
+      // from this point forward.
+      if (!coalescedIdsBuilder.getCoalescedIdList().isEmpty()) {
+        coalescedIdsBuilder.addCoalescedId(annotatedCallLogRow.getInt(idColumn));
+        return true;
+      }
 
-    // A row with FEATURES_ASSISTED_DIALING should not be combined with one without it.
-    if ((row1Features & TelephonyManagerCompat.FEATURES_ASSISTED_DIALING)
-        != (row2Features & TelephonyManagerCompat.FEATURES_ASSISTED_DIALING)) {
-      return false;
+      coalescedRowBuilder
+          .setTimestamp(annotatedCallLogRow.getLong(timestampColumn))
+          .setNumberPresentation(annotatedCallLogRow.getInt(numberPresentationColumn))
+          .setIsRead(annotatedCallLogRow.getInt(isReadColumn) == 1)
+          .setIsNew(annotatedCallLogRow.getInt(isNewColumn) == 1)
+          .setIsVoicemailCall(annotatedCallLogRow.getInt(isVoicemailCallColumn) == 1)
+          .setCallType(annotatedCallLogRow.getInt(callTypeColumn));
+
+      // Two different DialerPhoneNumbers could be combined if they are different but considered
+      // to be a match by libphonenumber; in this case we arbitrarily select the most recent one.
+      try {
+        coalescedRowBuilder.setNumber(
+            DialerPhoneNumber.parseFrom(annotatedCallLogRow.getBlob(numberColumn)));
+      } catch (InvalidProtocolBufferException e) {
+        throw Assert.createAssertionFailException("Unable to parse DialerPhoneNumber bytes", e);
+      }
+
+      String formattedNumber = annotatedCallLogRow.getString(formattedNumberColumn);
+      if (!TextUtils.isEmpty(formattedNumber)) {
+        coalescedRowBuilder.setFormattedNumber(formattedNumber);
+      }
+
+      String geocodedLocation = annotatedCallLogRow.getString(geocodedLocationColumn);
+      if (!TextUtils.isEmpty(geocodedLocation)) {
+        coalescedRowBuilder.setGeocodedLocation(geocodedLocation);
+      }
+
+      String phoneAccountComponentName =
+          annotatedCallLogRow.getString(phoneAccountComponentNameColumn);
+      if (!TextUtils.isEmpty(phoneAccountComponentName)) {
+        coalescedRowBuilder.setPhoneAccountComponentName(phoneAccountComponentName);
+      }
+
+      String phoneAccountId = annotatedCallLogRow.getString(phoneAccountIdColumn);
+      if (!TextUtils.isEmpty(phoneAccountId)) {
+        coalescedRowBuilder.setPhoneAccountId(phoneAccountId);
+      }
+
+      try {
+        coalescedRowBuilder.setNumberAttributes(
+            NumberAttributes.parseFrom(annotatedCallLogRow.getBlob(numberAttributesColumn)));
+      } catch (InvalidProtocolBufferException e) {
+        throw Assert.createAssertionFailException("Unable to parse NumberAttributes bytes", e);
+      }
+
+      String voicemailCallTag = annotatedCallLogRow.getString(voicemailCallTagColumn);
+      if (!TextUtils.isEmpty(voicemailCallTag)) {
+        coalescedRowBuilder.setVoicemailCallTag(voicemailCallTag);
+      }
+
+      coalescedIdsBuilder.addCoalescedId(annotatedCallLogRow.getInt(idColumn));
+      return true;
     }
 
-    // A video call should not be combined with one that is not a video call.
-    if ((row1Features & Calls.FEATURES_VIDEO) != (row2Features & Calls.FEATURES_VIDEO)) {
-      return false;
+    /** Builds a {@link CoalescedRow} based on all rows merged into the current group. */
+    CoalescedRow combine() {
+      return coalescedRowBuilder.setCoalescedIds(coalescedIdsBuilder.build()).build();
     }
 
-    // A RTT call should not be combined with one that is not a RTT call.
-    if ((row1Features & Calls.FEATURES_RTT) != (row2Features & Calls.FEATURES_RTT)) {
-      return false;
+    /**
+     * Returns true if the given {@link AnnotatedCallLog} row can be merged into the current group.
+     */
+    private boolean canMergeRow(Cursor annotatedCallLogRow) {
+      return coalescedIdsBuilder.getCoalescedIdList().isEmpty()
+          || (samePhoneAccount(annotatedCallLogRow)
+              && sameNumberPresentation(annotatedCallLogRow)
+              && meetsCallFeatureCriteria(annotatedCallLogRow)
+              && meetsDialerPhoneNumberCriteria(annotatedCallLogRow));
     }
 
-    return true;
-  }
+    private boolean samePhoneAccount(Cursor annotatedCallLogRow) {
+      PhoneAccountHandle groupPhoneAccountHandle =
+          TelecomUtil.composePhoneAccountHandle(
+              coalescedRowBuilder.getPhoneAccountComponentName(),
+              coalescedRowBuilder.getPhoneAccountId());
+      PhoneAccountHandle rowPhoneAccountHandle =
+          TelecomUtil.composePhoneAccountHandle(
+              annotatedCallLogRow.getString(phoneAccountComponentNameColumn),
+              annotatedCallLogRow.getString(phoneAccountIdColumn));
 
-  /**
-   * Delegates to data sources to aggregate individual columns to create a new coalesced row.
-   *
-   * @param individualRows {@link AnnotatedCallLog} rows sorted by timestamp descending
-   * @return a {@link CoalescedAnnotatedCallLog} row
-   */
-  private ContentValues coalesceRowsForAllDataSources(List<ContentValues> individualRows) {
-    ContentValues coalescedValues = new ContentValues();
-    for (CallLogDataSource dataSource : dataSources.getDataSourcesIncludingSystemCallLog()) {
-      coalescedValues.putAll(dataSource.coalesce(individualRows));
-    }
-    return coalescedValues;
-  }
-
-  /**
-   * Build a {@link CoalescedIds} proto that contains IDs of the rows in {@link AnnotatedCallLog}
-   * that are coalesced into one row in {@link CoalescedAnnotatedCallLog}.
-   *
-   * @param individualRows {@link AnnotatedCallLog} rows sorted by timestamp descending
-   * @return A {@link CoalescedIds} proto containing IDs of {@code individualRows}.
-   */
-  private CoalescedIds getCoalescedIds(List<ContentValues> individualRows) {
-    CoalescedIds.Builder coalescedIds = CoalescedIds.newBuilder();
-
-    for (ContentValues row : individualRows) {
-      coalescedIds.addCoalescedId(Preconditions.checkNotNull(row.getAsLong(AnnotatedCallLog._ID)));
+      return Objects.equals(groupPhoneAccountHandle, rowPhoneAccountHandle);
     }
 
-    return coalescedIds.build();
-  }
-
-  /**
-   * Creates a new {@link CoalescedRow} proto based on the provided {@link ContentValues}.
-   *
-   * <p>The provided {@link ContentValues} should be one for {@link CoalescedAnnotatedCallLog}.
-   */
-  @VisibleForTesting
-  static CoalescedRow toCoalescedRowProto(ContentValues coalescedContentValues) {
-    DialerPhoneNumber number;
-    try {
-      number =
-          DialerPhoneNumber.parseFrom(
-              coalescedContentValues.getAsByteArray(CoalescedAnnotatedCallLog.NUMBER));
-    } catch (InvalidProtocolBufferException e) {
-      throw new IllegalStateException("Couldn't parse DialerPhoneNumber bytes");
+    private boolean sameNumberPresentation(Cursor annotatedCallLogRow) {
+      return coalescedRowBuilder.getNumberPresentation()
+          == annotatedCallLogRow.getInt(numberPresentationColumn);
     }
 
-    CoalescedIds coalescedIds;
-    try {
-      coalescedIds =
-          CoalescedIds.parseFrom(
-              coalescedContentValues.getAsByteArray(CoalescedAnnotatedCallLog.COALESCED_IDS));
-    } catch (InvalidProtocolBufferException e) {
-      throw new IllegalStateException("Couldn't parse CoalescedIds bytes");
+    private boolean meetsCallFeatureCriteria(Cursor annotatedCallLogRow) {
+      int groupFeatures = coalescedRowBuilder.getFeatures();
+      int rowFeatures = annotatedCallLogRow.getInt(featuresColumn);
+
+      // A row with FEATURES_ASSISTED_DIALING should not be combined with one without it.
+      if ((groupFeatures & TelephonyManagerCompat.FEATURES_ASSISTED_DIALING)
+          != (rowFeatures & TelephonyManagerCompat.FEATURES_ASSISTED_DIALING)) {
+        return false;
+      }
+
+      // A video call should not be combined with one that is not a video call.
+      if ((groupFeatures & Calls.FEATURES_VIDEO) != (rowFeatures & Calls.FEATURES_VIDEO)) {
+        return false;
+      }
+
+      // A RTT call should not be combined with one that is not a RTT call.
+      if ((groupFeatures & Calls.FEATURES_RTT) != (rowFeatures & Calls.FEATURES_RTT)) {
+        return false;
+      }
+
+      return true;
     }
 
-    NumberAttributes numberAttributes;
-    try {
-      numberAttributes =
-          NumberAttributes.parseFrom(
-              coalescedContentValues.getAsByteArray(CoalescedAnnotatedCallLog.NUMBER_ATTRIBUTES));
-    } catch (InvalidProtocolBufferException e) {
-      throw new IllegalStateException("Couldn't parse NumberAttributes bytes");
+    private boolean meetsDialerPhoneNumberCriteria(Cursor annotatedCallLogRow) {
+      DialerPhoneNumber groupPhoneNumber = coalescedRowBuilder.getNumber();
+
+      DialerPhoneNumber rowPhoneNumber;
+      try {
+        byte[] rowPhoneNumberBytes = annotatedCallLogRow.getBlob(numberColumn);
+        if (rowPhoneNumberBytes == null) {
+          return false; // Empty numbers should not be combined.
+        }
+        rowPhoneNumber = DialerPhoneNumber.parseFrom(rowPhoneNumberBytes);
+      } catch (InvalidProtocolBufferException e) {
+        throw Assert.createAssertionFailException("Unable to parse DialerPhoneNumber bytes", e);
+      }
+
+      if (dialerPhoneNumberUtil == null) {
+        dialerPhoneNumberUtil = new DialerPhoneNumberUtil();
+      }
+
+      return dialerPhoneNumberUtil.isMatch(groupPhoneNumber, rowPhoneNumber);
     }
-
-    CoalescedRow.Builder coalescedRowBuilder =
-        CoalescedRow.newBuilder()
-            .setId(coalescedContentValues.getAsLong(CoalescedAnnotatedCallLog._ID))
-            .setTimestamp(coalescedContentValues.getAsLong(CoalescedAnnotatedCallLog.TIMESTAMP))
-            .setNumber(number)
-            .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(
-                coalescedContentValues.getAsInteger(CoalescedAnnotatedCallLog.IS_VOICEMAIL_CALL)
-                    == 1)
-            .setCoalescedIds(coalescedIds);
-
-    String formattedNumber =
-        coalescedContentValues.getAsString(CoalescedAnnotatedCallLog.FORMATTED_NUMBER);
-    if (!TextUtils.isEmpty(formattedNumber)) {
-      coalescedRowBuilder.setFormattedNumber(formattedNumber);
-    }
-
-    String geocodedLocation =
-        coalescedContentValues.getAsString(CoalescedAnnotatedCallLog.GEOCODED_LOCATION);
-    if (!TextUtils.isEmpty(geocodedLocation)) {
-      coalescedRowBuilder.setGeocodedLocation(geocodedLocation);
-    }
-
-    String phoneAccountComponentName =
-        coalescedContentValues.getAsString(CoalescedAnnotatedCallLog.PHONE_ACCOUNT_COMPONENT_NAME);
-    if (!TextUtils.isEmpty(phoneAccountComponentName)) {
-      coalescedRowBuilder.setPhoneAccountComponentName(
-          coalescedContentValues.getAsString(
-              CoalescedAnnotatedCallLog.PHONE_ACCOUNT_COMPONENT_NAME));
-    }
-
-    String phoneAccountId =
-        coalescedContentValues.getAsString(CoalescedAnnotatedCallLog.PHONE_ACCOUNT_ID);
-    if (!TextUtils.isEmpty(phoneAccountId)) {
-      coalescedRowBuilder.setPhoneAccountId(phoneAccountId);
-    }
-
-    String voicemailCallTag =
-        coalescedContentValues.getAsString(CoalescedAnnotatedCallLog.VOICEMAIL_CALL_TAG);
-    if (!TextUtils.isEmpty(voicemailCallTag)) {
-      coalescedRowBuilder.setVoicemailCallTag(voicemailCallTag);
-    }
-
-    return coalescedRowBuilder.build();
   }
 }
diff --git a/java/com/android/dialer/calllog/database/contract/AnnotatedCallLogContract.java b/java/com/android/dialer/calllog/database/contract/AnnotatedCallLogContract.java
index 5aa62c2..1fdf38a 100644
--- a/java/com/android/dialer/calllog/database/contract/AnnotatedCallLogContract.java
+++ b/java/com/android/dialer/calllog/database/contract/AnnotatedCallLogContract.java
@@ -28,124 +28,8 @@
 
   public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY);
 
-  /** Columns shared by {@link AnnotatedCallLog} and {@link CoalescedAnnotatedCallLog}. */
-  interface CommonColumns extends BaseColumns {
-
-    /**
-     * Timestamp of the entry, in milliseconds.
-     *
-     * <p>Type: INTEGER (long)
-     */
-    String TIMESTAMP = "timestamp";
-
-    /**
-     * The phone number called or number the call came from, encoded as a {@link
-     * com.android.dialer.DialerPhoneNumber} proto. The number may be empty if it was an incoming
-     * call and the number was unknown.
-     *
-     * <p>Type: BLOB
-     */
-    String NUMBER = "number";
-
-    /**
-     * The number formatted as it should be displayed to the user. Note that it may not always be
-     * displayed, for example if the number has a corresponding person or business name.
-     *
-     * <p>Type: TEXT
-     */
-    String FORMATTED_NUMBER = "formatted_number";
-
-    /**
-     * See {@link android.provider.CallLog.Calls#NUMBER_PRESENTATION}.
-     *
-     * <p>Type: INTEGER (int)
-     */
-    String NUMBER_PRESENTATION = "presentation";
-
-    /**
-     * See {@link android.provider.CallLog.Calls#IS_READ}.
-     *
-     * <p>TYPE: INTEGER (boolean)
-     */
-    String IS_READ = "is_read";
-
-    /**
-     * See {@link android.provider.CallLog.Calls#NEW}.
-     *
-     * <p>Type: INTEGER (boolean)
-     */
-    String NEW = "new";
-
-    /**
-     * See {@link android.provider.CallLog.Calls#GEOCODED_LOCATION}.
-     *
-     * <p>TYPE: TEXT
-     */
-    String GEOCODED_LOCATION = "geocoded_location";
-
-    /**
-     * See {@link android.provider.CallLog.Calls#PHONE_ACCOUNT_COMPONENT_NAME}.
-     *
-     * <p>TYPE: TEXT
-     */
-    String PHONE_ACCOUNT_COMPONENT_NAME = "phone_account_component_name";
-
-    /**
-     * See {@link android.provider.CallLog.Calls#PHONE_ACCOUNT_ID}.
-     *
-     * <p>TYPE: TEXT
-     */
-    String PHONE_ACCOUNT_ID = "phone_account_id";
-
-    /**
-     * See {@link android.provider.CallLog.Calls#FEATURES}.
-     *
-     * <p>TYPE: INTEGER (int)
-     */
-    String FEATURES = "features";
-
-    /**
-     * Additional attributes about the number.
-     *
-     * <p>TYPE: BLOB
-     *
-     * @see com.android.dialer.NumberAttributes
-     */
-    String NUMBER_ATTRIBUTES = "number_attributes";
-
-    /**
-     * Whether the call is to the voicemail inbox.
-     *
-     * <p>TYPE: INTEGER (boolean)
-     *
-     * @see android.telecom.TelecomManager#isVoiceMailNumber(android.telecom.PhoneAccountHandle,
-     *     String)
-     */
-    String IS_VOICEMAIL_CALL = "is_voicemail_call";
-
-    /**
-     * The "name" of the voicemail inbox. This is provided by the SIM to show as the caller ID
-     *
-     * <p>TYPE: TEXT
-     *
-     * @see android.telephony.TelephonyManager#getVoiceMailAlphaTag()
-     */
-    String VOICEMAIL_CALL_TAG = "voicemail_call_tag";
-
-    /**
-     * Copied from {@link android.provider.CallLog.Calls#TYPE}.
-     *
-     * <p>Type: INTEGER (int)
-     */
-    String CALL_TYPE = "call_type";
-  }
-
-  /**
-   * AnnotatedCallLog table.
-   *
-   * <p>This contains all of the non-coalesced call log entries.
-   */
-  public static final class AnnotatedCallLog implements CommonColumns {
+  /** AnnotatedCallLog table. */
+  public static final class AnnotatedCallLog implements BaseColumns {
 
     public static final String TABLE = "AnnotatedCallLog";
     public static final String DISTINCT_PHONE_NUMBERS = "DistinctPhoneNumbers";
@@ -162,6 +46,114 @@
     public static final String CONTENT_ITEM_TYPE = "vnd.android.cursor.item/annotated_call_log";
 
     /**
+     * Timestamp of the entry, in milliseconds.
+     *
+     * <p>Type: INTEGER (long)
+     */
+    public static final String TIMESTAMP = "timestamp";
+
+    /**
+     * The phone number called or number the call came from, encoded as a {@link
+     * com.android.dialer.DialerPhoneNumber} proto. The number may be empty if it was an incoming
+     * call and the number was unknown.
+     *
+     * <p>Type: BLOB
+     */
+    public static final String NUMBER = "number";
+
+    /**
+     * The number formatted as it should be displayed to the user. Note that it may not always be
+     * displayed, for example if the number has a corresponding person or business name.
+     *
+     * <p>Type: TEXT
+     */
+    public static final String FORMATTED_NUMBER = "formatted_number";
+
+    /**
+     * See {@link android.provider.CallLog.Calls#NUMBER_PRESENTATION}.
+     *
+     * <p>Type: INTEGER (int)
+     */
+    public static final String NUMBER_PRESENTATION = "presentation";
+
+    /**
+     * See {@link android.provider.CallLog.Calls#IS_READ}.
+     *
+     * <p>TYPE: INTEGER (boolean)
+     */
+    public static final String IS_READ = "is_read";
+
+    /**
+     * See {@link android.provider.CallLog.Calls#NEW}.
+     *
+     * <p>Type: INTEGER (boolean)
+     */
+    public static final String NEW = "new";
+
+    /**
+     * See {@link android.provider.CallLog.Calls#GEOCODED_LOCATION}.
+     *
+     * <p>TYPE: TEXT
+     */
+    public static final String GEOCODED_LOCATION = "geocoded_location";
+
+    /**
+     * See {@link android.provider.CallLog.Calls#PHONE_ACCOUNT_COMPONENT_NAME}.
+     *
+     * <p>TYPE: TEXT
+     */
+    public static final String PHONE_ACCOUNT_COMPONENT_NAME = "phone_account_component_name";
+
+    /**
+     * See {@link android.provider.CallLog.Calls#PHONE_ACCOUNT_ID}.
+     *
+     * <p>TYPE: TEXT
+     */
+    public static final String PHONE_ACCOUNT_ID = "phone_account_id";
+
+    /**
+     * See {@link android.provider.CallLog.Calls#FEATURES}.
+     *
+     * <p>TYPE: INTEGER (int)
+     */
+    public static final String FEATURES = "features";
+
+    /**
+     * Additional attributes about the number.
+     *
+     * <p>TYPE: BLOB
+     *
+     * @see com.android.dialer.NumberAttributes
+     */
+    public static final String NUMBER_ATTRIBUTES = "number_attributes";
+
+    /**
+     * Whether the call is to the voicemail inbox.
+     *
+     * <p>TYPE: INTEGER (boolean)
+     *
+     * @see android.telecom.TelecomManager#isVoiceMailNumber(android.telecom.PhoneAccountHandle,
+     *     String)
+     */
+    public static final String IS_VOICEMAIL_CALL = "is_voicemail_call";
+
+    /**
+     * The "name" of the voicemail inbox. This is provided by the SIM to show as the caller ID
+     *
+     * <p>TYPE: TEXT
+     *
+     * @see android.telephony.TelephonyManager#getVoiceMailAlphaTag()
+     */
+    public static final String VOICEMAIL_CALL_TAG = "voicemail_call_tag";
+
+    /**
+     * Copied from {@link android.provider.CallLog.Calls#TYPE}.
+     *
+     * <p>Type: INTEGER (int)
+     */
+    public static final String CALL_TYPE = "call_type";
+
+    /**
      * See {@link android.provider.CallLog.Calls#DATA_USAGE}.
      *
      * <p>Type: INTEGER (long)
@@ -209,21 +201,4 @@
      */
     public static final String CALL_MAPPING_ID = "call_mapping_id";
   }
-
-  /**
-   * Coalesced view of the AnnotatedCallLog table.
-   *
-   * <p>This is an in-memory view of the {@link AnnotatedCallLog} with some adjacent entries
-   * collapsed.
-   */
-  public static final class CoalescedAnnotatedCallLog implements CommonColumns {
-
-    /**
-     * IDs of rows in {@link AnnotatedCallLog} that are coalesced into one row in {@link
-     * CoalescedAnnotatedCallLog}, encoded as a {@link com.android.dialer.CoalescedIds} proto.
-     *
-     * <p>Type: BLOB
-     */
-    public static final String COALESCED_IDS = "coalesced_ids";
-  }
 }
diff --git a/java/com/android/dialer/calllog/datasources/CallLogDataSource.java b/java/com/android/dialer/calllog/datasources/CallLogDataSource.java
index 75f06d5..72676a2 100644
--- a/java/com/android/dialer/calllog/datasources/CallLogDataSource.java
+++ b/java/com/android/dialer/calllog/datasources/CallLogDataSource.java
@@ -16,12 +16,8 @@
 
 package com.android.dialer.calllog.datasources;
 
-import android.content.ContentValues;
 import android.support.annotation.MainThread;
-import android.support.annotation.WorkerThread;
-import com.android.dialer.calllog.database.contract.AnnotatedCallLogContract;
 import com.google.common.util.concurrent.ListenableFuture;
-import java.util.List;
 
 /**
  * A source of data for one or more columns in the annotated call log.
@@ -45,8 +41,6 @@
  *
  * <p>The same data source objects may be reused across multiple checkDirtyAndRebuild cycles, so
  * implementors should take care to clear any internal state at the start of a new cycle.
- *
- * <p>{@link #coalesce(List)} may be called from any worker thread at any time.
  */
 public interface CallLogDataSource {
 
@@ -85,22 +79,6 @@
    */
   ListenableFuture<Void> onSuccessfulFill();
 
-  /**
-   * Combines raw annotated call log rows into a single coalesced row.
-   *
-   * <p>May be called by any worker thread at any time so implementations should take care to be
-   * threadsafe. (Ideally no state should be required to implement this.)
-   *
-   * @param individualRowsSortedByTimestampDesc group of fully populated rows from {@link
-   *     AnnotatedCallLogContract.AnnotatedCallLog} which need to be combined for display purposes.
-   *     This method should not modify this list.
-   * @return a partial {@link AnnotatedCallLogContract.CoalescedAnnotatedCallLog} row containing
-   *     only columns which this data source is responsible for, which is the result of aggregating
-   *     {@code individualRowsSortedByTimestampDesc}.
-   */
-  @WorkerThread
-  ContentValues coalesce(List<ContentValues> individualRowsSortedByTimestampDesc);
-
   @MainThread
   void registerContentObservers();
 
diff --git a/java/com/android/dialer/calllog/datasources/phonelookup/PhoneLookupDataSource.java b/java/com/android/dialer/calllog/datasources/phonelookup/PhoneLookupDataSource.java
index 72e9e0f..a805212 100644
--- a/java/com/android/dialer/calllog/datasources/phonelookup/PhoneLookupDataSource.java
+++ b/java/com/android/dialer/calllog/datasources/phonelookup/PhoneLookupDataSource.java
@@ -31,7 +31,6 @@
 import com.android.dialer.calllog.database.contract.AnnotatedCallLogContract.AnnotatedCallLog;
 import com.android.dialer.calllog.datasources.CallLogDataSource;
 import com.android.dialer.calllog.datasources.CallLogMutations;
-import com.android.dialer.calllog.datasources.util.RowCombiner;
 import com.android.dialer.calllogutils.NumberAttributesBuilder;
 import com.android.dialer.common.Assert;
 import com.android.dialer.common.LogUtil;
@@ -54,7 +53,6 @@
 import com.google.protobuf.InvalidProtocolBufferException;
 import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
 import java.util.Set;
@@ -290,14 +288,6 @@
     return null;
   }
 
-  @WorkerThread
-  @Override
-  public ContentValues coalesce(List<ContentValues> individualRowsSortedByTimestampDesc) {
-    return new RowCombiner(individualRowsSortedByTimestampDesc)
-        .useMostRecentBlob(AnnotatedCallLog.NUMBER_ATTRIBUTES)
-        .combine();
-  }
-
   @MainThread
   @Override
   public void registerContentObservers() {
diff --git a/java/com/android/dialer/calllog/datasources/systemcalllog/SystemCallLogDataSource.java b/java/com/android/dialer/calllog/datasources/systemcalllog/SystemCallLogDataSource.java
index 1b66f50..7a12bc4 100644
--- a/java/com/android/dialer/calllog/datasources/systemcalllog/SystemCallLogDataSource.java
+++ b/java/com/android/dialer/calllog/datasources/systemcalllog/SystemCallLogDataSource.java
@@ -41,7 +41,6 @@
 import com.android.dialer.calllog.database.contract.AnnotatedCallLogContract.AnnotatedCallLog;
 import com.android.dialer.calllog.datasources.CallLogDataSource;
 import com.android.dialer.calllog.datasources.CallLogMutations;
-import com.android.dialer.calllog.datasources.util.RowCombiner;
 import com.android.dialer.calllog.observer.MarkDirtyObserver;
 import com.android.dialer.common.Assert;
 import com.android.dialer.common.LogUtil;
@@ -231,39 +230,6 @@
     return null;
   }
 
-  @Override
-  public ContentValues coalesce(List<ContentValues> individualRowsSortedByTimestampDesc) {
-    assertNoVoicemailsInRows(individualRowsSortedByTimestampDesc);
-
-    return new RowCombiner(individualRowsSortedByTimestampDesc)
-        .useMostRecentLong(AnnotatedCallLog.TIMESTAMP)
-        .useMostRecentLong(AnnotatedCallLog.NEW)
-        .useMostRecentLong(AnnotatedCallLog.IS_READ)
-        // Two different DialerPhoneNumbers could be combined if they are different but considered
-        // to be an "exact match" by libphonenumber; in this case we arbitrarily select the most
-        // recent one.
-        .useMostRecentBlob(AnnotatedCallLog.NUMBER)
-        .useMostRecentString(AnnotatedCallLog.FORMATTED_NUMBER)
-        .useSingleValueInt(AnnotatedCallLog.NUMBER_PRESENTATION)
-        .useMostRecentString(AnnotatedCallLog.GEOCODED_LOCATION)
-        .useSingleValueString(AnnotatedCallLog.PHONE_ACCOUNT_COMPONENT_NAME)
-        .useSingleValueString(AnnotatedCallLog.PHONE_ACCOUNT_ID)
-        .useMostRecentLong(AnnotatedCallLog.CALL_TYPE)
-        // If any call in a group includes a feature (like Wifi/HD), consider the group to have the
-        // feature.
-        .bitwiseOr(AnnotatedCallLog.FEATURES)
-        .combine();
-  }
-
-  private void assertNoVoicemailsInRows(List<ContentValues> individualRowsSortedByTimestampDesc) {
-    for (ContentValues contentValue : individualRowsSortedByTimestampDesc) {
-      if (contentValue.getAsLong(AnnotatedCallLog.CALL_TYPE) != null) {
-        Assert.checkArgument(
-            contentValue.getAsLong(AnnotatedCallLog.CALL_TYPE) != Calls.VOICEMAIL_TYPE);
-      }
-    }
-  }
-
   @TargetApi(Build.VERSION_CODES.N) // Uses try-with-resources
   private void handleInsertsAndUpdates(
       Context appContext, CallLogMutations mutations, Set<Long> existingAnnotatedCallLogIds) {
diff --git a/java/com/android/dialer/calllog/datasources/util/RowCombiner.java b/java/com/android/dialer/calllog/datasources/util/RowCombiner.java
deleted file mode 100644
index 2bb65cc..0000000
--- a/java/com/android/dialer/calllog/datasources/util/RowCombiner.java
+++ /dev/null
@@ -1,108 +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.calllog.datasources.util;
-
-import android.content.ContentValues;
-import com.android.dialer.common.Assert;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Objects;
-
-/** Convenience class for aggregating row values. */
-public class RowCombiner {
-  private final List<ContentValues> individualRowsSortedByTimestampDesc;
-  private final ContentValues combinedRow = new ContentValues();
-
-  public RowCombiner(List<ContentValues> individualRowsSortedByTimestampDesc) {
-    Assert.checkArgument(!individualRowsSortedByTimestampDesc.isEmpty());
-    this.individualRowsSortedByTimestampDesc = individualRowsSortedByTimestampDesc;
-  }
-
-  /** Use the most recent value for the specified column. */
-  public RowCombiner useMostRecentInt(String columnName) {
-    combinedRow.put(
-        columnName, individualRowsSortedByTimestampDesc.get(0).getAsInteger(columnName));
-    return this;
-  }
-
-  /** Use the most recent value for the specified column. */
-  public RowCombiner useMostRecentLong(String columnName) {
-    combinedRow.put(columnName, individualRowsSortedByTimestampDesc.get(0).getAsLong(columnName));
-    return this;
-  }
-
-  /** Use the most recent value for the specified column. */
-  public RowCombiner useMostRecentString(String columnName) {
-    combinedRow.put(columnName, individualRowsSortedByTimestampDesc.get(0).getAsString(columnName));
-    return this;
-  }
-
-  public RowCombiner useMostRecentBlob(String columnName) {
-    combinedRow.put(
-        columnName, individualRowsSortedByTimestampDesc.get(0).getAsByteArray(columnName));
-    return this;
-  }
-
-  /** Asserts that all column values for the given column name are the same, and uses it. */
-  public RowCombiner useSingleValueString(String columnName) {
-    Iterator<ContentValues> iterator = individualRowsSortedByTimestampDesc.iterator();
-    String singleValue = iterator.next().getAsString(columnName);
-    while (iterator.hasNext()) {
-      String current = iterator.next().getAsString(columnName);
-      Assert.checkState(Objects.equals(singleValue, current), "Values different for " + columnName);
-    }
-    combinedRow.put(columnName, singleValue);
-    return this;
-  }
-
-  /** Asserts that all column values for the given column name are the same, and uses it. */
-  public RowCombiner useSingleValueLong(String columnName) {
-    Iterator<ContentValues> iterator = individualRowsSortedByTimestampDesc.iterator();
-    Long singleValue = iterator.next().getAsLong(columnName);
-    while (iterator.hasNext()) {
-      Long current = iterator.next().getAsLong(columnName);
-      Assert.checkState(Objects.equals(singleValue, current), "Values different for " + columnName);
-    }
-    combinedRow.put(columnName, singleValue);
-    return this;
-  }
-
-  /** Asserts that all column values for the given column name are the same, and uses it. */
-  public RowCombiner useSingleValueInt(String columnName) {
-    Iterator<ContentValues> iterator = individualRowsSortedByTimestampDesc.iterator();
-    Integer singleValue = iterator.next().getAsInteger(columnName);
-    while (iterator.hasNext()) {
-      Integer current = iterator.next().getAsInteger(columnName);
-      Assert.checkState(Objects.equals(singleValue, current), "Values different for " + columnName);
-    }
-    combinedRow.put(columnName, singleValue);
-    return this;
-  }
-
-  /** Performs a bitwise OR on the specified column and yields the result. */
-  public RowCombiner bitwiseOr(String columnName) {
-    int combinedValue = 0;
-    for (ContentValues val : individualRowsSortedByTimestampDesc) {
-      combinedValue |= val.getAsInteger(columnName);
-    }
-    combinedRow.put(columnName, combinedValue);
-    return this;
-  }
-
-  public ContentValues combine() {
-    return combinedRow;
-  }
-}
diff --git a/java/com/android/dialer/calllog/datasources/voicemail/VoicemailDataSource.java b/java/com/android/dialer/calllog/datasources/voicemail/VoicemailDataSource.java
index 7a23022..cbda9ac 100644
--- a/java/com/android/dialer/calllog/datasources/voicemail/VoicemailDataSource.java
+++ b/java/com/android/dialer/calllog/datasources/voicemail/VoicemailDataSource.java
@@ -25,7 +25,6 @@
 import com.android.dialer.calllog.database.contract.AnnotatedCallLogContract.AnnotatedCallLog;
 import com.android.dialer.calllog.datasources.CallLogDataSource;
 import com.android.dialer.calllog.datasources.CallLogMutations;
-import com.android.dialer.calllog.datasources.util.RowCombiner;
 import com.android.dialer.common.concurrent.Annotations.BackgroundExecutor;
 import com.android.dialer.compat.telephony.TelephonyManagerCompat;
 import com.android.dialer.inject.ApplicationContext;
@@ -35,7 +34,6 @@
 import com.google.common.util.concurrent.ListenableFuture;
 import com.google.common.util.concurrent.ListeningExecutorService;
 import com.google.protobuf.InvalidProtocolBufferException;
-import java.util.List;
 import java.util.Map.Entry;
 import javax.inject.Inject;
 
@@ -104,14 +102,6 @@
   }
 
   @Override
-  public ContentValues coalesce(List<ContentValues> individualRowsSortedByTimestampDesc) {
-    return new RowCombiner(individualRowsSortedByTimestampDesc)
-        .useMostRecentInt(AnnotatedCallLog.IS_VOICEMAIL_CALL)
-        .useMostRecentString(AnnotatedCallLog.VOICEMAIL_CALL_TAG)
-        .combine();
-  }
-
-  @Override
   public void registerContentObservers() {}
 
   @Override