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.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 
     76     public ChartNetworkSeriesView(Context context) {
     77         this(context, null, 0);
     78     }
     79 
     80     public ChartNetworkSeriesView(Context context, AttributeSet attrs) {
     81         this(context, attrs, 0);
     82     }
     83 
     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 
    105     void init(ChartAxis horiz, ChartAxis vert) {
    106         mHoriz = Preconditions.checkNotNull(horiz, "missing horiz");
    107         mVert = Preconditions.checkNotNull(vert, "missing vert");
    108     }
    109 
    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 
    135     public void bindNetworkStats(NetworkStatsHistory stats) {
    136         mStats = stats;
    137         invalidatePath();
    138         invalidate();
    139     }
    140 
    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      */
    150     public void setPrimaryRange(long left, long right) {
    151         mPrimaryLeft = left;
    152         mPrimaryRight = right;
    153         invalidate();
    154     }
    155 
    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      */
    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 
    289     public void setEndTime(long endTime) {
    290         mEndTime = endTime;
    291     }
    292 
    293     public void setEstimateVisible(boolean estimateVisible) {
    294         mEstimateVisible = estimateVisible;
    295         invalidate();
    296     }
    297 
    298     public long getMaxEstimate() {
    299         return mMaxEstimate;
    300     }
    301 
    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
    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