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