1 /*
2  * Copyright (C) 2016 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
5  * except in compliance with the License. You may obtain a copy of the License at
6  *
7  *      http://www.apache.org/licenses/LICENSE-2.0
8  *
9  * Unless required by applicable law or agreed to in writing, software distributed under the
10  * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
11  * KIND, either express or implied. See the License for the specific language governing
12  * permissions and limitations under the License.
13  */
14 
15 package com.android.settings.datausage;
16 
17 import android.content.Context;
18 import android.content.res.Resources;
19 import android.net.NetworkPolicy;
20 import android.text.SpannableStringBuilder;
21 import android.text.TextUtils;
22 import android.text.format.DateUtils;
23 import android.text.format.Formatter;
24 import android.text.style.ForegroundColorSpan;
25 import android.util.AttributeSet;
26 import android.util.DataUnit;
27 import android.util.SparseIntArray;
28 
29 import androidx.annotation.NonNull;
30 import androidx.annotation.Nullable;
31 import androidx.annotation.VisibleForTesting;
32 import androidx.preference.Preference;
33 import androidx.preference.PreferenceViewHolder;
34 
35 import com.android.settings.R;
36 import com.android.settings.Utils;
37 import com.android.settings.datausage.lib.NetworkCycleChartData;
38 import com.android.settings.datausage.lib.NetworkUsageData;
39 import com.android.settings.widget.UsageView;
40 
41 import java.util.ArrayList;
42 import java.util.Comparator;
43 import java.util.List;
44 import java.util.Map;
45 import java.util.stream.Collectors;
46 
47 public class ChartDataUsagePreference extends Preference {
48 
49     // The resolution we show on the graph so that we can squash things down to ints.
50     // Set to half a meg for now.
51     private static final long RESOLUTION = DataUnit.MEBIBYTES.toBytes(1) / 2;
52 
53     private final int mWarningColor;
54     private final int mLimitColor;
55 
56     private final Resources mResources;
57     @Nullable private NetworkPolicy mPolicy;
58     private long mStart;
59     private long mEnd;
60     private NetworkCycleChartData mNetworkCycleChartData;
61 
ChartDataUsagePreference(Context context, AttributeSet attrs)62     public ChartDataUsagePreference(Context context, AttributeSet attrs) {
63         super(context, attrs);
64         mResources = context.getResources();
65         setSelectable(false);
66         mLimitColor = Utils.getColorAttrDefaultColor(context, android.R.attr.colorError);
67         mWarningColor = Utils.getColorAttrDefaultColor(context, android.R.attr.textColorSecondary);
68         setLayoutResource(R.layout.data_usage_graph);
69     }
70 
71     @Override
onBindViewHolder(@onNull PreferenceViewHolder holder)72     public void onBindViewHolder(@NonNull PreferenceViewHolder holder) {
73         super.onBindViewHolder(holder);
74         final UsageView chart = holder.itemView.requireViewById(R.id.data_usage);
75         final int top = getTop();
76         chart.clearPaths();
77         chart.configureGraph(toInt(mEnd - mStart), top);
78         if (mNetworkCycleChartData != null) {
79             calcPoints(chart, mNetworkCycleChartData.getDailyUsage());
80             setupContentDescription(chart, mNetworkCycleChartData.getDailyUsage());
81         }
82         chart.setBottomLabels(new CharSequence[] {
83                 Utils.formatDateRange(getContext(), mStart, mStart),
84                 Utils.formatDateRange(getContext(), mEnd, mEnd),
85         });
86 
87         bindNetworkPolicy(chart, mPolicy, top);
88     }
89 
getTop()90     public int getTop() {
91         final long totalData =
92                 mNetworkCycleChartData != null ? mNetworkCycleChartData.getTotal().getUsage() : 0;
93         final long policyMax =
94             mPolicy != null ? Math.max(mPolicy.limitBytes, mPolicy.warningBytes) : 0;
95         return (int) (Math.max(totalData, policyMax) / RESOLUTION);
96     }
97 
98     @VisibleForTesting
calcPoints(UsageView chart, @NonNull List<NetworkUsageData> usageSummary)99     void calcPoints(UsageView chart, @NonNull List<NetworkUsageData> usageSummary) {
100         final SparseIntArray points = new SparseIntArray();
101         points.put(0, 0);
102 
103         final long now = System.currentTimeMillis();
104         long totalData = 0;
105         for (NetworkUsageData data : usageSummary) {
106             final long startTime = data.getStartTime();
107             if (startTime > now) {
108                 break;
109             }
110             final long endTime = data.getEndTime();
111 
112             // increment by current bucket total
113             totalData += data.getUsage();
114 
115             points.put(toInt(startTime - mStart + 1), (int) (totalData / RESOLUTION));
116             points.put(toInt(endTime - mStart), (int) (totalData / RESOLUTION));
117         }
118         if (points.size() > 1) {
119             chart.addPath(points);
120         }
121     }
122 
setupContentDescription( UsageView chart, @NonNull List<NetworkUsageData> usageSummary)123     private void setupContentDescription(
124             UsageView chart, @NonNull List<NetworkUsageData> usageSummary) {
125         final Context context = getContext();
126         final StringBuilder contentDescription = new StringBuilder();
127         final int flags = DateUtils.FORMAT_SHOW_DATE;
128 
129         // Setup a brief content description.
130         final String startDate = DateUtils.formatDateTime(context, mStart, flags);
131         final String endDate = DateUtils.formatDateTime(context, mEnd, flags);
132         final String briefContentDescription = mResources
133                 .getString(R.string.data_usage_chart_brief_content_description, startDate, endDate);
134         contentDescription.append(briefContentDescription);
135 
136         if (usageSummary.isEmpty()) {
137             final String noDataContentDescription = mResources
138                     .getString(R.string.data_usage_chart_no_data_content_description);
139             contentDescription.append(noDataContentDescription);
140             chart.setContentDescription(contentDescription);
141             return;
142         }
143 
144         // Append more detailed stats.
145         String nodeDate;
146         String nodeContentDescription;
147         final List<DataUsageSummaryNode> densedStatsData = getDensedStatsData(usageSummary);
148         for (DataUsageSummaryNode data : densedStatsData) {
149             final int dataUsagePercentage = data.getDataUsagePercentage();
150             if (!data.isFromMultiNode() || dataUsagePercentage == 100) {
151                 nodeDate = DateUtils.formatDateTime(context, data.getStartTime(), flags);
152             } else {
153                 nodeDate = DateUtils.formatDateRange(context, data.getStartTime(),
154                         data.getEndTime(), flags);
155             }
156             nodeContentDescription = String.format("; %s, %d%%", nodeDate, dataUsagePercentage);
157 
158             contentDescription.append(nodeContentDescription);
159         }
160 
161         chart.setContentDescription(contentDescription);
162     }
163 
164     /**
165      * To avoid wordy data, e.g., Aug 2: 0%; Aug 3: 0%;...Aug 22: 0%; Aug 23: 2%.
166      * Collect the date of the same percentage, e.g., Aug 2 to Aug 22: 0%; Aug 23: 2%.
167      */
168     @VisibleForTesting
getDensedStatsData(@onNull List<NetworkUsageData> usageSummary)169     List<DataUsageSummaryNode> getDensedStatsData(@NonNull List<NetworkUsageData> usageSummary) {
170         final List<DataUsageSummaryNode> dataUsageSummaryNodes = new ArrayList<>();
171         final long overallDataUsage = Math.max(1L, usageSummary.stream()
172                 .mapToLong(NetworkUsageData::getUsage).sum());
173         long cumulatedDataUsage = 0L;
174 
175         // Collect List of DataUsageSummaryNode for data usage percentage information.
176         for (NetworkUsageData data : usageSummary) {
177             cumulatedDataUsage += data.getUsage();
178             int cumulatedDataUsagePercentage =
179                     (int) ((cumulatedDataUsage * 100) / overallDataUsage);
180 
181             final DataUsageSummaryNode node = new DataUsageSummaryNode(data.getStartTime(),
182                     data.getEndTime(), cumulatedDataUsagePercentage);
183             dataUsageSummaryNodes.add(node);
184         }
185 
186         // Group nodes of the same data usage percentage.
187         final Map<Integer, List<DataUsageSummaryNode>> nodesByDataUsagePercentage
188                 = dataUsageSummaryNodes.stream().collect(
189                         Collectors.groupingBy(DataUsageSummaryNode::getDataUsagePercentage));
190 
191         // Collect densed nodes from collection of the same  data usage percentage
192         final List<DataUsageSummaryNode> densedNodes = new ArrayList<>();
193         nodesByDataUsagePercentage.forEach((percentage, nodes) -> {
194             final long startTime = nodes.stream().mapToLong(DataUsageSummaryNode::getStartTime)
195                     .min().getAsLong();
196             final long endTime = nodes.stream().mapToLong(DataUsageSummaryNode::getEndTime)
197                     .max().getAsLong();
198 
199             final DataUsageSummaryNode densedNode = new DataUsageSummaryNode(
200                     startTime, endTime, percentage);
201             if (nodes.size() > 1) {
202                 densedNode.setFromMultiNode(true /* isFromMultiNode */);
203             }
204 
205             densedNodes.add(densedNode);
206         });
207 
208         return densedNodes.stream()
209                 .sorted(Comparator.comparingInt(DataUsageSummaryNode::getDataUsagePercentage))
210                 .collect(Collectors.toList());
211     }
212 
213     @VisibleForTesting
214     class DataUsageSummaryNode {
215         private long mStartTime;
216         private long mEndTime;
217         private int mDataUsagePercentage;
218         private boolean mIsFromMultiNode;
219 
DataUsageSummaryNode(long startTime, long endTime, int dataUsagePercentage)220         public DataUsageSummaryNode(long startTime, long endTime, int dataUsagePercentage) {
221             mStartTime = startTime;
222             mEndTime = endTime;
223             mDataUsagePercentage = dataUsagePercentage;
224             mIsFromMultiNode = false;
225         }
226 
getStartTime()227         public long getStartTime() {
228             return mStartTime;
229         }
230 
getEndTime()231         public long getEndTime() {
232             return mEndTime;
233         }
234 
getDataUsagePercentage()235         public int getDataUsagePercentage() {
236             return mDataUsagePercentage;
237         }
238 
setFromMultiNode(boolean isFromMultiNode)239         public void setFromMultiNode(boolean isFromMultiNode) {
240             mIsFromMultiNode = isFromMultiNode;
241         }
242 
isFromMultiNode()243         public boolean isFromMultiNode() {
244             return mIsFromMultiNode;
245         }
246     }
247 
toInt(long l)248     private int toInt(long l) {
249         // Don't need that much resolution on these times.
250         return (int) (l / (1000 * 60));
251     }
252 
bindNetworkPolicy(UsageView chart, NetworkPolicy policy, int top)253     private void bindNetworkPolicy(UsageView chart, NetworkPolicy policy, int top) {
254         CharSequence[] labels = new CharSequence[3];
255         int middleVisibility = 0;
256         int topVisibility = 0;
257         if (policy == null) {
258             return;
259         }
260 
261         if (policy.limitBytes != NetworkPolicy.LIMIT_DISABLED) {
262             topVisibility = mLimitColor;
263             labels[2] = getLabel(policy.limitBytes, R.string.data_usage_sweep_limit, mLimitColor);
264         }
265 
266         if (policy.warningBytes != NetworkPolicy.WARNING_DISABLED) {
267             int dividerLoc = (int) (policy.warningBytes / RESOLUTION);
268             chart.setDividerLoc(dividerLoc);
269             float weight = dividerLoc / (float) top;
270             float above = 1 - weight;
271             chart.setSideLabelWeights(above, weight);
272             middleVisibility = mWarningColor;
273             labels[1] = getLabel(policy.warningBytes, R.string.data_usage_sweep_warning,
274                     mWarningColor);
275         }
276 
277         chart.setSideLabels(labels);
278         chart.setDividerColors(middleVisibility, topVisibility);
279     }
280 
getLabel(long bytes, int str, int mLimitColor)281     private CharSequence getLabel(long bytes, int str, int mLimitColor) {
282         Formatter.BytesResult result = Formatter.formatBytes(mResources, bytes,
283                 Formatter.FLAG_SHORTER | Formatter.FLAG_IEC_UNITS);
284         CharSequence label = TextUtils.expandTemplate(getContext().getText(str),
285                 result.value, result.units);
286         return new SpannableStringBuilder().append(label, new ForegroundColorSpan(mLimitColor), 0);
287     }
288 
289     /** Sets network policy. */
setNetworkPolicy(@ullable NetworkPolicy policy)290     public void setNetworkPolicy(@Nullable NetworkPolicy policy) {
291         mPolicy = policy;
292         notifyChanged();
293     }
294 
295     /** Sets time. */
setTime(long start, long end)296     public void setTime(long start, long end) {
297         mStart = start;
298         mEnd = end;
299         notifyChanged();
300     }
301 
setNetworkCycleData(NetworkCycleChartData data)302     public void setNetworkCycleData(NetworkCycleChartData data) {
303         mNetworkCycleChartData = data;
304         notifyChanged();
305     }
306 }
307