Merge "Foreground/background network stats pie chart."

This commit is contained in:
Jeff Sharkey
2011-08-15 18:44:17 -07:00
committed by Android (Google) Code Review
20 changed files with 453 additions and 149 deletions

View File

@@ -25,6 +25,8 @@ import static android.net.NetworkPolicyManager.POLICY_NONE;
import static android.net.NetworkPolicyManager.POLICY_REJECT_METERED_BACKGROUND;
import static android.net.NetworkPolicyManager.computeLastCycleBoundary;
import static android.net.NetworkPolicyManager.computeNextCycleBoundary;
import static android.net.NetworkStats.SET_DEFAULT;
import static android.net.NetworkStats.SET_FOREGROUND;
import static android.net.NetworkStats.TAG_NONE;
import static android.net.NetworkStatsHistory.FIELD_RX_BYTES;
import static android.net.NetworkStatsHistory.FIELD_TX_BYTES;
@@ -60,6 +62,7 @@ import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.Resources;
import android.graphics.Color;
import android.graphics.drawable.Drawable;
import android.net.ConnectivityManager;
import android.net.INetworkPolicyManager;
@@ -83,6 +86,7 @@ import android.text.TextUtils;
import android.text.format.DateUtils;
import android.text.format.Formatter;
import android.util.Log;
import android.util.SparseArray;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
@@ -119,6 +123,7 @@ import com.android.settings.net.NetworkPolicyEditor;
import com.android.settings.net.SummaryForAllUidLoader;
import com.android.settings.widget.DataUsageChartView;
import com.android.settings.widget.DataUsageChartView.DataUsageChartListener;
import com.android.settings.widget.PieChartView;
import com.google.android.collect.Lists;
import java.util.ArrayList;
@@ -196,6 +201,9 @@ public class DataUsageSummary extends Fragment {
private View mAppDetail;
private ImageView mAppIcon;
private ViewGroup mAppTitles;
private PieChartView mAppPieChart;
private TextView mAppForeground;
private TextView mAppBackground;
private Button mAppSettings;
private LinearLayout mAppSwitches;
@@ -216,6 +224,8 @@ public class DataUsageSummary extends Fragment {
private NetworkStatsHistory mHistory;
private NetworkStatsHistory mDetailHistory;
private NetworkStatsHistory mDetailHistoryDefault;
private NetworkStatsHistory mDetailHistoryForeground;
private String mCurrentTab = null;
private String mIntentTab = null;
@@ -301,6 +311,9 @@ public class DataUsageSummary extends Fragment {
mAppDetail = mHeader.findViewById(R.id.app_detail);
mAppIcon = (ImageView) mAppDetail.findViewById(R.id.app_icon);
mAppTitles = (ViewGroup) mAppDetail.findViewById(R.id.app_titles);
mAppPieChart = (PieChartView) mAppDetail.findViewById(R.id.app_pie_chart);
mAppForeground = (TextView) mAppDetail.findViewById(R.id.app_foreground);
mAppBackground = (TextView) mAppDetail.findViewById(R.id.app_background);
mAppSwitches = (LinearLayout) mAppDetail.findViewById(R.id.app_switches);
mAppSettings = (Button) mAppDetail.findViewById(R.id.app_settings);
@@ -539,12 +552,8 @@ public class DataUsageSummary extends Fragment {
* Build {@link TabSpec} with thin indicator, and empty content.
*/
private TabSpec buildTabSpec(String tag, int titleRes) {
final LayoutInflater inflater = LayoutInflater.from(mTabWidget.getContext());
final View indicator = inflater.inflate(
R.layout.tab_indicator_thin_holo, mTabWidget, false);
final TextView title = (TextView) indicator.findViewById(android.R.id.title);
title.setText(titleRes);
return mTabHost.newTabSpec(tag).setIndicator(indicator).setContent(mEmptyTabContent);
return mTabHost.newTabSpec(tag).setIndicator(getText(titleRes)).setContent(
mEmptyTabContent);
}
private OnTabChangeListener mTabListener = new OnTabChangeListener() {
@@ -658,6 +667,10 @@ public class DataUsageSummary extends Fragment {
mAppDetail.setVisibility(View.GONE);
mCycleAdapter.setChangeVisible(true);
mDetailHistory = null;
mDetailHistoryDefault = null;
mDetailHistoryForeground = null;
// hide detail stats when not in detail mode
mChart.bindDetailNetworkStats(null);
return;
@@ -697,15 +710,20 @@ public class DataUsageSummary extends Fragment {
try {
// load stats for current uid and template
// TODO: read template from extras
mDetailHistory = mStatsService.getHistoryForUid(
mTemplate, mUid, TAG_NONE, FIELD_RX_BYTES | FIELD_TX_BYTES);
mDetailHistoryDefault = mStatsService.getHistoryForUid(
mTemplate, mUid, SET_DEFAULT, TAG_NONE, FIELD_RX_BYTES | FIELD_TX_BYTES);
mDetailHistoryForeground = mStatsService.getHistoryForUid(
mTemplate, mUid, SET_FOREGROUND, TAG_NONE, FIELD_RX_BYTES | FIELD_TX_BYTES);
} catch (RemoteException e) {
// since we can't do much without history, and we don't want to
// leave with half-baked UI, we bail hard.
throw new RuntimeException("problem reading network stats", e);
}
mDetailHistory = new NetworkStatsHistory(mDetailHistoryForeground.getBucketDuration());
mDetailHistory.recordEntireHistory(mDetailHistoryDefault);
mDetailHistory.recordEntireHistory(mDetailHistoryForeground);
// bind chart to historical stats
mChart.bindDetailNetworkStats(mDetailHistory);
@@ -1012,14 +1030,28 @@ public class DataUsageSummary extends Fragment {
final long now = System.currentTimeMillis();
final Context context = getActivity();
final NetworkStatsHistory.Entry entry;
if (isAppDetailMode()) {
if (mDetailHistory != null) {
entry = mDetailHistory.getValues(start, end, now, null);
} else {
entry = null;
}
NetworkStatsHistory.Entry entry = null;
if (isAppDetailMode() && mDetailHistory != null) {
// bind foreground/background to piechart and labels
entry = mDetailHistoryDefault.getValues(start, end, now, entry);
final long defaultBytes = entry.rxBytes + entry.txBytes;
entry = mDetailHistoryForeground.getValues(start, end, now, entry);
final long foregroundBytes = entry.rxBytes + entry.txBytes;
mAppPieChart.setOriginAngle(175);
mAppPieChart.removeAllSlices();
mAppPieChart.addSlice(foregroundBytes, Color.parseColor("#d88d3a"));
mAppPieChart.addSlice(defaultBytes, Color.parseColor("#666666"));
mAppPieChart.generatePath();
mAppBackground.setText(Formatter.formatFileSize(context, defaultBytes));
mAppForeground.setText(Formatter.formatFileSize(context, foregroundBytes));
// and finally leave with summary data for label below
entry = mDetailHistory.getValues(start, end, now, null);
getLoaderManager().destroyLoader(LOADER_SUMMARY);
@@ -1206,31 +1238,38 @@ public class DataUsageSummary extends Fragment {
public void bindStats(NetworkStats stats) {
mItems.clear();
if (stats != null) {
final AppUsageItem systemItem = new AppUsageItem();
systemItem.uid = android.os.Process.SYSTEM_UID;
final AppUsageItem systemItem = new AppUsageItem();
systemItem.uid = android.os.Process.SYSTEM_UID;
NetworkStats.Entry entry = null;
for (int i = 0; i < stats.size(); i++) {
entry = stats.getValues(i, entry);
final SparseArray<AppUsageItem> knownUids = new SparseArray<AppUsageItem>();
final boolean isApp = entry.uid >= android.os.Process.FIRST_APPLICATION_UID
&& entry.uid <= android.os.Process.LAST_APPLICATION_UID;
if (isApp || entry.uid == TrafficStats.UID_REMOVED) {
final AppUsageItem item = new AppUsageItem();
item.uid = entry.uid;
item.total = entry.rxBytes + entry.txBytes;
NetworkStats.Entry entry = null;
final int size = stats != null ? stats.size() : 0;
for (int i = 0; i < size; i++) {
entry = stats.getValues(i, entry);
final int uid = entry.uid;
final boolean isApp = uid >= android.os.Process.FIRST_APPLICATION_UID
&& uid <= android.os.Process.LAST_APPLICATION_UID;
if (isApp || uid == TrafficStats.UID_REMOVED) {
AppUsageItem item = knownUids.get(uid);
if (item == null) {
item = new AppUsageItem();
item.uid = uid;
knownUids.put(uid, item);
mItems.add(item);
} else {
systemItem.total += entry.rxBytes + entry.txBytes;
}
}
if (systemItem.total > 0) {
mItems.add(systemItem);
item.total += entry.rxBytes + entry.txBytes;
} else {
systemItem.total += entry.rxBytes + entry.txBytes;
}
}
if (systemItem.total > 0) {
mItems.add(systemItem);
}
Collections.sort(mItems);
mLargest = (mItems.size() > 0) ? mItems.get(0).total : 0;
notifyDataSetChanged();

View File

@@ -63,7 +63,7 @@ public class Settings extends PreferenceActivity implements ButtonBarHandler {
private static final String META_DATA_KEY_PARENT_FRAGMENT_CLASS =
"com.android.settings.PARENT_FRAGMENT_CLASS";
private static final String EXTRA_THEME = "settings:theme";
private static final String EXTRA_CLEAR_UI_OPTIONS = "settings:remove_ui_options";
private static final String SAVE_KEY_CURRENT_HEADER = "com.android.settings.CURRENT_HEADER";
private static final String SAVE_KEY_PARENT_HEADER = "com.android.settings.PARENT_HEADER";
@@ -82,9 +82,9 @@ public class Settings extends PreferenceActivity implements ButtonBarHandler {
@Override
protected void onCreate(Bundle savedInstanceState) {
final int theme = getIntent().getIntExtra(
EXTRA_THEME, android.R.style.Theme_Holo);
setTheme(theme);
if (getIntent().getBooleanExtra(EXTRA_CLEAR_UI_OPTIONS, false)) {
getWindow().setUiOptions(0);
}
getMetaData();
mInLocalHeaderSwitch = true;
@@ -288,12 +288,12 @@ public class Settings extends PreferenceActivity implements ButtonBarHandler {
Intent intent = super.onBuildStartFragmentIntent(fragmentName, args,
titleRes, shortTitleRes);
// some fragments would like a custom activity theme
// some fragments want to avoid split actionbar
if (DataUsageSummary.class.getName().equals(fragmentName) ||
PowerUsageSummary.class.getName().equals(fragmentName) ||
AccountSyncSettings.class.getName().equals(fragmentName) ||
UserDictionarySettings.class.getName().equals(fragmentName)) {
intent.putExtra(EXTRA_THEME, android.R.style.Theme_Holo);
intent.putExtra(EXTRA_CLEAR_UI_OPTIONS, true);
}
intent.setClass(this, SubSettings.class);

View File

@@ -105,7 +105,7 @@ public class ChartNetworkSeriesView extends View {
public void setChartColor(int stroke, int fill, int fillSecondary) {
mPaintStroke = new Paint();
mPaintStroke.setStrokeWidth(6.0f);
mPaintStroke.setStrokeWidth(4.0f * getResources().getDisplayMetrics().density);
mPaintStroke.setColor(stroke);
mPaintStroke.setStyle(Style.STROKE);
mPaintStroke.setAntiAlias(true);
@@ -165,7 +165,10 @@ public class ChartNetworkSeriesView extends View {
mPathEstimate.reset();
// bail when not enough stats to render
if (mStats == null || mStats.size() < 2) return;
if (mStats == null || mStats.size() < 2) {
invalidate();
return;
}
final int width = getWidth();
final int height = getHeight();
@@ -263,6 +266,8 @@ public class ChartNetworkSeriesView extends View {
}
mMaxEstimate = totalData;
invalidate();
}
public void setEndTime(long endTime) {

View File

@@ -32,7 +32,6 @@ 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;
@@ -41,7 +40,7 @@ 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 {
public class ChartSweepView extends View {
private Drawable mSweep;
private Rect mSweepPadding = new Rect();
@@ -78,7 +77,7 @@ public class ChartSweepView extends FrameLayout {
private MotionEvent mTracking;
public ChartSweepView(Context context) {
this(context, null, 0);
this(context, null);
}
public ChartSweepView(Context context, AttributeSet attrs) {
@@ -101,8 +100,6 @@ public class ChartSweepView extends FrameLayout {
a.recycle();
setClipToPadding(false);
setClipChildren(false);
setWillNotDraw(false);
}

View File

@@ -19,12 +19,16 @@ package com.android.settings.widget;
import static com.google.common.base.Preconditions.checkNotNull;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.View;
import android.view.ViewDebug;
import android.widget.FrameLayout;
import com.android.settings.R;
/**
* Container for two-dimensional chart, drawn with a combination of
* {@link ChartGridView}, {@link ChartNetworkSeriesView} and {@link ChartSweepView}
@@ -41,6 +45,10 @@ public class ChartView extends FrameLayout {
ChartAxis mHoriz;
ChartAxis mVert;
@ViewDebug.ExportedProperty
private int mOptimalWidth = -1;
private float mOptimalWidthWeight = 0;
private Rect mContent = new Rect();
public ChartView(Context context) {
@@ -54,6 +62,12 @@ public class ChartView extends FrameLayout {
public ChartView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
final TypedArray a = context.obtainStyledAttributes(
attrs, R.styleable.ChartView, defStyle, 0);
setOptimalWidth(a.getDimensionPixelSize(R.styleable.ChartView_optimalWidth, -1),
a.getFloat(R.styleable.ChartView_optimalWidthWeight, 0));
a.recycle();
setClipToPadding(false);
setClipChildren(false);
}
@@ -63,6 +77,24 @@ public class ChartView extends FrameLayout {
mVert = checkNotNull(vert, "missing vert");
}
public void setOptimalWidth(int optimalWidth, float optimalWidthWeight) {
mOptimalWidth = optimalWidth;
mOptimalWidthWeight = optimalWidthWeight;
requestLayout();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
final int slack = getMeasuredWidth() - mOptimalWidth;
if (mOptimalWidth > 0 && slack > 0) {
final int targetWidth = (int) (mOptimalWidth + (slack * mOptimalWidthWeight));
widthMeasureSpec = MeasureSpec.makeMeasureSpec(targetWidth, MeasureSpec.EXACTLY);
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
mContent.set(getPaddingLeft(), getPaddingTop(), r - l - getPaddingRight(),

View File

@@ -226,8 +226,6 @@ public class DataUsageChartView extends ChartView {
mDetailSeries.generatePath();
mGrid.invalidate();
mSeries.invalidate();
mDetailSeries.invalidate();
// since we just changed axis, make sweep recalculate its value
if (activeSweep != null) {
@@ -362,7 +360,6 @@ public class DataUsageChartView extends ChartView {
requestLayout();
mSeries.generatePath();
mSeries.invalidate();
updateVertAxisBounds(null);
updateEstimateVisible();

View File

@@ -0,0 +1,202 @@
/*
* 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.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.graphics.Path;
import android.graphics.Path.Direction;
import android.graphics.RadialGradient;
import android.graphics.RectF;
import android.graphics.Shader.TileMode;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import com.google.common.collect.Lists;
import java.util.ArrayList;
/**
* Pie chart with multiple items.
*/
public class PieChartView extends View {
public static final String TAG = "PieChartView";
public static final boolean LOGD = true;
private ArrayList<Slice> mSlices = Lists.newArrayList();
private int mOriginAngle;
private Paint mPaintPrimary = new Paint();
private Paint mPaintShadow = new Paint();
private Path mPathSide = new Path();
private Path mPathSideShadow = new Path();
private Path mPathShadow = new Path();
private int mSideWidth;
public class Slice {
public long value;
public Path pathPrimary = new Path();
public Path pathShadow = new Path();
public Paint paintPrimary;
public Slice(long value, int color) {
this.value = value;
this.paintPrimary = buildFillPaint(color, getResources());
}
}
public PieChartView(Context context) {
this(context, null);
}
public PieChartView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public PieChartView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
mPaintPrimary = buildFillPaint(Color.parseColor("#666666"), getResources());
mPaintShadow.setColor(Color.BLACK);
mPaintShadow.setStyle(Style.STROKE);
mPaintShadow.setStrokeWidth(3f * getResources().getDisplayMetrics().density);
mPaintShadow.setAntiAlias(true);
mSideWidth = (int) (20 * getResources().getDisplayMetrics().density);
setWillNotDraw(false);
}
private static Paint buildFillPaint(int color, Resources res) {
final Paint paint = new Paint();
paint.setColor(color);
paint.setStyle(Style.FILL_AND_STROKE);
paint.setAntiAlias(true);
final int width = (int) (280 * res.getDisplayMetrics().density);
paint.setShader(new RadialGradient(0, 0, width, color, darken(color), TileMode.MIRROR));
return paint;
}
public void setOriginAngle(int originAngle) {
mOriginAngle = originAngle;
}
public void addSlice(long value, int color) {
mSlices.add(new Slice(value, color));
}
public void removeAllSlices() {
mSlices.clear();
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
generatePath();
}
public void generatePath() {
if (LOGD) Log.d(TAG, "generatePath()");
long total = 0;
for (Slice slice : mSlices) {
slice.pathPrimary.reset();
slice.pathShadow.reset();
total += slice.value;
}
mPathSide.reset();
mPathSideShadow.reset();
mPathShadow.reset();
// bail when not enough stats to render
if (total == 0) {
invalidate();
return;
}
final int width = getWidth();
final int height = getHeight();
final RectF rect = new RectF(0, 0, width, height);
mPathSide.addOval(rect, Direction.CW);
mPathSideShadow.addOval(rect, Direction.CW);
mPathShadow.addOval(rect, Direction.CW);
int startAngle = mOriginAngle;
for (Slice slice : mSlices) {
final int sweepAngle = (int) (slice.value * 360 / total);
slice.pathPrimary.moveTo(rect.centerX(), rect.centerY());
slice.pathPrimary.arcTo(rect, startAngle, sweepAngle);
slice.pathPrimary.lineTo(rect.centerX(), rect.centerY());
slice.pathShadow.moveTo(rect.centerX(), rect.centerY());
slice.pathShadow.arcTo(rect, startAngle, 0);
slice.pathShadow.moveTo(rect.centerX(), rect.centerY());
slice.pathShadow.arcTo(rect, startAngle + sweepAngle, 0);
startAngle += sweepAngle;
}
invalidate();
}
@Override
protected void onDraw(Canvas canvas) {
canvas.translate(getWidth() * 0.25f, getHeight() * -0.05f);
canvas.rotate(-40, getWidth() * 0.5f, getHeight());
canvas.scale(0.7f, 1.0f, getWidth(), getHeight());
canvas.save();
canvas.translate(-mSideWidth, 0);
canvas.drawPath(mPathSide, mPaintPrimary);
canvas.drawPath(mPathSideShadow, mPaintShadow);
canvas.restore();
for (Slice slice : mSlices) {
canvas.drawPath(slice.pathPrimary, slice.paintPrimary);
canvas.drawPath(slice.pathShadow, mPaintShadow);
}
canvas.drawPath(mPathShadow, mPaintShadow);
}
public static int darken(int color) {
float[] hsv = new float[3];
Color.colorToHSV(color, hsv);
hsv[2] /= 2;
hsv[1] /= 2;
return Color.HSVToColor(hsv);
}
}