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