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