From 351ee3a882063bbb14bcae71accce396edaad425 Mon Sep 17 00:00:00 2001 From: Arc Wang Date: Fri, 27 Sep 2019 18:39:29 +0800 Subject: [PATCH] [A11Y] Improve data usage chart TalkBack content 1. Set null content description for all labels of the chart. 2. Set 2 parts in the content description of the chart: I. Brief description of the chart. II. Stats of the data usage. Bug: 141093026 Test: ChartDataUsagePreferenceTest Manually listen to TalkBack speaking Change-Id: I82cefd9987793f40a5bba5bf3ea5f4017da37640 --- res/layout/usage_view.xml | 15 +- res/values/strings.xml | 6 + .../datausage/ChartDataUsagePreference.java | 137 +++++++++++++++++- .../ChartDataUsagePreferenceTest.java | 69 ++++++++- 4 files changed, 215 insertions(+), 12 deletions(-) diff --git a/res/layout/usage_view.xml b/res/layout/usage_view.xml index c24f28974ac..a98ec3e1f8f 100644 --- a/res/layout/usage_view.xml +++ b/res/layout/usage_view.xml @@ -35,7 +35,8 @@ android:orientation="vertical"> + layout="@layout/usage_side_label" + android:contentDescription="@null" /> + layout="@layout/usage_side_label" + android:contentDescription="@null" /> + layout="@layout/usage_side_label" + android:contentDescription="@null" /> @@ -82,7 +85,8 @@ android:layout_weight="1" android:orientation="horizontal"> + layout="@layout/usage_side_label" + android:contentDescription="@null" /> + layout="@layout/usage_side_label" + android:contentDescription="@null" /> diff --git a/res/values/strings.xml b/res/values/strings.xml index 53f96a0b622..abc73bdc4bc 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -9860,6 +9860,12 @@ ^1 left + + Graph showing data usage between %1$s and %2$s. + + + No data in this date range + %d day left diff --git a/src/com/android/settings/datausage/ChartDataUsagePreference.java b/src/com/android/settings/datausage/ChartDataUsagePreference.java index 17f23c4ec8f..6c845a951fa 100644 --- a/src/com/android/settings/datausage/ChartDataUsagePreference.java +++ b/src/com/android/settings/datausage/ChartDataUsagePreference.java @@ -15,10 +15,12 @@ package com.android.settings.datausage; import android.content.Context; +import android.content.res.Resources; import android.net.NetworkPolicy; import android.net.TrafficStats; import android.text.SpannableStringBuilder; import android.text.TextUtils; +import android.text.format.DateUtils; import android.text.format.Formatter; import android.text.style.ForegroundColorSpan; import android.util.AttributeSet; @@ -34,7 +36,11 @@ import com.android.settings.widget.UsageView; import com.android.settingslib.net.NetworkCycleChartData; import com.android.settingslib.net.NetworkCycleData; +import java.util.ArrayList; +import java.util.Comparator; import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; public class ChartDataUsagePreference extends Preference { @@ -45,6 +51,7 @@ public class ChartDataUsagePreference extends Preference { private final int mWarningColor; private final int mLimitColor; + private Resources mResources; private NetworkPolicy mPolicy; private long mStart; private long mEnd; @@ -54,6 +61,7 @@ public class ChartDataUsagePreference extends Preference { public ChartDataUsagePreference(Context context, AttributeSet attrs) { super(context, attrs); + mResources = context.getResources(); setSelectable(false); mLimitColor = Utils.getColorAttrDefaultColor(context, android.R.attr.colorError); mWarningColor = Utils.getColorAttrDefaultColor(context, android.R.attr.textColorSecondary); @@ -72,6 +80,7 @@ public class ChartDataUsagePreference extends Preference { chart.clearPaths(); chart.configureGraph(toInt(mEnd - mStart), top); calcPoints(chart, mNetworkCycleChartData.getUsageBuckets()); + setupContentDescription(chart, mNetworkCycleChartData.getUsageBuckets()); chart.setBottomLabels(new CharSequence[] { Utils.formatDateRange(getContext(), mStart, mStart), Utils.formatDateRange(getContext(), mEnd, mEnd), @@ -118,6 +127,130 @@ public class ChartDataUsagePreference extends Preference { } } + private void setupContentDescription(UsageView chart, List usageSummary) { + final Context context = getContext(); + final StringBuilder contentDescription = new StringBuilder(); + final int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_ABBREV_MONTH; + + // Setup a brief content description. + final String startDate = DateUtils.formatDateTime(context, mStart, flags); + final String endDate = DateUtils.formatDateTime(context, mEnd, flags); + final String briefContentDescription = mResources + .getString(R.string.data_usage_chart_brief_content_description, startDate, endDate); + contentDescription.append(briefContentDescription); + + if (usageSummary == null || usageSummary.isEmpty()) { + final String noDataContentDescription = mResources + .getString(R.string.data_usage_chart_no_data_content_description); + contentDescription.append(noDataContentDescription); + chart.setContentDescription(contentDescription); + return; + } + + // Append more detailed stats. + String nodeDate; + String nodeContentDescription; + final List densedStatsData = getDensedStatsData(usageSummary); + for (DataUsageSummaryNode data : densedStatsData) { + final int dataUsagePercentage = data.getDataUsagePercentage(); + if (!data.isFromMultiNode() || dataUsagePercentage == 100) { + nodeDate = DateUtils.formatDateTime(context, data.getStartTime(), flags); + } else { + nodeDate = DateUtils.formatDateRange(context, data.getStartTime(), + data.getEndTime(), flags); + } + nodeContentDescription = String.format(";%s %d%%", nodeDate, dataUsagePercentage); + + contentDescription.append(nodeContentDescription); + } + + chart.setContentDescription(contentDescription); + } + + /** + * To avoid wordy data, e.g., Aug 2: 0%; Aug 3: 0%;...Aug 22: 0%; Aug 23: 2%. + * Collect the date of the same percentage, e.g., Aug 2 to Aug 22: 0%; Aug 23: 2%. + */ + @VisibleForTesting + List getDensedStatsData(List usageSummary) { + final List dataUsageSummaryNodes = new ArrayList<>(); + final long overallDataUsage = usageSummary.stream() + .mapToLong(NetworkCycleData::getTotalUsage).sum(); + long cumulatedDataUsage = 0L; + int cumulatedDataUsagePercentage = 0; + + // Collect List of DataUsageSummaryNode for data usage percentage information. + for (NetworkCycleData data : usageSummary) { + cumulatedDataUsage += data.getTotalUsage(); + cumulatedDataUsagePercentage = (int) ((cumulatedDataUsage * 100) / overallDataUsage); + + final DataUsageSummaryNode node = new DataUsageSummaryNode(data.getStartTime(), + data.getEndTime(), cumulatedDataUsagePercentage); + dataUsageSummaryNodes.add(node); + } + + // Group nodes of the same data usage percentage. + final Map> nodesByDataUsagePercentage + = dataUsageSummaryNodes.stream().collect( + Collectors.groupingBy(DataUsageSummaryNode::getDataUsagePercentage)); + + // Collect densed nodes from collection of the same data usage percentage + final List densedNodes = new ArrayList<>(); + nodesByDataUsagePercentage.forEach((percentage, nodes) -> { + final long startTime = nodes.stream().mapToLong(DataUsageSummaryNode::getStartTime) + .min().getAsLong(); + final long endTime = nodes.stream().mapToLong(DataUsageSummaryNode::getEndTime) + .max().getAsLong(); + + final DataUsageSummaryNode densedNode = new DataUsageSummaryNode( + startTime, endTime, percentage); + if (nodes.size() > 1) { + densedNode.setFromMultiNode(true /* isFromMultiNode */); + } + + densedNodes.add(densedNode); + }); + + return densedNodes.stream() + .sorted(Comparator.comparingInt(DataUsageSummaryNode::getDataUsagePercentage)) + .collect(Collectors.toList()); + } + + @VisibleForTesting + class DataUsageSummaryNode { + private long mStartTime; + private long mEndTime; + private int mDataUsagePercentage; + private boolean mIsFromMultiNode; + + public DataUsageSummaryNode(long startTime, long endTime, int dataUsagePercentage) { + mStartTime = startTime; + mEndTime = endTime; + mDataUsagePercentage = dataUsagePercentage; + mIsFromMultiNode = false; + } + + public long getStartTime() { + return mStartTime; + } + + public long getEndTime() { + return mEndTime; + } + + public int getDataUsagePercentage() { + return mDataUsagePercentage; + } + + public void setFromMultiNode(boolean isFromMultiNode) { + mIsFromMultiNode = isFromMultiNode; + } + + public boolean isFromMultiNode() { + return mIsFromMultiNode; + } + } + private int toInt(long l) { // Don't need that much resolution on these times. return (int) (l / (1000 * 60)); @@ -151,8 +284,8 @@ public class ChartDataUsagePreference extends Preference { } private CharSequence getLabel(long bytes, int str, int mLimitColor) { - Formatter.BytesResult result = Formatter.formatBytes(getContext().getResources(), - bytes, Formatter.FLAG_SHORTER | Formatter.FLAG_IEC_UNITS); + Formatter.BytesResult result = Formatter.formatBytes(mResources, bytes, + Formatter.FLAG_SHORTER | Formatter.FLAG_IEC_UNITS); CharSequence label = TextUtils.expandTemplate(getContext().getText(str), result.value, result.units); return new SpannableStringBuilder().append(label, new ForegroundColorSpan(mLimitColor), 0); diff --git a/tests/robotests/src/com/android/settings/datausage/ChartDataUsagePreferenceTest.java b/tests/robotests/src/com/android/settings/datausage/ChartDataUsagePreferenceTest.java index 39bf9cfa412..8881b49321e 100644 --- a/tests/robotests/src/com/android/settings/datausage/ChartDataUsagePreferenceTest.java +++ b/tests/robotests/src/com/android/settings/datausage/ChartDataUsagePreferenceTest.java @@ -18,11 +18,19 @@ package com.android.settings.datausage; import static com.google.common.truth.Truth.assertThat; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; -import android.content.Context; +import android.app.Activity; import android.util.SparseIntArray; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.TextView; +import androidx.preference.PreferenceViewHolder; + +import com.android.settings.R; +import com.android.settings.datausage.ChartDataUsagePreference.DataUsageSummaryNode; import com.android.settings.widget.UsageView; import com.android.settingslib.net.NetworkCycleChartData; import com.android.settingslib.net.NetworkCycleData; @@ -32,8 +40,8 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.MockitoAnnotations; +import org.robolectric.Robolectric; import org.robolectric.RobolectricTestRunner; -import org.robolectric.RuntimeEnvironment; import java.util.ArrayList; import java.util.List; @@ -49,15 +57,20 @@ public class ChartDataUsagePreferenceTest { private List mNetworkCycleData; private NetworkCycleChartData mNetworkCycleChartData; - private Context mContext; private ChartDataUsagePreference mPreference; + private Activity mActivity; + private PreferenceViewHolder mHolder; @Before public void setUp() { MockitoAnnotations.initMocks(this); - mContext = RuntimeEnvironment.application; - mPreference = new ChartDataUsagePreference(mContext, null); + mActivity = spy(Robolectric.setupActivity(Activity.class)); + mPreference = new ChartDataUsagePreference(mActivity, null /* attrs */); + LayoutInflater inflater = LayoutInflater.from(mActivity); + View view = inflater.inflate(mPreference.getLayoutResource(), null /* root */, + false /* attachToRoot */); + mHolder = spy(PreferenceViewHolder.createInstanceForTests(view)); } @Test @@ -148,6 +161,40 @@ public class ChartDataUsagePreferenceTest { assertThat(points.keyAt(6)).isEqualTo(TimeUnit.DAYS.toMinutes(5)); } + @Test + public void notifyChange_nonEmptyDataUsage_shouldHaveSingleContentDescription() { + final UsageView chart = (UsageView) mHolder.findViewById(R.id.data_usage); + final TextView labelTop = (TextView) mHolder.findViewById(R.id.label_top); + final TextView labelMiddle = (TextView) mHolder.findViewById(R.id.label_middle); + final TextView labelBottom = (TextView) mHolder.findViewById(R.id.label_bottom); + final TextView labelStart = (TextView) mHolder.findViewById(R.id.label_start); + final TextView labelEnd = (TextView) mHolder.findViewById(R.id.label_end); + createTestNetworkData(); + mPreference.setNetworkCycleData(mNetworkCycleChartData); + + mPreference.onBindViewHolder(mHolder); + + assertThat(chart.getContentDescription()).isNotNull(); + assertThat(labelTop.getContentDescription()).isNull(); + assertThat(labelMiddle.getContentDescription()).isNull(); + assertThat(labelBottom.getContentDescription()).isNull(); + assertThat(labelStart.getContentDescription()).isNull(); + assertThat(labelEnd.getContentDescription()).isNull(); + } + + @Test + public void getDensedStatsData_someSamePercentageNodes_getDifferentPercentageNodes() { + createSomeSamePercentageNetworkData(); + final List densedStatsData = + mPreference.getDensedStatsData(mNetworkCycleData); + + assertThat(mNetworkCycleData.size()).isEqualTo(8); + assertThat(densedStatsData.size()).isEqualTo(3); + assertThat(densedStatsData.get(0).getDataUsagePercentage()).isEqualTo(33); + assertThat(densedStatsData.get(1).getDataUsagePercentage()).isEqualTo(99); + assertThat(densedStatsData.get(2).getDataUsagePercentage()).isEqualTo(100); + } + private void createTestNetworkData() { mNetworkCycleData = new ArrayList<>(); // create 10 arbitrary network data @@ -169,6 +216,18 @@ public class ChartDataUsagePreferenceTest { mNetworkCycleChartData = builder.build(); } + private void createSomeSamePercentageNetworkData() { + mNetworkCycleData = new ArrayList<>(); + mNetworkCycleData.add(createNetworkCycleData(1521583200000L, 1521586800000L, 100));//33% + mNetworkCycleData.add(createNetworkCycleData(1521586800000L, 1521590400000L, 1)); //33% + mNetworkCycleData.add(createNetworkCycleData(1521590400000L, 1521655200000L, 0)); //33% + mNetworkCycleData.add(createNetworkCycleData(1521655200000L, 1521658800000L, 0)); //33% + mNetworkCycleData.add(createNetworkCycleData(1521658800000L, 1521662400000L, 200));//99% + mNetworkCycleData.add(createNetworkCycleData(1521662400000L, 1521666000000L, 1)); //99% + mNetworkCycleData.add(createNetworkCycleData(1521666000000L, 1521669600000L, 1)); //100 + mNetworkCycleData.add(createNetworkCycleData(1521669600000L, 1521673200000L, 0)); //100% + } + private NetworkCycleData createNetworkCycleData(long start, long end, long usage) { return new NetworkCycleData.Builder() .setStartTime(start).setEndTime(end).setTotalUsage(usage).build();