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