| /* |
| * 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 static android.text.format.DateUtils.DAY_IN_MILLIS; |
| import static android.text.format.DateUtils.WEEK_IN_MILLIS; |
| |
| 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.settings.R; |
| import com.google.common.base.Preconditions; |
| |
| /** |
| * {@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 long mStart; |
| private long mEnd; |
| |
| private long mPrimaryLeft; |
| private long mPrimaryRight; |
| |
| /** Series will be extended to reach this end time. */ |
| private long mEndTime = Long.MIN_VALUE; |
| |
| private boolean mPathValid = false; |
| private boolean mEstimateVisible = 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); |
| |
| setChartColor(stroke, fill, fillSecondary); |
| 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 bindNetworkStats(NetworkStatsHistory stats) { |
| mStats = stats; |
| invalidatePath(); |
| invalidate(); |
| } |
| |
| public void setBounds(long start, long end) { |
| mStart = start; |
| mEnd = end; |
| } |
| |
| /** |
| * Set the range to paint with {@link #mPaintFill}, leaving the remaining |
| * area to be painted with {@link #mPaintFillSecondary}. |
| */ |
| public void setPrimaryRange(long left, long right) { |
| mPrimaryLeft = left; |
| mPrimaryRight = right; |
| invalidate(); |
| } |
| |
| 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(); |
| } |
| |
| final float primaryLeftPoint = mHoriz.convertToPoint(mPrimaryLeft); |
| final float primaryRightPoint = mHoriz.convertToPoint(mPrimaryRight); |
| |
| if (mEstimateVisible) { |
| save = canvas.save(); |
| canvas.clipRect(0, 0, getWidth(), getHeight()); |
| canvas.drawPath(mPathEstimate, mPaintEstimate); |
| canvas.restoreToCount(save); |
| } |
| |
| save = canvas.save(); |
| canvas.clipRect(0, 0, primaryLeftPoint, getHeight()); |
| canvas.drawPath(mPathFill, mPaintFillSecondary); |
| canvas.restoreToCount(save); |
| |
| save = canvas.save(); |
| canvas.clipRect(primaryRightPoint, 0, getWidth(), getHeight()); |
| canvas.drawPath(mPathFill, mPaintFillSecondary); |
| canvas.restoreToCount(save); |
| |
| save = canvas.save(); |
| canvas.clipRect(primaryLeftPoint, 0, primaryRightPoint, getHeight()); |
| canvas.drawPath(mPathFill, mPaintFill); |
| canvas.drawPath(mPathStroke, mPaintStroke); |
| canvas.restoreToCount(save); |
| |
| } |
| } |