Chromium Code Reviews| Index: third_party/android_misc/java/src/org/chromium/third_party/android/datausagechart/ChartDataUsageView.java |
| diff --git a/third_party/android_misc/java/src/org/chromium/third_party/android/datausagechart/ChartDataUsageView.java b/third_party/android_misc/java/src/org/chromium/third_party/android/datausagechart/ChartDataUsageView.java |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..b610a6097ba55c3bf0a2b6248d144a3453ad6204 |
| --- /dev/null |
| +++ b/third_party/android_misc/java/src/org/chromium/third_party/android/datausagechart/ChartDataUsageView.java |
| @@ -0,0 +1,494 @@ |
| +/* |
| + * Copyright (C) 2011 The Android Open Source Project |
| + * |
| + * Licensed under the Apache License, Version 2.0 (the "License"); |
| + * you may not use this file except in compliance with the License. |
| + * You may obtain a copy of the License at |
| + * |
| + * http://www.apache.org/licenses/LICENSE-2.0 |
| + * |
| + * Unless required by applicable law or agreed to in writing, software |
| + * distributed under the License is distributed on an "AS IS" BASIS, |
| + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| + * See the License for the specific language governing permissions and |
| + * limitations under the License. |
| + */ |
| + |
| +package org.chromium.third_party.android.datausagechart; |
| + |
| +import android.content.Context; |
| +import android.content.res.Resources; |
| +import android.text.Spannable; |
| +import android.text.SpannableStringBuilder; |
| +import android.text.TextUtils; |
| +import android.text.format.DateUtils; |
| +import android.text.format.Time; |
| +import android.util.AttributeSet; |
| +import android.view.MotionEvent; |
| +import android.view.View; |
| + |
| +import org.chromium.third_party.android.R; |
| + |
| +import java.util.Arrays; |
| +import java.util.Calendar; |
| +import java.util.Locale; |
| +import java.util.TimeZone; |
| + |
| +/** |
| + * Specific {@link ChartView} that displays {@link ChartNetworkSeriesView} for inspection ranges. |
| + * This is derived from com.android.settings.widget.ChartDataUsageView. |
| + */ |
| +public class ChartDataUsageView extends ChartView { |
| + public static final int DAYS_IN_CHART = 30; |
| + |
| + private static final long MB_IN_BYTES = 1024 * 1024; |
| + private static final long GB_IN_BYTES = 1024 * 1024 * 1024; |
| + |
| + private ChartNetworkSeriesView mOriginalSeries; |
| + private ChartNetworkSeriesView mCompressedSeries; |
| + |
| + private NetworkStatsHistory mHistory; |
| + |
| + private long mLeft; |
| + private long mRight; |
| + |
| + /** Current maximum value of {@link #mVert}. */ |
| + private long mVertMax; |
| + |
| + /** |
| + * Constructs a new {@link ChartDataUsageView} with the appropriate context. |
| + */ |
| + public ChartDataUsageView(Context context) { |
| + this(context, null, 0); |
| + } |
| + |
| + /** |
| + * Constructs a new {@link ChartDataUsageView} with the appropriate context and attributes. |
| + */ |
| + public ChartDataUsageView(Context context, AttributeSet attrs) { |
| + this(context, attrs, 0); |
| + } |
| + |
| + /** |
| + * Constructs a new {@link ChartDataUsageView} with the appropriate context, attributes, and |
| + * style. |
| + */ |
| + public ChartDataUsageView(Context context, AttributeSet attrs, int defStyle) { |
| + super(context, attrs, defStyle); |
| + init(new TimeAxis(), new InvertedChartAxis(new DataAxis())); |
| + } |
| + |
| + @Override |
| + protected void onFinishInflate() { |
| + super.onFinishInflate(); |
| + |
| + mOriginalSeries = (ChartNetworkSeriesView) findViewById(R.id.series); |
| + mCompressedSeries = (ChartNetworkSeriesView) findViewById(R.id.detail_series); |
| + |
| + // tell everyone about our axis |
|
bengr
2015/01/26 19:24:57
Remove this comment.
|
| + mOriginalSeries.init(mHoriz, mVert); |
| + mCompressedSeries.init(mHoriz, mVert); |
| + setActivated(false); |
| + } |
| + |
| + public void bindOriginalNetworkStats(NetworkStatsHistory stats) { |
| + mOriginalSeries.bindNetworkStats(stats); |
| + // Compensate for time zone adjustments when setting the end time. |
| + mHistory = stats; |
| + updateVertAxisBounds(); |
| + updateEstimateVisible(); |
| + updatePrimaryRange(); |
| + requestLayout(); |
| + } |
| + |
| + public void bindCompressedNetworkStats(NetworkStatsHistory stats) { |
| + mCompressedSeries.bindNetworkStats(stats); |
| + mCompressedSeries.setVisibility(stats != null ? View.VISIBLE : View.GONE); |
| + if (mHistory != null) { |
| + // Compensate for time zone adjustments when setting the end time. |
| + mOriginalSeries.setEndTime(mHistory.getEnd() |
| + - TimeZone.getDefault().getOffset(mHistory.getEnd())); |
| + mCompressedSeries.setEndTime(mHistory.getEnd() |
| + - TimeZone.getDefault().getOffset(mHistory.getEnd())); |
| + } |
| + updateEstimateVisible(); |
| + updatePrimaryRange(); |
| + requestLayout(); |
| + } |
| + |
| + /** |
| + * Update {@link #mVert} to show data from {@link NetworkStatsHistory}. |
|
bengr
2015/01/26 19:24:57
Updates
|
| + */ |
| + private void updateVertAxisBounds() { |
| + long newMax = 0; |
| + |
| + // always show known data and policy lines |
|
bengr
2015/01/26 19:24:57
// Show known data and policy lines always.
|
| + final long maxSeries = Math.max(mOriginalSeries.getMaxVisible(), |
| + mCompressedSeries.getMaxVisible()); |
| + final long maxVisible = Math.max(maxSeries, 0) * 12 / 10; |
| + final long maxDefault = Math.max(maxVisible, 1 * MB_IN_BYTES); |
| + newMax = Math.max(maxDefault, newMax); |
| + |
| + // only invalidate when vertMax actually changed |
|
bengr
2015/01/26 19:24:57
Invalidate only when vertMax actually changed.
|
| + if (newMax != mVertMax) { |
| + mVertMax = newMax; |
| + |
|
bengr
2015/01/26 19:24:57
Remove blank line.
|
| + final boolean changed = mVert.setBounds(0L, newMax); |
| + |
|
bengr
2015/01/26 19:24:57
Remove blank line.
|
| + if (changed) { |
| + mOriginalSeries.invalidatePath(); |
| + mCompressedSeries.invalidatePath(); |
| + } |
| + } |
| + } |
| + |
| + private void updateEstimateVisible() { |
| + mOriginalSeries.setEstimateVisible(false); |
| + } |
| + |
| + @Override |
| + public boolean onTouchEvent(MotionEvent event) { |
| + if (isActivated()) return false; |
| + switch (event.getAction()) { |
| + case MotionEvent.ACTION_DOWN: { |
| + return true; |
| + } |
| + case MotionEvent.ACTION_UP: { |
| + setActivated(true); |
| + return true; |
| + } |
| + default: { |
| + return false; |
| + } |
| + } |
| + } |
| + |
| + public long getInspectStart() { |
| + return mLeft; |
| + } |
| + |
| + public long getInspectEnd() { |
| + return mRight; |
| + } |
| + |
| + /** |
| + * Set the exact time range that should be displayed, updating how |
|
bengr
2015/01/26 19:24:57
Sets
|
| + * {@link ChartNetworkSeriesView} paints. Moves inspection ranges to be the |
| + * last "week" of available data, without triggering listener events. |
| + */ |
| + public void setVisibleRange(long visibleStart, long visibleEnd, long start, |
| + long end) { |
| + long timeZoneOffset = TimeZone.getDefault().getOffset(end); |
| + final boolean changed = mHoriz.setBounds(visibleStart, visibleEnd); |
| + mOriginalSeries.setBounds(visibleStart, visibleEnd); |
| + mCompressedSeries.setBounds(visibleStart, visibleEnd); |
| + |
| + final long validEnd = visibleEnd; |
| + |
| + long max = validEnd; |
| + long min = Math.max( |
| + visibleStart, (max - DateUtils.DAY_IN_MILLIS * DAYS_IN_CHART)); |
| + if (visibleEnd - DateUtils.HOUR_IN_MILLIS |
| + - DateUtils.DAY_IN_MILLIS * DAYS_IN_CHART != start |
| + || visibleEnd != end + timeZoneOffset) { |
|
bengr
2015/01/26 19:24:57
Move || up one line and align visibleEnd to visibl
|
| + min = start; |
| + max = end; |
| + setActivated(true); |
| + } |
| + |
| + mLeft = min; |
| + mRight = max; |
| + |
| + requestLayout(); |
| + if (changed) { |
| + mOriginalSeries.invalidatePath(); |
| + mCompressedSeries.invalidatePath(); |
| + } |
| + |
| + updateVertAxisBounds(); |
| + updateEstimateVisible(); |
| + updatePrimaryRange(); |
| + } |
| + |
| + private void updatePrimaryRange() { |
| + final long left = mLeft; |
| + final long right = mRight; |
| + |
| + // prefer showing primary range on detail series, when available |
|
bengr
2015/01/26 19:24:57
Prefer ... available.
|
| + if (mCompressedSeries.getVisibility() == View.VISIBLE) { |
| + mCompressedSeries.setPrimaryRange(left, right); |
| + // Overlay the compressed series, when available, on top of the series. |
| + mOriginalSeries.setPrimaryRange(left, right); |
| + } else { |
| + mOriginalSeries.setPrimaryRange(left, right); |
| + } |
| + } |
| + |
| + /** |
| + * A chart axis that represents time. |
| + */ |
| + public static class TimeAxis implements ChartAxis { |
| + private static final int FIRST_DAY_OF_WEEK = Calendar.getInstance().getFirstDayOfWeek() - 1; |
| + |
| + private long mMin; |
| + private long mMax; |
| + private float mSize; |
| + |
| + public TimeAxis() { |
| + final long currentTime = System.currentTimeMillis(); |
| + setBounds(currentTime - DateUtils.DAY_IN_MILLIS * DAYS_IN_CHART, currentTime); |
| + } |
| + |
| + /** |
| + * Generates a hash code for multiple values. The hash code is generated by |
| + * calling {@link Arrays#hashCode(Object[])}. |
| + * |
| + * <p>This is useful for implementing {@link Object#hashCode()}. For example, |
| + * in an object that has three properties, {@code x}, {@code y}, and |
| + * {@code z}, one could write: |
| + * <pre> |
| + * public int hashCode() { |
| + * return Objects.hashCode(getX(), getY(), getZ()); |
| + * }</pre> |
| + * |
| + * <b>Warning</b>: When a single object is supplied, the returned hash code |
| + * does not equal the hash code of that object. |
| + */ |
| + public int objectsHashCode(Object... objects) { |
| + return Arrays.hashCode(objects); |
| + } |
| + |
| + @Override |
| + public int hashCode() { |
| + return objectsHashCode(mMin, mMax, mSize); |
| + } |
| + |
| + @Override |
| + public boolean setBounds(long min, long max) { |
| + if (mMin != min || mMax != max) { |
| + mMin = min; |
| + mMax = max; |
| + return true; |
| + } else { |
| + return false; |
| + } |
| + } |
| + |
| + @Override |
| + public boolean setSize(float size) { |
| + if (mSize != size) { |
| + mSize = size; |
| + return true; |
| + } else { |
| + return false; |
| + } |
| + } |
| + |
| + @Override |
| + public float convertToPoint(long value) { |
| + return (mSize * (value - mMin)) / (mMax - mMin); |
| + } |
| + |
| + @Override |
| + public long convertToValue(float point) { |
| + return (long) (mMin + ((point * (mMax - mMin)) / mSize)); |
| + } |
| + |
| + @Override |
| + public long buildLabel(Resources res, SpannableStringBuilder builder, long value) { |
| + // TODO: convert to better string |
|
bengr
2015/01/26 19:24:57
Remove.
newt (away)
2015/01/27 02:35:53
Done.
|
| + builder.replace(0, builder.length(), Long.toString(value)); |
| + return value; |
| + } |
| + |
| + @Override |
| + public float[] getTickPoints() { |
| + final float[] ticks = new float[32]; |
| + int i = 0; |
| + |
| + // tick mark for first day of each week |
|
bengr
2015/01/26 19:24:57
// Add a tick mark for the first day of each week.
|
| + final Time time = new Time(); |
| + time.set(mMax); |
| + time.monthDay -= time.weekDay - FIRST_DAY_OF_WEEK; |
| + time.hour = time.minute = time.second = 0; |
| + |
| + time.normalize(true); |
| + long timeMillis = time.toMillis(true); |
| + while (timeMillis > mMin) { |
| + if (timeMillis <= mMax) { |
| + ticks[i++] = convertToPoint(timeMillis); |
| + } |
| + time.monthDay -= 7; |
| + time.normalize(true); |
| + timeMillis = time.toMillis(true); |
| + } |
| + |
| + return Arrays.copyOf(ticks, i); |
| + } |
| + |
| + @Override |
| + public int shouldAdjustAxis(long value) { |
| + // time axis never adjusts |
|
bengr
2015/01/26 19:24:57
// The time axis never adjusts.
|
| + return 0; |
| + } |
| + } |
| + |
| + /** |
| + * A chart axis that represents aggregate transmitted data. |
| + */ |
| + public static class DataAxis implements ChartAxis { |
| + private long mMin; |
| + private long mMax; |
| + private float mSize; |
| + |
| + private static final boolean LOG_SCALE = false; |
| + |
| + public int objectsHashCode(Object... objects) { |
| + return Arrays.hashCode(objects); |
| + } |
| + |
| + @Override |
| + public int hashCode() { |
| + return objectsHashCode(mMin, mMax, mSize); |
| + } |
| + |
| + @Override |
| + public boolean setBounds(long min, long max) { |
| + if (mMin != min || mMax != max) { |
| + mMin = min; |
| + mMax = max; |
| + return true; |
| + } else { |
| + return false; |
| + } |
| + } |
| + |
| + @Override |
| + public boolean setSize(float size) { |
| + if (mSize != size) { |
| + mSize = size; |
| + return true; |
| + } else { |
| + return false; |
| + } |
| + } |
| + |
| + @Override |
| + public float convertToPoint(long value) { |
| + if (LOG_SCALE) { |
| + // derived polynomial fit to make lower values more visible |
|
bengr
2015/01/26 19:24:57
Use a derived polynomial fit to make the lower val
|
| + final double normalized = ((double) value - mMin) / (mMax - mMin); |
| + final double fraction = Math.pow(10, |
| + 0.36884343106175121463 * Math.log10(normalized) + -0.04328199452018252624); |
| + return (float) (fraction * mSize); |
| + } else { |
| + return (mSize * (value - mMin)) / (mMax - mMin); |
| + } |
| + } |
| + |
| + @Override |
| + public long convertToValue(float point) { |
| + if (LOG_SCALE) { |
| + final double normalized = point / mSize; |
| + final double fraction = 1.3102228476089056629 |
| + * Math.pow(normalized, 2.7111774693164631640); |
| + return (long) (mMin + (fraction * (mMax - mMin))); |
| + } else { |
| + return (long) (mMin + ((point * (mMax - mMin)) / mSize)); |
| + } |
| + } |
| + |
| + private static final Object sSpanSize = new Object(); |
| + private static final Object sSpanUnit = new Object(); |
| + |
| + @Override |
| + public long buildLabel(Resources res, SpannableStringBuilder builder, long value) { |
| + |
| + final CharSequence unit; |
| + final long unitFactor; |
| + if (value < 1000 * MB_IN_BYTES) { |
| + unit = "MB"; // TODO: res.getText(R.string.origin_settings_storage_mbytes); |
|
bengr
2015/01/26 19:24:57
Make these TODOs bengr.
newt (away)
2015/01/27 02:35:53
Done.
|
| + unitFactor = MB_IN_BYTES; |
| + } else { |
| + unit = "GB"; // TODO: res.getText(R.string.origin_settings_storage_gbytes); |
| + unitFactor = GB_IN_BYTES; |
| + } |
| + |
| + final double result = (double) value / unitFactor; |
| + final double resultRounded; |
| + final CharSequence size; |
| + |
| + if (result < 10) { |
| + size = String.format(Locale.getDefault(), "%.1f", result); |
| + resultRounded = (unitFactor * Math.round(result * 10)) / 10d; |
| + } else { |
| + size = String.format(Locale.getDefault(), "%.0f", result); |
| + resultRounded = unitFactor * Math.round(result); |
| + } |
| + |
| + setText(builder, sSpanSize, size, "^1"); |
| + setText(builder, sSpanUnit, unit, "^2"); |
| + |
| + return (long) resultRounded; |
| + } |
| + |
| + @Override |
| + public float[] getTickPoints() { |
| + final long range = mMax - mMin; |
| + |
| + // target about 16 ticks on screen, rounded to nearest power of 2 |
|
bengr
2015/01/26 19:24:57
Target .. 2.
|
| + final long tickJump = roundUpToPowerOfTwo(range / 16); |
| + final int tickCount = (int) (range / tickJump); |
| + final float[] tickPoints = new float[tickCount]; |
| + long value = mMin; |
| + for (int i = 0; i < tickPoints.length; i++) { |
| + tickPoints[i] = convertToPoint(value); |
| + value += tickJump; |
| + } |
| + |
| + return tickPoints; |
| + } |
| + |
| + @Override |
| + public int shouldAdjustAxis(long value) { |
| + final float point = convertToPoint(value); |
| + if (point < mSize * 0.1) { |
| + return -1; |
| + } else if (point > mSize * 0.85) { |
| + return 1; |
| + } else { |
| + return 0; |
| + } |
| + } |
| + } |
| + |
| + private static void setText( |
| + SpannableStringBuilder builder, Object key, CharSequence text, String bootstrap) { |
| + int start = builder.getSpanStart(key); |
| + int end = builder.getSpanEnd(key); |
| + if (start == -1) { |
| + start = TextUtils.indexOf(builder, bootstrap); |
| + end = start + bootstrap.length(); |
| + builder.setSpan(key, start, end, Spannable.SPAN_INCLUSIVE_INCLUSIVE); |
| + } |
| + builder.replace(start, end, text); |
| + } |
| + |
| + private static long roundUpToPowerOfTwo(long i) { |
| + // NOTE: borrowed from Hashtable.roundUpToPowerOfTwo() |
|
bengr
2015/01/26 19:24:57
Borrowed .. Two().
|
| + |
| + i--; // If input is a power of two, shift its high-order bit right |
|
bengr
2015/01/26 19:24:57
right.
|
| + |
| + // "Smear" the high-order bit all the way to the right |
| + i |= i >>> 1; |
| + i |= i >>> 2; |
| + i |= i >>> 4; |
| + i |= i >>> 8; |
| + i |= i >>> 16; |
| + i |= i >>> 32; |
| + |
| + i++; |
| + |
| + return i > 0 ? i : Long.MAX_VALUE; |
| + } |
| +} |