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