Better a11y for new call log entries.

Bug: 70989658
Test: CallLogDatesTest, CallLogEntryDescriptionsTest, NewCallLogViewHolderTest
PiperOrigin-RevId: 197811739
Change-Id: I0f9d1e79d8e687efffbb1dac01aaf6fa26a45f6a
diff --git a/java/com/android/dialer/calllog/ui/NewCallLogViewHolder.java b/java/com/android/dialer/calllog/ui/NewCallLogViewHolder.java
index 4def69c..3b21a60 100644
--- a/java/com/android/dialer/calllog/ui/NewCallLogViewHolder.java
+++ b/java/com/android/dialer/calllog/ui/NewCallLogViewHolder.java
@@ -26,12 +26,16 @@
 import android.telecom.PhoneAccountHandle;
 import android.text.TextUtils;
 import android.view.View;
+import android.view.View.AccessibilityDelegate;
+import android.view.accessibility.AccessibilityNodeInfo;
+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;
+import com.android.dialer.calllogutils.CallLogEntryDescriptions;
 import com.android.dialer.calllogutils.CallLogEntryText;
 import com.android.dialer.calllogutils.CallLogRowActions;
 import com.android.dialer.calllogutils.PhoneAccountUtils;
@@ -62,6 +66,7 @@
   private final ImageView assistedDialIcon;
   private final TextView phoneAccountView;
   private final ImageView menuButton;
+  private final View callLogEntryRootView;
 
   private final Clock clock;
   private final RealtimeRowProcessor realtimeRowProcessor;
@@ -78,6 +83,7 @@
       PopCounts popCounts) {
     super(view);
     this.activity = activity;
+    callLogEntryRootView = view;
     contactPhotoView = view.findViewById(R.id.contact_photo_view);
     primaryTextView = view.findViewById(R.id.primary_text);
     callCountTextView = view.findViewById(R.id.call_count);
@@ -107,6 +113,7 @@
     // 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);
 
     // 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).
@@ -142,6 +149,28 @@
     setOnClickListenerForMenuButon(row);
   }
 
+  private void configA11yForRow(CoalescedRow row) {
+    callLogEntryRootView.setContentDescription(
+        CallLogEntryDescriptions.buildDescriptionForEntry(activity, clock, row));
+
+    // Inform a11y users that double tapping an entry now makes a call.
+    // This will instruct TalkBack to say "double tap to call" instead of
+    // "double tap to activate".
+    callLogEntryRootView.setAccessibilityDelegate(
+        new AccessibilityDelegate() {
+          @Override
+          public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
+            super.onInitializeAccessibilityNodeInfo(host, info);
+            info.addAction(
+                new AccessibilityAction(
+                    AccessibilityNodeInfo.ACTION_CLICK,
+                    activity
+                        .getResources()
+                        .getString(R.string.a11y_new_call_log_entry_tap_action)));
+          }
+        });
+  }
+
   private void setNumberCalls(CoalescedRow row) {
     int numberCalls = row.getCoalescedIds().getCoalescedIdCount();
     if (numberCalls > 1) {
@@ -274,6 +303,12 @@
 
   private void setOnClickListenerForMenuButon(CoalescedRow row) {
     menuButton.setOnClickListener(NewCallLogMenu.createOnClickListener(activity, row));
+    menuButton.setContentDescription(
+        activity
+            .getResources()
+            .getString(
+                R.string.a11y_new_call_log_entry_expand_menu,
+                CallLogEntryText.buildPrimaryText(activity, row)));
   }
 
   private class RealtimeRowFutureCallback implements FutureCallback<CoalescedRow> {
diff --git a/java/com/android/dialer/calllog/ui/res/layout/new_call_log_entry.xml b/java/com/android/dialer/calllog/ui/res/layout/new_call_log_entry.xml
index 0acd815..726c53b 100644
--- a/java/com/android/dialer/calllog/ui/res/layout/new_call_log_entry.xml
+++ b/java/com/android/dialer/calllog/ui/res/layout/new_call_log_entry.xml
@@ -30,13 +30,19 @@
       android:layout_marginEnd="8dp"
       android:layout_centerVertical="true"/>
 
+  <!--
+    A vertical linear layout of three rows: primary info, secondary info, and phone account info.
+    It is marked as not important for a11y as we will set a more user-friendly content description
+    for the entire entry view in Java code.
+  -->
   <LinearLayout
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:layout_centerVertical="true"
       android:layout_toEndOf="@+id/contact_photo_view"
       android:layout_toStartOf="@+id/menu_button"
-      android:orientation="vertical">
+      android:orientation="vertical"
+      android:importantForAccessibility="noHideDescendants">
 
     <!-- 1st row: primary info -->
     <LinearLayout
@@ -134,6 +140,10 @@
 
   </LinearLayout>
 
+  <!--
+    The button to expand the bottom sheet for an entry.
+    Its content description is set in Java code.
+  -->
   <ImageView
       android:id="@+id/menu_button"
       android:layout_width="56dp"
@@ -141,8 +151,8 @@
       android:layout_alignParentEnd="true"
       android:layout_centerVertical="true"
       android:background="?android:attr/selectableItemBackgroundBorderless"
-      android:contentDescription="@string/a11y_new_call_log_expand_menu_for_entry"
       android:scaleType="center"
       android:src="@drawable/quantum_ic_more_vert_vd_theme_24"
-      android:tint="?colorIcon"/>
+      android:tint="?colorIcon"
+      tools:ignore="ContentDescription"/>
 </RelativeLayout>
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 3f6462c..112044f 100644
--- a/java/com/android/dialer/calllog/ui/res/values/strings.xml
+++ b/java/com/android/dialer/calllog/ui/res/values/strings.xml
@@ -16,12 +16,21 @@
   -->
 
 <resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+
   <!--
-    A string to describe available action for accessibility user.
-    It will be read as "expand menu for this call log entry".
+    A string informing a11y users that activating a call log entry will place a call.
+    Note: the word "call" here is a verb.
+    [CHAR LIMIT=NONE]
   -->
-  <string name="a11y_new_call_log_expand_menu_for_entry">
-    Expand menu for this call log entry
+  <string name="a11y_new_call_log_entry_tap_action">call</string>
+
+  <!--
+    A string describing the menu button of a call log entry for a11y users.
+    An example will be read as "expand call log menu for Jane Smith".
+    [CHAR LIMIT=NONE]
+  -->
+  <string name="a11y_new_call_log_entry_expand_menu">
+    Expand call log menu for <xliff:g example="Jane Smith" id="primaryTextForEntry">%1$s</xliff:g>
   </string>
 
   <!-- Header in call log to group calls from the current day.  [CHAR LIMIT=30] -->
diff --git a/java/com/android/dialer/calllogutils/CallLogDates.java b/java/com/android/dialer/calllogutils/CallLogDates.java
index 2c33290..9c04c05 100644
--- a/java/com/android/dialer/calllogutils/CallLogDates.java
+++ b/java/com/android/dialer/calllogutils/CallLogDates.java
@@ -36,13 +36,16 @@
    *   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").
+   *   else if < 7 days: day of week (e.g., "Wed");
+   *   else if < 1 year: date with month, day, but no year (e.g., "Jan 15");
+   *   else: date with month, day, and year (e.g., "Jan 15, 2018").
    * </pre>
+   *
+   * <p>Callers can decide whether to abbreviate date/time by specifying flag {@code
+   * abbreviateDateTime}.
    */
   public static CharSequence newCallLogTimestampLabel(
-      Context context, long nowMillis, long timestampMillis) {
+      Context context, long nowMillis, long timestampMillis, boolean abbreviateDateTime) {
     // For calls logged less than 1 minute ago, display "Just now".
     if (nowMillis - timestampMillis < TimeUnit.MINUTES.toMillis(1)) {
       return context.getString(R.string.just_now);
@@ -50,16 +53,19 @@
 
     // 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)
-          .toString()
-          // The platform method DateUtils#getRelativeTimeSpanString adds a dot ('.') after the
-          // abbreviated time unit for some languages (e.g., "8 min. ago") but we prefer not to have
-          // the dot.
-          .replace(".", "");
+      return abbreviateDateTime
+          ? DateUtils.getRelativeTimeSpanString(
+                  timestampMillis,
+                  nowMillis,
+                  DateUtils.MINUTE_IN_MILLIS,
+                  DateUtils.FORMAT_ABBREV_RELATIVE)
+              .toString()
+              // The platform method DateUtils#getRelativeTimeSpanString adds a dot ('.') after the
+              // abbreviated time unit for some languages (e.g., "8 min. ago") but we prefer not to
+              // have the dot.
+              .replace(".", "")
+          : DateUtils.getRelativeTimeSpanString(
+              timestampMillis, nowMillis, DateUtils.MINUTE_IN_MILLIS);
     }
 
     int dayDifference = getDayDifference(nowMillis, timestampMillis);
@@ -69,19 +75,19 @@
       return DateUtils.formatDateTime(context, timestampMillis, DateUtils.FORMAT_SHOW_TIME);
     }
 
-    // For calls logged within a week, display the abbreviated day of week (e.g., "Wed").
+    // For calls logged within a week, display the day of week (e.g., "Wed").
     if (dayDifference < 7) {
-      return formatDayOfWeek(context, timestampMillis);
+      return formatDayOfWeek(context, timestampMillis, abbreviateDateTime);
     }
 
-    // For calls logged within a year, display abbreviated month, day, but no year (e.g., "Jan 15").
+    // For calls logged within a year, display month, day, but no year (e.g., "Jan 15").
     if (isWithinOneYear(nowMillis, timestampMillis)) {
-      return formatAbbreviatedDate(context, timestampMillis, /* showYear = */ false);
+      return formatDate(context, timestampMillis, /* showYear = */ false, abbreviateDateTime);
     }
 
-    // For calls logged no less than one year ago, display abbreviated month, day, and year
+    // For calls logged no less than one year ago, display month, day, and year
     // (e.g., "Jan 15, 2018").
-    return formatAbbreviatedDate(context, timestampMillis, /* showYear = */ true);
+    return formatDate(context, timestampMillis, /* showYear = */ true, abbreviateDateTime);
   }
 
   /**
@@ -106,36 +112,41 @@
   }
 
   /**
-   * Formats the provided timestamp (in milliseconds) into abbreviated day of week.
-   *
-   * <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 timestamp) {
-    return toTitleCase(
-        DateUtils.formatDateTime(
-            context, timestamp, DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_ABBREV_WEEKDAY));
-  }
-
-  /**
-   * Formats the provided timestamp (in milliseconds) into the month abbreviation, day, and
-   * optionally, year.
+   * Formats the provided timestamp (in milliseconds) into the month, day, and optionally, year.
    *
    * <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 formatAbbreviatedDate(
-      Context context, long timestamp, boolean showYear) {
-    int flags = DateUtils.FORMAT_ABBREV_MONTH;
+  private static CharSequence formatDate(
+      Context context, long timestamp, boolean showYear, boolean abbreviateDateTime) {
+    int formatFlags = 0;
+    if (abbreviateDateTime) {
+      formatFlags |= DateUtils.FORMAT_ABBREV_MONTH;
+    }
     if (!showYear) {
-      flags |= DateUtils.FORMAT_NO_YEAR;
+      formatFlags |= DateUtils.FORMAT_NO_YEAR;
     }
 
-    return toTitleCase(DateUtils.formatDateTime(context, timestamp, flags));
+    return toTitleCase(DateUtils.formatDateTime(context, timestamp, formatFlags));
+  }
+
+  /**
+   * Formats the provided timestamp (in milliseconds) into day of week.
+   *
+   * <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 timestamp, boolean abbreviateDateTime) {
+    int formatFlags =
+        abbreviateDateTime
+            ? (DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_ABBREV_WEEKDAY)
+            : DateUtils.FORMAT_SHOW_WEEKDAY;
+    return toTitleCase(DateUtils.formatDateTime(context, timestamp, formatFlags));
   }
 
   private static CharSequence toTitleCase(CharSequence value) {
diff --git a/java/com/android/dialer/calllogutils/CallLogEntryDescriptions.java b/java/com/android/dialer/calllogutils/CallLogEntryDescriptions.java
new file mode 100644
index 0000000..2440879
--- /dev/null
+++ b/java/com/android/dialer/calllogutils/CallLogEntryDescriptions.java
@@ -0,0 +1,154 @@
+/*
+ * 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.calllogutils;
+
+import android.content.Context;
+import android.provider.CallLog.Calls;
+import android.support.annotation.PluralsRes;
+import android.telecom.PhoneAccountHandle;
+import android.text.TextUtils;
+import com.android.dialer.calllog.model.CoalescedRow;
+import com.android.dialer.telecom.TelecomUtil;
+import com.android.dialer.time.Clock;
+import com.google.common.collect.Collections2;
+import java.util.List;
+
+/** Builds descriptions of call log entries for accessibility users. */
+public final class CallLogEntryDescriptions {
+
+  private CallLogEntryDescriptions() {}
+
+  /**
+   * Builds the content description for a call log entry.
+   *
+   * <p>The description is of format<br>
+   * {primary description}, {secondary description}, {phone account description}.
+   *
+   * <ul>
+   *   <li>The primary description depends on the number of calls in the entry. For example:<br>
+   *       "1 answered call from Jane Smith", or<br>
+   *       "2 calls, the latest is an answered call from Jane Smith".
+   *   <li>The secondary description is the same as the secondary text for the call log entry,
+   *       except that date/time is not abbreviated. For example:<br>
+   *       "mobile, 11 minutes ago".
+   *   <li>The phone account description is of format "on {phone_account_label}, via {number}". For
+   *       example:<br>
+   *       "on SIM 1, via 6502531234".<br>
+   *       Note that the phone account description will be empty if the device has only one SIM.
+   * </ul>
+   *
+   * <p>An example of the full description can be:<br>
+   * "2 calls, the latest is an answered call from Jane Smith, mobile, 11 minutes ago, on SIM 1, via
+   * 6502531234".
+   */
+  public static CharSequence buildDescriptionForEntry(
+      Context context, Clock clock, CoalescedRow row) {
+
+    // Build the primary description.
+    // Examples:
+    //   (1) For an entry containing only 1 call:
+    //         "1 missed call from James Smith".
+    //   (2) For entries containing multiple calls:
+    //         "2 calls, the latest is a missed call from Jame Smith".
+    CharSequence primaryDescription =
+        context
+            .getResources()
+            .getQuantityString(
+                getPrimaryDescriptionResIdForCallType(row),
+                row.getCoalescedIds().getCoalescedIdCount(),
+                row.getCoalescedIds().getCoalescedIdCount(),
+                CallLogEntryText.buildPrimaryText(context, row));
+
+    // Build the secondary description.
+    // An example: "mobile, 11 minutes ago".
+    CharSequence secondaryDescription =
+        joinSecondaryTextComponents(
+            CallLogEntryText.buildSecondaryTextListForEntries(
+                context, clock, row, /* abbreviateDateTime = */ false));
+
+    // Build the phone account description.
+    // Note that this description can be an empty string.
+    CharSequence phoneAccountDescription = buildPhoneAccountDescription(context, row);
+
+    return TextUtils.isEmpty(phoneAccountDescription)
+        ? context
+            .getResources()
+            .getString(
+                R.string.a11y_new_call_log_entry_full_description_without_phone_account_info,
+                primaryDescription,
+                secondaryDescription)
+        : context
+            .getResources()
+            .getString(
+                R.string.a11y_new_call_log_entry_full_description_with_phone_account_info,
+                primaryDescription,
+                secondaryDescription,
+                phoneAccountDescription);
+  }
+
+  private static @PluralsRes int getPrimaryDescriptionResIdForCallType(CoalescedRow row) {
+    switch (row.getCallType()) {
+      case Calls.INCOMING_TYPE:
+      case Calls.ANSWERED_EXTERNALLY_TYPE:
+        return R.plurals.a11y_new_call_log_entry_answered_call;
+      case Calls.OUTGOING_TYPE:
+        return R.plurals.a11y_new_call_log_entry_outgoing_call;
+      case Calls.MISSED_TYPE:
+        return R.plurals.a11y_new_call_log_entry_missed_call;
+      case Calls.VOICEMAIL_TYPE:
+        throw new IllegalStateException("Voicemails not expected in call log");
+      case Calls.BLOCKED_TYPE:
+        return R.plurals.a11y_new_call_log_entry_blocked_call;
+      default:
+        // It is possible for users to end up with calls with unknown call types in their
+        // call history, possibly due to 3rd party call log implementations (e.g. to
+        // distinguish between rejected and missed calls). Instead of crashing, just
+        // assume that all unknown call types are missed calls.
+        return R.plurals.a11y_new_call_log_entry_missed_call;
+    }
+  }
+
+  private static CharSequence buildPhoneAccountDescription(Context context, CoalescedRow row) {
+    PhoneAccountHandle phoneAccountHandle =
+        TelecomUtil.composePhoneAccountHandle(
+            row.getPhoneAccountComponentName(), row.getPhoneAccountId());
+    if (phoneAccountHandle == null) {
+      return "";
+    }
+
+    String phoneAccountLabel = PhoneAccountUtils.getAccountLabel(context, phoneAccountHandle);
+    if (TextUtils.isEmpty(phoneAccountLabel)) {
+      return "";
+    }
+
+    if (TextUtils.isEmpty(row.getNumber().getNormalizedNumber())) {
+      return "";
+    }
+
+    return context
+        .getResources()
+        .getString(
+            R.string.a11y_new_call_log_entry_phone_account,
+            phoneAccountLabel,
+            row.getNumber().getNormalizedNumber());
+  }
+
+  private static CharSequence joinSecondaryTextComponents(List<CharSequence> components) {
+    return TextUtils.join(
+        ", ", Collections2.filter(components, (text) -> !TextUtils.isEmpty(text)));
+  }
+}
diff --git a/java/com/android/dialer/calllogutils/CallLogEntryText.java b/java/com/android/dialer/calllogutils/CallLogEntryText.java
index acf8ef9..895497f 100644
--- a/java/com/android/dialer/calllogutils/CallLogEntryText.java
+++ b/java/com/android/dialer/calllogutils/CallLogEntryText.java
@@ -26,6 +26,7 @@
 import com.google.common.base.Optional;
 import com.google.common.collect.Collections2;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 
 /**
@@ -76,45 +77,69 @@
   }
 
   /**
-   * The secondary text to show in the main call log entry list.
+   * The secondary text to be shown in the main call log entry list.
+   *
+   * <p>This method first obtains a list of strings to be shown in order and then concatenates them
+   * with " • ".
+   *
+   * <p>Examples:
+   *
+   * <ul>
+   *   <li>Mobile, Duo video • 10 min ago
+   *   <li>Spam • Mobile • Now
+   *   <li>Blocked • Spam • Mobile • Now
+   * </ul>
+   *
+   * @see #buildSecondaryTextListForEntries(Context, Clock, CoalescedRow, boolean) for details.
+   */
+  public static CharSequence buildSecondaryTextForEntries(
+      Context context, Clock clock, CoalescedRow row) {
+    return joinSecondaryTextComponents(
+        buildSecondaryTextListForEntries(context, clock, row, /* abbreviateDateTime = */ true));
+  }
+
+  /**
+   * Returns a list of strings to be shown in order as the main call log entry's secondary text.
    *
    * <p>Rules:
    *
    * <ul>
-   *   <li>An emergency number: Date
+   *   <li>An emergency number: [{Date}]
    *   <li>Number - not blocked, call - not spam:
-   *       <p>$Label(, Duo video|Carrier video)?|$Location • Date
+   *       <p>[{$Label(, Duo video|Carrier video)?|$Location}, {Date}]
    *   <li>Number - blocked, call - not spam:
-   *       <p>Blocked • $Label(, Duo video|Carrier video)?|$Location • Date
+   *       <p>["Blocked", {$Label(, Duo video|Carrier video)?|$Location}, {Date}]
    *   <li>Number - not blocked, call - spam:
-   *       <p>Spam • $Label(, Duo video|Carrier video)? • Date
+   *       <p>["Spam", {$Label(, Duo video|Carrier video)?}, {Date}]
    *   <li>Number - blocked, call - spam:
-   *       <p>Blocked • Spam • $Label(, Duo video|Carrier video)? • Date
+   *       <p>["Blocked, Spam", {$Label(, Duo video|Carrier video)?}, {Date}]
    * </ul>
    *
    * <p>Examples:
    *
    * <ul>
-   *   <li>Mobile, Duo video • Now
-   *   <li>Duo video • 10 min ago
-   *   <li>Mobile • 11:45 PM
-   *   <li>Mobile • Sun
-   *   <li>Blocked • Mobile, Duo video • Now
-   *   <li>Blocked • Brooklyn, NJ • 10 min ago
-   *   <li>Spam • Mobile • Now
-   *   <li>Spam • Now
-   *   <li>Blocked • Spam • Mobile • Now
-   *   <li>Brooklyn, NJ • Jan 15
+   *   <li>["Mobile, Duo video", "Now"]
+   *   <li>["Duo video", "10 min ago"]
+   *   <li>["Mobile", "11:45 PM"]
+   *   <li>["Mobile", "Sun"]
+   *   <li>["Blocked", "Mobile, Duo video", "Now"]
+   *   <li>["Blocked", "Brooklyn, NJ", "10 min ago"]
+   *   <li>["Spam", "Mobile", "Now"]
+   *   <li>["Spam", "Now"]
+   *   <li>["Blocked", "Spam", "Mobile", "Now"]
+   *   <li>["Brooklyn, NJ", "Jan 15"]
    * </ul>
    *
-   * <p>See {@link CallLogDates#newCallLogTimestampLabel(Context, long, long)} for date rules.
+   * <p>See {@link CallLogDates#newCallLogTimestampLabel(Context, long, long, boolean)} for date
+   * rules.
    */
-  public static CharSequence buildSecondaryTextForEntries(
-      Context context, Clock clock, CoalescedRow row) {
+  static List<CharSequence> buildSecondaryTextListForEntries(
+      Context context, Clock clock, CoalescedRow row, boolean abbreviateDateTime) {
     // For emergency numbers, the secondary text should contain only the timestamp.
     if (row.getNumberAttributes().getIsEmergencyNumber()) {
-      return CallLogDates.newCallLogTimestampLabel(
-          context, clock.currentTimeMillis(), row.getTimestamp());
+      return Collections.singletonList(
+          CallLogDates.newCallLogTimestampLabel(
+              context, clock.currentTimeMillis(), row.getTimestamp(), abbreviateDateTime));
     }
 
     List<CharSequence> components = new ArrayList<>();
@@ -130,8 +155,8 @@
 
     components.add(
         CallLogDates.newCallLogTimestampLabel(
-            context, clock.currentTimeMillis(), row.getTimestamp()));
-    return joinSecondaryTextComponents(components);
+            context, clock.currentTimeMillis(), row.getTimestamp(), abbreviateDateTime));
+    return components;
   }
 
   /**
diff --git a/java/com/android/dialer/calllogutils/res/values/strings.xml b/java/com/android/dialer/calllogutils/res/values/strings.xml
index e476bdd..52b6d34 100644
--- a/java/com/android/dialer/calllogutils/res/values/strings.xml
+++ b/java/com/android/dialer/calllogutils/res/values/strings.xml
@@ -145,4 +145,67 @@
 
   <!-- String used to display calls from spam numbers in the call log.   [CHAR LIMIT=30] -->
   <string name="new_call_log_secondary_spam">Spam</string>
+
+  <!--
+    String introducing to a11y users a call log entry in which the latest call is a missed call.
+    [CHAR LIMIT=NONE]
+  -->
+  <plurals name="a11y_new_call_log_entry_missed_call">
+    <item quantity="one"><xliff:g example="1" id="count">%1$d</xliff:g> missed call from <xliff:g example="Jane Smith" id="primaryInfoForEntry">%2$s</xliff:g></item>
+    <item quantity="other"><xliff:g example="2" id="count">%1$d</xliff:g> calls, the latest is a missed call from <xliff:g example="Jane Smith" id="primaryInfoForEntry">%2$s</xliff:g></item>
+  </plurals>
+
+  <!--
+    String introducing to a11y users a call log entry in which the latest call is an answered call.
+    [CHAR LIMIT=NONE]
+  -->
+  <plurals name="a11y_new_call_log_entry_answered_call">
+    <item quantity="one"><xliff:g example="1" id="count">%1$d</xliff:g> answered call from <xliff:g example="Jane Smith" id="primaryInfoForEntry">%2$s</xliff:g></item>
+    <item quantity="other"><xliff:g example="2" id="count">%1$d</xliff:g> calls, the latest is an answered call from <xliff:g example="Jane Smith" id="primaryInfoForEntry">%2$s</xliff:g></item>
+  </plurals>
+
+  <!--
+    String introducing to a11y users a call log entry in which the latest call is an outgoing call.
+    [CHAR LIMIT=NONE]
+  -->
+  <plurals name="a11y_new_call_log_entry_outgoing_call">
+    <item quantity="one"><xliff:g example="1" id="count">%1$d</xliff:g> outgoing call to <xliff:g example="Jane Smith" id="primaryInfoForEntry">%2$s</xliff:g></item>
+    <item quantity="other"><xliff:g example="2" id="count">%1$d</xliff:g> calls, the latest is an outgoing call to <xliff:g example="Jane Smith" id="primaryInfoForEntry">%2$s</xliff:g></item>
+  </plurals>
+
+  <!--
+    String introducing to a11y users a call log entry in which the latest call is a blocked call.
+    [CHAR LIMIT=NONE]
+  -->
+  <plurals name="a11y_new_call_log_entry_blocked_call">
+    <item quantity="one"><xliff:g example="1" id="count">%1$d</xliff:g> blocked call from <xliff:g example="Jane Smith" id="primaryInfoForEntry">%2$s</xliff:g></item>
+    <item quantity="other"><xliff:g example="2" id="count">%1$d</xliff:g> calls, the latest is a blocked call from <xliff:g example="Jane Smith" id="primaryInfoForEntry">%2$s</xliff:g></item>
+  </plurals>
+
+
+  <!--
+    String describing to a11y users the phone account used to make/receive the latest call in a call
+    log entry.
+    [CHAR LIMIT=NONE]
+  -->
+  <string name="a11y_new_call_log_entry_phone_account">
+    on <xliff:g example="SIM 1" id="phoneAccount">%1$s</xliff:g>,
+    via <xliff:g example="(555) 555-5555" id="number">%2$s</xliff:g>
+  </string>
+
+  <!--
+    String template describing to a11y users a call log entry without phone account info.
+    [CHAR LIMIT=NONE]
+  -->
+  <string name="a11y_new_call_log_entry_full_description_without_phone_account_info">
+    <xliff:g example="A missed call from Jane Smith" id="primaryDescriptionForEntry">%1$s</xliff:g>, <xliff:g example="mobile, 30 minutes ago" id="secondaryDescriptionForEntry">%2$s</xliff:g>.
+  </string>
+
+  <!--
+    String template describing to a11y users a call log entry with phone account info.
+    [CHAR LIMIT=NONE]
+  -->
+  <string name="a11y_new_call_log_entry_full_description_with_phone_account_info">
+    <xliff:g example="A missed call from Jane Smith" id="primaryDescriptionForEntry">%1$s</xliff:g>, <xliff:g example="mobile, 30 minutes ago" id="secondaryDescriptionForEntry">%2$s</xliff:g>, <xliff:g example="on SIM 1, via (555) 555-5555">%3$s</xliff:g>.
+  </string>
 </resources>
diff --git a/java/com/android/dialer/voicemail/listui/VoicemailEntryText.java b/java/com/android/dialer/voicemail/listui/VoicemailEntryText.java
index 973f9b1..dd53dff 100644
--- a/java/com/android/dialer/voicemail/listui/VoicemailEntryText.java
+++ b/java/com/android/dialer/voicemail/listui/VoicemailEntryText.java
@@ -80,7 +80,10 @@
     }
     secondaryText.append(
         CallLogDates.newCallLogTimestampLabel(
-            context, clock.currentTimeMillis(), voicemailEntry.getTimestamp()));
+            context,
+            clock.currentTimeMillis(),
+            voicemailEntry.getTimestamp(),
+            /* abbreviateDateTime = */ true));
 
     long duration = voicemailEntry.getDuration();
     if (duration >= 0) {