Add the "Yesterday" header in the new call log

Bug: 70989598
Test: NewCallLogAdapterTest, CallLogDatesTest
PiperOrigin-RevId: 182567571
Change-Id: Ieabbe709668d843334bc3bf4a128834fddb57cb8
diff --git a/java/com/android/dialer/calllog/ui/NewCallLogAdapter.java b/java/com/android/dialer/calllog/ui/NewCallLogAdapter.java
index 5618c4d..05a3399 100644
--- a/java/com/android/dialer/calllog/ui/NewCallLogAdapter.java
+++ b/java/com/android/dialer/calllog/ui/NewCallLogAdapter.java
@@ -34,14 +34,21 @@
 
   /** IntDef for the different types of rows that can be shown in the call log. */
   @Retention(RetentionPolicy.SOURCE)
-  @IntDef({RowType.HEADER_TODAY, RowType.HEADER_OLDER, RowType.CALL_LOG_ENTRY})
+  @IntDef({
+    RowType.HEADER_TODAY,
+    RowType.HEADER_YESTERDAY,
+    RowType.HEADER_OLDER,
+    RowType.CALL_LOG_ENTRY
+  })
   @interface RowType {
     /** Header that displays "Today". */
     int HEADER_TODAY = 1;
+    /** Header that displays "Yesterday". */
+    int HEADER_YESTERDAY = 2;
     /** Header that displays "Older". */
-    int HEADER_OLDER = 2;
+    int HEADER_OLDER = 3;
     /** A row representing a call log entry (which could represent one or more calls). */
-    int CALL_LOG_ENTRY = 3;
+    int CALL_LOG_ENTRY = 4;
   }
 
   private final Clock clock;
@@ -49,9 +56,13 @@
 
   private Cursor cursor;
 
-  /** Null when the "Today" header should not be displayed. */
+  /** Position of the "Today" header. Null when it should not be displayed. */
   @Nullable private Integer todayHeaderPosition;
-  /** Null when the "Older" header should not be displayed. */
+
+  /** Position of the "Yesterday" header. Null when it should not be displayed. */
+  @Nullable private Integer yesterdayHeaderPosition;
+
+  /** Position of the "Older" header. Null when it should not be displayed. */
   @Nullable private Integer olderHeaderPosition;
 
   NewCallLogAdapter(Context context, Cursor cursor, Clock clock) {
@@ -75,38 +86,49 @@
   }
 
   private void setHeaderPositions() {
-    // Calculate header adapter positions by reading cursor.
-    long currentTimeMillis = clock.currentTimeMillis();
-    if (cursor.moveToFirst()) {
-      long firstTimestamp = CoalescedAnnotatedCallLogCursorLoader.getTimestamp(cursor);
-      if (CallLogDates.isSameDay(currentTimeMillis, firstTimestamp)) {
-        this.todayHeaderPosition = 0;
-        int adapterPosition = 2; // Accounted for "Today" header and first row.
-        while (cursor.moveToNext()) {
-          long timestamp = CoalescedAnnotatedCallLogCursorLoader.getTimestamp(cursor);
-
-          if (CallLogDates.isSameDay(currentTimeMillis, timestamp)) {
-            adapterPosition++;
-          } else {
-            this.olderHeaderPosition = adapterPosition;
-            return;
-          }
-        }
-        this.olderHeaderPosition = null; // Didn't find any "Older" rows.
-      } else {
-        this.todayHeaderPosition = null; // Didn't find any "Today" rows.
-        this.olderHeaderPosition = 0;
-      }
-    } else { // There are no rows, just need to set these because they are final.
-      this.todayHeaderPosition = null;
-      this.olderHeaderPosition = null;
+    // If there are no rows to display, set all header positions to null.
+    if (!cursor.moveToFirst()) {
+      todayHeaderPosition = null;
+      yesterdayHeaderPosition = null;
+      olderHeaderPosition = null;
+      return;
     }
+
+    long currentTimeMillis = clock.currentTimeMillis();
+
+    int numItemsInToday = 0;
+    int numItemsInYesterday = 0;
+    do {
+      long timestamp = CoalescedAnnotatedCallLogCursorLoader.getTimestamp(cursor);
+      long dayDifference = CallLogDates.getDayDifference(currentTimeMillis, timestamp);
+      if (dayDifference == 0) {
+        numItemsInToday++;
+      } else if (dayDifference == 1) {
+        numItemsInYesterday++;
+      } else {
+        break;
+      }
+    } while (cursor.moveToNext());
+
+    if (numItemsInToday > 0) {
+      numItemsInToday++; // including the "Today" header;
+    }
+    if (numItemsInYesterday > 0) {
+      numItemsInYesterday++; // including the "Yesterday" 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 ? 0 : null;
+    yesterdayHeaderPosition = numItemsInYesterday > 0 ? numItemsInToday : null;
+    olderHeaderPosition = !cursor.isAfterLast() ? numItemsInToday + numItemsInYesterday : null;
   }
 
   @Override
   public ViewHolder onCreateViewHolder(ViewGroup viewGroup, @RowType int viewType) {
     switch (viewType) {
       case RowType.HEADER_TODAY:
+      case RowType.HEADER_YESTERDAY:
       case RowType.HEADER_OLDER:
         return new HeaderViewHolder(
             LayoutInflater.from(viewGroup.getContext())
@@ -124,29 +146,36 @@
 
   @Override
   public void onBindViewHolder(ViewHolder viewHolder, int position) {
-    if (viewHolder instanceof HeaderViewHolder) {
-      HeaderViewHolder headerViewHolder = (HeaderViewHolder) viewHolder;
-      @RowType int viewType = getItemViewType(position);
-      if (viewType == RowType.HEADER_OLDER) {
-        headerViewHolder.setHeader(R.string.new_call_log_header_older);
-      } else if (viewType == RowType.HEADER_TODAY) {
-        headerViewHolder.setHeader(R.string.new_call_log_header_today);
-      } else {
+    @RowType int viewType = getItemViewType(position);
+    switch (viewType) {
+      case RowType.HEADER_TODAY:
+        ((HeaderViewHolder) viewHolder).setHeader(R.string.new_call_log_header_today);
+        break;
+      case RowType.HEADER_YESTERDAY:
+        ((HeaderViewHolder) viewHolder).setHeader(R.string.new_call_log_header_yesterday);
+        break;
+      case RowType.HEADER_OLDER:
+        ((HeaderViewHolder) viewHolder).setHeader(R.string.new_call_log_header_older);
+        break;
+      case RowType.CALL_LOG_ENTRY:
+        NewCallLogViewHolder newCallLogViewHolder = (NewCallLogViewHolder) viewHolder;
+        int previousHeaders = 0;
+        if (todayHeaderPosition != null && position > todayHeaderPosition) {
+          previousHeaders++;
+        }
+        if (yesterdayHeaderPosition != null && position > yesterdayHeaderPosition) {
+          previousHeaders++;
+        }
+        if (olderHeaderPosition != null && position > olderHeaderPosition) {
+          previousHeaders++;
+        }
+        cursor.moveToPosition(position - previousHeaders);
+        newCallLogViewHolder.bind(cursor);
+        break;
+      default:
         throw Assert.createIllegalStateFailException(
             "Unexpected view type " + viewType + " at position: " + position);
-      }
-      return;
     }
-    NewCallLogViewHolder newCallLogViewHolder = (NewCallLogViewHolder) viewHolder;
-    int previousHeaders = 0;
-    if (todayHeaderPosition != null && position > todayHeaderPosition) {
-      previousHeaders++;
-    }
-    if (olderHeaderPosition != null && position > olderHeaderPosition) {
-      previousHeaders++;
-    }
-    cursor.moveToPosition(position - previousHeaders);
-    newCallLogViewHolder.bind(cursor);
   }
 
   @Override
@@ -155,6 +184,9 @@
     if (todayHeaderPosition != null && position == todayHeaderPosition) {
       return RowType.HEADER_TODAY;
     }
+    if (yesterdayHeaderPosition != null && position == yesterdayHeaderPosition) {
+      return RowType.HEADER_YESTERDAY;
+    }
     if (olderHeaderPosition != null && position == olderHeaderPosition) {
       return RowType.HEADER_OLDER;
     }
@@ -167,6 +199,9 @@
     if (todayHeaderPosition != null) {
       numberOfHeaders++;
     }
+    if (yesterdayHeaderPosition != null) {
+      numberOfHeaders++;
+    }
     if (olderHeaderPosition != null) {
       numberOfHeaders++;
     }
diff --git a/java/com/android/dialer/calllog/ui/res/values/strings.xml b/java/com/android/dialer/calllog/ui/res/values/strings.xml
index 0ef0eaf..ebddc35 100644
--- a/java/com/android/dialer/calllog/ui/res/values/strings.xml
+++ b/java/com/android/dialer/calllog/ui/res/values/strings.xml
@@ -20,7 +20,10 @@
   <!-- Header in call log to group calls from the current day.  [CHAR LIMIT=30] -->
   <string name="new_call_log_header_today">Today</string>
 
-  <!-- Header in call log to group calls from before the current day.  [CHAR LIMIT=30] -->
+  <!-- Header in call log to group calls from the previous day.  [CHAR LIMIT=30] -->
+  <string name="new_call_log_header_yesterday">Yesterday</string>
+
+  <!-- Header in call log to group calls from before yesterday.  [CHAR LIMIT=30] -->
   <string name="new_call_log_header_older">Older</string>
 
 </resources>
\ No newline at end of file
diff --git a/java/com/android/dialer/calllogutils/CallLogDates.java b/java/com/android/dialer/calllogutils/CallLogDates.java
index 82e8e40..84e52df 100644
--- a/java/com/android/dialer/calllogutils/CallLogDates.java
+++ b/java/com/android/dialer/calllogutils/CallLogDates.java
@@ -50,7 +50,7 @@
       return DateUtils.formatDateTime(
           context, timestampMillis, DateUtils.FORMAT_SHOW_TIME); // e.g. 12:15 PM
     }
-    if (isWithin3Days(nowMillis, timestampMillis)) {
+    if (getDayDifference(nowMillis, timestampMillis) < 3) {
       return formatDayOfWeek(context, timestampMillis); // e.g. "Wednesday"
     }
     return formatAbbreviatedMonthAndDay(context, timestampMillis); // e.g. "Jan 15"
@@ -129,26 +129,53 @@
         UCharacter.TITLECASE_NO_LOWERCASE);
   }
 
-  private static boolean isWithin3Days(long nowMillis, long timestampMillis) {
-    Calendar threeDaysAgoStartOfDay = Calendar.getInstance();
-    threeDaysAgoStartOfDay.setTimeInMillis(nowMillis);
+  /**
+   * Returns the absolute difference in days between two timestamps. It is the caller's
+   * responsibility to ensure both timestamps are in milliseconds. Failure to do so will result in
+   * undefined behavior.
+   *
+   * <p>Note that the difference is based on day boundaries, not 24-hour periods.
+   *
+   * <p>Examples:
+   *
+   * <ul>
+   *   <li>The difference between 01/19/2018 00:00 and 01/19/2018 23:59 is 0.
+   *   <li>The difference between 01/18/2018 23:59 and 01/19/2018 23:59 is 1.
+   *   <li>The difference between 01/18/2018 00:00 and 01/19/2018 23:59 is 1.
+   *   <li>The difference between 01/17/2018 23:59 and 01/19/2018 00:00 is 2.
+   * </ul>
+   */
+  public static int getDayDifference(long firstTimestamp, long secondTimestamp) {
+    // Ensure secondMillis is no less than firstMillis
+    if (secondTimestamp < firstTimestamp) {
+      long t = firstTimestamp;
+      firstTimestamp = secondTimestamp;
+      secondTimestamp = t;
+    }
 
-    // This is attempting to find the start of the current day, but it's not quite right due to
+    // Use secondTimestamp as reference
+    Calendar startOfReferenceDay = Calendar.getInstance();
+    startOfReferenceDay.setTimeInMillis(secondTimestamp);
+
+    // This is attempting to find the start of the reference day, but it's not quite right due to
     // daylight savings. Unfortunately there doesn't seem to be a way to get the correct start of
     // the day without using Joda or Java8, both of which are disallowed. This means that the wrong
     // formatting may be applied on days with time changes (though the displayed values will be
     // correct).
-    threeDaysAgoStartOfDay.add(
-        Calendar.HOUR_OF_DAY, -threeDaysAgoStartOfDay.get(Calendar.HOUR_OF_DAY));
-    threeDaysAgoStartOfDay.add(Calendar.MINUTE, -threeDaysAgoStartOfDay.get(Calendar.MINUTE));
-    threeDaysAgoStartOfDay.add(Calendar.SECOND, -threeDaysAgoStartOfDay.get(Calendar.SECOND));
+    startOfReferenceDay.add(Calendar.HOUR_OF_DAY, -startOfReferenceDay.get(Calendar.HOUR_OF_DAY));
+    startOfReferenceDay.add(Calendar.MINUTE, -startOfReferenceDay.get(Calendar.MINUTE));
+    startOfReferenceDay.add(Calendar.SECOND, -startOfReferenceDay.get(Calendar.SECOND));
 
-    threeDaysAgoStartOfDay.add(Calendar.DATE, -2);
+    Calendar other = Calendar.getInstance();
+    other.setTimeInMillis(firstTimestamp);
 
-    Calendar then = Calendar.getInstance();
-    then.setTimeInMillis(timestampMillis);
+    int dayDifference = 0;
+    while (other.before(startOfReferenceDay)) {
+      startOfReferenceDay.add(Calendar.DATE, -1);
+      dayDifference++;
+    }
 
-    return then.equals(threeDaysAgoStartOfDay) || then.after(threeDaysAgoStartOfDay);
+    return dayDifference;
   }
 
   /** Returns true if the provided timestamps are from the same day in the default time zone. */