• 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.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 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 
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 
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 
init(ChartAxis horiz, ChartAxis vert)107     void init(ChartAxis horiz, ChartAxis vert) {
108         mHoriz = Preconditions.checkNotNull(horiz, "missing horiz");
109         mVert = Preconditions.checkNotNull(vert, "missing vert");
110     }
111 
setChartColor(int stroke, int fill, int fillSecondary)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 
bindNetworkStats(NetworkStatsHistory stats)137     public void bindNetworkStats(NetworkStatsHistory stats) {
138         mStats = stats;
139         invalidatePath();
140         invalidate();
141     }
142 
setBounds(long start, long end)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      */
setPrimaryRange(long left, long right)152     public void setPrimaryRange(long left, long right) {
153         mPrimaryLeft = left;
154         mPrimaryRight = right;
155         invalidate();
156     }
157 
invalidatePath()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      */
generatePath()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 
setEndTime(long endTime)293     public void setEndTime(long endTime) {
294         mEndTime = endTime;
295     }
296 
setEstimateVisible(boolean estimateVisible)297     public void setEstimateVisible(boolean estimateVisible) {
298         mEstimateVisible = ESTIMATE_ENABLED ? estimateVisible : false;
299         invalidate();
300     }
301 
getMaxEstimate()302     public long getMaxEstimate() {
303         return mMaxEstimate;
304     }
305 
getMaxVisible()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
onDraw(Canvas canvas)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