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 android.content.Context;
     20 import android.content.res.Resources;
     21 import android.net.NetworkPolicy;
     22 import android.net.NetworkStatsHistory;
     23 import android.net.TrafficStats;
     24 import android.os.Handler;
     25 import android.os.Message;
     26 import android.text.Spannable;
     27 import android.text.SpannableStringBuilder;
     28 import android.text.TextUtils;
     29 import android.text.format.DateUtils;
     30 import android.text.format.Formatter;
     31 import android.text.format.Formatter.BytesResult;
     32 import android.text.format.Time;
     33 import android.util.AttributeSet;
     34 import android.util.MathUtils;
     35 import android.view.MotionEvent;
     36 import android.view.View;
     37 
     38 import com.android.settings.R;
     39 import com.android.settings.widget.ChartSweepView.OnSweepListener;
     40 
     41 import java.util.Arrays;
     42 import java.util.Calendar;
     43 import java.util.Objects;
     44 
     45 import static android.net.TrafficStats.MB_IN_BYTES;
     46 
     47 /**
     48  * Specific {@link ChartView} that displays {@link ChartNetworkSeriesView} along
     49  * with {@link ChartSweepView} for inspection ranges and warning/limits.
     50  */
     51 public class ChartDataUsageView extends ChartView {
     52 
     53     private static final int MSG_UPDATE_AXIS = 100;
     54     private static final long DELAY_MILLIS = 250;
     55 
     56     private ChartGridView mGrid;
     57     private ChartNetworkSeriesView mSeries;
     58     private ChartNetworkSeriesView mDetailSeries;
     59 
     60     private NetworkStatsHistory mHistory;
     61 
     62     private ChartSweepView mSweepWarning;
     63     private ChartSweepView mSweepLimit;
     64 
     65     private long mInspectStart;
     66     private long mInspectEnd;
     67 
     68     private Handler mHandler;
     69 
     70     /** Current maximum value of {@link #mVert}. */
     71     private long mVertMax;
     72 
     73     public interface DataUsageChartListener {
     74         public void onWarningChanged();
     75         public void onLimitChanged();
     76         public void requestWarningEdit();
     77         public void requestLimitEdit();
     78     }
     79 
     80     private DataUsageChartListener mListener;
     81 
     82     public ChartDataUsageView(Context context) {
     83         this(context, null, 0);
     84     }
     85 
     86     public ChartDataUsageView(Context context, AttributeSet attrs) {
     87         this(context, attrs, 0);
     88     }
     89 
     90     public ChartDataUsageView(Context context, AttributeSet attrs, int defStyle) {
     91         super(context, attrs, defStyle);
     92         init(new TimeAxis(), new InvertedChartAxis(new DataAxis()));
     93 
     94         mHandler = new Handler() {
     95             @Override
     96             public void handleMessage(Message msg) {
     97                 final ChartSweepView sweep = (ChartSweepView) msg.obj;
     98                 updateVertAxisBounds(sweep);
     99                 updateEstimateVisible();
    100 
    101                 // we keep dispatching repeating updates until sweep is dropped
    102                 sendUpdateAxisDelayed(sweep, true);
    103             }
    104         };
    105     }
    106 
    107     @Override
    108     protected void onFinishInflate() {
    109         super.onFinishInflate();
    110 
    111         mGrid = (ChartGridView) findViewById(R.id.grid);
    112         mSeries = (ChartNetworkSeriesView) findViewById(R.id.series);
    113         mDetailSeries = (ChartNetworkSeriesView) findViewById(R.id.detail_series);
    114         mDetailSeries.setVisibility(View.GONE);
    115 
    116         mSweepLimit = (ChartSweepView) findViewById(R.id.sweep_limit);
    117         mSweepWarning = (ChartSweepView) findViewById(R.id.sweep_warning);
    118 
    119         // prevent sweeps from crossing each other
    120         mSweepWarning.setValidRangeDynamic(null, mSweepLimit);
    121         mSweepLimit.setValidRangeDynamic(mSweepWarning, null);
    122 
    123         // mark neighbors for checking touch events against
    124         mSweepLimit.setNeighbors(mSweepWarning);
    125         mSweepWarning.setNeighbors(mSweepLimit);
    126 
    127         mSweepWarning.addOnSweepListener(mVertListener);
    128         mSweepLimit.addOnSweepListener(mVertListener);
    129 
    130         mSweepWarning.setDragInterval(5 * MB_IN_BYTES);
    131         mSweepLimit.setDragInterval(5 * MB_IN_BYTES);
    132 
    133         // tell everyone about our axis
    134         mGrid.init(mHoriz, mVert);
    135         mSeries.init(mHoriz, mVert);
    136         mDetailSeries.init(mHoriz, mVert);
    137         mSweepWarning.init(mVert);
    138         mSweepLimit.init(mVert);
    139 
    140         setActivated(false);
    141     }
    142 
    143     public void setListener(DataUsageChartListener listener) {
    144         mListener = listener;
    145     }
    146 
    147     public void bindNetworkStats(NetworkStatsHistory stats) {
    148         mSeries.bindNetworkStats(stats);
    149         mHistory = stats;
    150         updateVertAxisBounds(null);
    151         updateEstimateVisible();
    152         updatePrimaryRange();
    153         requestLayout();
    154     }
    155 
    156     public void bindDetailNetworkStats(NetworkStatsHistory stats) {
    157         mDetailSeries.bindNetworkStats(stats);
    158         mDetailSeries.setVisibility(stats != null ? View.VISIBLE : View.GONE);
    159         if (mHistory != null) {
    160             mDetailSeries.setEndTime(mHistory.getEnd());
    161         }
    162         updateVertAxisBounds(null);
    163         updateEstimateVisible();
    164         updatePrimaryRange();
    165         requestLayout();
    166     }
    167 
    168     public void bindNetworkPolicy(NetworkPolicy policy) {
    169         if (policy == null) {
    170             mSweepLimit.setVisibility(View.INVISIBLE);
    171             mSweepLimit.setValue(-1);
    172             mSweepWarning.setVisibility(View.INVISIBLE);
    173             mSweepWarning.setValue(-1);
    174             return;
    175         }
    176 
    177         if (policy.limitBytes != NetworkPolicy.LIMIT_DISABLED) {
    178             mSweepLimit.setVisibility(View.VISIBLE);
    179             mSweepLimit.setEnabled(true);
    180             mSweepLimit.setValue(policy.limitBytes);
    181         } else {
    182             mSweepLimit.setVisibility(View.INVISIBLE);
    183             mSweepLimit.setEnabled(false);
    184             mSweepLimit.setValue(-1);
    185         }
    186 
    187         if (policy.warningBytes != NetworkPolicy.WARNING_DISABLED) {
    188             mSweepWarning.setVisibility(View.VISIBLE);
    189             mSweepWarning.setValue(policy.warningBytes);
    190         } else {
    191             mSweepWarning.setVisibility(View.INVISIBLE);
    192             mSweepWarning.setValue(-1);
    193         }
    194 
    195         updateVertAxisBounds(null);
    196         requestLayout();
    197         invalidate();
    198     }
    199 
    200     /**
    201      * Update {@link #mVert} to both show data from {@link NetworkStatsHistory}
    202      * and controls from {@link NetworkPolicy}.
    203      */
    204     private void updateVertAxisBounds(ChartSweepView activeSweep) {
    205         final long max = mVertMax;
    206 
    207         long newMax = 0;
    208         if (activeSweep != null) {
    209             final int adjustAxis = activeSweep.shouldAdjustAxis();
    210             if (adjustAxis > 0) {
    211                 // hovering around upper edge, grow axis
    212                 newMax = max * 11 / 10;
    213             } else if (adjustAxis < 0) {
    214                 // hovering around lower edge, shrink axis
    215                 newMax = max * 9 / 10;
    216             } else {
    217                 newMax = max;
    218             }
    219         }
    220 
    221         // always show known data and policy lines
    222         final long maxSweep = Math.max(mSweepWarning.getValue(), mSweepLimit.getValue());
    223         final long maxSeries = Math.max(mSeries.getMaxVisible(), mDetailSeries.getMaxVisible());
    224         final long maxVisible = Math.max(maxSeries, maxSweep) * 12 / 10;
    225         final long maxDefault = Math.max(maxVisible, 50 * MB_IN_BYTES);
    226         newMax = Math.max(maxDefault, newMax);
    227 
    228         // only invalidate when vertMax actually changed
    229         if (newMax != mVertMax) {
    230             mVertMax = newMax;
    231 
    232             final boolean changed = mVert.setBounds(0L, newMax);
    233             mSweepWarning.setValidRange(0L, newMax);
    234             mSweepLimit.setValidRange(0L, newMax);
    235 
    236             if (changed) {
    237                 mSeries.invalidatePath();
    238                 mDetailSeries.invalidatePath();
    239             }
    240 
    241             mGrid.invalidate();
    242 
    243             // since we just changed axis, make sweep recalculate its value
    244             if (activeSweep != null) {
    245                 activeSweep.updateValueFromPosition();
    246             }
    247 
    248             // layout other sweeps to match changed axis
    249             // TODO: find cleaner way of doing this, such as requesting full
    250             // layout and making activeSweep discard its tracking MotionEvent.
    251             if (mSweepLimit != activeSweep) {
    252                 layoutSweep(mSweepLimit);
    253             }
    254             if (mSweepWarning != activeSweep) {
    255                 layoutSweep(mSweepWarning);
    256             }
    257         }
    258     }
    259 
    260     /**
    261      * Control {@link ChartNetworkSeriesView#setEstimateVisible(boolean)} based
    262      * on how close estimate comes to {@link #mSweepWarning}.
    263      */
    264     private void updateEstimateVisible() {
    265         final long maxEstimate = mSeries.getMaxEstimate();
    266 
    267         // show estimate when near warning/limit
    268         long interestLine = Long.MAX_VALUE;
    269         if (mSweepWarning.isEnabled()) {
    270             interestLine = mSweepWarning.getValue();
    271         } else if (mSweepLimit.isEnabled()) {
    272             interestLine = mSweepLimit.getValue();
    273         }
    274 
    275         if (interestLine < 0) {
    276             interestLine = Long.MAX_VALUE;
    277         }
    278 
    279         final boolean estimateVisible = (maxEstimate >= interestLine * 7 / 10);
    280         mSeries.setEstimateVisible(estimateVisible);
    281     }
    282 
    283     private void sendUpdateAxisDelayed(ChartSweepView sweep, boolean force) {
    284         if (force || !mHandler.hasMessages(MSG_UPDATE_AXIS, sweep)) {
    285             mHandler.sendMessageDelayed(
    286                     mHandler.obtainMessage(MSG_UPDATE_AXIS, sweep), DELAY_MILLIS);
    287         }
    288     }
    289 
    290     private void clearUpdateAxisDelayed(ChartSweepView sweep) {
    291         mHandler.removeMessages(MSG_UPDATE_AXIS, sweep);
    292     }
    293 
    294     private OnSweepListener mVertListener = new OnSweepListener() {
    295         @Override
    296         public void onSweep(ChartSweepView sweep, boolean sweepDone) {
    297             if (sweepDone) {
    298                 clearUpdateAxisDelayed(sweep);
    299                 updateEstimateVisible();
    300 
    301                 if (sweep == mSweepWarning && mListener != null) {
    302                     mListener.onWarningChanged();
    303                 } else if (sweep == mSweepLimit && mListener != null) {
    304                     mListener.onLimitChanged();
    305                 }
    306             } else {
    307                 // while moving, kick off delayed grow/shrink axis updates
    308                 sendUpdateAxisDelayed(sweep, false);
    309             }
    310         }
    311 
    312         @Override
    313         public void requestEdit(ChartSweepView sweep) {
    314             if (sweep == mSweepWarning && mListener != null) {
    315                 mListener.requestWarningEdit();
    316             } else if (sweep == mSweepLimit && mListener != null) {
    317                 mListener.requestLimitEdit();
    318             }
    319         }
    320     };
    321 
    322     @Override
    323     public boolean onTouchEvent(MotionEvent event) {
    324         if (isActivated()) return false;
    325         switch (event.getAction()) {
    326             case MotionEvent.ACTION_DOWN: {
    327                 return true;
    328             }
    329             case MotionEvent.ACTION_UP: {
    330                 setActivated(true);
    331                 return true;
    332             }
    333             default: {
    334                 return false;
    335             }
    336         }
    337     }
    338 
    339     public long getInspectStart() {
    340         return mInspectStart;
    341     }
    342 
    343     public long getInspectEnd() {
    344         return mInspectEnd;
    345     }
    346 
    347     public long getWarningBytes() {
    348         return mSweepWarning.getLabelValue();
    349     }
    350 
    351     public long getLimitBytes() {
    352         return mSweepLimit.getLabelValue();
    353     }
    354 
    355     /**
    356      * Set the exact time range that should be displayed, updating how
    357      * {@link ChartNetworkSeriesView} paints. Moves inspection ranges to be the
    358      * last "week" of available data, without triggering listener events.
    359      */
    360     public void setVisibleRange(long visibleStart, long visibleEnd) {
    361         final boolean changed = mHoriz.setBounds(visibleStart, visibleEnd);
    362         mGrid.setBounds(visibleStart, visibleEnd);
    363         mSeries.setBounds(visibleStart, visibleEnd);
    364         mDetailSeries.setBounds(visibleStart, visibleEnd);
    365 
    366         mInspectStart = visibleStart;
    367         mInspectEnd = visibleEnd;
    368 
    369         requestLayout();
    370         if (changed) {
    371             mSeries.invalidatePath();
    372             mDetailSeries.invalidatePath();
    373         }
    374 
    375         updateVertAxisBounds(null);
    376         updateEstimateVisible();
    377         updatePrimaryRange();
    378     }
    379 
    380     private void updatePrimaryRange() {
    381         // prefer showing primary range on detail series, when available
    382         if (mDetailSeries.getVisibility() == View.VISIBLE) {
    383             mSeries.setSecondary(true);
    384         } else {
    385             mSeries.setSecondary(false);
    386         }
    387     }
    388 
    389     public static class TimeAxis implements ChartAxis {
    390         private static final int FIRST_DAY_OF_WEEK = Calendar.getInstance().getFirstDayOfWeek() - 1;
    391 
    392         private long mMin;
    393         private long mMax;
    394         private float mSize;
    395 
    396         public TimeAxis() {
    397             final long currentTime = System.currentTimeMillis();
    398             setBounds(currentTime - DateUtils.DAY_IN_MILLIS * 30, currentTime);
    399         }
    400 
    401         @Override
    402         public int hashCode() {
    403             return Objects.hash(mMin, mMax, mSize);
    404         }
    405 
    406         @Override
    407         public boolean setBounds(long min, long max) {
    408             if (mMin != min || mMax != max) {
    409                 mMin = min;
    410                 mMax = max;
    411                 return true;
    412             } else {
    413                 return false;
    414             }
    415         }
    416 
    417         @Override
    418         public boolean setSize(float size) {
    419             if (mSize != size) {
    420                 mSize = size;
    421                 return true;
    422             } else {
    423                 return false;
    424             }
    425         }
    426 
    427         @Override
    428         public float convertToPoint(long value) {
    429             return (mSize * (value - mMin)) / (mMax - mMin);
    430         }
    431 
    432         @Override
    433         public long convertToValue(float point) {
    434             return (long) (mMin + ((point * (mMax - mMin)) / mSize));
    435         }
    436 
    437         @Override
    438         public long buildLabel(Resources res, SpannableStringBuilder builder, long value) {
    439             // TODO: convert to better string
    440             builder.replace(0, builder.length(), Long.toString(value));
    441             return value;
    442         }
    443 
    444         @Override
    445         public float[] getTickPoints() {
    446             final float[] ticks = new float[32];
    447             int i = 0;
    448 
    449             // tick mark for first day of each week
    450             final Time time = new Time();
    451             time.set(mMax);
    452             time.monthDay -= time.weekDay - FIRST_DAY_OF_WEEK;
    453             time.hour = time.minute = time.second = 0;
    454 
    455             time.normalize(true);
    456             long timeMillis = time.toMillis(true);
    457             while (timeMillis > mMin) {
    458                 if (timeMillis <= mMax) {
    459                     ticks[i++] = convertToPoint(timeMillis);
    460                 }
    461                 time.monthDay -= 7;
    462                 time.normalize(true);
    463                 timeMillis = time.toMillis(true);
    464             }
    465 
    466             return Arrays.copyOf(ticks, i);
    467         }
    468 
    469         @Override
    470         public int shouldAdjustAxis(long value) {
    471             // time axis never adjusts
    472             return 0;
    473         }
    474     }
    475 
    476     public static class DataAxis implements ChartAxis {
    477         private long mMin;
    478         private long mMax;
    479         private float mSize;
    480 
    481         private static final boolean LOG_SCALE = false;
    482 
    483         @Override
    484         public int hashCode() {
    485             return Objects.hash(mMin, mMax, mSize);
    486         }
    487 
    488         @Override
    489         public boolean setBounds(long min, long max) {
    490             if (mMin != min || mMax != max) {
    491                 mMin = min;
    492                 mMax = max;
    493                 return true;
    494             } else {
    495                 return false;
    496             }
    497         }
    498 
    499         @Override
    500         public boolean setSize(float size) {
    501             if (mSize != size) {
    502                 mSize = size;
    503                 return true;
    504             } else {
    505                 return false;
    506             }
    507         }
    508 
    509         @Override
    510         public float convertToPoint(long value) {
    511             if (LOG_SCALE) {
    512                 // derived polynomial fit to make lower values more visible
    513                 final double normalized = ((double) value - mMin) / (mMax - mMin);
    514                 final double fraction = Math.pow(10,
    515                         0.36884343106175121463 * Math.log10(normalized) + -0.04328199452018252624);
    516                 return (float) (fraction * mSize);
    517             } else {
    518                 return (mSize * (value - mMin)) / (mMax - mMin);
    519             }
    520         }
    521 
    522         @Override
    523         public long convertToValue(float point) {
    524             if (LOG_SCALE) {
    525                 final double normalized = point / mSize;
    526                 final double fraction = 1.3102228476089056629
    527                         * Math.pow(normalized, 2.7111774693164631640);
    528                 return (long) (mMin + (fraction * (mMax - mMin)));
    529             } else {
    530                 return (long) (mMin + ((point * (mMax - mMin)) / mSize));
    531             }
    532         }
    533 
    534         private static final Object sSpanSize = new Object();
    535         private static final Object sSpanUnit = new Object();
    536 
    537         @Override
    538         public long buildLabel(Resources res, SpannableStringBuilder builder, long value) {
    539             value = MathUtils.constrain(value, 0, TrafficStats.TB_IN_BYTES);
    540             final BytesResult result = Formatter.formatBytes(res, value,
    541                     Formatter.FLAG_SHORTER | Formatter.FLAG_CALCULATE_ROUNDED);
    542             setText(builder, sSpanSize, result.value, "^1");
    543             setText(builder, sSpanUnit, result.units, "^2");
    544             return result.roundedBytes;
    545         }
    546 
    547         @Override
    548         public float[] getTickPoints() {
    549             final long range = mMax - mMin;
    550 
    551             // target about 16 ticks on screen, rounded to nearest power of 2
    552             final long tickJump = roundUpToPowerOfTwo(range / 16);
    553             final int tickCount = (int) (range / tickJump);
    554             final float[] tickPoints = new float[tickCount];
    555             long value = mMin;
    556             for (int i = 0; i < tickPoints.length; i++) {
    557                 tickPoints[i] = convertToPoint(value);
    558                 value += tickJump;
    559             }
    560 
    561             return tickPoints;
    562         }
    563 
    564         @Override
    565         public int shouldAdjustAxis(long value) {
    566             final float point = convertToPoint(value);
    567             if (point < mSize * 0.1) {
    568                 return -1;
    569             } else if (point > mSize * 0.85) {
    570                 return 1;
    571             } else {
    572                 return 0;
    573             }
    574         }
    575     }
    576 
    577     private static void setText(
    578             SpannableStringBuilder builder, Object key, CharSequence text, String bootstrap) {
    579         int start = builder.getSpanStart(key);
    580         int end = builder.getSpanEnd(key);
    581         if (start == -1) {
    582             start = TextUtils.indexOf(builder, bootstrap);
    583             end = start + bootstrap.length();
    584             builder.setSpan(key, start, end, Spannable.SPAN_INCLUSIVE_INCLUSIVE);
    585         }
    586         builder.replace(start, end, text);
    587     }
    588 
    589     private static long roundUpToPowerOfTwo(long i) {
    590         // NOTE: borrowed from Hashtable.roundUpToPowerOfTwo()
    591 
    592         i--; // If input is a power of two, shift its high-order bit right
    593 
    594         // "Smear" the high-order bit all the way to the right
    595         i |= i >>>  1;
    596         i |= i >>>  2;
    597         i |= i >>>  4;
    598         i |= i >>>  8;
    599         i |= i >>> 16;
    600         i |= i >>> 32;
    601 
    602         i++;
    603 
    604         return i > 0 ? i : Long.MAX_VALUE;
    605     }
    606 }
    607