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