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();