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