diff options
author | linyuh <linyuh@google.com> | 2018-05-23 16:41:50 -0700 |
---|---|---|
committer | Eric Erfanian <erfanian@google.com> | 2018-05-30 14:03:01 +0000 |
commit | 19a7c0eda9730798100994e0b5a6e99197f04f3d (patch) | |
tree | 728f98a3d2b71fc7bd662a38a0f7f22b4e0009e4 /java/com/android/dialer/calllogutils | |
parent | 2ad3c08bc26edee0c721505e21c9764c10e3e5f7 (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')
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> |