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

View File

@@ -9690,6 +9690,12 @@
<!-- Optional part of data usage showing the remaining amount [CHAR LIMIT=13] --> <!-- 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> <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] --> <!-- Informational text about time left in billing cycle [CHAR LIMIT=60] -->
<plurals name="billing_cycle_days_left"> <plurals name="billing_cycle_days_left">
<item quantity="one">%d day left</item> <item quantity="one">%d day left</item>

View File

@@ -15,10 +15,12 @@
package com.android.settings.datausage; package com.android.settings.datausage;
import android.content.Context; import android.content.Context;
import android.content.res.Resources;
import android.net.NetworkPolicy; import android.net.NetworkPolicy;
import android.net.TrafficStats; import android.net.TrafficStats;
import android.text.SpannableStringBuilder; import android.text.SpannableStringBuilder;
import android.text.TextUtils; import android.text.TextUtils;
import android.text.format.DateUtils;
import android.text.format.Formatter; import android.text.format.Formatter;
import android.text.style.ForegroundColorSpan; import android.text.style.ForegroundColorSpan;
import android.util.AttributeSet; 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.NetworkCycleChartData;
import com.android.settingslib.net.NetworkCycleData; import com.android.settingslib.net.NetworkCycleData;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class ChartDataUsagePreference extends Preference { public class ChartDataUsagePreference extends Preference {
@@ -45,6 +51,7 @@ public class ChartDataUsagePreference extends Preference {
private final int mWarningColor; private final int mWarningColor;
private final int mLimitColor; private final int mLimitColor;
private Resources mResources;
private NetworkPolicy mPolicy; private NetworkPolicy mPolicy;
private long mStart; private long mStart;
private long mEnd; private long mEnd;
@@ -54,6 +61,7 @@ public class ChartDataUsagePreference extends Preference {
public ChartDataUsagePreference(Context context, AttributeSet attrs) { public ChartDataUsagePreference(Context context, AttributeSet attrs) {
super(context, attrs); super(context, attrs);
mResources = context.getResources();
setSelectable(false); setSelectable(false);
mLimitColor = Utils.getColorAttrDefaultColor(context, android.R.attr.colorError); mLimitColor = Utils.getColorAttrDefaultColor(context, android.R.attr.colorError);
mWarningColor = Utils.getColorAttrDefaultColor(context, android.R.attr.textColorSecondary); mWarningColor = Utils.getColorAttrDefaultColor(context, android.R.attr.textColorSecondary);
@@ -72,6 +80,7 @@ public class ChartDataUsagePreference extends Preference {
chart.clearPaths(); chart.clearPaths();
chart.configureGraph(toInt(mEnd - mStart), top); chart.configureGraph(toInt(mEnd - mStart), top);
calcPoints(chart, mNetworkCycleChartData.getUsageBuckets()); calcPoints(chart, mNetworkCycleChartData.getUsageBuckets());
setupContentDescription(chart, mNetworkCycleChartData.getUsageBuckets());
chart.setBottomLabels(new CharSequence[] { chart.setBottomLabels(new CharSequence[] {
Utils.formatDateRange(getContext(), mStart, mStart), Utils.formatDateRange(getContext(), mStart, mStart),
Utils.formatDateRange(getContext(), mEnd, mEnd), 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) { private int toInt(long l) {
// Don't need that much resolution on these times. // Don't need that much resolution on these times.
return (int) (l / (1000 * 60)); return (int) (l / (1000 * 60));
@@ -151,8 +284,8 @@ public class ChartDataUsagePreference extends Preference {
} }
private CharSequence getLabel(long bytes, int str, int mLimitColor) { private CharSequence getLabel(long bytes, int str, int mLimitColor) {
Formatter.BytesResult result = Formatter.formatBytes(getContext().getResources(), Formatter.BytesResult result = Formatter.formatBytes(mResources, bytes,
bytes, Formatter.FLAG_SHORTER | Formatter.FLAG_IEC_UNITS); Formatter.FLAG_SHORTER | Formatter.FLAG_IEC_UNITS);
CharSequence label = TextUtils.expandTemplate(getContext().getText(str), CharSequence label = TextUtils.expandTemplate(getContext().getText(str),
result.value, result.units); result.value, result.units);
return new SpannableStringBuilder().append(label, new ForegroundColorSpan(mLimitColor), 0); 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 com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import android.content.Context; import android.app.Activity;
import android.util.SparseIntArray; 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.settings.widget.UsageView;
import com.android.settingslib.net.NetworkCycleChartData; import com.android.settingslib.net.NetworkCycleChartData;
import com.android.settingslib.net.NetworkCycleData; import com.android.settingslib.net.NetworkCycleData;
@@ -32,8 +40,8 @@ import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor; import org.mockito.ArgumentCaptor;
import org.mockito.MockitoAnnotations; import org.mockito.MockitoAnnotations;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricTestRunner; import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -49,15 +57,20 @@ public class ChartDataUsagePreferenceTest {
private List<NetworkCycleData> mNetworkCycleData; private List<NetworkCycleData> mNetworkCycleData;
private NetworkCycleChartData mNetworkCycleChartData; private NetworkCycleChartData mNetworkCycleChartData;
private Context mContext;
private ChartDataUsagePreference mPreference; private ChartDataUsagePreference mPreference;
private Activity mActivity;
private PreferenceViewHolder mHolder;
@Before @Before
public void setUp() { public void setUp() {
MockitoAnnotations.initMocks(this); MockitoAnnotations.initMocks(this);
mContext = RuntimeEnvironment.application; mActivity = spy(Robolectric.setupActivity(Activity.class));
mPreference = new ChartDataUsagePreference(mContext, null); 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 @Test
@@ -148,6 +161,40 @@ public class ChartDataUsagePreferenceTest {
assertThat(points.keyAt(6)).isEqualTo(TimeUnit.DAYS.toMinutes(5)); 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() { private void createTestNetworkData() {
mNetworkCycleData = new ArrayList<>(); mNetworkCycleData = new ArrayList<>();
// create 10 arbitrary network data // create 10 arbitrary network data
@@ -169,6 +216,18 @@ public class ChartDataUsagePreferenceTest {
mNetworkCycleChartData = builder.build(); 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) { private NetworkCycleData createNetworkCycleData(long start, long end, long usage) {
return new NetworkCycleData.Builder() return new NetworkCycleData.Builder()
.setStartTime(start).setEndTime(end).setTotalUsage(usage).build(); .setStartTime(start).setEndTime(end).setTotalUsage(usage).build();