Properly display date and/or time of entries in the new call log.

Bug: 70989595
Test: CallLogDatesTest, CallLogEntryTextTest
PiperOrigin-RevId: 182809700
Change-Id: I84b699536ae7f77e6c27db0b1b008e3ebc6216c2
diff --git a/java/com/android/dialer/calllogutils/CallLogDates.java b/java/com/android/dialer/calllogutils/CallLogDates.java
index 84e52df..bdf6215 100644
--- a/java/com/android/dialer/calllogutils/CallLogDates.java
+++ b/java/com/android/dialer/calllogutils/CallLogDates.java
@@ -35,29 +35,52 @@
    * <p>Rules:
    *
    * <pre>
-   *   if < 1 minute ago: "Now";
-   *   else if today: "12:15 PM"
-   *   else if < 3 days ago: "Wednesday";
-   *   else: "Jan 15"
+   *   if < 1 minute ago: "Just now";
+   *   else if < 1 hour ago: time relative to now (e.g., "8 min. ago");
+   *   else if today: time (e.g., "12:15 PM");
+   *   else if < 7 days: abbreviated day of week (e.g., "Wed");
+   *   else if < 1 year: date with abbreviated month, day, but no year (e.g., "Jan 15");
+   *   else: date with abbreviated month, day, and year (e.g., "Jan 15, 2018").
    * </pre>
    */
   public static CharSequence newCallLogTimestampLabel(
       Context context, long nowMillis, long timestampMillis) {
+    // For calls logged less than 1 minute ago, display "Just now".
     if (nowMillis - timestampMillis < TimeUnit.MINUTES.toMillis(1)) {
-      return context.getString(R.string.now);
+      return context.getString(R.string.just_now);
     }
-    if (isSameDay(nowMillis, timestampMillis)) {
-      return DateUtils.formatDateTime(
-          context, timestampMillis, DateUtils.FORMAT_SHOW_TIME); // e.g. 12:15 PM
+
+    // For calls logged less than 1 hour ago, display time relative to now (e.g., "8 min. ago").
+    if (nowMillis - timestampMillis < TimeUnit.HOURS.toMillis(1)) {
+      return DateUtils.getRelativeTimeSpanString(
+          timestampMillis, nowMillis, DateUtils.MINUTE_IN_MILLIS, DateUtils.FORMAT_ABBREV_RELATIVE);
     }
-    if (getDayDifference(nowMillis, timestampMillis) < 3) {
-      return formatDayOfWeek(context, timestampMillis); // e.g. "Wednesday"
+
+    int dayDifference = getDayDifference(nowMillis, timestampMillis);
+
+    // For calls logged today, display time (e.g., "12:15 PM").
+    if (dayDifference == 0) {
+      return DateUtils.formatDateTime(context, timestampMillis, DateUtils.FORMAT_SHOW_TIME);
     }
-    return formatAbbreviatedMonthAndDay(context, timestampMillis); // e.g. "Jan 15"
+
+    // For calls logged within a week, display the abbreviated day of week (e.g., "Wed").
+    if (dayDifference < 7) {
+      return formatDayOfWeek(context, timestampMillis);
+    }
+
+    // For calls logged within a year, display abbreviated month, day, but no year (e.g., "Jan 15").
+    if (isWithinOneYear(nowMillis, timestampMillis)) {
+      return formatAbbreviatedDate(context, timestampMillis, /* showYear = */ false);
+    }
+
+    // For calls logged no less than one year ago, display abbreviated month, day, and year
+    // (e.g., "Jan 15, 2018").
+    return formatAbbreviatedDate(context, timestampMillis, /* showYear = */ true);
   }
 
   /**
-   * Formats the provided date into a value suitable for display in the current locale.
+   * Formats the provided timestamp (in milliseconds) into date and time suitable for display in the
+   * current locale.
    *
    * <p>For example, returns a string like "Wednesday, May 25, 2016, 8:02PM" or "Chorshanba, 2016
    * may 25,20:02".
@@ -65,11 +88,11 @@
    * <p>For pre-N devices, the returned value may not start with a capital if the local convention
    * is to not capitalize day names. On N+ devices, the returned value is always capitalized.
    */
-  public static CharSequence formatDate(Context context, long callDateMillis) {
+  public static CharSequence formatDate(Context context, long timestamp) {
     return toTitleCase(
         DateUtils.formatDateTime(
             context,
-            callDateMillis,
+            timestamp,
             DateUtils.FORMAT_SHOW_TIME
                 | DateUtils.FORMAT_SHOW_DATE
                 | DateUtils.FORMAT_SHOW_WEEKDAY
@@ -77,30 +100,36 @@
   }
 
   /**
-   * Formats the provided date into the day of week.
+   * Formats the provided timestamp (in milliseconds) into abbreviated day of week.
    *
-   * <p>For example, returns a string like "Wednesday" or "Chorshanba".
+   * <p>For example, returns a string like "Wed" or "Chor".
    *
    * <p>For pre-N devices, the returned value may not start with a capital if the local convention
    * is to not capitalize day names. On N+ devices, the returned value is always capitalized.
    */
-  private static CharSequence formatDayOfWeek(Context context, long callDateMillis) {
+  private static CharSequence formatDayOfWeek(Context context, long timestamp) {
     return toTitleCase(
-        DateUtils.formatDateTime(context, callDateMillis, DateUtils.FORMAT_SHOW_WEEKDAY));
+        DateUtils.formatDateTime(
+            context, timestamp, DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_ABBREV_WEEKDAY));
   }
 
   /**
-   * Formats the provided date into the month abbreviation and day.
+   * Formats the provided timestamp (in milliseconds) into the month abbreviation, day, and
+   * optionally, year.
    *
-   * <p>For example, returns a string like "Jan 15".
+   * <p>For example, returns a string like "Jan 15" or "Jan 15, 2018".
    *
    * <p>For pre-N devices, the returned value may not start with a capital if the local convention
    * is to not capitalize day names. On N+ devices, the returned value is always capitalized.
    */
-  private static CharSequence formatAbbreviatedMonthAndDay(Context context, long callDateMillis) {
-    return toTitleCase(
-        DateUtils.formatDateTime(
-            context, callDateMillis, DateUtils.FORMAT_ABBREV_MONTH | DateUtils.FORMAT_NO_YEAR));
+  private static CharSequence formatAbbreviatedDate(
+      Context context, long timestamp, boolean showYear) {
+    int flags = DateUtils.FORMAT_ABBREV_MONTH;
+    if (!showYear) {
+      flags |= DateUtils.FORMAT_NO_YEAR;
+    }
+
+    return toTitleCase(DateUtils.formatDateTime(context, timestamp, flags));
   }
 
   private static CharSequence toTitleCase(CharSequence value) {
@@ -146,7 +175,7 @@
    * </ul>
    */
   public static int getDayDifference(long firstTimestamp, long secondTimestamp) {
-    // Ensure secondMillis is no less than firstMillis
+    // Ensure secondTimestamp is no less than firstTimestamp
     if (secondTimestamp < firstTimestamp) {
       long t = firstTimestamp;
       firstTimestamp = secondTimestamp;
@@ -178,16 +207,36 @@
     return dayDifference;
   }
 
-  /** Returns true if the provided timestamps are from the same day in the default time zone. */
-  public static boolean isSameDay(long firstMillis, long secondMillis) {
-    Calendar first = Calendar.getInstance();
-    first.setTimeInMillis(firstMillis);
+  /**
+   * Returns true if the two timestamps are within one year. 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 365/366-day periods.
+   *
+   * <p>Examples:
+   *
+   * <ul>
+   *   <li>01/01/2018 00:00 and 12/31/2018 23:59 is within one year.
+   *   <li>12/31/2017 23:59 and 12/31/2018 23:59 is not within one year.
+   *   <li>12/31/2017 23:59 and 01/01/2018 00:00 is within one year.
+   * </ul>
+   */
+  private static boolean isWithinOneYear(long firstTimestamp, long secondTimestamp) {
+    // Ensure secondTimestamp is no less than firstTimestamp
+    if (secondTimestamp < firstTimestamp) {
+      long t = firstTimestamp;
+      firstTimestamp = secondTimestamp;
+      secondTimestamp = t;
+    }
 
-    Calendar second = Calendar.getInstance();
-    second.setTimeInMillis(secondMillis);
+    // Use secondTimestamp as reference
+    Calendar reference = Calendar.getInstance();
+    reference.setTimeInMillis(secondTimestamp);
+    reference.add(Calendar.YEAR, -1);
 
-    return first.get(Calendar.YEAR) == second.get(Calendar.YEAR)
-        && first.get(Calendar.MONTH) == second.get(Calendar.MONTH)
-        && first.get(Calendar.DAY_OF_MONTH) == second.get(Calendar.DAY_OF_MONTH);
+    Calendar other = Calendar.getInstance();
+    other.setTimeInMillis(firstTimestamp);
+
+    return reference.before(other);
   }
 }
diff --git a/java/com/android/dialer/calllogutils/CallLogEntryText.java b/java/com/android/dialer/calllogutils/CallLogEntryText.java
index 1df4458..25fe864 100644
--- a/java/com/android/dialer/calllogutils/CallLogEntryText.java
+++ b/java/com/android/dialer/calllogutils/CallLogEntryText.java
@@ -49,22 +49,25 @@
     return primaryText.toString();
   }
 
-  /** The secondary text to show in the main call log entry list. */
+  /**
+   * The secondary text to show in the main call log entry list.
+   *
+   * <p>Rules: (Duo video, )?$Label|$Location • Date
+   *
+   * <p>Examples:
+   *
+   * <ul>
+   *   <li>Duo Video, Mobile • Now
+   *   <li>Duo Video • 10 min. ago
+   *   <li>Mobile • 11:45 PM
+   *   <li>Mobile • Sun
+   *   <li>Brooklyn, NJ • Jan 15
+   * </ul>
+   *
+   * <p>See {@link CallLogDates#newCallLogTimestampLabel(Context, long, long)} for date rules.
+   */
   public static CharSequence buildSecondaryTextForEntries(
       Context context, Clock clock, CoalescedRow row) {
-    /*
-     * Rules: (Duo video, )?$Label|$Location • Date
-     *
-     * Examples:
-     *   Duo Video, Mobile • Now
-     *   Duo Video • 11:45pm
-     *   Mobile • 11:45pm
-     *   Mobile • Sunday
-     *   Brooklyn, NJ • Jan 15
-     *
-     * Date rules:
-     *   if < 1 minute ago: "Now"; else if today: HH:MM(am|pm); else if < 3 days: day; else: MON D
-     */
     StringBuilder secondaryText = secondaryTextPrefix(context, row);
 
     if (secondaryText.length() > 0) {
diff --git a/java/com/android/dialer/calllogutils/res/values/strings.xml b/java/com/android/dialer/calllogutils/res/values/strings.xml
index b8ba5b1..8784bf8 100644
--- a/java/com/android/dialer/calllogutils/res/values/strings.xml
+++ b/java/com/android/dialer/calllogutils/res/values/strings.xml
@@ -129,7 +129,7 @@
   <string name="voicemail_string">Voicemail</string>
 
   <!-- String to be displayed to indicate in the call log that a call just now occurred. -->
-  <string name="now">Now</string>
+  <string name="just_now">Just now</string>
 
   <!-- Text to show in call log for a video call. [CHAR LIMIT=16] -->
   <string name="new_call_log_video">Video</string>
diff --git a/java/com/android/dialer/voicemail/listui/NewVoicemailAdapter.java b/java/com/android/dialer/voicemail/listui/NewVoicemailAdapter.java
index 86d3860..c9bf6e1 100644
--- a/java/com/android/dialer/voicemail/listui/NewVoicemailAdapter.java
+++ b/java/com/android/dialer/voicemail/listui/NewVoicemailAdapter.java
@@ -104,7 +104,7 @@
   private final NewVoicemailMediaPlayer mediaPlayer =
       new NewVoicemailMediaPlayer(new MediaPlayer());
 
-  /** @param cursor whose projection is {@link VoicemailCursorLoader.VOICEMAIL_COLUMNS} */
+  /** @param cursor whose projection is {@link VoicemailCursorLoader#VOICEMAIL_COLUMNS} */
   NewVoicemailAdapter(Cursor cursor, Clock clock, FragmentManager fragmentManager) {
     LogUtil.enterBlock("NewVoicemailAdapter");
     this.cursor = cursor;
@@ -133,14 +133,14 @@
     long currentTimeMillis = clock.currentTimeMillis();
     if (cursor.moveToNext()) {
       long firstTimestamp = VoicemailCursorLoader.getTimestamp(cursor);
-      if (CallLogDates.isSameDay(currentTimeMillis, firstTimestamp)) {
+      if (CallLogDates.getDayDifference(currentTimeMillis, firstTimestamp) == 0) {
         this.todayHeaderPosition = 0 + alertOffSet;
         int adapterPosition =
             2 + alertOffSet; // Accounted for the "Alert", "Today" header and first row.
         while (cursor.moveToNext()) {
           long timestamp = VoicemailCursorLoader.getTimestamp(cursor);
 
-          if (CallLogDates.isSameDay(currentTimeMillis, timestamp)) {
+          if (CallLogDates.getDayDifference(currentTimeMillis, timestamp) == 0) {
             adapterPosition++;
           } else {
             this.olderHeaderPosition = adapterPosition;