Merge "[A11Y] Improve data usage chart TalkBack content"

This commit is contained in:
Arc Wang
2019-10-09 07:17:24 +00:00
committed by Android (Google) Code Review
4 changed files with 215 additions and 12 deletions

View File

@@ -35,7 +35,8 @@
android:orientation="vertical">
<include android:id="@+id/label_top"
layout="@layout/usage_side_label" />
layout="@layout/usage_side_label"
android:contentDescription="@null" />
<Space
android:id="@+id/space1"
@@ -44,7 +45,8 @@
android:layout_weight="1" />
<include android:id="@+id/label_middle"
layout="@layout/usage_side_label" />
layout="@layout/usage_side_label"
android:contentDescription="@null" />
<Space
android:id="@+id/space2"
@@ -53,7 +55,8 @@
android:layout_weight="1" />
<include android:id="@+id/label_bottom"
layout="@layout/usage_side_label" />
layout="@layout/usage_side_label"
android:contentDescription="@null" />
</LinearLayout>
@@ -82,7 +85,8 @@
android:layout_weight="1"
android:orientation="horizontal">
<include android:id="@+id/label_start"
layout="@layout/usage_side_label" />
layout="@layout/usage_side_label"
android:contentDescription="@null" />
<Space
android:id="@+id/spacer"
@@ -91,7 +95,8 @@
android:layout_weight="1" />
<include android:id="@+id/label_end"
layout="@layout/usage_side_label" />
layout="@layout/usage_side_label"
android:contentDescription="@null" />
</com.android.settings.widget.BottomLabelLayout>
</LinearLayout>

View File

@@ -9690,6 +9690,12 @@
<!-- Optional part of data usage showing the remaining amount [CHAR LIMIT=13] -->
<string name="data_remaining"><xliff:g name="bytes" example="2 GB">^1</xliff:g> left</string>
<!-- Brief content description for data usage chart [CHAR LIMIT=NONE] -->
<string name="data_usage_chart_brief_content_description">Graph showing data usage between <xliff:g id="start_date" example="August 19">%1$s</xliff:g> and <xliff:g id="end_date" example="September 16">%2$s</xliff:g>.</string>
<!-- Content description for data usage chart when data is not available [CHAR LIMIT=NONE] -->
<string name="data_usage_chart_no_data_content_description">No data in this date range</string>
<!-- Informational text about time left in billing cycle [CHAR LIMIT=60] -->
<plurals name="billing_cycle_days_left">
<item quantity="one">%d day left</item>

View File

@@ -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<NetworkCycleData> 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<DataUsageSummaryNode> 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<DataUsageSummaryNode> getDensedStatsData(List<NetworkCycleData> usageSummary) {
final List<DataUsageSummaryNode> 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<Integer, List<DataUsageSummaryNode>> nodesByDataUsagePercentage
= dataUsageSummaryNodes.stream().collect(
Collectors.groupingBy(DataUsageSummaryNode::getDataUsagePercentage));
// Collect densed nodes from collection of the same data usage percentage
final List<DataUsageSummaryNode> 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);

View File

@@ -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<NetworkCycleData> 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<DataUsageSummaryNode> 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();