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.net.TrafficStats; 21 import android.text.SpannableStringBuilder; 22 import android.text.TextUtils; 23 import android.text.format.DateUtils; 24 import android.text.format.Formatter; 25 import android.text.style.ForegroundColorSpan; 26 import android.util.AttributeSet; 27 import android.util.SparseIntArray; 28 29 import androidx.annotation.VisibleForTesting; 30 import androidx.preference.Preference; 31 import androidx.preference.PreferenceViewHolder; 32 33 import com.android.settings.R; 34 import com.android.settings.Utils; 35 import com.android.settings.widget.UsageView; 36 import com.android.settingslib.net.NetworkCycleChartData; 37 import com.android.settingslib.net.NetworkCycleData; 38 39 import java.util.ArrayList; 40 import java.util.Comparator; 41 import java.util.List; 42 import java.util.Map; 43 import java.util.stream.Collectors; 44 45 public class ChartDataUsagePreference extends Preference { 46 47 // The resolution we show on the graph so that we can squash things down to ints. 48 // Set to half a meg for now. 49 private static final long RESOLUTION = TrafficStats.MB_IN_BYTES / 2; 50 51 private final int mWarningColor; 52 private final int mLimitColor; 53 54 private Resources mResources; 55 private NetworkPolicy mPolicy; 56 private long mStart; 57 private long mEnd; 58 private NetworkCycleChartData mNetworkCycleChartData; 59 private int mSecondaryColor; 60 private int mSeriesColor; 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(PreferenceViewHolder holder)72 public void onBindViewHolder(PreferenceViewHolder holder) { 73 super.onBindViewHolder(holder); 74 final UsageView chart = (UsageView) holder.findViewById(R.id.data_usage); 75 if (mNetworkCycleChartData == null) { 76 return; 77 } 78 79 final int top = getTop(); 80 chart.clearPaths(); 81 chart.configureGraph(toInt(mEnd - mStart), top); 82 calcPoints(chart, mNetworkCycleChartData.getUsageBuckets()); 83 setupContentDescription(chart, mNetworkCycleChartData.getUsageBuckets()); 84 chart.setBottomLabels(new CharSequence[] { 85 Utils.formatDateRange(getContext(), mStart, mStart), 86 Utils.formatDateRange(getContext(), mEnd, mEnd), 87 }); 88 89 bindNetworkPolicy(chart, mPolicy, top); 90 } 91 getTop()92 public int getTop() { 93 final long totalData = mNetworkCycleChartData.getTotalUsage(); 94 final long policyMax = 95 mPolicy != null ? Math.max(mPolicy.limitBytes, mPolicy.warningBytes) : 0; 96 return (int) (Math.max(totalData, policyMax) / RESOLUTION); 97 } 98 99 @VisibleForTesting calcPoints(UsageView chart, List<NetworkCycleData> usageSummary)100 void calcPoints(UsageView chart, List<NetworkCycleData> usageSummary) { 101 if (usageSummary == null) { 102 return; 103 } 104 final SparseIntArray points = new SparseIntArray(); 105 points.put(0, 0); 106 107 final long now = System.currentTimeMillis(); 108 long totalData = 0; 109 for (NetworkCycleData data : usageSummary) { 110 final long startTime = data.getStartTime(); 111 if (startTime > now) { 112 break; 113 } 114 final long endTime = data.getEndTime(); 115 116 // increment by current bucket total 117 totalData += data.getTotalUsage(); 118 119 if (points.size() == 1) { 120 points.put(toInt(startTime - mStart) - 1, -1); 121 } 122 points.put(toInt(startTime - mStart + 1), (int) (totalData / RESOLUTION)); 123 points.put(toInt(endTime - mStart), (int) (totalData / RESOLUTION)); 124 } 125 if (points.size() > 1) { 126 chart.addPath(points); 127 } 128 } 129 setupContentDescription(UsageView chart, List<NetworkCycleData> usageSummary)130 private void setupContentDescription(UsageView chart, List<NetworkCycleData> usageSummary) { 131 final Context context = getContext(); 132 final StringBuilder contentDescription = new StringBuilder(); 133 final int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_ABBREV_MONTH; 134 135 // Setup a brief content description. 136 final String startDate = DateUtils.formatDateTime(context, mStart, flags); 137 final String endDate = DateUtils.formatDateTime(context, mEnd, flags); 138 final String briefContentDescription = mResources 139 .getString(R.string.data_usage_chart_brief_content_description, startDate, endDate); 140 contentDescription.append(briefContentDescription); 141 142 if (usageSummary == null || usageSummary.isEmpty()) { 143 final String noDataContentDescription = mResources 144 .getString(R.string.data_usage_chart_no_data_content_description); 145 contentDescription.append(noDataContentDescription); 146 chart.setContentDescription(contentDescription); 147 return; 148 } 149 150 // Append more detailed stats. 151 String nodeDate; 152 String nodeContentDescription; 153 final List<DataUsageSummaryNode> densedStatsData = getDensedStatsData(usageSummary); 154 for (DataUsageSummaryNode data : densedStatsData) { 155 final int dataUsagePercentage = data.getDataUsagePercentage(); 156 if (!data.isFromMultiNode() || dataUsagePercentage == 100) { 157 nodeDate = DateUtils.formatDateTime(context, data.getStartTime(), flags); 158 } else { 159 nodeDate = DateUtils.formatDateRange(context, data.getStartTime(), 160 data.getEndTime(), flags); 161 } 162 nodeContentDescription = String.format(";%s %d%%", nodeDate, dataUsagePercentage); 163 164 contentDescription.append(nodeContentDescription); 165 } 166 167 chart.setContentDescription(contentDescription); 168 } 169 170 /** 171 * To avoid wordy data, e.g., Aug 2: 0%; Aug 3: 0%;...Aug 22: 0%; Aug 23: 2%. 172 * Collect the date of the same percentage, e.g., Aug 2 to Aug 22: 0%; Aug 23: 2%. 173 */ 174 @VisibleForTesting getDensedStatsData(List<NetworkCycleData> usageSummary)175 List<DataUsageSummaryNode> getDensedStatsData(List<NetworkCycleData> usageSummary) { 176 final List<DataUsageSummaryNode> dataUsageSummaryNodes = new ArrayList<>(); 177 final long overallDataUsage = Math.max(1L, usageSummary.stream() 178 .mapToLong(NetworkCycleData::getTotalUsage).sum()); 179 long cumulatedDataUsage = 0L; 180 int cumulatedDataUsagePercentage = 0; 181 182 // Collect List of DataUsageSummaryNode for data usage percentage information. 183 for (NetworkCycleData data : usageSummary) { 184 cumulatedDataUsage += data.getTotalUsage(); 185 cumulatedDataUsagePercentage = (int) ((cumulatedDataUsage * 100) / overallDataUsage); 186 187 final DataUsageSummaryNode node = new DataUsageSummaryNode(data.getStartTime(), 188 data.getEndTime(), cumulatedDataUsagePercentage); 189 dataUsageSummaryNodes.add(node); 190 } 191 192 // Group nodes of the same data usage percentage. 193 final Map<Integer, List<DataUsageSummaryNode>> nodesByDataUsagePercentage 194 = dataUsageSummaryNodes.stream().collect( 195 Collectors.groupingBy(DataUsageSummaryNode::getDataUsagePercentage)); 196 197 // Collect densed nodes from collection of the same data usage percentage 198 final List<DataUsageSummaryNode> densedNodes = new ArrayList<>(); 199 nodesByDataUsagePercentage.forEach((percentage, nodes) -> { 200 final long startTime = nodes.stream().mapToLong(DataUsageSummaryNode::getStartTime) 201 .min().getAsLong(); 202 final long endTime = nodes.stream().mapToLong(DataUsageSummaryNode::getEndTime) 203 .max().getAsLong(); 204 205 final DataUsageSummaryNode densedNode = new DataUsageSummaryNode( 206 startTime, endTime, percentage); 207 if (nodes.size() > 1) { 208 densedNode.setFromMultiNode(true /* isFromMultiNode */); 209 } 210 211 densedNodes.add(densedNode); 212 }); 213 214 return densedNodes.stream() 215 .sorted(Comparator.comparingInt(DataUsageSummaryNode::getDataUsagePercentage)) 216 .collect(Collectors.toList()); 217 } 218 219 @VisibleForTesting 220 class DataUsageSummaryNode { 221 private long mStartTime; 222 private long mEndTime; 223 private int mDataUsagePercentage; 224 private boolean mIsFromMultiNode; 225 DataUsageSummaryNode(long startTime, long endTime, int dataUsagePercentage)226 public DataUsageSummaryNode(long startTime, long endTime, int dataUsagePercentage) { 227 mStartTime = startTime; 228 mEndTime = endTime; 229 mDataUsagePercentage = dataUsagePercentage; 230 mIsFromMultiNode = false; 231 } 232 getStartTime()233 public long getStartTime() { 234 return mStartTime; 235 } 236 getEndTime()237 public long getEndTime() { 238 return mEndTime; 239 } 240 getDataUsagePercentage()241 public int getDataUsagePercentage() { 242 return mDataUsagePercentage; 243 } 244 setFromMultiNode(boolean isFromMultiNode)245 public void setFromMultiNode(boolean isFromMultiNode) { 246 mIsFromMultiNode = isFromMultiNode; 247 } 248 isFromMultiNode()249 public boolean isFromMultiNode() { 250 return mIsFromMultiNode; 251 } 252 } 253 toInt(long l)254 private int toInt(long l) { 255 // Don't need that much resolution on these times. 256 return (int) (l / (1000 * 60)); 257 } 258 bindNetworkPolicy(UsageView chart, NetworkPolicy policy, int top)259 private void bindNetworkPolicy(UsageView chart, NetworkPolicy policy, int top) { 260 CharSequence[] labels = new CharSequence[3]; 261 int middleVisibility = 0; 262 int topVisibility = 0; 263 if (policy == null) { 264 return; 265 } 266 267 if (policy.limitBytes != NetworkPolicy.LIMIT_DISABLED) { 268 topVisibility = mLimitColor; 269 labels[2] = getLabel(policy.limitBytes, R.string.data_usage_sweep_limit, mLimitColor); 270 } 271 272 if (policy.warningBytes != NetworkPolicy.WARNING_DISABLED) { 273 chart.setDividerLoc((int) (policy.warningBytes / RESOLUTION)); 274 float weight = policy.warningBytes / RESOLUTION / (float) top; 275 float above = 1 - weight; 276 chart.setSideLabelWeights(above, weight); 277 middleVisibility = mWarningColor; 278 labels[1] = getLabel(policy.warningBytes, R.string.data_usage_sweep_warning, 279 mWarningColor); 280 } 281 282 chart.setSideLabels(labels); 283 chart.setDividerColors(middleVisibility, topVisibility); 284 } 285 getLabel(long bytes, int str, int mLimitColor)286 private CharSequence getLabel(long bytes, int str, int mLimitColor) { 287 Formatter.BytesResult result = Formatter.formatBytes(mResources, bytes, 288 Formatter.FLAG_SHORTER | Formatter.FLAG_IEC_UNITS); 289 CharSequence label = TextUtils.expandTemplate(getContext().getText(str), 290 result.value, result.units); 291 return new SpannableStringBuilder().append(label, new ForegroundColorSpan(mLimitColor), 0); 292 } 293 setNetworkPolicy(NetworkPolicy policy)294 public void setNetworkPolicy(NetworkPolicy policy) { 295 mPolicy = policy; 296 notifyChanged(); 297 } 298 getInspectStart()299 public long getInspectStart() { 300 return mStart; 301 } 302 getInspectEnd()303 public long getInspectEnd() { 304 return mEnd; 305 } 306 setNetworkCycleData(NetworkCycleChartData data)307 public void setNetworkCycleData(NetworkCycleChartData data) { 308 mNetworkCycleChartData = data; 309 mStart = data.getStartTime(); 310 mEnd = data.getEndTime(); 311 notifyChanged(); 312 } 313 setColors(int seriesColor, int secondaryColor)314 public void setColors(int seriesColor, int secondaryColor) { 315 mSeriesColor = seriesColor; 316 mSecondaryColor = secondaryColor; 317 notifyChanged(); 318 } 319 } 320