diff options
author | Jeff Sharkey <jsharkey@android.com> | 2011-05-30 16:19:56 -0700 |
---|---|---|
committer | Jeff Sharkey <jsharkey@android.com> | 2011-06-09 09:26:30 -0700 |
commit | ab2d8d3a38857b8c155e6c6393c5821f5a341aae (patch) | |
tree | fe7fecb67b8300c245bbbfc0434fd3f45774d03c | |
parent | 32232fd9f2751b8618111831749ace5c9df021e8 (diff) | |
download | packages_apps_settings-ab2d8d3a38857b8c155e6c6393c5821f5a341aae.zip packages_apps_settings-ab2d8d3a38857b8c155e6c6393c5821f5a341aae.tar.gz packages_apps_settings-ab2d8d3a38857b8c155e6c6393c5821f5a341aae.tar.bz2 |
Checkpoint of data usage UI, graphs and lists.
Chart of network usage over time, with draggable "sweep" bars for
inspection region and warning/limits. Talks with NetworkStatsService
for live data, and updates list of application usage as inspection
region changes.
Change-Id: I2a406e6776daf7d74143c07ec683c10fe711c277
-rw-r--r-- | Android.mk | 2 | ||||
-rw-r--r-- | AndroidManifest.xml | 14 | ||||
-rw-r--r-- | res/layout/data_usage_summary.xml | 33 | ||||
-rw-r--r-- | res/values/strings.xml | 4 | ||||
-rw-r--r-- | res/xml/wireless_settings.xml | 5 | ||||
-rw-r--r-- | src/com/android/settings/DataUsageSummary.java | 353 | ||||
-rw-r--r-- | src/com/android/settings/Settings.java | 1 | ||||
-rw-r--r-- | src/com/android/settings/widget/ChartAxis.java | 34 | ||||
-rw-r--r-- | src/com/android/settings/widget/ChartGridView.java | 79 | ||||
-rw-r--r-- | src/com/android/settings/widget/ChartNetworkSeriesView.java | 183 | ||||
-rw-r--r-- | src/com/android/settings/widget/ChartSweepView.java | 165 | ||||
-rw-r--r-- | src/com/android/settings/widget/ChartView.java | 120 | ||||
-rw-r--r-- | src/com/android/settings/widget/InvertedChartAxis.java | 59 |
13 files changed, 1052 insertions, 0 deletions
@@ -1,6 +1,8 @@ LOCAL_PATH:= $(call my-dir) include $(CLEAR_VARS) +LOCAL_STATIC_JAVA_LIBRARIES := guava + LOCAL_MODULE_TAGS := optional LOCAL_SRC_FILES := $(call all-java-files-under, src) diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 5634cbf..bb64059 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -1128,6 +1128,19 @@ android:resource="@id/security_settings" /> </activity> + <activity android:name="Settings$DataUsageSummaryActivity" + android:label="@string/data_usage_summary_title"> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <action android:name="android.intent.action.DATA_USAGE_SUMMARY" /> + <category android:name="android.intent.category.DEFAULT" /> + </intent-filter> + <meta-data android:name="com.android.settings.FRAGMENT_CLASS" + android:value="com.android.settings.DataUsageSummary" /> + <meta-data android:name="com.android.settings.TOP_LEVEL_HEADER_ID" + android:resource="@id/wireless_settings" /> + </activity> + <receiver android:name=".widget.SettingsAppWidgetProvider" android:label="@string/gadget_title" android:exported="false" @@ -1142,5 +1155,6 @@ </intent-filter> <meta-data android:name="android.appwidget.provider" android:resource="@xml/appwidget_info" /> </receiver> + </application> </manifest> diff --git a/res/layout/data_usage_summary.xml b/res/layout/data_usage_summary.xml new file mode 100644 index 0000000..9a356ae --- /dev/null +++ b/res/layout/data_usage_summary.xml @@ -0,0 +1,33 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- 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. +--> + +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + + <FrameLayout + android:id="@+id/chart_container" + android:layout_width="match_parent" + android:layout_height="200dip" /> + + <ListView + android:id="@+id/list" + android:layout_width="match_parent" + android:layout_height="0dip" + android:layout_weight="1" /> + +</LinearLayout> diff --git a/res/values/strings.xml b/res/values/strings.xml index 7cb5bc3..eaacf44 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -3358,4 +3358,8 @@ found in the list of installed applications.</string> <string name="hdcp_checking_title">HDCP checking</string> <!-- HDCP checking dialog title, used for debug purposes only. [CHAR LIMIT=25] --> <string name="hdcp_checking_dialog_title">Set HDCP checking behavior</string> + + <!-- Activity title for network data usage summary. [CHAR LIMIT=25] --> + <string name="data_usage_summary_title">Data usage</string> + </resources> diff --git a/res/xml/wireless_settings.xml b/res/xml/wireless_settings.xml index 466df7b..321facb 100644 --- a/res/xml/wireless_settings.xml +++ b/res/xml/wireless_settings.xml @@ -89,4 +89,9 @@ android:summary="@string/proxy_settings_summary" > </PreferenceScreen> + <PreferenceScreen + android:fragment="com.android.settings.DataUsageSummary" + android:key="data_usage_summary" + android:title="@string/data_usage_summary_title" /> + </PreferenceScreen> diff --git a/src/com/android/settings/DataUsageSummary.java b/src/com/android/settings/DataUsageSummary.java new file mode 100644 index 0000000..b9d1929 --- /dev/null +++ b/src/com/android/settings/DataUsageSummary.java @@ -0,0 +1,353 @@ +/* + * 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; + +import static com.android.settings.widget.ChartView.buildChartParams; +import static com.android.settings.widget.ChartView.buildSweepParams; + +import android.app.Fragment; +import android.content.Context; +import android.content.pm.PackageManager; +import android.graphics.Color; +import android.net.INetworkStatsService; +import android.net.NetworkStats; +import android.net.NetworkStatsHistory; +import android.net.TrafficStats; +import android.os.Bundle; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.text.format.DateUtils; +import android.text.format.Formatter; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.ListView; +import android.widget.TextView; + +import com.android.settings.widget.ChartAxis; +import com.android.settings.widget.ChartGridView; +import com.android.settings.widget.ChartNetworkSeriesView; +import com.android.settings.widget.ChartSweepView; +import com.android.settings.widget.ChartSweepView.OnSweepListener; +import com.android.settings.widget.ChartView; +import com.android.settings.widget.InvertedChartAxis; +import com.google.android.collect.Lists; + +import java.util.ArrayList; +import java.util.Collections; + +public class DataUsageSummary extends Fragment { + private static final String TAG = "DataUsage"; + + // TODO: teach about wifi-vs-mobile data with tabs + + 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; + + private INetworkStatsService mStatsService; + + private ViewGroup mChartContainer; + private ListView mList; + + private ChartAxis mAxisTime; + private ChartAxis mAxisData; + + private ChartView mChart; + private ChartNetworkSeriesView mSeries; + + private ChartSweepView mSweepTime1; + private ChartSweepView mSweepTime2; + private ChartSweepView mSweepDataWarn; + private ChartSweepView mSweepDataLimit; + + private DataUsageAdapter mAdapter; + + // TODO: persist warning/limit into policy service + private static final long DATA_WARN = (long) 3.2 * GB_IN_BYTES; + private static final long DATA_LIMIT = (long) 4.8 * GB_IN_BYTES; + + @Override + public View onCreateView( + LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + + final Context context = inflater.getContext(); + final long now = System.currentTimeMillis(); + + mStatsService = INetworkStatsService.Stub.asInterface( + ServiceManager.getService(Context.NETWORK_STATS_SERVICE)); + + mAxisTime = new TimeAxis(); + mAxisData = new InvertedChartAxis(new DataAxis()); + + mChart = new ChartView(context, mAxisTime, mAxisData); + mChart.setPadding(20, 20, 20, 20); + + mChart.addView(new ChartGridView(context, mAxisTime, mAxisData), buildChartParams()); + + mSeries = new ChartNetworkSeriesView(context, mAxisTime, mAxisData); + mChart.addView(mSeries, buildChartParams()); + + mSweepTime1 = new ChartSweepView(context, mAxisTime, now - DateUtils.DAY_IN_MILLIS * 14, + Color.parseColor("#ffffff")); + mSweepTime2 = new ChartSweepView(context, mAxisTime, now - DateUtils.DAY_IN_MILLIS * 7, + Color.parseColor("#ffffff")); + mSweepDataWarn = new ChartSweepView( + context, mAxisData, DATA_WARN, Color.parseColor("#f7931d")); + mSweepDataLimit = new ChartSweepView( + context, mAxisData, DATA_LIMIT, Color.parseColor("#be1d2c")); + + mChart.addView(mSweepTime1, buildSweepParams()); + mChart.addView(mSweepTime2, buildSweepParams()); + mChart.addView(mSweepDataWarn, buildSweepParams()); + mChart.addView(mSweepDataLimit, buildSweepParams()); + + mSeries.bindSweepRange(mSweepTime1, mSweepTime2); + + mSweepTime1.addOnSweepListener(mSweepListener); + mSweepTime2.addOnSweepListener(mSweepListener); + + mAdapter = new DataUsageAdapter(); + + final View view = inflater.inflate(R.layout.data_usage_summary, container, false); + + mChartContainer = (ViewGroup) view.findViewById(R.id.chart_container); + mChartContainer.addView(mChart); + + mList = (ListView) view.findViewById(R.id.list); + mList.setAdapter(mAdapter); + + return view; + } + + @Override + public void onResume() { + super.onResume(); + + updateSummaryData(); + updateDetailData(); + + } + + private void updateSummaryData() { + try { + final NetworkStatsHistory history = mStatsService.getHistoryForNetwork( + TrafficStats.TEMPLATE_MOBILE_ALL); + mSeries.bindNetworkStats(history); + } catch (RemoteException e) { + Log.w(TAG, "problem reading stats"); + } + } + + private void updateDetailData() { + final long sweep1 = mSweepTime1.getValue(); + final long sweep2 = mSweepTime2.getValue(); + + final long start = Math.min(sweep1, sweep2); + final long end = Math.max(sweep1, sweep2); + + try { + final NetworkStats stats = mStatsService.getSummaryForAllUid( + start, end, TrafficStats.TEMPLATE_MOBILE_ALL); + mAdapter.bindStats(stats); + } catch (RemoteException e) { + Log.w(TAG, "problem reading stats"); + } + } + + private OnSweepListener mSweepListener = new OnSweepListener() { + public void onSweep(ChartSweepView sweep, boolean sweepDone) { + // always update graph clip region + mSeries.invalidate(); + + // update detail list only when done sweeping + if (sweepDone) { + updateDetailData(); + } + } + }; + + + /** + * Adapter of applications, sorted by total usage descending. + */ + public static class DataUsageAdapter extends BaseAdapter { + private ArrayList<UsageRecord> mData = Lists.newArrayList(); + + private static class UsageRecord implements Comparable<UsageRecord> { + public int uid; + public long total; + + /** {@inheritDoc} */ + public int compareTo(UsageRecord another) { + return Long.compare(another.total, total); + } + } + + public void bindStats(NetworkStats stats) { + mData.clear(); + + for (int i = 0; i < stats.length(); i++) { + final UsageRecord record = new UsageRecord(); + record.uid = stats.uid[i]; + record.total = stats.rx[i] + stats.tx[i]; + mData.add(record); + } + + Collections.sort(mData); + notifyDataSetChanged(); + } + + @Override + public int getCount() { + return mData.size(); + } + + @Override + public Object getItem(int position) { + return mData.get(position); + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + if (convertView == null) { + convertView = LayoutInflater.from(parent.getContext()).inflate( + android.R.layout.simple_list_item_2, parent, false); + } + + final Context context = parent.getContext(); + final PackageManager pm = context.getPackageManager(); + + final TextView text1 = (TextView) convertView.findViewById(android.R.id.text1); + final TextView text2 = (TextView) convertView.findViewById(android.R.id.text2); + + final UsageRecord record = mData.get(position); + text1.setText(pm.getNameForUid(record.uid)); + text2.setText(Formatter.formatFileSize(context, record.total)); + + return convertView; + } + + } + + + 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() { + // TODO: hook up these ranges to policy service + mMax = System.currentTimeMillis(); + mMin = mMax - DateUtils.DAY_IN_MILLIS * 30; + } + + /** {@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 CharSequence getLabel(long value) { + // TODO: convert to string + return 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; + } + + } + + // TODO: make data axis log-scale + + 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. + mMin = 0; + mMax = 5 * GB_IN_BYTES; + } + + /** {@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 CharSequence getLabel(long value) { + // TODO: convert to string + return Long.toString(value); + } + + /** {@inheritDoc} */ + public float[] getTickPoints() { + final float[] tickPoints = new float[16]; + + long value = mMax; + float mult = 0.8f; + for (int i = 0; i < tickPoints.length; i++) { + tickPoints[i] = convertToPoint(value); + value = (long) (value * mult); + mult *= 0.9; + } + return tickPoints; + } + } + + +} diff --git a/src/com/android/settings/Settings.java b/src/com/android/settings/Settings.java index c68ea5d..6d314ac 100644 --- a/src/com/android/settings/Settings.java +++ b/src/com/android/settings/Settings.java @@ -343,4 +343,5 @@ public class Settings extends PreferenceActivity implements ButtonBarHandler { public static class AccountSyncSettingsInAddAccountActivity extends Settings { } public static class CryptKeeperSettingsActivity extends Settings { } public static class DeviceAdminSettingsActivity extends Settings { } + public static class DataUsageSummaryActivity extends Settings { } } diff --git a/src/com/android/settings/widget/ChartAxis.java b/src/com/android/settings/widget/ChartAxis.java new file mode 100644 index 0000000..0b77ac6 --- /dev/null +++ b/src/com/android/settings/widget/ChartAxis.java @@ -0,0 +1,34 @@ +/* + * 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; + +/** + * Axis along a {@link ChartView} that knows how to convert between raw point + * and screen coordinate systems. + */ +public interface ChartAxis { + + public void setSize(float size); + + public float convertToPoint(long value); + public long convertToValue(float point); + + public CharSequence getLabel(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..be71890 --- /dev/null +++ b/src/com/android/settings/widget/ChartGridView.java @@ -0,0 +1,79 @@ +/* + * 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.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Paint.Style; +import android.view.View; + +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 { + + private final ChartAxis mHoriz; + private final ChartAxis mVert; + + private final Paint mPaintHoriz; + private final Paint mPaintVert; + + public ChartGridView(Context context, ChartAxis horiz, ChartAxis vert) { + super(context); + + mHoriz = Preconditions.checkNotNull(horiz, "missing horiz"); + mVert = Preconditions.checkNotNull(vert, "missing vert"); + + setWillNotDraw(false); + + // TODO: convert these colors to resources + mPaintHoriz = new Paint(); + mPaintHoriz.setColor(Color.parseColor("#667bb5")); + mPaintHoriz.setStrokeWidth(2.0f); + mPaintHoriz.setStyle(Style.STROKE); + mPaintHoriz.setAntiAlias(true); + + mPaintVert = new Paint(); + mPaintVert.setColor(Color.parseColor("#28262c")); + mPaintVert.setStrokeWidth(1.0f); + mPaintVert.setStyle(Style.STROKE); + mPaintVert.setAntiAlias(true); + } + + @Override + protected void onDraw(Canvas canvas) { + final int width = getWidth(); + final int height = getHeight(); + + final float[] vertTicks = mVert.getTickPoints(); + for (float y : vertTicks) { + canvas.drawLine(0, y, width, y, mPaintVert); + } + + final float[] horizTicks = mHoriz.getTickPoints(); + for (float x : horizTicks) { + canvas.drawLine(x, 0, x, height, mPaintHoriz); + } + + canvas.drawRect(0, 0, width, height, mPaintHoriz); + } +} diff --git a/src/com/android/settings/widget/ChartNetworkSeriesView.java b/src/com/android/settings/widget/ChartNetworkSeriesView.java new file mode 100644 index 0000000..1008761 --- /dev/null +++ b/src/com/android/settings/widget/ChartNetworkSeriesView.java @@ -0,0 +1,183 @@ +/* + * 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.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.Log; +import android.view.View; + +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 = false; + + private final ChartAxis mHoriz; + private final ChartAxis mVert; + + private final Paint mPaintStroke; + private final Paint mPaintFill; + private final Paint mPaintFillDisabled; + + private NetworkStatsHistory mStats; + + private Path mPathStroke; + private Path mPathFill; + + private ChartSweepView mSweep1; + private ChartSweepView mSweep2; + + public ChartNetworkSeriesView(Context context, ChartAxis horiz, ChartAxis vert) { + super(context); + + mHoriz = Preconditions.checkNotNull(horiz, "missing horiz"); + mVert = Preconditions.checkNotNull(vert, "missing vert"); + + mPaintStroke = new Paint(); + mPaintStroke.setStrokeWidth(6.0f); + mPaintStroke.setColor(Color.parseColor("#24aae1")); + mPaintStroke.setStyle(Style.STROKE); + mPaintStroke.setAntiAlias(true); + + mPaintFill = new Paint(); + mPaintFill.setColor(Color.parseColor("#c050ade5")); + mPaintFill.setStyle(Style.FILL); + mPaintFill.setAntiAlias(true); + + mPaintFillDisabled = new Paint(); + mPaintFillDisabled.setColor(Color.parseColor("#88566abc")); + mPaintFillDisabled.setStyle(Style.FILL); + mPaintFillDisabled.setAntiAlias(true); + + mPathStroke = new Path(); + mPathFill = new Path(); + } + + public void bindNetworkStats(NetworkStatsHistory stats) { + mStats = stats; + } + + public void bindSweepRange(ChartSweepView sweep1, ChartSweepView sweep2) { + // TODO: generalize to support vertical sweeps + // TODO: enforce that both sweeps are along same dimension + + mSweep1 = Preconditions.checkNotNull(sweep1, "missing sweep1"); + mSweep2 = Preconditions.checkNotNull(sweep2, "missing sweep2"); + } + + @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. + */ + private void generatePath() { + mPathStroke.reset(); + mPathFill.reset(); + + // bail when not enough stats to render + if (mStats == null || mStats.bucketCount < 2) return; + + final int width = getWidth(); + final int height = getHeight(); + + boolean started = false; + float firstX = 0; + float lastX = 0; + float lastY = 0; + + long totalData = 0; + + for (int i = 0; i < mStats.bucketCount; i++) { + final float x = mHoriz.convertToPoint(mStats.bucketStart[i]); + 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 += mStats.rx[i] + mStats.tx[i]; + } + + // 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()); + } + + // drop to bottom of graph from current location + mPathFill.lineTo(lastX, height); + mPathFill.lineTo(firstX, height); + } + + @Override + protected void onDraw(Canvas canvas) { + + // clip to sweep area + final float sweep1 = mSweep1.getPoint(); + final float sweep2 = mSweep2.getPoint(); + final float sweepLeft = Math.min(sweep1, sweep2); + final float sweepRight = Math.max(sweep1, sweep2); + + int save; + + save = canvas.save(); + canvas.clipRect(0, 0, sweepLeft, getHeight()); + canvas.drawPath(mPathFill, mPaintFillDisabled); + canvas.restoreToCount(save); + + save = canvas.save(); + canvas.clipRect(sweepRight, 0, getWidth(), getHeight()); + canvas.drawPath(mPathFill, mPaintFillDisabled); + canvas.restoreToCount(save); + + save = canvas.save(); + canvas.clipRect(sweepLeft, 0, sweepRight, 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..e3130ce --- /dev/null +++ b/src/com/android/settings/widget/ChartSweepView.java @@ -0,0 +1,165 @@ +/* + * 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.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Paint.Style; +import android.view.MotionEvent; +import android.view.View; + +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 View { + + private final Paint mPaintSweep; + private final Paint mPaintShadow; + + private final ChartAxis mAxis; + private long mValue; + + public interface OnSweepListener { + public void onSweep(ChartSweepView sweep, boolean sweepDone); + } + + private OnSweepListener mListener; + + private boolean mHorizontal; + private MotionEvent mTracking; + + public ChartSweepView(Context context, ChartAxis axis, long value, int color) { + super(context); + + mAxis = Preconditions.checkNotNull(axis, "missing axis"); + mValue = value; + + mPaintSweep = new Paint(); + mPaintSweep.setColor(color); + mPaintSweep.setStrokeWidth(3.0f); + mPaintSweep.setStyle(Style.FILL_AND_STROKE); + mPaintSweep.setAntiAlias(true); + + mPaintShadow = new Paint(); + mPaintShadow.setColor(Color.BLACK); + mPaintShadow.setStrokeWidth(6.0f); + mPaintShadow.setStyle(Style.FILL_AND_STROKE); + mPaintShadow.setAntiAlias(true); + + } + + public void addOnSweepListener(OnSweepListener listener) { + mListener = listener; + } + + private void dispatchOnSweep(boolean sweepDone) { + if (mListener != null) { + mListener.onSweep(this, sweepDone); + } + } + + public ChartAxis getAxis() { + return mAxis; + } + + public long getValue() { + return mValue; + } + + public float getPoint() { + return mAxis.convertToPoint(mValue); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + final View parent = (View) getParent(); + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: { + mTracking = event.copy(); + return true; + } + case MotionEvent.ACTION_MOVE: { + if (mHorizontal) { + setTranslationY(event.getRawY() - mTracking.getRawY()); + final float point = (getTop() + getTranslationY() + (getHeight() / 2)) + - parent.getPaddingTop(); + mValue = mAxis.convertToValue(point); + dispatchOnSweep(false); + } else { + setTranslationX(event.getRawX() - mTracking.getRawX()); + final float point = (getLeft() + getTranslationX() + (getWidth() / 2)) + - parent.getPaddingLeft(); + mValue = mAxis.convertToValue(point); + dispatchOnSweep(false); + } + return true; + } + case MotionEvent.ACTION_UP: { + mTracking = null; + setTranslationX(0); + setTranslationY(0); + requestLayout(); + dispatchOnSweep(true); + return true; + } + default: { + return false; + } + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + // need at least 50px in each direction for grippies + // TODO: provide this value through params + setMeasuredDimension(50, 50); + } + + @Override + protected void onDraw(Canvas canvas) { + + // draw line across larger dimension + final int width = getWidth(); + final int height = getHeight(); + + mHorizontal = width > height; + + if (mHorizontal) { + final int centerY = height / 2; + final int endX = width - height; + + canvas.drawLine(0, centerY, endX, centerY, mPaintShadow); + canvas.drawLine(0, centerY, endX, centerY, mPaintSweep); + canvas.drawCircle(endX, centerY, 4.0f, mPaintShadow); + canvas.drawCircle(endX, centerY, 4.0f, mPaintSweep); + } else { + final int centerX = width / 2; + final int endY = height - width; + + canvas.drawLine(centerX, 0, centerX, endY, mPaintShadow); + canvas.drawLine(centerX, 0, centerX, endY, mPaintSweep); + canvas.drawCircle(centerX, endY, 4.0f, mPaintShadow); + canvas.drawCircle(centerX, endY, 4.0f, mPaintSweep); + } + } + +} diff --git a/src/com/android/settings/widget/ChartView.java b/src/com/android/settings/widget/ChartView.java new file mode 100644 index 0000000..bcb54f0 --- /dev/null +++ b/src/com/android/settings/widget/ChartView.java @@ -0,0 +1,120 @@ +/* + * 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 android.view.ViewGroup.LayoutParams.MATCH_PARENT; +import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT; +import static com.google.common.base.Preconditions.checkNotNull; + +import android.content.Context; +import android.graphics.Rect; +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 final ChartAxis mHoriz; + private final ChartAxis mVert; + + private Rect mContent = new Rect(); + + public ChartView(Context context, ChartAxis horiz, ChartAxis vert) { + super(context); + + mHoriz = checkNotNull(horiz, "missing horiz"); + mVert = checkNotNull(vert, "missing vert"); + + setClipToPadding(false); + setClipChildren(false); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + mContent.set(l + getPaddingLeft(), t + getPaddingTop(), r - getPaddingRight(), + b - 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 ChartAxis axis = sweep.getAxis(); + final float point = sweep.getPoint(); + + if (axis == mHoriz) { + parentRect.left = parentRect.right = (int) point + getPaddingLeft(); + parentRect.bottom += child.getMeasuredWidth(); + Gravity.apply(params.gravity, child.getMeasuredWidth(), parentRect.height(), + parentRect, childRect); + + } else if (axis == mVert) { + parentRect.top = parentRect.bottom = (int) point + getPaddingTop(); + parentRect.right += child.getMeasuredHeight(); + Gravity.apply(params.gravity, parentRect.width(), child.getMeasuredHeight(), + parentRect, childRect); + + } else { + throw new IllegalStateException("unexpected axis"); + } + } + + child.layout(childRect.left, childRect.top, childRect.right, childRect.bottom); + } + } + + public static LayoutParams buildChartParams() { + final LayoutParams params = new LayoutParams(MATCH_PARENT, MATCH_PARENT); + params.gravity = Gravity.LEFT | Gravity.BOTTOM; + return params; + } + + public static LayoutParams buildSweepParams() { + final LayoutParams params = new LayoutParams(WRAP_CONTENT, WRAP_CONTENT); + params.gravity = Gravity.CENTER; + return params; + } + +} diff --git a/src/com/android/settings/widget/InvertedChartAxis.java b/src/com/android/settings/widget/InvertedChartAxis.java new file mode 100644 index 0000000..2bda320 --- /dev/null +++ b/src/com/android/settings/widget/InvertedChartAxis.java @@ -0,0 +1,59 @@ +/* + * 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; + +/** + * 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 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 CharSequence getLabel(long value) { + return mWrapped.getLabel(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; + } +} |