1 /* 2 * Copyright (C) 2011 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.settings.widget; 18 19 import static android.text.format.DateUtils.DAY_IN_MILLIS; 20 import static android.text.format.DateUtils.WEEK_IN_MILLIS; 21 22 import android.content.Context; 23 import android.content.res.TypedArray; 24 import android.graphics.Canvas; 25 import android.graphics.Color; 26 import android.graphics.DashPathEffect; 27 import android.graphics.Paint; 28 import android.graphics.Paint.Style; 29 import android.graphics.Path; 30 import android.graphics.RectF; 31 import android.net.NetworkStatsHistory; 32 import android.util.AttributeSet; 33 import android.util.Log; 34 import android.view.View; 35 36 import com.android.settings.R; 37 import com.google.common.base.Preconditions; 38 39 /** 40 * {@link NetworkStatsHistory} series to render inside a {@link ChartView}, 41 * using {@link ChartAxis} to map into screen coordinates. 42 */ 43 public class ChartNetworkSeriesView extends View { 44 private static final String TAG = "ChartNetworkSeriesView"; 45 private static final boolean LOGD = false; 46 47 private static final boolean ESTIMATE_ENABLED = false; 48 49 private ChartAxis mHoriz; 50 private ChartAxis mVert; 51 52 private Paint mPaintStroke; 53 private Paint mPaintFill; 54 private Paint mPaintFillSecondary; 55 private Paint mPaintEstimate; 56 57 private NetworkStatsHistory mStats; 58 59 private Path mPathStroke; 60 private Path mPathFill; 61 private Path mPathEstimate; 62 63 private long mStart; 64 private long mEnd; 65 66 private long mPrimaryLeft; 67 private long mPrimaryRight; 68 69 /** Series will be extended to reach this end time. */ 70 private long mEndTime = Long.MIN_VALUE; 71 72 private boolean mPathValid = false; 73 private boolean mEstimateVisible = false; 74 75 private long mMax; 76 private long mMaxEstimate; 77 78 public ChartNetworkSeriesView(Context context) { 79 this(context, null, 0); 80 } 81 82 public ChartNetworkSeriesView(Context context, AttributeSet attrs) { 83 this(context, attrs, 0); 84 } 85 86 public ChartNetworkSeriesView(Context context, AttributeSet attrs, int defStyle) { 87 super(context, attrs, defStyle); 88 89 final TypedArray a = context.obtainStyledAttributes( 90 attrs, R.styleable.ChartNetworkSeriesView, defStyle, 0); 91 92 final int stroke = a.getColor(R.styleable.ChartNetworkSeriesView_strokeColor, Color.RED); 93 final int fill = a.getColor(R.styleable.ChartNetworkSeriesView_fillColor, Color.RED); 94 final int fillSecondary = a.getColor( 95 R.styleable.ChartNetworkSeriesView_fillColorSecondary, Color.RED); 96 97 setChartColor(stroke, fill, fillSecondary); 98 setWillNotDraw(false); 99 100 a.recycle(); 101 102 mPathStroke = new Path(); 103 mPathFill = new Path(); 104 mPathEstimate = new Path(); 105 } 106 107 void init(ChartAxis horiz, ChartAxis vert) { 108 mHoriz = Preconditions.checkNotNull(horiz, "missing horiz"); 109 mVert = Preconditions.checkNotNull(vert, "missing vert"); 110 } 111 112 public void setChartColor(int stroke, int fill, int fillSecondary) { 113 mPaintStroke = new Paint(); 114 mPaintStroke.setStrokeWidth(4.0f * getResources().getDisplayMetrics().density); 115 mPaintStroke.setColor(stroke); 116 mPaintStroke.setStyle(Style.STROKE); 117 mPaintStroke.setAntiAlias(true); 118 119 mPaintFill = new Paint(); 120 mPaintFill.setColor(fill); 121 mPaintFill.setStyle(Style.FILL); 122 mPaintFill.setAntiAlias(true); 123 124 mPaintFillSecondary = new Paint(); 125 mPaintFillSecondary.setColor(fillSecondary); 126 mPaintFillSecondary.setStyle(Style.FILL); 127 mPaintFillSecondary.setAntiAlias(true); 128 129 mPaintEstimate = new Paint(); 130 mPaintEstimate.setStrokeWidth(3.0f); 131 mPaintEstimate.setColor(fillSecondary); 132 mPaintEstimate.setStyle(Style.STROKE); 133 mPaintEstimate.setAntiAlias(true); 134 mPaintEstimate.setPathEffect(new DashPathEffect(new float[] { 10, 10 }, 1)); 135 } 136 137 public void bindNetworkStats(NetworkStatsHistory stats) { 138 mStats = stats; 139 invalidatePath(); 140 invalidate(); 141 } 142 143 public void setBounds(long start, long end) { 144 mStart = start; 145 mEnd = end; 146 } 147 148 /** 149 * Set the range to paint with {@link #mPaintFill}, leaving the remaining 150 * area to be painted with {@link #mPaintFillSecondary}. 151 */ 152 public void setPrimaryRange(long left, long right) { 153 mPrimaryLeft = left; 154 mPrimaryRight = right; 155 invalidate(); 156 } 157 158 public void invalidatePath() { 159 mPathValid = false; 160 mMax = 0; 161 invalidate(); 162 } 163 164 /** 165 * Erase any existing {@link Path} and generate series outline based on 166 * currently bound {@link NetworkStatsHistory} data. 167 */ 168 private void generatePath() { 169 if (LOGD) Log.d(TAG, "generatePath()"); 170 171 mMax = 0; 172 mPathStroke.reset(); 173 mPathFill.reset(); 174 mPathEstimate.reset(); 175 mPathValid = true; 176 177 // bail when not enough stats to render 178 if (mStats == null || mStats.size() < 2) { 179 return; 180 } 181 182 final int width = getWidth(); 183 final int height = getHeight(); 184 185 boolean started = false; 186 float lastX = 0; 187 float lastY = height; 188 long lastTime = mHoriz.convertToValue(lastX); 189 190 // move into starting position 191 mPathStroke.moveTo(lastX, lastY); 192 mPathFill.moveTo(lastX, lastY); 193 194 // TODO: count fractional data from first bucket crossing start; 195 // currently it only accepts first full bucket. 196 197 long totalData = 0; 198 199 NetworkStatsHistory.Entry entry = null; 200 201 final int start = mStats.getIndexBefore(mStart); 202 final int end = mStats.getIndexAfter(mEnd); 203 for (int i = start; i <= end; i++) { 204 entry = mStats.getValues(i, entry); 205 206 final long startTime = entry.bucketStart; 207 final long endTime = startTime + entry.bucketDuration; 208 209 final float startX = mHoriz.convertToPoint(startTime); 210 final float endX = mHoriz.convertToPoint(endTime); 211 212 // skip until we find first stats on screen 213 if (endX < 0) continue; 214 215 // increment by current bucket total 216 totalData += entry.rxBytes + entry.txBytes; 217 218 final float startY = lastY; 219 final float endY = mVert.convertToPoint(totalData); 220 221 if (lastTime != startTime) { 222 // gap in buckets; line to start of current bucket 223 mPathStroke.lineTo(startX, startY); 224 mPathFill.lineTo(startX, startY); 225 } 226 227 // always draw to end of current bucket 228 mPathStroke.lineTo(endX, endY); 229 mPathFill.lineTo(endX, endY); 230 231 lastX = endX; 232 lastY = endY; 233 lastTime = endTime; 234 } 235 236 // when data falls short, extend to requested end time 237 if (lastTime < mEndTime) { 238 lastX = mHoriz.convertToPoint(mEndTime); 239 240 mPathStroke.lineTo(lastX, lastY); 241 mPathFill.lineTo(lastX, lastY); 242 } 243 244 if (LOGD) { 245 final RectF bounds = new RectF(); 246 mPathFill.computeBounds(bounds, true); 247 Log.d(TAG, "onLayout() rendered with bounds=" + bounds.toString() + " and totalData=" 248 + totalData); 249 } 250 251 // drop to bottom of graph from current location 252 mPathFill.lineTo(lastX, height); 253 mPathFill.lineTo(0, height); 254 255 mMax = totalData; 256 257 if (ESTIMATE_ENABLED) { 258 // build estimated data 259 mPathEstimate.moveTo(lastX, lastY); 260 261 final long now = System.currentTimeMillis(); 262 final long bucketDuration = mStats.getBucketDuration(); 263 264 // long window is average over two weeks 265 entry = mStats.getValues(lastTime - WEEK_IN_MILLIS * 2, lastTime, now, entry); 266 final long longWindow = (entry.rxBytes + entry.txBytes) * bucketDuration 267 / entry.bucketDuration; 268 269 long futureTime = 0; 270 while (lastX < width) { 271 futureTime += bucketDuration; 272 273 // short window is day average last week 274 final long lastWeekTime = lastTime - WEEK_IN_MILLIS + (futureTime % WEEK_IN_MILLIS); 275 entry = mStats.getValues(lastWeekTime - DAY_IN_MILLIS, lastWeekTime, now, entry); 276 final long shortWindow = (entry.rxBytes + entry.txBytes) * bucketDuration 277 / entry.bucketDuration; 278 279 totalData += (longWindow * 7 + shortWindow * 3) / 10; 280 281 lastX = mHoriz.convertToPoint(lastTime + futureTime); 282 lastY = mVert.convertToPoint(totalData); 283 284 mPathEstimate.lineTo(lastX, lastY); 285 } 286 287 mMaxEstimate = totalData; 288 } 289 290 invalidate(); 291 } 292 293 public void setEndTime(long endTime) { 294 mEndTime = endTime; 295 } 296 297 public void setEstimateVisible(boolean estimateVisible) { 298 mEstimateVisible = ESTIMATE_ENABLED ? estimateVisible : false; 299 invalidate(); 300 } 301 302 public long getMaxEstimate() { 303 return mMaxEstimate; 304 } 305 306 public long getMaxVisible() { 307 final long maxVisible = mEstimateVisible ? mMaxEstimate : mMax; 308 if (maxVisible <= 0 && mStats != null) { 309 // haven't generated path yet; fall back to raw data 310 final NetworkStatsHistory.Entry entry = mStats.getValues(mStart, mEnd, null); 311 return entry.rxBytes + entry.txBytes; 312 } else { 313 return maxVisible; 314 } 315 } 316 317 @Override 318 protected void onDraw(Canvas canvas) { 319 int save; 320 321 if (!mPathValid) { 322 generatePath(); 323 } 324 325 final float primaryLeftPoint = mHoriz.convertToPoint(mPrimaryLeft); 326 final float primaryRightPoint = mHoriz.convertToPoint(mPrimaryRight); 327 328 if (mEstimateVisible) { 329 save = canvas.save(); 330 canvas.clipRect(0, 0, getWidth(), getHeight()); 331 canvas.drawPath(mPathEstimate, mPaintEstimate); 332 canvas.restoreToCount(save); 333 } 334 335 save = canvas.save(); 336 canvas.clipRect(0, 0, primaryLeftPoint, getHeight()); 337 canvas.drawPath(mPathFill, mPaintFillSecondary); 338 canvas.restoreToCount(save); 339 340 save = canvas.save(); 341 canvas.clipRect(primaryRightPoint, 0, getWidth(), getHeight()); 342 canvas.drawPath(mPathFill, mPaintFillSecondary); 343 canvas.restoreToCount(save); 344 345 save = canvas.save(); 346 canvas.clipRect(primaryLeftPoint, 0, primaryRightPoint, getHeight()); 347 canvas.drawPath(mPathFill, mPaintFill); 348 canvas.drawPath(mPathStroke, mPaintStroke); 349 canvas.restoreToCount(save); 350 351 } 352 } 353