Data usage UI fixes; sweeps, combined history.

Fix sweep z-order so that limit are always above inspection range,
and draw shadows behind sweep labels.  Narrower margins for sweeps
with labels; push labels to keep from overlapping.

Generous touch targets on sweeps, and delegate touches to neighboring
sweep if nearer.  Refresh sweep layout during axis zoom, and don't
allow zoom below default minimum.  Let inspection sweeps move beyond
valid data ranges.  Draw less-frequent tick marks when working with
large axis ranges.

Remove Wi-Fi policies but continue showing historical data.  Write
NetworkPolicy if modified during read, and snapshot when async write
requested.

Handle combined UID histories for "Android OS."

Bug: 5191421, 5092579, 5225988, 5221101, 5221065, 5221005, 5150906, 5058025
Change-Id: Id51652e8a10bb90e1345f7a8af01bd70cb8ac677
This commit is contained in:
Jeff Sharkey
2011-08-27 17:09:43 -07:00
parent d66b61908d
commit 55d18a57e4
6 changed files with 381 additions and 160 deletions

View File

@@ -27,6 +27,7 @@ import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
@@ -37,7 +38,7 @@ 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 {
public class ChartDataUsageView extends ChartView {
private static final long KB_IN_BYTES = 1024;
private static final long MB_IN_BYTES = KB_IN_BYTES * 1024;
@@ -46,6 +47,8 @@ public class DataUsageChartView extends ChartView {
private static final int MSG_UPDATE_AXIS = 100;
private static final long DELAY_MILLIS = 250;
private static final boolean LIMIT_SWEEPS_TO_VALID_DATA = false;
private ChartGridView mGrid;
private ChartNetworkSeriesView mSeries;
private ChartNetworkSeriesView mDetailSeries;
@@ -70,15 +73,15 @@ public class DataUsageChartView extends ChartView {
private DataUsageChartListener mListener;
public DataUsageChartView(Context context) {
public ChartDataUsageView(Context context) {
this(context, null, 0);
}
public DataUsageChartView(Context context, AttributeSet attrs) {
public ChartDataUsageView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public DataUsageChartView(Context context, AttributeSet attrs, int defStyle) {
public ChartDataUsageView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(new TimeAxis(), new InvertedChartAxis(new DataAxis()));
@@ -186,6 +189,7 @@ public class DataUsageChartView extends ChartView {
updateVertAxisBounds(null);
requestLayout();
invalidate();
}
/**
@@ -194,7 +198,8 @@ public class DataUsageChartView extends ChartView {
*/
private void updateVertAxisBounds(ChartSweepView activeSweep) {
final long max = mVertMax;
final long newMax;
long newMax = 0;
if (activeSweep != null) {
final int adjustAxis = activeSweep.shouldAdjustAxis();
if (adjustAxis > 0) {
@@ -206,14 +211,14 @@ public class DataUsageChartView extends ChartView {
} else {
newMax = max;
}
} else {
// try showing all known data and policy
final long maxSweep = Math.max(mSweepWarning.getValue(), mSweepLimit.getValue());
final long maxVisible = Math.max(mSeries.getMaxVisible(), maxSweep) * 12 / 10;
newMax = Math.max(maxVisible, 2 * GB_IN_BYTES);
}
// always show known data and policy lines
final long maxSweep = Math.max(mSweepWarning.getValue(), mSweepLimit.getValue());
final long maxVisible = Math.max(mSeries.getMaxVisible(), maxSweep) * 12 / 10;
final long maxDefault = Math.max(maxVisible, 2 * GB_IN_BYTES);
newMax = Math.max(maxDefault, newMax);
// only invalidate when vertMax actually changed
if (newMax != mVertMax) {
mVertMax = newMax;
@@ -231,6 +236,16 @@ public class DataUsageChartView extends ChartView {
if (activeSweep != null) {
activeSweep.updateValueFromPosition();
}
// layout other sweeps to match changed axis
// TODO: find cleaner way of doing this, such as requesting full
// layout and making activeSweep discard its tracking MotionEvent.
if (mSweepLimit != activeSweep) {
layoutSweep(mSweepLimit);
}
if (mSweepWarning != activeSweep) {
layoutSweep(mSweepWarning);
}
}
}
@@ -346,9 +361,14 @@ public class DataUsageChartView extends ChartView {
final long validStart = Math.max(visibleStart, getStatsStart());
final long validEnd = Math.min(visibleEnd, getStatsEnd());
// prevent time sweeps from leaving valid data
mSweepLeft.setValidRange(validStart, validEnd);
mSweepRight.setValidRange(validStart, validEnd);
if (LIMIT_SWEEPS_TO_VALID_DATA) {
// prevent time sweeps from leaving valid data
mSweepLeft.setValidRange(validStart, validEnd);
mSweepRight.setValidRange(validStart, validEnd);
} else {
mSweepLeft.setValidRange(visibleStart, visibleEnd);
mSweepRight.setValidRange(visibleStart, visibleEnd);
}
// default sweeps to last week of data
final long halfRange = (visibleEnd + visibleStart) / 2;
@@ -424,7 +444,7 @@ public class DataUsageChartView extends ChartView {
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));
tickPoints[i] = convertToPoint(mMax - (TICK_INTERVAL * (i + 1)));
}
return tickPoints;
}
@@ -501,7 +521,14 @@ public class DataUsageChartView extends ChartView {
/** {@inheritDoc} */
public float[] getTickPoints() {
final long range = mMax - mMin;
final long tickJump = 256 * MB_IN_BYTES;
final long tickJump;
if (range < 6 * GB_IN_BYTES) {
tickJump = 256 * MB_IN_BYTES;
} else if (range < 12 * GB_IN_BYTES) {
tickJump = 512 * MB_IN_BYTES;
} else {
tickJump = 1 * GB_IN_BYTES;
}
final int tickCount = (int) (range / tickJump);
final float[] tickPoints = new float[tickCount];

View File

@@ -21,6 +21,7 @@ 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.Point;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
@@ -42,8 +43,14 @@ import com.google.common.base.Preconditions;
*/
public class ChartSweepView extends View {
private static final boolean DRAW_OUTLINE = false;
private Drawable mSweep;
private Rect mSweepPadding = new Rect();
/** Offset of content inside this view. */
private Point mContentOffset = new Point();
/** Offset of {@link #mSweep} inside this view. */
private Point mSweepOffset = new Point();
private Rect mMargins = new Rect();
@@ -66,6 +73,8 @@ public class ChartSweepView extends View {
private ChartSweepView mValidAfterDynamic;
private ChartSweepView mValidBeforeDynamic;
private Paint mOutlinePaint = new Paint();
public static final int HORIZONTAL = 0;
public static final int VERTICAL = 1;
@@ -98,6 +107,10 @@ public class ChartSweepView extends View {
setLabelTemplate(a.getResourceId(R.styleable.ChartSweepView_labelTemplate, 0));
setLabelColor(a.getColor(R.styleable.ChartSweepView_labelColor, Color.BLUE));
mOutlinePaint.setColor(Color.RED);
mOutlinePaint.setStrokeWidth(1f);
mOutlinePaint.setStyle(Style.STROKE);
a.recycle();
setWillNotDraw(false);
@@ -123,11 +136,11 @@ public class ChartSweepView extends View {
if (mFollowAxis == VERTICAL) {
final float targetHeight = mSweep.getIntrinsicHeight() - mSweepPadding.top
- mSweepPadding.bottom;
return mSweepPadding.top + (targetHeight / 2);
return mSweepPadding.top + (targetHeight / 2) + mSweepOffset.y;
} else {
final float targetWidth = mSweep.getIntrinsicWidth() - mSweepPadding.left
- mSweepPadding.right;
return mSweepPadding.left + (targetWidth / 2);
return mSweepPadding.left + (targetWidth / 2) + mSweepOffset.x;
}
}
@@ -195,6 +208,7 @@ public class ChartSweepView extends View {
paint.density = getResources().getDisplayMetrics().density;
paint.setCompatibilityScaling(getResources().getCompatibilityInfo().applicationScale);
paint.setColor(mLabelColor);
paint.setShadowLayer(4 * paint.density, 0, 0, Color.BLACK);
mLabelTemplate = new SpannableStringBuilder(template);
mLabelLayout = new DynamicLayout(
@@ -283,6 +297,26 @@ public class ChartSweepView extends View {
mValidBeforeDynamic = validBefore;
}
/**
* Test if given {@link MotionEvent} is closer to another
* {@link ChartSweepView} compared to ourselves.
*/
public boolean isTouchCloserTo(MotionEvent eventInParent, ChartSweepView another) {
if (another == null) return false;
if (mFollowAxis == HORIZONTAL) {
final float selfDist = Math.abs(eventInParent.getX() - (getX() + getTargetInset()));
final float anotherDist = Math.abs(
eventInParent.getX() - (another.getX() + another.getTargetInset()));
return anotherDist < selfDist;
} else {
final float selfDist = Math.abs(eventInParent.getY() - (getY() + getTargetInset()));
final float anotherDist = Math.abs(
eventInParent.getY() - (another.getY() + another.getTargetInset()));
return anotherDist < selfDist;
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (!isEnabled()) return false;
@@ -294,9 +328,18 @@ public class ChartSweepView extends View {
// only start tracking when in sweet spot
final boolean accept;
if (mFollowAxis == VERTICAL) {
accept = event.getX() > getWidth() - (mSweepPadding.right * 2);
accept = event.getX() > getWidth() - (mSweepPadding.right * 3);
} else {
accept = event.getY() > getHeight() - (mSweepPadding.bottom * 2);
accept = event.getY() > getHeight() - (mSweepPadding.bottom * 3);
}
final MotionEvent eventInParent = event.copy();
eventInParent.offsetLocation(getLeft(), getTop());
// ignore event when closer to a neighbor
if (isTouchCloserTo(eventInParent, mValidAfterDynamic)
|| isTouchCloserTo(eventInParent, mValidBeforeDynamic)) {
return false;
}
if (accept) {
@@ -460,6 +503,7 @@ public class ChartSweepView extends View {
final int templateHeight = mLabelLayout.getHeight();
mSweepOffset.x = 0;
mSweepOffset.y = 0;
mSweepOffset.y = (int) ((templateHeight / 2) - getTargetInset());
setMeasuredDimension(mSweep.getIntrinsicWidth(), Math.max(sweepHeight, templateHeight));
@@ -485,6 +529,23 @@ public class ChartSweepView extends View {
mMargins.bottom = mSweepPadding.bottom;
}
mContentOffset.x = 0;
mContentOffset.y = 0;
// make touch target area larger
if (mFollowAxis == HORIZONTAL) {
final int widthBefore = getMeasuredWidth();
final int widthAfter = widthBefore * 3;
setMeasuredDimension(widthAfter, getMeasuredHeight());
mContentOffset.offset((widthAfter - widthBefore) / 2, 0);
} else {
final int heightBefore = getMeasuredHeight();
final int heightAfter = heightBefore * 3;
setMeasuredDimension(getMeasuredWidth(), heightAfter);
mContentOffset.offset(0, (heightAfter - heightBefore) / 2);
}
mSweepOffset.offset(mContentOffset.x, mContentOffset.y);
mMargins.offset(-mSweepOffset.x, -mSweepOffset.y);
}
@@ -493,9 +554,43 @@ public class ChartSweepView extends View {
final int width = getWidth();
final int height = getHeight();
if (DRAW_OUTLINE) {
canvas.drawRect(0, 0, width, height, mOutlinePaint);
}
// when overlapping with neighbor, split difference and push label
float margin;
float labelOffset = 0;
if (mFollowAxis == VERTICAL) {
if (mValidAfterDynamic != null) {
margin = getLabelTop(mValidAfterDynamic) - getLabelBottom(this);
if (margin < 0) {
labelOffset = margin / 2;
}
} else if (mValidBeforeDynamic != null) {
margin = getLabelTop(this) - getLabelBottom(mValidBeforeDynamic);
if (margin < 0) {
labelOffset = -margin / 2;
}
}
} else {
// TODO: implement horizontal labels
}
// when offsetting label, neighbor probably needs to offset too
if (labelOffset != 0) {
if (mValidAfterDynamic != null) mValidAfterDynamic.invalidate();
if (mValidBeforeDynamic != null) mValidBeforeDynamic.invalidate();
}
final int labelSize;
if (isEnabled() && mLabelLayout != null) {
mLabelLayout.draw(canvas);
final int count = canvas.save();
{
canvas.translate(mContentOffset.x, mContentOffset.y + labelOffset);
mLabelLayout.draw(canvas);
}
canvas.restoreToCount(count);
labelSize = mLabelSize;
} else {
labelSize = 0;
@@ -512,4 +607,11 @@ public class ChartSweepView extends View {
mSweep.draw(canvas);
}
public static float getLabelTop(ChartSweepView view) {
return view.getY() + view.mContentOffset.y;
}
public static float getLabelBottom(ChartSweepView view) {
return getLabelTop(view) + view.mLabelLayout.getHeight();
}
}

View File

@@ -36,8 +36,6 @@ import com.android.settings.R;
* 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;
@@ -122,29 +120,39 @@ public class ChartView extends FrameLayout {
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);
}
layoutSweep((ChartSweepView) child, parentRect, childRect);
child.layout(childRect.left, childRect.top, childRect.right, childRect.bottom);
}
}
}
child.layout(childRect.left, childRect.top, childRect.right, childRect.bottom);
protected void layoutSweep(ChartSweepView sweep) {
final Rect parentRect = new Rect(mContent);
final Rect childRect = new Rect();
layoutSweep(sweep, parentRect, childRect);
sweep.layout(childRect.left, childRect.top, childRect.right, childRect.bottom);
}
protected void layoutSweep(ChartSweepView sweep, Rect parentRect, Rect childRect) {
final Rect sweepMargins = sweep.getMargins();
// sweep is always placed along specific dimension
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(), sweep.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, sweep.getMeasuredWidth(), parentRect.height(),
parentRect, childRect);
}
}