/* * Copyright (C) 2011 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.settings.widget; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.DashPathEffect; import android.graphics.Paint; import android.graphics.Paint.Style; import android.graphics.Path; import android.graphics.RectF; import android.net.NetworkStatsHistory; import android.util.AttributeSet; import android.util.Log; import android.view.View; import com.android.internal.util.Preconditions; import com.android.settings.R; import static android.text.format.DateUtils.DAY_IN_MILLIS; import static android.text.format.DateUtils.WEEK_IN_MILLIS; /** * {@link NetworkStatsHistory} series to render inside a {@link ChartView}, * using {@link ChartAxis} to map into screen coordinates. */ public class ChartNetworkSeriesView extends View { private static final String TAG = "ChartNetworkSeriesView"; private static final boolean LOGD = false; private static final boolean ESTIMATE_ENABLED = false; private ChartAxis mHoriz; private ChartAxis mVert; private Paint mPaintStroke; private Paint mPaintFill; private Paint mPaintFillSecondary; private Paint mPaintEstimate; private NetworkStatsHistory mStats; private Path mPathStroke; private Path mPathFill; private Path mPathEstimate; private int mSafeRegion; private long mStart; private long mEnd; /** Series will be extended to reach this end time. */ private long mEndTime = Long.MIN_VALUE; private boolean mPathValid = false; private boolean mEstimateVisible = false; private boolean mSecondary = false; private long mMax; private long mMaxEstimate; public ChartNetworkSeriesView(Context context) { this(context, null, 0); } public ChartNetworkSeriesView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public ChartNetworkSeriesView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); final TypedArray a = context.obtainStyledAttributes( attrs, R.styleable.ChartNetworkSeriesView, defStyle, 0); final int stroke = a.getColor(R.styleable.ChartNetworkSeriesView_strokeColor, Color.RED); final int fill = a.getColor(R.styleable.ChartNetworkSeriesView_fillColor, Color.RED); final int fillSecondary = a.getColor( R.styleable.ChartNetworkSeriesView_fillColorSecondary, Color.RED); final int safeRegion = a.getDimensionPixelSize( R.styleable.ChartNetworkSeriesView_safeRegion, 0); setChartColor(stroke, fill, fillSecondary); setSafeRegion(safeRegion); setWillNotDraw(false); a.recycle(); mPathStroke = new Path(); mPathFill = new Path(); mPathEstimate = new Path(); } void init(ChartAxis horiz, ChartAxis vert) { mHoriz = Preconditions.checkNotNull(horiz, "missing horiz"); mVert = Preconditions.checkNotNull(vert, "missing vert"); } public void setChartColor(int stroke, int fill, int fillSecondary) { mPaintStroke = new Paint(); mPaintStroke.setStrokeWidth(4.0f * getResources().getDisplayMetrics().density); mPaintStroke.setColor(stroke); mPaintStroke.setStyle(Style.STROKE); mPaintStroke.setAntiAlias(true); mPaintFill = new Paint(); mPaintFill.setColor(fill); mPaintFill.setStyle(Style.FILL); mPaintFill.setAntiAlias(true); mPaintFillSecondary = new Paint(); mPaintFillSecondary.setColor(fillSecondary); mPaintFillSecondary.setStyle(Style.FILL); mPaintFillSecondary.setAntiAlias(true); mPaintEstimate = new Paint(); mPaintEstimate.setStrokeWidth(3.0f); mPaintEstimate.setColor(fillSecondary); mPaintEstimate.setStyle(Style.STROKE); mPaintEstimate.setAntiAlias(true); mPaintEstimate.setPathEffect(new DashPathEffect(new float[] { 10, 10 }, 1)); } public void setSafeRegion(int safeRegion) { mSafeRegion = safeRegion; } public void bindNetworkStats(NetworkStatsHistory stats) { mStats = stats; invalidatePath(); invalidate(); } public void setBounds(long start, long end) { mStart = start; mEnd = end; } public void setSecondary(boolean secondary) { mSecondary = secondary; } public void invalidatePath() { mPathValid = false; mMax = 0; invalidate(); } /** * Erase any existing {@link Path} and generate series outline based on * currently bound {@link NetworkStatsHistory} data. */ private void generatePath() { if (LOGD) Log.d(TAG, "generatePath()"); mMax = 0; mPathStroke.reset(); mPathFill.reset(); mPathEstimate.reset(); mPathValid = true; // bail when not enough stats to render if (mStats == null || mStats.size() < 2) { return; } final int width = getWidth(); final int height = getHeight(); boolean started = false; float lastX = 0; float lastY = height; long lastTime = mHoriz.convertToValue(lastX); // move into starting position mPathStroke.moveTo(lastX, lastY); mPathFill.moveTo(lastX, lastY); // TODO: count fractional data from first bucket crossing start; // currently it only accepts first full bucket. long totalData = 0; NetworkStatsHistory.Entry entry = null; final int start = mStats.getIndexBefore(mStart); final int end = mStats.getIndexAfter(mEnd); for (int i = start; i <= end; i++) { entry = mStats.getValues(i, entry); final long startTime = entry.bucketStart; final long endTime = startTime + entry.bucketDuration; final float startX = mHoriz.convertToPoint(startTime); final float endX = mHoriz.convertToPoint(endTime); // skip until we find first stats on screen if (endX < 0) continue; // increment by current bucket total totalData += entry.rxBytes + entry.txBytes; final float startY = lastY; final float endY = mVert.convertToPoint(totalData); if (lastTime != startTime) { // gap in buckets; line to start of current bucket mPathStroke.lineTo(startX, startY); mPathFill.lineTo(startX, startY); } // always draw to end of current bucket mPathStroke.lineTo(endX, endY); mPathFill.lineTo(endX, endY); lastX = endX; lastY = endY; lastTime = endTime; } // when data falls short, extend to requested end time if (lastTime < mEndTime) { lastX = mHoriz.convertToPoint(mEndTime); mPathStroke.lineTo(lastX, lastY); mPathFill.lineTo(lastX, lastY); } if (LOGD) { final RectF bounds = new RectF(); mPathFill.computeBounds(bounds, true); Log.d(TAG, "onLayout() rendered with bounds=" + bounds.toString() + " and totalData=" + totalData); } // drop to bottom of graph from current location mPathFill.lineTo(lastX, height); mPathFill.lineTo(0, height); mMax = totalData; if (ESTIMATE_ENABLED) { // build estimated data mPathEstimate.moveTo(lastX, lastY); final long now = System.currentTimeMillis(); final long bucketDuration = mStats.getBucketDuration(); // long window is average over two weeks entry = mStats.getValues(lastTime - WEEK_IN_MILLIS * 2, lastTime, now, entry); final long longWindow = (entry.rxBytes + entry.txBytes) * bucketDuration / entry.bucketDuration; long futureTime = 0; while (lastX < width) { futureTime += bucketDuration; // short window is day average last week final long lastWeekTime = lastTime - WEEK_IN_MILLIS + (futureTime % WEEK_IN_MILLIS); entry = mStats.getValues(lastWeekTime - DAY_IN_MILLIS, lastWeekTime, now, entry); final long shortWindow = (entry.rxBytes + entry.txBytes) * bucketDuration / entry.bucketDuration; totalData += (longWindow * 7 + shortWindow * 3) / 10; lastX = mHoriz.convertToPoint(lastTime + futureTime); lastY = mVert.convertToPoint(totalData); mPathEstimate.lineTo(lastX, lastY); } mMaxEstimate = totalData; } invalidate(); } public void setEndTime(long endTime) { mEndTime = endTime; } public void setEstimateVisible(boolean estimateVisible) { mEstimateVisible = ESTIMATE_ENABLED ? estimateVisible : false; invalidate(); } public long getMaxEstimate() { return mMaxEstimate; } public long getMaxVisible() { final long maxVisible = mEstimateVisible ? mMaxEstimate : mMax; if (maxVisible <= 0 && mStats != null) { // haven't generated path yet; fall back to raw data final NetworkStatsHistory.Entry entry = mStats.getValues(mStart, mEnd, null); return entry.rxBytes + entry.txBytes; } else { return maxVisible; } } @Override protected void onDraw(Canvas canvas) { int save; if (!mPathValid) { generatePath(); } if (mEstimateVisible) { save = canvas.save(); canvas.clipRect(0, 0, getWidth(), getHeight()); canvas.drawPath(mPathEstimate, mPaintEstimate); canvas.restoreToCount(save); } final Paint paintFill = mSecondary ? mPaintFillSecondary : mPaintFill; save = canvas.save(); canvas.clipRect(mSafeRegion, 0, getWidth(), getHeight() - mSafeRegion); canvas.drawPath(mPathFill, paintFill); canvas.restoreToCount(save); } }