summaryrefslogtreecommitdiffstats
path: root/src/com/android/settings/widget
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/settings/widget')
-rw-r--r--src/com/android/settings/widget/ChartAxis.java38
-rw-r--r--src/com/android/settings/widget/ChartGridView.java102
-rw-r--r--src/com/android/settings/widget/ChartNetworkSeriesView.java222
-rw-r--r--src/com/android/settings/widget/ChartSweepView.java437
-rw-r--r--src/com/android/settings/widget/ChartView.java119
-rw-r--r--src/com/android/settings/widget/DataUsageChartView.java406
-rw-r--r--src/com/android/settings/widget/InvertedChartAxis.java67
7 files changed, 1391 insertions, 0 deletions
diff --git a/src/com/android/settings/widget/ChartAxis.java b/src/com/android/settings/widget/ChartAxis.java
new file mode 100644
index 0000000..463541f
--- /dev/null
+++ b/src/com/android/settings/widget/ChartAxis.java
@@ -0,0 +1,38 @@
+/*
+ * 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 com.android.settings.widget;
+
+import android.content.res.Resources;
+import android.text.SpannableStringBuilder;
+
+/**
+ * Axis along a {@link ChartView} that knows how to convert between raw point
+ * and screen coordinate systems.
+ */
+public interface ChartAxis {
+
+ public void setBounds(long min, long max);
+ public void setSize(float size);
+
+ public float convertToPoint(long value);
+ public long convertToValue(float point);
+
+ public void buildLabel(Resources res, SpannableStringBuilder builder, long value);
+
+ public float[] getTickPoints();
+
+}
diff --git a/src/com/android/settings/widget/ChartGridView.java b/src/com/android/settings/widget/ChartGridView.java
new file mode 100644
index 0000000..7a83fbf
--- /dev/null
+++ b/src/com/android/settings/widget/ChartGridView.java
@@ -0,0 +1,102 @@
+/*
+ * 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 com.android.settings.widget;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.View;
+
+import com.android.settings.R;
+import com.google.common.base.Preconditions;
+
+/**
+ * Background of {@link ChartView} that renders grid lines as requested by
+ * {@link ChartAxis#getTickPoints()}.
+ */
+public class ChartGridView extends View {
+
+ // TODO: eventually teach about drawing chart labels
+
+ private ChartAxis mHoriz;
+ private ChartAxis mVert;
+
+ private Drawable mPrimary;
+ private Drawable mSecondary;
+ private Drawable mBorder;
+
+ public ChartGridView(Context context) {
+ this(context, null, 0);
+ }
+
+ public ChartGridView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public ChartGridView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ setWillNotDraw(false);
+
+ final TypedArray a = context.obtainStyledAttributes(
+ attrs, R.styleable.ChartGridView, defStyle, 0);
+
+ mPrimary = a.getDrawable(R.styleable.ChartGridView_primaryDrawable);
+ mSecondary = a.getDrawable(R.styleable.ChartGridView_secondaryDrawable);
+ mBorder = a.getDrawable(R.styleable.ChartGridView_borderDrawable);
+ // TODO: eventually read labelColor
+
+ a.recycle();
+ }
+
+ void init(ChartAxis horiz, ChartAxis vert) {
+ mHoriz = Preconditions.checkNotNull(horiz, "missing horiz");
+ mVert = Preconditions.checkNotNull(vert, "missing vert");
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ final int width = getWidth();
+ final int height = getHeight();
+
+ final Drawable secondary = mSecondary;
+ final int secondaryHeight = mSecondary.getIntrinsicHeight();
+
+ final float[] vertTicks = mVert.getTickPoints();
+ for (float y : vertTicks) {
+ final int bottom = (int) Math.min(y + secondaryHeight, height);
+ secondary.setBounds(0, (int) y, width, bottom);
+ secondary.draw(canvas);
+ }
+
+ final Drawable primary = mPrimary;
+ final int primaryWidth = mPrimary.getIntrinsicWidth();
+ final int primaryHeight = mPrimary.getIntrinsicHeight();
+
+ final float[] horizTicks = mHoriz.getTickPoints();
+ for (float x : horizTicks) {
+ final int right = (int) Math.min(x + primaryWidth, width);
+ primary.setBounds((int) x, 0, right, height);
+ primary.draw(canvas);
+ }
+
+ mBorder.setBounds(0, 0, width, height);
+ mBorder.draw(canvas);
+ }
+}
diff --git a/src/com/android/settings/widget/ChartNetworkSeriesView.java b/src/com/android/settings/widget/ChartNetworkSeriesView.java
new file mode 100644
index 0000000..10d8976
--- /dev/null
+++ b/src/com/android/settings/widget/ChartNetworkSeriesView.java
@@ -0,0 +1,222 @@
+/*
+ * 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 com.android.settings.widget;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Paint.Style;
+import android.graphics.Path;
+import android.graphics.RectF;
+import android.net.NetworkStatsHistory;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.View;
+
+import com.android.settings.R;
+import com.google.common.base.Preconditions;
+
+/**
+ * {@link NetworkStatsHistory} series to render inside a {@link ChartView},
+ * using {@link ChartAxis} to map into screen coordinates.
+ */
+public class ChartNetworkSeriesView extends View {
+ private static final String TAG = "ChartNetworkSeriesView";
+ private static final boolean LOGD = true;
+
+ private ChartAxis mHoriz;
+ private ChartAxis mVert;
+
+ private Paint mPaintStroke;
+ private Paint mPaintFill;
+ private Paint mPaintFillSecondary;
+
+ private NetworkStatsHistory mStats;
+
+ private Path mPathStroke;
+ private Path mPathFill;
+
+ private long mPrimaryLeft;
+ private long mPrimaryRight;
+
+ public ChartNetworkSeriesView(Context context) {
+ this(context, null, 0);
+ }
+
+ public ChartNetworkSeriesView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public ChartNetworkSeriesView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ final TypedArray a = context.obtainStyledAttributes(
+ attrs, R.styleable.ChartNetworkSeriesView, defStyle, 0);
+
+ final int stroke = a.getColor(R.styleable.ChartNetworkSeriesView_strokeColor, Color.RED);
+ final int fill = a.getColor(R.styleable.ChartNetworkSeriesView_fillColor, Color.RED);
+ final int fillSecondary = a.getColor(
+ R.styleable.ChartNetworkSeriesView_fillColorSecondary, Color.RED);
+
+ setChartColor(stroke, fill, fillSecondary);
+ setWillNotDraw(false);
+
+ a.recycle();
+
+ mPathStroke = new Path();
+ mPathFill = new Path();
+ }
+
+ void init(ChartAxis horiz, ChartAxis vert) {
+ mHoriz = Preconditions.checkNotNull(horiz, "missing horiz");
+ mVert = Preconditions.checkNotNull(vert, "missing vert");
+ }
+
+ public void setChartColor(int stroke, int fill, int fillSecondary) {
+ mPaintStroke = new Paint();
+ mPaintStroke.setStrokeWidth(6.0f);
+ mPaintStroke.setColor(stroke);
+ mPaintStroke.setStyle(Style.STROKE);
+ mPaintStroke.setAntiAlias(true);
+
+ mPaintFill = new Paint();
+ mPaintFill.setColor(fill);
+ mPaintFill.setStyle(Style.FILL);
+ mPaintFill.setAntiAlias(true);
+
+ mPaintFillSecondary = new Paint();
+ mPaintFillSecondary.setColor(fillSecondary);
+ mPaintFillSecondary.setStyle(Style.FILL);
+ mPaintFillSecondary.setAntiAlias(true);
+ }
+
+ public void bindNetworkStats(NetworkStatsHistory stats) {
+ mStats = stats;
+
+ mPathStroke.reset();
+ mPathFill.reset();
+ invalidate();
+ }
+
+ /**
+ * Set the range to paint with {@link #mPaintFill}, leaving the remaining
+ * area to be painted with {@link #mPaintFillSecondary}.
+ */
+ public void setPrimaryRange(long left, long right) {
+ mPrimaryLeft = left;
+ mPrimaryRight = right;
+ invalidate();
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ generatePath();
+ }
+
+ /**
+ * Erase any existing {@link Path} and generate series outline based on
+ * currently bound {@link NetworkStatsHistory} data.
+ */
+ public void generatePath() {
+ if (LOGD) Log.d(TAG, "generatePath()");
+
+ mPathStroke.reset();
+ mPathFill.reset();
+
+ // bail when not enough stats to render
+ if (mStats == null || mStats.size() < 2) return;
+
+ final int width = getWidth();
+ final int height = getHeight();
+
+ boolean started = false;
+ float firstX = 0;
+ float lastX = 0;
+ float lastY = 0;
+
+ // TODO: count fractional data from first bucket crossing start;
+ // currently it only accepts first full bucket.
+
+ long totalData = 0;
+
+ NetworkStatsHistory.Entry entry = null;
+ for (int i = 0; i < mStats.size(); i++) {
+ entry = mStats.getValues(i, entry);
+
+ final float x = mHoriz.convertToPoint(entry.bucketStart);
+ final float y = mVert.convertToPoint(totalData);
+
+ // skip until we find first stats on screen
+ if (i > 0 && !started && x > 0) {
+ mPathStroke.moveTo(lastX, lastY);
+ mPathFill.moveTo(lastX, lastY);
+ started = true;
+ firstX = x;
+ }
+
+ if (started) {
+ mPathStroke.lineTo(x, y);
+ mPathFill.lineTo(x, y);
+ totalData += entry.rxBytes + entry.txBytes;
+ }
+
+ // skip if beyond view
+ if (x > width) break;
+
+ lastX = x;
+ lastY = y;
+ }
+
+ if (LOGD) {
+ final RectF bounds = new RectF();
+ mPathFill.computeBounds(bounds, true);
+ Log.d(TAG, "onLayout() rendered with bounds=" + bounds.toString() + " and totalData="
+ + totalData);
+ }
+
+ // drop to bottom of graph from current location
+ mPathFill.lineTo(lastX, height);
+ mPathFill.lineTo(firstX, height);
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ int save;
+
+ final float primaryLeftPoint = mHoriz.convertToPoint(mPrimaryLeft);
+ final float primaryRightPoint = mHoriz.convertToPoint(mPrimaryRight);
+
+ save = canvas.save();
+ canvas.clipRect(0, 0, primaryLeftPoint, getHeight());
+ canvas.drawPath(mPathFill, mPaintFillSecondary);
+ canvas.restoreToCount(save);
+
+ save = canvas.save();
+ canvas.clipRect(primaryRightPoint, 0, getWidth(), getHeight());
+ canvas.drawPath(mPathFill, mPaintFillSecondary);
+ canvas.restoreToCount(save);
+
+ save = canvas.save();
+ canvas.clipRect(primaryLeftPoint, 0, primaryRightPoint, getHeight());
+ canvas.drawPath(mPathFill, mPaintFill);
+ canvas.drawPath(mPathStroke, mPaintStroke);
+ canvas.restoreToCount(save);
+
+ }
+}
diff --git a/src/com/android/settings/widget/ChartSweepView.java b/src/com/android/settings/widget/ChartSweepView.java
new file mode 100644
index 0000000..4e37657
--- /dev/null
+++ b/src/com/android/settings/widget/ChartSweepView.java
@@ -0,0 +1,437 @@
+/*
+ * 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 com.android.settings.widget;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.text.DynamicLayout;
+import android.text.Layout.Alignment;
+import android.text.SpannableStringBuilder;
+import android.text.TextPaint;
+import android.util.AttributeSet;
+import android.util.MathUtils;
+import android.view.MotionEvent;
+import android.view.View;
+import android.widget.FrameLayout;
+
+import com.android.settings.R;
+import com.google.common.base.Preconditions;
+
+/**
+ * Sweep across a {@link ChartView} at a specific {@link ChartAxis} value, which
+ * a user can drag.
+ */
+public class ChartSweepView extends FrameLayout {
+
+ private Drawable mSweep;
+ private Rect mSweepPadding = new Rect();
+ private Point mSweepOffset = new Point();
+
+ private Rect mMargins = new Rect();
+
+ private int mFollowAxis;
+
+ private int mLabelSize;
+ private int mLabelTemplateRes;
+ private int mLabelColor;
+
+ private SpannableStringBuilder mLabelTemplate;
+ private DynamicLayout mLabelLayout;
+
+ private ChartAxis mAxis;
+ private long mValue;
+
+ private ChartSweepView mClampAfter;
+ private ChartSweepView mClampBefore;
+
+ public static final int HORIZONTAL = 0;
+ public static final int VERTICAL = 1;
+
+ public interface OnSweepListener {
+ public void onSweep(ChartSweepView sweep, boolean sweepDone);
+ }
+
+ private OnSweepListener mListener;
+ private MotionEvent mTracking;
+
+ public ChartSweepView(Context context) {
+ this(context, null, 0);
+ }
+
+ public ChartSweepView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public ChartSweepView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ final TypedArray a = context.obtainStyledAttributes(
+ attrs, R.styleable.ChartSweepView, defStyle, 0);
+
+ setSweepDrawable(a.getDrawable(R.styleable.ChartSweepView_sweepDrawable));
+ setFollowAxis(a.getInt(R.styleable.ChartSweepView_followAxis, -1));
+
+ setLabelSize(a.getDimensionPixelSize(R.styleable.ChartSweepView_labelSize, 0));
+ setLabelTemplate(a.getResourceId(R.styleable.ChartSweepView_labelTemplate, 0));
+ setLabelColor(a.getColor(R.styleable.ChartSweepView_labelColor, Color.BLUE));
+
+ a.recycle();
+
+ setClipToPadding(false);
+ setClipChildren(false);
+ setWillNotDraw(false);
+ }
+
+ void init(ChartAxis axis) {
+ mAxis = Preconditions.checkNotNull(axis, "missing axis");
+ }
+
+ public int getFollowAxis() {
+ return mFollowAxis;
+ }
+
+ public Rect getMargins() {
+ return mMargins;
+ }
+
+ /**
+ * Return the number of pixels that the "target" area is inset from the
+ * {@link View} edge, along the current {@link #setFollowAxis(int)}.
+ */
+ private float getTargetInset() {
+ if (mFollowAxis == VERTICAL) {
+ final float targetHeight = mSweep.getIntrinsicHeight() - mSweepPadding.top
+ - mSweepPadding.bottom;
+ return mSweepPadding.top + (targetHeight / 2);
+ } else {
+ final float targetWidth = mSweep.getIntrinsicWidth() - mSweepPadding.left
+ - mSweepPadding.right;
+ return mSweepPadding.left + (targetWidth / 2);
+ }
+ }
+
+ public void addOnSweepListener(OnSweepListener listener) {
+ mListener = listener;
+ }
+
+ private void dispatchOnSweep(boolean sweepDone) {
+ if (mListener != null) {
+ mListener.onSweep(this, sweepDone);
+ }
+ }
+
+ @Override
+ public void setEnabled(boolean enabled) {
+ super.setEnabled(enabled);
+ requestLayout();
+ }
+
+ public void setSweepDrawable(Drawable sweep) {
+ if (mSweep != null) {
+ mSweep.setCallback(null);
+ unscheduleDrawable(mSweep);
+ }
+
+ if (sweep != null) {
+ sweep.setCallback(this);
+ if (sweep.isStateful()) {
+ sweep.setState(getDrawableState());
+ }
+ sweep.setVisible(getVisibility() == VISIBLE, false);
+ mSweep = sweep;
+ sweep.getPadding(mSweepPadding);
+ } else {
+ mSweep = null;
+ }
+
+ invalidate();
+ }
+
+ public void setFollowAxis(int followAxis) {
+ mFollowAxis = followAxis;
+ }
+
+ public void setLabelSize(int size) {
+ mLabelSize = size;
+ invalidateLabelTemplate();
+ }
+
+ public void setLabelTemplate(int resId) {
+ mLabelTemplateRes = resId;
+ invalidateLabelTemplate();
+ }
+
+ public void setLabelColor(int color) {
+ mLabelColor = color;
+ invalidateLabelTemplate();
+ }
+
+ private void invalidateLabelTemplate() {
+ if (mLabelTemplateRes != 0) {
+ final CharSequence template = getResources().getText(mLabelTemplateRes);
+
+ final TextPaint paint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
+ paint.density = getResources().getDisplayMetrics().density;
+ paint.setCompatibilityScaling(getResources().getCompatibilityInfo().applicationScale);
+ paint.setColor(mLabelColor);
+
+ mLabelTemplate = new SpannableStringBuilder(template);
+ mLabelLayout = new DynamicLayout(
+ mLabelTemplate, paint, mLabelSize, Alignment.ALIGN_RIGHT, 1f, 0f, false);
+ invalidateLabel();
+
+ } else {
+ mLabelTemplate = null;
+ mLabelLayout = null;
+ }
+
+ invalidate();
+ requestLayout();
+ }
+
+ private void invalidateLabel() {
+ if (mLabelTemplate != null && mAxis != null) {
+ mAxis.buildLabel(getResources(), mLabelTemplate, mValue);
+ invalidate();
+ }
+ }
+
+ @Override
+ public void jumpDrawablesToCurrentState() {
+ super.jumpDrawablesToCurrentState();
+ if (mSweep != null) {
+ mSweep.jumpToCurrentState();
+ }
+ }
+
+ @Override
+ public void setVisibility(int visibility) {
+ super.setVisibility(visibility);
+ if (mSweep != null) {
+ mSweep.setVisible(visibility == VISIBLE, false);
+ }
+ }
+
+ @Override
+ protected boolean verifyDrawable(Drawable who) {
+ return who == mSweep || super.verifyDrawable(who);
+ }
+
+ public ChartAxis getAxis() {
+ return mAxis;
+ }
+
+ public void setValue(long value) {
+ mValue = value;
+ invalidateLabel();
+ }
+
+ public long getValue() {
+ return mValue;
+ }
+
+ public float getPoint() {
+ if (isEnabled()) {
+ return mAxis.convertToPoint(mValue);
+ } else {
+ // when disabled, show along top edge
+ return 0;
+ }
+ }
+
+ public void setClampAfter(ChartSweepView clampAfter) {
+ mClampAfter = clampAfter;
+ }
+
+ public void setClampBefore(ChartSweepView clampBefore) {
+ mClampBefore = clampBefore;
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent event) {
+ if (!isEnabled()) return false;
+
+ final View parent = (View) getParent();
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN: {
+
+ // only start tracking when in sweet spot
+ final boolean accept;
+ if (mFollowAxis == VERTICAL) {
+ accept = event.getX() > getWidth() - (mSweepPadding.right * 2);
+ } else {
+ accept = event.getY() > getHeight() - (mSweepPadding.bottom * 2);
+ }
+
+ if (accept) {
+ mTracking = event.copy();
+ return true;
+ } else {
+ return false;
+ }
+ }
+ case MotionEvent.ACTION_MOVE: {
+ getParent().requestDisallowInterceptTouchEvent(true);
+
+ // content area of parent
+ final Rect parentContent = new Rect(parent.getPaddingLeft(), parent.getPaddingTop(),
+ parent.getWidth() - parent.getPaddingRight(),
+ parent.getHeight() - parent.getPaddingBottom());
+ final Rect clampRect = computeClampRect(parentContent);
+
+ if (mFollowAxis == VERTICAL) {
+ final float currentTargetY = getTop() - mMargins.top;
+ final float requestedTargetY = currentTargetY
+ + (event.getRawY() - mTracking.getRawY());
+ final float clampedTargetY = MathUtils.constrain(
+ requestedTargetY, clampRect.top, clampRect.bottom);
+ setTranslationY(clampedTargetY - currentTargetY);
+
+ setValue(mAxis.convertToValue(clampedTargetY - parentContent.top));
+ } else {
+ final float currentTargetX = getLeft() - mMargins.left;
+ final float requestedTargetX = currentTargetX
+ + (event.getRawX() - mTracking.getRawX());
+ final float clampedTargetX = MathUtils.constrain(
+ requestedTargetX, clampRect.left, clampRect.right);
+ setTranslationX(clampedTargetX - currentTargetX);
+
+ setValue(mAxis.convertToValue(clampedTargetX - parentContent.left));
+ }
+
+ dispatchOnSweep(false);
+ return true;
+ }
+ case MotionEvent.ACTION_UP: {
+ mTracking = null;
+ dispatchOnSweep(true);
+ setTranslationX(0);
+ setTranslationY(0);
+ requestLayout();
+ return true;
+ }
+ default: {
+ return false;
+ }
+ }
+ }
+
+ /**
+ * Compute {@link Rect} in {@link #getParent()} coordinates that we should
+ * be clamped inside of, usually from {@link #setClampAfter(ChartSweepView)}
+ * style rules.
+ */
+ private Rect computeClampRect(Rect parentContent) {
+ final Rect clampRect = new Rect(parentContent);
+
+ final ChartSweepView after = mClampAfter;
+ final ChartSweepView before = mClampBefore;
+
+ if (mFollowAxis == VERTICAL) {
+ if (after != null) {
+ clampRect.top += after.getPoint();
+ }
+ if (before != null) {
+ clampRect.bottom -= clampRect.height() - before.getPoint();
+ }
+ } else {
+ if (after != null) {
+ clampRect.left += after.getPoint();
+ }
+ if (before != null) {
+ clampRect.right -= clampRect.width() - before.getPoint();
+ }
+ }
+ return clampRect;
+ }
+
+ @Override
+ protected void drawableStateChanged() {
+ super.drawableStateChanged();
+ if (mSweep.isStateful()) {
+ mSweep.setState(getDrawableState());
+ }
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+
+ // TODO: handle vertical labels
+ if (isEnabled() && mLabelLayout != null) {
+ final int sweepHeight = mSweep.getIntrinsicHeight();
+ final int templateHeight = mLabelLayout.getHeight();
+
+ mSweepOffset.x = 0;
+ mSweepOffset.y = (int) ((templateHeight / 2) - getTargetInset());
+ setMeasuredDimension(mSweep.getIntrinsicWidth(), Math.max(sweepHeight, templateHeight));
+
+ } else {
+ mSweepOffset.x = 0;
+ mSweepOffset.y = 0;
+ setMeasuredDimension(mSweep.getIntrinsicWidth(), mSweep.getIntrinsicHeight());
+ }
+
+ if (mFollowAxis == VERTICAL) {
+ final int targetHeight = mSweep.getIntrinsicHeight() - mSweepPadding.top
+ - mSweepPadding.bottom;
+ mMargins.top = -(mSweepPadding.top + (targetHeight / 2));
+ mMargins.bottom = 0;
+ mMargins.left = -mSweepPadding.left;
+ mMargins.right = mSweepPadding.right;
+ } else {
+ final int targetWidth = mSweep.getIntrinsicWidth() - mSweepPadding.left
+ - mSweepPadding.right;
+ mMargins.left = -(mSweepPadding.left + (targetWidth / 2));
+ mMargins.right = 0;
+ mMargins.top = -mSweepPadding.top;
+ mMargins.bottom = mSweepPadding.bottom;
+ }
+
+ mMargins.offset(-mSweepOffset.x, -mSweepOffset.y);
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ final int width = getWidth();
+ final int height = getHeight();
+
+ final int labelSize;
+ if (isEnabled() && mLabelLayout != null) {
+ mLabelLayout.draw(canvas);
+ labelSize = mLabelSize;
+ } else {
+ labelSize = 0;
+ }
+
+ if (mFollowAxis == VERTICAL) {
+ mSweep.setBounds(labelSize, mSweepOffset.y, width,
+ mSweepOffset.y + mSweep.getIntrinsicHeight());
+ } else {
+ mSweep.setBounds(mSweepOffset.x, labelSize,
+ mSweepOffset.x + mSweep.getIntrinsicWidth(), height);
+ }
+
+ mSweep.draw(canvas);
+ }
+
+}
diff --git a/src/com/android/settings/widget/ChartView.java b/src/com/android/settings/widget/ChartView.java
new file mode 100644
index 0000000..a5b8b09
--- /dev/null
+++ b/src/com/android/settings/widget/ChartView.java
@@ -0,0 +1,119 @@
+/*
+ * 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 com.android.settings.widget;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.View;
+import android.widget.FrameLayout;
+
+/**
+ * Container for two-dimensional chart, drawn with a combination of
+ * {@link ChartGridView}, {@link ChartNetworkSeriesView} and {@link ChartSweepView}
+ * children. The entire chart uses {@link ChartAxis} to map between raw values
+ * and screen coordinates.
+ */
+public class ChartView extends FrameLayout {
+ private static final String TAG = "ChartView";
+
+ // TODO: extend something that supports two-dimensional scrolling
+
+ private static final int SWEEP_GRAVITY = Gravity.TOP | Gravity.LEFT;
+
+ ChartAxis mHoriz;
+ ChartAxis mVert;
+
+ private Rect mContent = new Rect();
+
+ public ChartView(Context context) {
+ this(context, null, 0);
+ }
+
+ public ChartView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public ChartView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+
+ setClipToPadding(false);
+ setClipChildren(false);
+ }
+
+ void init(ChartAxis horiz, ChartAxis vert) {
+ mHoriz = checkNotNull(horiz, "missing horiz");
+ mVert = checkNotNull(vert, "missing vert");
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int l, int t, int r, int b) {
+ mContent.set(getPaddingLeft(), getPaddingTop(), r - l - getPaddingRight(),
+ b - t - getPaddingBottom());
+ final int width = mContent.width();
+ final int height = mContent.height();
+
+ // no scrolling yet, so tell dimensions to fill exactly
+ mHoriz.setSize(width);
+ mVert.setSize(height);
+
+ final Rect parentRect = new Rect();
+ final Rect childRect = new Rect();
+
+ for (int i = 0; i < getChildCount(); i++) {
+ final View child = getChildAt(i);
+ final LayoutParams params = (LayoutParams) child.getLayoutParams();
+
+ parentRect.set(mContent);
+
+ if (child instanceof ChartNetworkSeriesView || child instanceof ChartGridView) {
+ // series are always laid out to fill entire graph area
+ // TODO: handle scrolling for series larger than content area
+ Gravity.apply(params.gravity, width, height, parentRect, childRect);
+ child.layout(childRect.left, childRect.top, childRect.right, childRect.bottom);
+
+ } else if (child instanceof ChartSweepView) {
+ // sweep is always placed along specific dimension
+ final ChartSweepView sweep = (ChartSweepView) child;
+ final Rect sweepMargins = sweep.getMargins();
+
+ if (sweep.getFollowAxis() == ChartSweepView.VERTICAL) {
+ parentRect.top += sweepMargins.top + (int) sweep.getPoint();
+ parentRect.bottom = parentRect.top;
+ parentRect.left += sweepMargins.left;
+ parentRect.right += sweepMargins.right;
+ Gravity.apply(SWEEP_GRAVITY, parentRect.width(), child.getMeasuredHeight(),
+ parentRect, childRect);
+
+ } else {
+ parentRect.left += sweepMargins.left + (int) sweep.getPoint();
+ parentRect.right = parentRect.left;
+ parentRect.top += sweepMargins.top;
+ parentRect.bottom += sweepMargins.bottom;
+ Gravity.apply(SWEEP_GRAVITY, child.getMeasuredWidth(), parentRect.height(),
+ parentRect, childRect);
+ }
+ }
+
+ child.layout(childRect.left, childRect.top, childRect.right, childRect.bottom);
+ }
+ }
+
+}
diff --git a/src/com/android/settings/widget/DataUsageChartView.java b/src/com/android/settings/widget/DataUsageChartView.java
new file mode 100644
index 0000000..affede0
--- /dev/null
+++ b/src/com/android/settings/widget/DataUsageChartView.java
@@ -0,0 +1,406 @@
+/*
+ * 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 com.android.settings.widget;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.net.NetworkPolicy;
+import android.net.NetworkStatsHistory;
+import android.text.Spannable;
+import android.text.SpannableStringBuilder;
+import android.text.TextUtils;
+import android.text.format.DateUtils;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+
+import com.android.settings.R;
+import com.android.settings.widget.ChartSweepView.OnSweepListener;
+
+/**
+ * Specific {@link ChartView} that displays {@link ChartNetworkSeriesView} along
+ * with {@link ChartSweepView} for inspection ranges and warning/limits.
+ */
+public class DataUsageChartView extends ChartView {
+
+ private static final long KB_IN_BYTES = 1024;
+ private static final long MB_IN_BYTES = KB_IN_BYTES * 1024;
+ private static final long GB_IN_BYTES = MB_IN_BYTES * 1024;
+
+ // TODO: enforce that sweeps cant cross each other
+
+ private ChartGridView mGrid;
+ private ChartNetworkSeriesView mSeries;
+ private ChartNetworkSeriesView mDetailSeries;
+
+ private ChartSweepView mSweepLeft;
+ private ChartSweepView mSweepRight;
+ private ChartSweepView mSweepWarning;
+ private ChartSweepView mSweepLimit;
+
+ public interface DataUsageChartListener {
+ public void onInspectRangeChanged();
+ public void onWarningChanged();
+ public void onLimitChanged();
+ }
+
+ private DataUsageChartListener mListener;
+
+ public DataUsageChartView(Context context) {
+ this(context, null, 0);
+ }
+
+ public DataUsageChartView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public DataUsageChartView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ init(new TimeAxis(), new InvertedChartAxis(new DataAxis()));
+ }
+
+ @Override
+ protected void onFinishInflate() {
+ super.onFinishInflate();
+
+ mGrid = (ChartGridView) findViewById(R.id.grid);
+ mSeries = (ChartNetworkSeriesView) findViewById(R.id.series);
+ mDetailSeries = (ChartNetworkSeriesView) findViewById(R.id.detail_series);
+ mDetailSeries.setVisibility(View.GONE);
+
+ mSweepLeft = (ChartSweepView) findViewById(R.id.sweep_left);
+ mSweepRight = (ChartSweepView) findViewById(R.id.sweep_right);
+ mSweepLimit = (ChartSweepView) findViewById(R.id.sweep_limit);
+ mSweepWarning = (ChartSweepView) findViewById(R.id.sweep_warning);
+
+ // prevent sweeps from crossing each other
+ mSweepLeft.setClampBefore(mSweepRight);
+ mSweepRight.setClampAfter(mSweepLeft);
+ mSweepLimit.setClampBefore(mSweepWarning);
+ mSweepWarning.setClampAfter(mSweepLimit);
+
+ mSweepLeft.addOnSweepListener(mSweepListener);
+ mSweepRight.addOnSweepListener(mSweepListener);
+ mSweepWarning.addOnSweepListener(mWarningListener);
+ mSweepLimit.addOnSweepListener(mLimitListener);
+
+ // tell everyone about our axis
+ mGrid.init(mHoriz, mVert);
+ mSeries.init(mHoriz, mVert);
+ mDetailSeries.init(mHoriz, mVert);
+ mSweepLeft.init(mHoriz);
+ mSweepRight.init(mHoriz);
+ mSweepWarning.init(mVert);
+ mSweepLimit.init(mVert);
+
+ setActivated(false);
+ }
+
+ public void setListener(DataUsageChartListener listener) {
+ mListener = listener;
+ }
+
+ public void bindNetworkStats(NetworkStatsHistory stats) {
+ mSeries.bindNetworkStats(stats);
+ updatePrimaryRange();
+ requestLayout();
+ }
+
+ public void bindDetailNetworkStats(NetworkStatsHistory stats) {
+ mDetailSeries.bindNetworkStats(stats);
+ mDetailSeries.setVisibility(stats != null ? View.VISIBLE : View.GONE);
+ updatePrimaryRange();
+ requestLayout();
+ }
+
+ public void bindNetworkPolicy(NetworkPolicy policy) {
+ if (policy == null) {
+ mSweepLimit.setVisibility(View.INVISIBLE);
+ mSweepWarning.setVisibility(View.INVISIBLE);
+ return;
+ }
+
+ if (policy.limitBytes != NetworkPolicy.LIMIT_DISABLED) {
+ mSweepLimit.setVisibility(View.VISIBLE);
+ mSweepLimit.setValue(policy.limitBytes);
+ mSweepLimit.setEnabled(true);
+ } else {
+ mSweepLimit.setVisibility(View.VISIBLE);
+ mSweepLimit.setEnabled(false);
+ }
+
+ if (policy.warningBytes != NetworkPolicy.WARNING_DISABLED) {
+ mSweepWarning.setVisibility(View.VISIBLE);
+ mSweepWarning.setValue(policy.warningBytes);
+ } else {
+ mSweepWarning.setVisibility(View.INVISIBLE);
+ }
+
+ requestLayout();
+ }
+
+ private OnSweepListener mSweepListener = new OnSweepListener() {
+ public void onSweep(ChartSweepView sweep, boolean sweepDone) {
+ updatePrimaryRange();
+
+ // update detail list only when done sweeping
+ if (sweepDone && mListener != null) {
+ mListener.onInspectRangeChanged();
+ }
+ }
+ };
+
+ private OnSweepListener mWarningListener = new OnSweepListener() {
+ public void onSweep(ChartSweepView sweep, boolean sweepDone) {
+ if (sweepDone && mListener != null) {
+ mListener.onWarningChanged();
+ }
+ }
+ };
+
+ private OnSweepListener mLimitListener = new OnSweepListener() {
+ public void onSweep(ChartSweepView sweep, boolean sweepDone) {
+ if (sweepDone && mListener != null) {
+ mListener.onLimitChanged();
+ }
+ }
+ };
+
+ @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;
+ }
+ }
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ if (!isActivated()) {
+ return true;
+ } else {
+ return super.onInterceptTouchEvent(ev);
+ }
+ }
+
+ public long getInspectStart() {
+ return mSweepLeft.getValue();
+ }
+
+ public long getInspectEnd() {
+ return mSweepRight.getValue();
+ }
+
+ public long getWarningBytes() {
+ return mSweepWarning.getValue();
+ }
+
+ public long getLimitBytes() {
+ return mSweepLimit.getValue();
+ }
+
+ /**
+ * Set the exact time range that should be displayed, updating how
+ * {@link ChartNetworkSeriesView} paints. Moves inspection ranges to be the
+ * last "week" of available data, without triggering listener events.
+ */
+ public void setVisibleRange(long start, long end, long dataBoundary) {
+ mHoriz.setBounds(start, end);
+
+ // default sweeps to last week of data
+ final long halfRange = (end + start) / 2;
+ final long sweepMax = Math.min(end, dataBoundary);
+ final long sweepMin = Math.max(start, (sweepMax - DateUtils.WEEK_IN_MILLIS));
+
+ mSweepLeft.setValue(sweepMin);
+ mSweepRight.setValue(sweepMax);
+ updatePrimaryRange();
+
+ requestLayout();
+ mSeries.generatePath();
+ mSeries.invalidate();
+ }
+
+ private void updatePrimaryRange() {
+ final long left = mSweepLeft.getValue();
+ final long right = mSweepRight.getValue();
+
+ // prefer showing primary range on detail series, when available
+ if (mDetailSeries.getVisibility() == View.VISIBLE) {
+ mDetailSeries.setPrimaryRange(left, right);
+ mSeries.setPrimaryRange(0, 0);
+ } else {
+ mSeries.setPrimaryRange(left, right);
+ }
+ }
+
+ public static class TimeAxis implements ChartAxis {
+ private static final long TICK_INTERVAL = DateUtils.DAY_IN_MILLIS * 7;
+
+ private long mMin;
+ private long mMax;
+ private float mSize;
+
+ public TimeAxis() {
+ final long currentTime = System.currentTimeMillis();
+ setBounds(currentTime - DateUtils.DAY_IN_MILLIS * 30, currentTime);
+ }
+
+ /** {@inheritDoc} */
+ public void setBounds(long min, long max) {
+ mMin = min;
+ mMax = max;
+ }
+
+ /** {@inheritDoc} */
+ public void setSize(float size) {
+ this.mSize = size;
+ }
+
+ /** {@inheritDoc} */
+ public float convertToPoint(long value) {
+ return (mSize * (value - mMin)) / (mMax - mMin);
+ }
+
+ /** {@inheritDoc} */
+ public long convertToValue(float point) {
+ return (long) (mMin + ((point * (mMax - mMin)) / mSize));
+ }
+
+ /** {@inheritDoc} */
+ public void buildLabel(Resources res, SpannableStringBuilder builder, long value) {
+ // TODO: convert to better string
+ builder.replace(0, builder.length(), Long.toString(value));
+ }
+
+ /** {@inheritDoc} */
+ public float[] getTickPoints() {
+ // tick mark for every week
+ final int tickCount = (int) ((mMax - mMin) / TICK_INTERVAL);
+ final float[] tickPoints = new float[tickCount];
+ for (int i = 0; i < tickCount; i++) {
+ tickPoints[i] = convertToPoint(mMax - (TICK_INTERVAL * i));
+ }
+ return tickPoints;
+ }
+ }
+
+ public static class DataAxis implements ChartAxis {
+ private long mMin;
+ private long mMax;
+ private float mSize;
+
+ public DataAxis() {
+ // TODO: adapt ranges to show when history >5GB, and handle 4G
+ // interfaces with higher limits.
+ setBounds(0, 5 * GB_IN_BYTES);
+ }
+
+ /** {@inheritDoc} */
+ public void setBounds(long min, long max) {
+ mMin = min;
+ mMax = max;
+ }
+
+ /** {@inheritDoc} */
+ public void setSize(float size) {
+ mSize = size;
+ }
+
+ /** {@inheritDoc} */
+ public float convertToPoint(long value) {
+ // TODO: this assumes range of [0,5]GB
+ final double fraction = Math.pow(
+ 10, 0.36884343106175160321 * Math.log10(value) + -3.62828151137812282556);
+ return (float) fraction * mSize;
+ }
+
+ /** {@inheritDoc} */
+ public long convertToValue(float point) {
+ final double y = point / mSize;
+ // TODO: this assumes range of [0,5]GB
+ final double fraction = 6.869341163271789302 * Math.pow(10, 9)
+ * Math.pow(y, 2.71117746931646030774);
+ return (long) fraction;
+ }
+
+ private static final Object sSpanSize = new Object();
+ private static final Object sSpanUnit = new Object();
+
+ /** {@inheritDoc} */
+ public void buildLabel(Resources res, SpannableStringBuilder builder, long value) {
+
+ float result = value;
+ final CharSequence unit;
+ if (result <= 100 * MB_IN_BYTES) {
+ unit = res.getText(com.android.internal.R.string.megabyteShort);
+ result /= MB_IN_BYTES;
+ } else {
+ unit = res.getText(com.android.internal.R.string.gigabyteShort);
+ result /= GB_IN_BYTES;
+ }
+
+ final CharSequence size;
+ if (result < 10) {
+ size = String.format("%.1f", result);
+ } else {
+ size = String.format("%.0f", result);
+ }
+
+ final int[] sizeBounds = findOrCreateSpan(builder, sSpanSize, "^1");
+ builder.replace(sizeBounds[0], sizeBounds[1], size);
+ final int[] unitBounds = findOrCreateSpan(builder, sSpanUnit, "^2");
+ builder.replace(unitBounds[0], unitBounds[1], unit);
+ }
+
+ /** {@inheritDoc} */
+ public float[] getTickPoints() {
+ final float[] tickPoints = new float[16];
+
+ final long jump = ((mMax - mMin) / tickPoints.length);
+ long value = mMin;
+ for (int i = 0; i < tickPoints.length; i++) {
+ tickPoints[i] = convertToPoint(value);
+ value += jump;
+ }
+
+ return tickPoints;
+ }
+ }
+
+ private static int[] findOrCreateSpan(
+ SpannableStringBuilder builder, Object key, CharSequence 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);
+ }
+ return new int[] { start, end };
+ }
+
+}
diff --git a/src/com/android/settings/widget/InvertedChartAxis.java b/src/com/android/settings/widget/InvertedChartAxis.java
new file mode 100644
index 0000000..e589da9
--- /dev/null
+++ b/src/com/android/settings/widget/InvertedChartAxis.java
@@ -0,0 +1,67 @@
+/*
+ * 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 com.android.settings.widget;
+
+import android.content.res.Resources;
+import android.text.SpannableStringBuilder;
+
+/**
+ * Utility to invert another {@link ChartAxis}.
+ */
+public class InvertedChartAxis implements ChartAxis {
+ private final ChartAxis mWrapped;
+ private float mSize;
+
+ public InvertedChartAxis(ChartAxis wrapped) {
+ mWrapped = wrapped;
+ }
+
+ /** {@inheritDoc} */
+ public void setBounds(long min, long max) {
+ mWrapped.setBounds(min, max);
+ }
+
+ /** {@inheritDoc} */
+ public void setSize(float size) {
+ mSize = size;
+ mWrapped.setSize(size);
+ }
+
+ /** {@inheritDoc} */
+ public float convertToPoint(long value) {
+ return mSize - mWrapped.convertToPoint(value);
+ }
+
+ /** {@inheritDoc} */
+ public long convertToValue(float point) {
+ return mWrapped.convertToValue(mSize - point);
+ }
+
+ /** {@inheritDoc} */
+ public void buildLabel(Resources res, SpannableStringBuilder builder, long value) {
+ mWrapped.buildLabel(res, builder, value);
+ }
+
+ /** {@inheritDoc} */
+ public float[] getTickPoints() {
+ final float[] points = mWrapped.getTickPoints();
+ for (int i = 0; i < points.length; i++) {
+ points[i] = mSize - points[i];
+ }
+ return points;
+ }
+}