summaryrefslogtreecommitdiff
path: root/java/com/android/dialer/calllogutils
diff options
context:
space:
mode:
authorlinyuh <linyuh@google.com>2018-05-23 16:41:50 -0700
committerEric Erfanian <erfanian@google.com>2018-05-30 14:03:01 +0000
commit19a7c0eda9730798100994e0b5a6e99197f04f3d (patch)
tree728f98a3d2b71fc7bd662a38a0f7f22b4e0009e4 /java/com/android/dialer/calllogutils
parent2ad3c08bc26edee0c721505e21c9764c10e3e5f7 (diff)
Better a11y for new call log entries.
Bug: 70989658 Test: CallLogDatesTest, CallLogEntryDescriptionsTest, NewCallLogViewHolderTest PiperOrigin-RevId: 197811739 Change-Id: I0f9d1e79d8e687efffbb1dac01aaf6fa26a45f6a
Diffstat (limited to 'java/com/android/dialer/calllogutils')
-rw-r--r--java/com/android/dialer/calllogutils/CallLogDates.java85
-rw-r--r--java/com/android/dialer/calllogutils/CallLogEntryDescriptions.java154
-rw-r--r--java/com/android/dialer/calllogutils/CallLogEntryText.java71
-rw-r--r--java/com/android/dialer/calllogutils/res/values/strings.xml63
4 files changed, 313 insertions, 60 deletions
diff --git a/java/com/android/dialer/calllogutils/CallLogDates.java b/java/com/android/dialer/calllogutils/CallLogDates.java
index 2c332901c..9c04c05f7 100644
--- a/java/com/android/dialer/calllogutils/CallLogDates.java
+++ b/java/com/android/dialer/calllogutils/CallLogDates.java
@@ -36,13 +36,16 @@ public final class CallLogDates {
* 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 @@ public final class CallLogDates {
// 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 @@ public final class CallLogDates {
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 @@ public final class CallLogDates {
}
/**
- * Formats the provided timestamp (in milliseconds) into abbreviated day of week.
+ * Formats the provided timestamp (in milliseconds) into the month, day, and optionally, year.
*
- * <p>For example, returns a string like "Wed" or "Chor".
+ * <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 formatDayOfWeek(Context context, long timestamp) {
- return toTitleCase(
- DateUtils.formatDateTime(
- context, timestamp, DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_ABBREV_WEEKDAY));
+ private static CharSequence formatDate(
+ Context context, long timestamp, boolean showYear, boolean abbreviateDateTime) {
+ int formatFlags = 0;
+ if (abbreviateDateTime) {
+ formatFlags |= DateUtils.FORMAT_ABBREV_MONTH;
+ }
+ if (!showYear) {
+ formatFlags |= DateUtils.FORMAT_NO_YEAR;
+ }
+
+ return toTitleCase(DateUtils.formatDateTime(context, timestamp, formatFlags));
}
/**
- * Formats the provided timestamp (in milliseconds) into the month abbreviation, day, and
- * optionally, year.
+ * Formats the provided timestamp (in milliseconds) into day of week.
*
- * <p>For example, returns a string like "Jan 15" or "Jan 15, 2018".
+ * <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 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 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 000000000..244087989
--- /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 acf8ef932..895497f0f 100644
--- a/java/com/android/dialer/calllogutils/CallLogEntryText.java
+++ b/java/com/android/dialer/calllogutils/CallLogEntryText.java
@@ -26,6 +26,7 @@ import com.android.dialer.time.Clock;
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 @@ public final class CallLogEntryText {
}
/**
- * 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 @@ public final class CallLogEntryText {
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 e476bdd6c..52b6d3408 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>