• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 {
onWarningChanged()74         public void onWarningChanged();
onLimitChanged()75         public void onLimitChanged();
requestWarningEdit()76         public void requestWarningEdit();
requestLimitEdit()77         public void requestLimitEdit();
78     }
79 
80     private DataUsageChartListener mListener;
81 
ChartDataUsageView(Context context)82     public ChartDataUsageView(Context context) {
83         this(context, null, 0);
84     }
85 
ChartDataUsageView(Context context, AttributeSet attrs)86     public ChartDataUsageView(Context context, AttributeSet attrs) {
87         this(context, attrs, 0);
88     }
89 
ChartDataUsageView(Context context, AttributeSet attrs, int defStyle)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
onFinishInflate()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 
setListener(DataUsageChartListener listener)143     public void setListener(DataUsageChartListener listener) {
144         mListener = listener;
145     }
146 
bindNetworkStats(NetworkStatsHistory stats)147     public void bindNetworkStats(NetworkStatsHistory stats) {
148         mSeries.bindNetworkStats(stats);
149         mHistory = stats;
150         updateVertAxisBounds(null);
151         updateEstimateVisible();
152         updatePrimaryRange();
153         requestLayout();
154     }
155 
bindDetailNetworkStats(NetworkStatsHistory stats)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 
bindNetworkPolicy(NetworkPolicy policy)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      */
updateVertAxisBounds(ChartSweepView activeSweep)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      */
updateEstimateVisible()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 
sendUpdateAxisDelayed(ChartSweepView sweep, boolean force)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 
clearUpdateAxisDelayed(ChartSweepView sweep)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
onTouchEvent(MotionEvent event)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 
getInspectStart()339     public long getInspectStart() {
340         return mInspectStart;
341     }
342 
getInspectEnd()343     public long getInspectEnd() {
344         return mInspectEnd;
345     }
346 
getWarningBytes()347     public long getWarningBytes() {
348         return mSweepWarning.getLabelValue();
349     }
350 
getLimitBytes()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      */
setVisibleRange(long visibleStart, long visibleEnd)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 
updatePrimaryRange()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 
TimeAxis()396         public TimeAxis() {
397             final long currentTime = System.currentTimeMillis();
398             setBounds(currentTime - DateUtils.DAY_IN_MILLIS * 30, currentTime);
399         }
400 
401         @Override
hashCode()402         public int hashCode() {
403             return Objects.hash(mMin, mMax, mSize);
404         }
405 
406         @Override
setBounds(long min, long max)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
setSize(float size)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
convertToPoint(long value)428         public float convertToPoint(long value) {
429             return (mSize * (value - mMin)) / (mMax - mMin);
430         }
431 
432         @Override
convertToValue(float point)433         public long convertToValue(float point) {
434             return (long) (mMin + ((point * (mMax - mMin)) / mSize));
435         }
436 
437         @Override
buildLabel(Resources res, SpannableStringBuilder builder, long value)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
getTickPoints()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
shouldAdjustAxis(long value)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
hashCode()484         public int hashCode() {
485             return Objects.hash(mMin, mMax, mSize);
486         }
487 
488         @Override
setBounds(long min, long max)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
setSize(float size)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
convertToPoint(long value)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
convertToValue(float point)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
buildLabel(Resources res, SpannableStringBuilder builder, long value)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
getTickPoints()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
shouldAdjustAxis(long value)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 
setText( SpannableStringBuilder builder, Object key, CharSequence text, String bootstrap)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 
roundUpToPowerOfTwo(long i)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