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