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 
17 package com.android.settings.widget;
18 
19 import static android.text.format.DateUtils.DAY_IN_MILLIS;
20 import static android.text.format.DateUtils.WEEK_IN_MILLIS;
21 
22 import android.content.Context;
23 import android.content.res.TypedArray;
24 import android.graphics.Canvas;
25 import android.graphics.Color;
26 import android.graphics.DashPathEffect;
27 import android.graphics.Paint;
28 import android.graphics.Paint.Style;
29 import android.graphics.Path;
30 import android.graphics.RectF;
31 import android.net.NetworkStatsHistory;
32 import android.util.AttributeSet;
33 import android.util.Log;
34 import android.view.View;
35 
36 import com.android.internal.util.Preconditions;
37 import 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  */
43 public 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 
ChartNetworkSeriesView(Context context)78     public ChartNetworkSeriesView(Context context) {
79         this(context, null, 0);
80     }
81 
ChartNetworkSeriesView(Context context, AttributeSet attrs)82     public ChartNetworkSeriesView(Context context, AttributeSet attrs) {
83         this(context, attrs, 0);
84     }
85 
ChartNetworkSeriesView(Context context, AttributeSet attrs, int defStyle)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 
init(ChartAxis horiz, ChartAxis vert)110     void init(ChartAxis horiz, ChartAxis vert) {
111         mHoriz = Preconditions.checkNotNull(horiz, "missing horiz");
112         mVert = Preconditions.checkNotNull(vert, "missing vert");
113     }
114 
setChartColor(int stroke, int fill, int fillSecondary)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 
setSafeRegion(int safeRegion)140     public void setSafeRegion(int safeRegion) {
141         mSafeRegion = safeRegion;
142     }
143 
bindNetworkStats(NetworkStatsHistory stats)144     public void bindNetworkStats(NetworkStatsHistory stats) {
145         mStats = stats;
146         invalidatePath();
147         invalidate();
148     }
149 
setBounds(long start, long end)150     public void setBounds(long start, long end) {
151         mStart = start;
152         mEnd = end;
153     }
154 
setSecondary(boolean secondary)155     public void setSecondary(boolean secondary) {
156         mSecondary = secondary;
157     }
158 
invalidatePath()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      */
generatePath()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 
setEndTime(long endTime)294     public void setEndTime(long endTime) {
295         mEndTime = endTime;
296     }
297 
setEstimateVisible(boolean estimateVisible)298     public void setEstimateVisible(boolean estimateVisible) {
299         mEstimateVisible = ESTIMATE_ENABLED ? estimateVisible : false;
300         invalidate();
301     }
302 
getMaxEstimate()303     public long getMaxEstimate() {
304         return mMaxEstimate;
305     }
306 
getMaxVisible()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
onDraw(Canvas canvas)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