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