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