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