Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 351ee3a8 authored by Arc Wang's avatar Arc Wang
Browse files

[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
parent ab1277f6
Loading
Loading
Loading
Loading
+10 −5
Original line number Diff line number Diff line
@@ -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>

+6 −0
Original line number Diff line number Diff line
@@ -9860,6 +9860,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>
+135 −2
Original line number Diff line number Diff line
@@ -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);
+64 −5
Original line number Diff line number Diff line
@@ -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();