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 17package com.android.settings.widget; 18 19import static android.text.format.DateUtils.DAY_IN_MILLIS; 20import static android.text.format.DateUtils.WEEK_IN_MILLIS; 21 22import android.content.Context; 23import android.content.res.TypedArray; 24import android.graphics.Canvas; 25import android.graphics.Color; 26import android.graphics.DashPathEffect; 27import android.graphics.Paint; 28import android.graphics.Paint.Style; 29import android.graphics.Path; 30import android.graphics.RectF; 31import android.net.NetworkStatsHistory; 32import android.util.AttributeSet; 33import android.util.Log; 34import android.view.View; 35 36import com.android.internal.util.Preconditions; 37import com.android.settings.R; 38 39/** 40 * {@link NetworkStatsHistory} series to render inside a {@link ChartView}, 41 * using {@link ChartAxis} to map into screen coordinates. 42 */ 43public 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 int mSafeRegion; 64 65 private long mStart; 66 private long mEnd; 67 68 /** Series will be extended to reach this end time. */ 69 private long mEndTime = Long.MIN_VALUE; 70 71 private boolean mPathValid = false; 72 private boolean mEstimateVisible = false; 73 private boolean mSecondary = 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 final int safeRegion = a.getDimensionPixelSize( 97 R.styleable.ChartNetworkSeriesView_safeRegion, 0); 98 99 setChartColor(stroke, fill, fillSecondary); 100 setSafeRegion(safeRegion); 101 setWillNotDraw(false); 102 103 a.recycle(); 104 105 mPathStroke = new Path(); 106 mPathFill = new Path(); 107 mPathEstimate = new Path(); 108 } 109 110 void init(ChartAxis horiz, ChartAxis vert) { 111 mHoriz = Preconditions.checkNotNull(horiz, "missing horiz"); 112 mVert = Preconditions.checkNotNull(vert, "missing vert"); 113 } 114 115 public void setChartColor(int stroke, int fill, int fillSecondary) { 116 mPaintStroke = new Paint(); 117 mPaintStroke.setStrokeWidth(4.0f * getResources().getDisplayMetrics().density); 118 mPaintStroke.setColor(stroke); 119 mPaintStroke.setStyle(Style.STROKE); 120 mPaintStroke.setAntiAlias(true); 121 122 mPaintFill = new Paint(); 123 mPaintFill.setColor(fill); 124 mPaintFill.setStyle(Style.FILL); 125 mPaintFill.setAntiAlias(true); 126 127 mPaintFillSecondary = new Paint(); 128 mPaintFillSecondary.setColor(fillSecondary); 129 mPaintFillSecondary.setStyle(Style.FILL); 130 mPaintFillSecondary.setAntiAlias(true); 131 132 mPaintEstimate = new Paint(); 133 mPaintEstimate.setStrokeWidth(3.0f); 134 mPaintEstimate.setColor(fillSecondary); 135 mPaintEstimate.setStyle(Style.STROKE); 136 mPaintEstimate.setAntiAlias(true); 137 mPaintEstimate.setPathEffect(new DashPathEffect(new float[] { 10, 10 }, 1)); 138 } 139 140 public void setSafeRegion(int safeRegion) { 141 mSafeRegion = safeRegion; 142 } 143 144 public void bindNetworkStats(NetworkStatsHistory stats) { 145 mStats = stats; 146 invalidatePath(); 147 invalidate(); 148 } 149 150 public void setBounds(long start, long end) { 151 mStart = start; 152 mEnd = end; 153 } 154 155 public void setSecondary(boolean secondary) { 156 mSecondary = secondary; 157 } 158 159 public void invalidatePath() { 160 mPathValid = false; 161 mMax = 0; 162 invalidate(); 163 } 164 165 /** 166 * Erase any existing {@link Path} and generate series outline based on 167 * currently bound {@link NetworkStatsHistory} data. 168 */ 169 private void generatePath() { 170 if (LOGD) Log.d(TAG, "generatePath()"); 171 172 mMax = 0; 173 mPathStroke.reset(); 174 mPathFill.reset(); 175 mPathEstimate.reset(); 176 mPathValid = true; 177 178 // bail when not enough stats to render 179 if (mStats == null || mStats.size() < 2) { 180 return; 181 } 182 183 final int width = getWidth(); 184 final int height = getHeight(); 185 186 boolean started = false; 187 float lastX = 0; 188 float lastY = height; 189 long lastTime = mHoriz.convertToValue(lastX); 190 191 // move into starting position 192 mPathStroke.moveTo(lastX, lastY); 193 mPathFill.moveTo(lastX, lastY); 194 195 // TODO: count fractional data from first bucket crossing start; 196 // currently it only accepts first full bucket. 197 198 long totalData = 0; 199 200 NetworkStatsHistory.Entry entry = null; 201 202 final int start = mStats.getIndexBefore(mStart); 203 final int end = mStats.getIndexAfter(mEnd); 204 for (int i = start; i <= end; i++) { 205 entry = mStats.getValues(i, entry); 206 207 final long startTime = entry.bucketStart; 208 final long endTime = startTime + entry.bucketDuration; 209 210 final float startX = mHoriz.convertToPoint(startTime); 211 final float endX = mHoriz.convertToPoint(endTime); 212 213 // skip until we find first stats on screen 214 if (endX < 0) continue; 215 216 // increment by current bucket total 217 totalData += entry.rxBytes + entry.txBytes; 218 219 final float startY = lastY; 220 final float endY = mVert.convertToPoint(totalData); 221 222 if (lastTime != startTime) { 223 // gap in buckets; line to start of current bucket 224 mPathStroke.lineTo(startX, startY); 225 mPathFill.lineTo(startX, startY); 226 } 227 228 // always draw to end of current bucket 229 mPathStroke.lineTo(endX, endY); 230 mPathFill.lineTo(endX, endY); 231 232 lastX = endX; 233 lastY = endY; 234 lastTime = endTime; 235 } 236 237 // when data falls short, extend to requested end time 238 if (lastTime < mEndTime) { 239 lastX = mHoriz.convertToPoint(mEndTime); 240 241 mPathStroke.lineTo(lastX, lastY); 242 mPathFill.lineTo(lastX, lastY); 243 } 244 245 if (LOGD) { 246 final RectF bounds = new RectF(); 247 mPathFill.computeBounds(bounds, true); 248 Log.d(TAG, "onLayout() rendered with bounds=" + bounds.toString() + " and totalData=" 249 + totalData); 250 } 251 252 // drop to bottom of graph from current location 253 mPathFill.lineTo(lastX, height); 254 mPathFill.lineTo(0, height); 255 256 mMax = totalData; 257 258 if (ESTIMATE_ENABLED) { 259 // build estimated data 260 mPathEstimate.moveTo(lastX, lastY); 261 262 final long now = System.currentTimeMillis(); 263 final long bucketDuration = mStats.getBucketDuration(); 264 265 // long window is average over two weeks 266 entry = mStats.getValues(lastTime - WEEK_IN_MILLIS * 2, lastTime, now, entry); 267 final long longWindow = (entry.rxBytes + entry.txBytes) * bucketDuration 268 / entry.bucketDuration; 269 270 long futureTime = 0; 271 while (lastX < width) { 272 futureTime += bucketDuration; 273 274 // short window is day average last week 275 final long lastWeekTime = lastTime - WEEK_IN_MILLIS + (futureTime % WEEK_IN_MILLIS); 276 entry = mStats.getValues(lastWeekTime - DAY_IN_MILLIS, lastWeekTime, now, entry); 277 final long shortWindow = (entry.rxBytes + entry.txBytes) * bucketDuration 278 / entry.bucketDuration; 279 280 totalData += (longWindow * 7 + shortWindow * 3) / 10; 281 282 lastX = mHoriz.convertToPoint(lastTime + futureTime); 283 lastY = mVert.convertToPoint(totalData); 284 285 mPathEstimate.lineTo(lastX, lastY); 286 } 287 288 mMaxEstimate = totalData; 289 } 290 291 invalidate(); 292 } 293 294 public void setEndTime(long endTime) { 295 mEndTime = endTime; 296 } 297 298 public void setEstimateVisible(boolean estimateVisible) { 299 mEstimateVisible = ESTIMATE_ENABLED ? estimateVisible : false; 300 invalidate(); 301 } 302 303 public long getMaxEstimate() { 304 return mMaxEstimate; 305 } 306 307 public long getMaxVisible() { 308 final long maxVisible = mEstimateVisible ? mMaxEstimate : mMax; 309 if (maxVisible <= 0 && mStats != null) { 310 // haven't generated path yet; fall back to raw data 311 final NetworkStatsHistory.Entry entry = mStats.getValues(mStart, mEnd, null); 312 return entry.rxBytes + entry.txBytes; 313 } else { 314 return maxVisible; 315 } 316 } 317 318 @Override 319 protected void onDraw(Canvas canvas) { 320 int save; 321 322 if (!mPathValid) { 323 generatePath(); 324 } 325 326 if (mEstimateVisible) { 327 save = canvas.save(); 328 canvas.clipRect(0, 0, getWidth(), getHeight()); 329 canvas.drawPath(mPathEstimate, mPaintEstimate); 330 canvas.restoreToCount(save); 331 } 332 333 final Paint paintFill = mSecondary ? mPaintFillSecondary : mPaintFill; 334 335 save = canvas.save(); 336 canvas.clipRect(mSafeRegion, 0, getWidth(), getHeight() - mSafeRegion); 337 canvas.drawPath(mPathFill, paintFill); 338 canvas.restoreToCount(save); 339 } 340} 341