Show automatic grid lines with power-of-two spacing, avoiding ANR when trying to render thousands of gridlines. Lower minimum chart height to 50MB to give better view of linear data. Also disable estimates rendering. Bug: 5500204, 6005240 Change-Id: Iacfed11b32c0095c9c2d08bee6a1b5e29637de98
353 lines
11 KiB
Java
353 lines
11 KiB
Java
/*
|
|
* 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);
|
|
|
|
}
|
|
}
|