Home | History | Annotate | Download | only in widget
      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 
     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