summaryrefslogtreecommitdiffstats
path: root/src/com
diff options
context:
space:
mode:
authorJeff Sharkey <jsharkey@android.com>2011-06-10 13:31:21 -0700
committerJeff Sharkey <jsharkey@android.com>2011-06-10 16:17:21 -0700
commit8a50364a71e7c261b54840210f8bacff5abecb34 (patch)
tree6ddcd421fd26829af3c6b0a6bcd656c6986dc182 /src/com
parent86432504d356e3de7e63b48251f653245bcafd32 (diff)
downloadpackages_apps_Settings-8a50364a71e7c261b54840210f8bacff5abecb34.zip
packages_apps_Settings-8a50364a71e7c261b54840210f8bacff5abecb34.tar.gz
packages_apps_Settings-8a50364a71e7c261b54840210f8bacff5abecb34.tar.bz2
Iterating on data usage; tabs, scrolling, cycles.
Added ActionBar items to control complexity of data surfaced; checked state causes tabs to be shown/hidden for "Mobile", "2G-3G", "4G", and "Wi-Fi" network templates. Loading historical stats and policy from system services based on selected tab. Change entire body under tabs to scroll, treating network options and chart as ListView headers. Teach chart sweep to disable intercept to play with ListView, and draw sweep disabled as dashed line. Hijacking Preference views for toggles to offer consistency. No policy updates are persisted yet. Based on available historical network stats and policy cycle reset day, build list of user-selectable cycles. Wired up chart to display cycle data and reset inspection region to last week of available data. Change-Id: Ia561578276fa23908b745fbc06a6ef828d9ccc2e
Diffstat (limited to 'src/com')
-rw-r--r--src/com/android/settings/DataUsageSummary.java686
-rw-r--r--src/com/android/settings/widget/ChartAxis.java1
-rw-r--r--src/com/android/settings/widget/ChartNetworkSeriesView.java12
-rw-r--r--src/com/android/settings/widget/ChartSweepView.java23
-rw-r--r--src/com/android/settings/widget/ChartView.java9
-rw-r--r--src/com/android/settings/widget/DataUsageChartView.java276
-rw-r--r--src/com/android/settings/widget/InvertedChartAxis.java5
7 files changed, 813 insertions, 199 deletions
diff --git a/src/com/android/settings/DataUsageSummary.java b/src/com/android/settings/DataUsageSummary.java
index b9d1929..e27227f 100644
--- a/src/com/android/settings/DataUsageSummary.java
+++ b/src/com/android/settings/DataUsageSummary.java
@@ -16,122 +16,167 @@
package com.android.settings;
-import static com.android.settings.widget.ChartView.buildChartParams;
-import static com.android.settings.widget.ChartView.buildSweepParams;
+import static android.net.NetworkPolicyManager.computeLastCycleBoundary;
+import static android.net.NetworkPolicyManager.computeNextCycleBoundary;
+import static android.net.TrafficStats.TEMPLATE_MOBILE_3G_LOWER;
+import static android.net.TrafficStats.TEMPLATE_MOBILE_4G;
+import static android.net.TrafficStats.TEMPLATE_MOBILE_ALL;
+import static android.net.TrafficStats.TEMPLATE_WIFI;
+import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
import android.app.Fragment;
import android.content.Context;
import android.content.pm.PackageManager;
-import android.graphics.Color;
+import android.net.INetworkPolicyManager;
import android.net.INetworkStatsService;
+import android.net.NetworkPolicy;
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.preference.CheckBoxPreference;
+import android.preference.Preference;
import android.text.format.DateUtils;
import android.text.format.Formatter;
+import android.text.format.Time;
import android.util.Log;
import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
import android.view.View;
+import android.view.View.OnClickListener;
import android.view.ViewGroup;
+import android.widget.AbsListView;
+import android.widget.AdapterView;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.AdapterView.OnItemSelectedListener;
+import android.widget.ArrayAdapter;
import android.widget.BaseAdapter;
+import android.widget.LinearLayout;
import android.widget.ListView;
+import android.widget.Spinner;
+import android.widget.TabHost;
+import android.widget.TabHost.OnTabChangeListener;
+import android.widget.TabHost.TabContentFactory;
+import android.widget.TabHost.TabSpec;
+import android.widget.TabWidget;
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.android.settings.widget.DataUsageChartView;
+import com.android.settings.widget.DataUsageChartView.DataUsageChartListener;
import com.google.android.collect.Lists;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
+import java.util.Locale;
public class DataUsageSummary extends Fragment {
private static final String TAG = "DataUsage";
+ private static final boolean LOGD = true;
- // TODO: teach about wifi-vs-mobile data with tabs
+ private static final int TEMPLATE_INVALID = -1;
+
+ private static final String TAB_3G = "3g";
+ private static final String TAB_4G = "4g";
+ private static final String TAB_MOBILE = "mobile";
+ private static final String TAB_WIFI = "wifi";
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 INetworkPolicyManager mPolicyService;
- private ViewGroup mChartContainer;
- private ListView mList;
+ private TabHost mTabHost;
+ private TabWidget mTabWidget;
+ private ListView mListView;
+ private DataUsageAdapter mAdapter;
- private ChartAxis mAxisTime;
- private ChartAxis mAxisData;
+ private View mHeader;
+ private LinearLayout mSwitches;
- private ChartView mChart;
- private ChartNetworkSeriesView mSeries;
+ private CheckBoxPreference mDataEnabled;
+ private CheckBoxPreference mDisableAtLimit;
+ private View mDataEnabledView;
+ private View mDisableAtLimitView;
- private ChartSweepView mSweepTime1;
- private ChartSweepView mSweepTime2;
- private ChartSweepView mSweepDataWarn;
- private ChartSweepView mSweepDataLimit;
+ private DataUsageChartView mChart;
- private DataUsageAdapter mAdapter;
+ private Spinner mCycleSpinner;
+ private CycleAdapter mCycleAdapter;
- // 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;
+ private boolean mSplit4G = false;
+ private boolean mShowWifi = false;
- @Override
- public View onCreateView(
- LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ private int mTemplate = TEMPLATE_INVALID;
- final Context context = inflater.getContext();
- final long now = System.currentTimeMillis();
+ private NetworkPolicy mPolicy;
+ private NetworkStatsHistory mHistory;
+
+ // TODO: policy service should always provide valid stub policy
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setHasOptionsMenu(true);
mStatsService = INetworkStatsService.Stub.asInterface(
ServiceManager.getService(Context.NETWORK_STATS_SERVICE));
+ mPolicyService = INetworkPolicyManager.Stub.asInterface(
+ ServiceManager.getService(Context.NETWORK_POLICY_SERVICE));
+ }
- mAxisTime = new TimeAxis();
- mAxisData = new InvertedChartAxis(new DataAxis());
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
- mChart = new ChartView(context, mAxisTime, mAxisData);
- mChart.setPadding(20, 20, 20, 20);
+ final Context context = inflater.getContext();
+ final View view = inflater.inflate(R.layout.data_usage_summary, container, false);
- mChart.addView(new ChartGridView(context, mAxisTime, mAxisData), buildChartParams());
+ mTabHost = (TabHost) view.findViewById(android.R.id.tabhost);
+ mTabWidget = (TabWidget) view.findViewById(android.R.id.tabs);
+ mListView = (ListView) view.findViewById(android.R.id.list);
- mSeries = new ChartNetworkSeriesView(context, mAxisTime, mAxisData);
- mChart.addView(mSeries, buildChartParams());
+ mTabHost.setup();
+ mTabHost.setOnTabChangedListener(mTabListener);
- 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"));
+ mHeader = inflater.inflate(R.layout.data_usage_header, mListView, false);
+ mListView.addHeaderView(mHeader, null, false);
- mChart.addView(mSweepTime1, buildSweepParams());
- mChart.addView(mSweepTime2, buildSweepParams());
- mChart.addView(mSweepDataWarn, buildSweepParams());
- mChart.addView(mSweepDataLimit, buildSweepParams());
+ mDataEnabled = new CheckBoxPreference(context);
+ mDisableAtLimit = new CheckBoxPreference(context);
- mSeries.bindSweepRange(mSweepTime1, mSweepTime2);
+ // kick refresh once to force-create views
+ refreshPreferenceViews();
- mSweepTime1.addOnSweepListener(mSweepListener);
- mSweepTime2.addOnSweepListener(mSweepListener);
+ // TODO: remove once thin preferences are supported (48dip)
+ mDataEnabledView.setLayoutParams(new LinearLayout.LayoutParams(MATCH_PARENT, 72));
+ mDisableAtLimitView.setLayoutParams(new LinearLayout.LayoutParams(MATCH_PARENT, 72));
- mAdapter = new DataUsageAdapter();
+ mDataEnabledView.setOnClickListener(mDataEnabledListener);
+ mDisableAtLimitView.setOnClickListener(mDisableAtLimitListener);
- final View view = inflater.inflate(R.layout.data_usage_summary, container, false);
+ mSwitches = (LinearLayout) mHeader.findViewById(R.id.switches);
+ mSwitches.addView(mDataEnabledView);
+ mSwitches.addView(mDisableAtLimitView);
+
+ mCycleSpinner = (Spinner) mHeader.findViewById(R.id.cycles);
+ mCycleAdapter = new CycleAdapter(context);
+ mCycleSpinner.setAdapter(mCycleAdapter);
+ mCycleSpinner.setOnItemSelectedListener(mCycleListener);
- mChartContainer = (ViewGroup) view.findViewById(R.id.chart_container);
- mChartContainer.addView(mChart);
+ mChart = new DataUsageChartView(context);
+ mChart.setListener(mChartListener);
+ mChart.setLayoutParams(new AbsListView.LayoutParams(MATCH_PARENT, 350));
+ mListView.addHeaderView(mChart, null, false);
- mList = (ListView) view.findViewById(R.id.list);
- mList.setAdapter(mAdapter);
+ mAdapter = new DataUsageAdapter();
+ mListView.setOnItemClickListener(mListListener);
+ mListView.setAdapter(mAdapter);
return view;
}
@@ -140,213 +185,472 @@ public class DataUsageSummary extends Fragment {
public void onResume() {
super.onResume();
- updateSummaryData();
- updateDetailData();
+ // this kicks off chain reaction which creates tabs, binds the body to
+ // selected network, and binds chart, cycles and detail list.
+ updateTabs();
+ }
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ inflater.inflate(R.menu.data_usage, menu);
}
- 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");
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ // TODO: persist checked-ness of options to restore tabs later
+
+ switch (item.getItemId()) {
+ case R.id.action_split_4g: {
+ mSplit4G = !item.isChecked();
+ item.setChecked(mSplit4G);
+ updateTabs();
+ return true;
+ }
+ case R.id.action_show_wifi: {
+ mShowWifi = !item.isChecked();
+ item.setChecked(mShowWifi);
+ updateTabs();
+ return true;
+ }
}
+ return false;
}
- private void updateDetailData() {
- final long sweep1 = mSweepTime1.getValue();
- final long sweep2 = mSweepTime2.getValue();
+ /**
+ * Rebuild all tabs based on {@link #mSplit4G} and {@link #mShowWifi},
+ * hiding the tabs entirely when applicable. Selects first tab, and kicks
+ * off a full rebind of body contents.
+ */
+ private void updateTabs() {
+ // TODO: persist/restore if user wants mobile split, or wifi visibility
- final long start = Math.min(sweep1, sweep2);
- final long end = Math.max(sweep1, sweep2);
+ final boolean tabsVisible = mSplit4G || mShowWifi;
+ mTabWidget.setVisibility(tabsVisible ? View.VISIBLE : View.GONE);
+ mTabHost.clearAllTabs();
- try {
- final NetworkStats stats = mStatsService.getSummaryForAllUid(
- start, end, TrafficStats.TEMPLATE_MOBILE_ALL);
- mAdapter.bindStats(stats);
- } catch (RemoteException e) {
- Log.w(TAG, "problem reading stats");
+ if (mSplit4G) {
+ mTabHost.addTab(buildTabSpec(TAB_3G, R.string.data_usage_tab_3g));
+ mTabHost.addTab(buildTabSpec(TAB_4G, R.string.data_usage_tab_4g));
}
- }
-
- 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();
+ if (mShowWifi) {
+ if (!mSplit4G) {
+ mTabHost.addTab(buildTabSpec(TAB_MOBILE, R.string.data_usage_tab_mobile));
}
+ mTabHost.addTab(buildTabSpec(TAB_WIFI, R.string.data_usage_tab_wifi));
}
- };
+ if (mTabWidget.getTabCount() > 0) {
+ // select first tab, which will kick off updateBody()
+ mTabHost.setCurrentTab(0);
+ } else {
+ // no tabs shown; update body manually
+ updateBody();
+ }
+ }
/**
- * Adapter of applications, sorted by total usage descending.
+ * Factory that provide empty {@link View} to make {@link TabHost} happy.
*/
- public static class DataUsageAdapter extends BaseAdapter {
- private ArrayList<UsageRecord> mData = Lists.newArrayList();
+ private TabContentFactory mEmptyTabContent = new TabContentFactory() {
+ /** {@inheritDoc} */
+ public View createTabContent(String tag) {
+ return new View(mTabHost.getContext());
+ }
+ };
- private static class UsageRecord implements Comparable<UsageRecord> {
- public int uid;
- public long total;
+ /**
+ * 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);
+ }
- /** {@inheritDoc} */
- public int compareTo(UsageRecord another) {
- return Long.compare(another.total, total);
- }
+ private OnTabChangeListener mTabListener = new OnTabChangeListener() {
+ /** {@inheritDoc} */
+ public void onTabChanged(String tabId) {
+ // user changed tab; update body
+ updateBody();
}
+ };
- public void bindStats(NetworkStats stats) {
- mData.clear();
+ /**
+ * Update body content based on current tab. Loads
+ * {@link NetworkStatsHistory} and {@link NetworkPolicy} from system, and
+ * binds them to visible controls.
+ */
+ private void updateBody() {
+ final String tabTag = mTabHost.getCurrentTabTag();
+ final String currentTab = tabTag != null ? tabTag : TAB_MOBILE;
+
+ if (LOGD) Log.d(TAG, "updateBody() with currentTab=" + currentTab);
+
+ if (TAB_WIFI.equals(currentTab)) {
+ // wifi doesn't have any controls
+ mDataEnabledView.setVisibility(View.GONE);
+ mDisableAtLimitView.setVisibility(View.GONE);
+ mTemplate = TEMPLATE_WIFI;
+
+ } else {
+ // make sure we show for non-wifi
+ mDataEnabledView.setVisibility(View.VISIBLE);
+ mDisableAtLimitView.setVisibility(View.VISIBLE);
+ }
- 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);
- }
+ if (TAB_MOBILE.equals(currentTab)) {
+ mDataEnabled.setTitle(R.string.data_usage_enable_mobile);
+ mDisableAtLimit.setTitle(R.string.data_usage_disable_mobile_limit);
+ mTemplate = TEMPLATE_MOBILE_ALL;
+
+ } else if (TAB_3G.equals(currentTab)) {
+ mDataEnabled.setTitle(R.string.data_usage_enable_3g);
+ mDisableAtLimit.setTitle(R.string.data_usage_disable_3g_limit);
+ mTemplate = TEMPLATE_MOBILE_3G_LOWER;
+
+ } else if (TAB_4G.equals(currentTab)) {
+ mDataEnabled.setTitle(R.string.data_usage_enable_4g);
+ mDisableAtLimit.setTitle(R.string.data_usage_disable_4g_limit);
+ mTemplate = TEMPLATE_MOBILE_4G;
- Collections.sort(mData);
- notifyDataSetChanged();
}
- @Override
- public int getCount() {
- return mData.size();
+ // TODO: populate checkbox based on radio preferences
+ mDataEnabled.setChecked(true);
+
+ try {
+ // load policy and stats for current template
+ mPolicy = mPolicyService.getNetworkPolicy(mTemplate, null);
+ mHistory = mStatsService.getHistoryForNetwork(mTemplate);
+ } catch (RemoteException e) {
+ // since we can't do much without policy or history, and we don't
+ // want to leave with half-baked UI, we bail hard.
+ throw new RuntimeException("problem reading network policy or stats", e);
}
- @Override
- public Object getItem(int position) {
- return mData.get(position);
+ // TODO: eventually service will always provide stub policy
+ if (mPolicy == null) {
+ mPolicy = new NetworkPolicy(1, 4 * GB_IN_BYTES, -1);
}
- @Override
- public long getItemId(int position) {
- return position;
+ // bind chart to historical stats
+ mChart.bindNetworkPolicy(mPolicy);
+ mChart.bindNetworkStats(mHistory);
+
+ // generate cycle list based on policy and available history
+ updateCycleList();
+
+ // reflect policy limit in checkbox
+ mDisableAtLimit.setChecked(mPolicy.limitBytes != -1);
+
+ // force scroll to top of body
+ mListView.smoothScrollToPosition(0);
+
+ // kick preference views so they rebind from changes above
+ refreshPreferenceViews();
+ }
+
+ /**
+ * Return full time bounds (earliest and latest time recorded) of the given
+ * {@link NetworkStatsHistory}.
+ */
+ private static long[] getHistoryBounds(NetworkStatsHistory history) {
+ final long currentTime = System.currentTimeMillis();
+
+ long start = currentTime;
+ long end = currentTime;
+ if (history.bucketCount > 0) {
+ start = history.bucketStart[0];
+ end = history.bucketStart[history.bucketCount - 1];
}
- @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);
- }
+ return new long[] { start, end };
+ }
- final Context context = parent.getContext();
- final PackageManager pm = context.getPackageManager();
+ /**
+ * Rebuild {@link #mCycleAdapter} based on {@link NetworkPolicy#cycleDay}
+ * and available {@link NetworkStatsHistory} data. Always selects the newest
+ * item, updating the inspection range on {@link #mChart}.
+ */
+ private void updateCycleList() {
+ mCycleAdapter.clear();
- final TextView text1 = (TextView) convertView.findViewById(android.R.id.text1);
- final TextView text2 = (TextView) convertView.findViewById(android.R.id.text2);
+ final Context context = mCycleSpinner.getContext();
- final UsageRecord record = mData.get(position);
- text1.setText(pm.getNameForUid(record.uid));
- text2.setText(Formatter.formatFileSize(context, record.total));
+ final long[] bounds = getHistoryBounds(mHistory);
+ final long historyStart = bounds[0];
+ final long historyEnd = bounds[1];
- return convertView;
+ // find the next cycle boundary
+ long cycleEnd = computeNextCycleBoundary(historyEnd, mPolicy);
+
+ int guardCount = 0;
+
+ // walk backwards, generating all valid cycle ranges
+ while (cycleEnd > historyStart) {
+ final long cycleStart = computeLastCycleBoundary(cycleEnd, mPolicy);
+ Log.d(TAG, "generating cs=" + cycleStart + " to ce=" + cycleEnd + " waiting for hs="
+ + historyStart);
+ mCycleAdapter.add(new CycleItem(context, cycleStart, cycleEnd));
+ cycleEnd = cycleStart;
+
+ // TODO: remove this guard once we have better testing
+ if (guardCount++ > 50) {
+ Log.wtf(TAG, "stuck generating ranges for bounds=" + Arrays.toString(bounds)
+ + " and policy=" + mPolicy);
+ }
}
+ // one last cycle entry to change date
+ mCycleAdapter.add(new CycleChangeItem(context));
+
+ // force pick the current cycle (first item)
+ mCycleSpinner.setSelection(0);
+ mCycleListener.onItemSelected(mCycleSpinner, null, 0, 0);
}
+ /**
+ * Force rebind of hijacked {@link Preference} views.
+ */
+ private void refreshPreferenceViews() {
+ mDataEnabledView = mDataEnabled.getView(mDataEnabledView, mListView);
+ mDisableAtLimitView = mDisableAtLimit.getView(mDisableAtLimitView, mListView);
+ }
- public static class TimeAxis implements ChartAxis {
- private static final long TICK_INTERVAL = DateUtils.DAY_IN_MILLIS * 7;
+ private OnClickListener mDataEnabledListener = new OnClickListener() {
+ /** {@inheritDoc} */
+ public void onClick(View v) {
+ mDataEnabled.setChecked(!mDataEnabled.isChecked());
+ refreshPreferenceViews();
- private long mMin;
- private long mMax;
- private float mSize;
+ // TODO: wire up to telephony to enable/disable radios
+ }
+ };
- public TimeAxis() {
- // TODO: hook up these ranges to policy service
- mMax = System.currentTimeMillis();
- mMin = mMax - DateUtils.DAY_IN_MILLIS * 30;
+ private OnClickListener mDisableAtLimitListener = new OnClickListener() {
+ /** {@inheritDoc} */
+ public void onClick(View v) {
+ final boolean disableAtLimit = !mDisableAtLimit.isChecked();
+ mDisableAtLimit.setChecked(disableAtLimit);
+ refreshPreferenceViews();
+
+ // TODO: push updated policy to service
+ // TODO: show interstitial warning dialog to user
+ final long limitBytes = disableAtLimit ? 5 * GB_IN_BYTES : -1;
+ mPolicy = new NetworkPolicy(mPolicy.cycleDay, mPolicy.warningBytes, limitBytes);
+ mChart.bindNetworkPolicy(mPolicy);
}
+ };
+ private OnItemClickListener mListListener = new OnItemClickListener() {
/** {@inheritDoc} */
- public void setSize(float size) {
- this.mSize = size;
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ final Object object = parent.getItemAtPosition(position);
+
+ // TODO: show app details
+ Log.d(TAG, "showing app details for " + object);
}
+ };
+ private OnItemSelectedListener mCycleListener = new OnItemSelectedListener() {
/** {@inheritDoc} */
- public float convertToPoint(long value) {
- return (mSize * (value - mMin)) / (mMax - mMin);
+ public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
+ final CycleItem cycle = (CycleItem) parent.getItemAtPosition(position);
+ if (cycle instanceof CycleChangeItem) {
+ // TODO: show "define cycle" dialog
+ // also reset back to first cycle
+ Log.d(TAG, "CHANGE CYCLE DIALOG!!");
+
+ } else {
+ if (LOGD) Log.d(TAG, "shoiwng cycle " + cycle);
+
+ // update chart to show selected cycle, and update detail data
+ // to match updated sweep bounds.
+ final long[] bounds = getHistoryBounds(mHistory);
+ mChart.setVisibleRange(cycle.start, cycle.end, bounds[1]);
+
+ updateDetailData();
+ }
}
/** {@inheritDoc} */
- public long convertToValue(float point) {
- return (long) (mMin + ((point * (mMax - mMin)) / mSize));
+ public void onNothingSelected(AdapterView<?> parent) {
+ // ignored
}
+ };
+
+ /**
+ * Update {@link #mAdapter} with sorted list of applications data usage,
+ * based on current inspection from {@link #mChart}.
+ */
+ private void updateDetailData() {
+ if (LOGD) Log.d(TAG, "updateDetailData()");
+ try {
+ final long[] range = mChart.getInspectRange();
+ final NetworkStats stats = mStatsService.getSummaryForAllUid(
+ range[0], range[1], mTemplate);
+ mAdapter.bindStats(stats);
+ } catch (RemoteException e) {
+ Log.w(TAG, "problem reading stats");
+ }
+ }
+
+ private DataUsageChartListener mChartListener = new DataUsageChartListener() {
/** {@inheritDoc} */
- public CharSequence getLabel(long value) {
- // TODO: convert to string
- return Long.toString(value);
+ public void onInspectRangeChanged() {
+ if (LOGD) Log.d(TAG, "onInspectRangeChanged()");
+ updateDetailData();
}
/** {@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));
+ public void onLimitsChanged() {
+ if (LOGD) Log.d(TAG, "onLimitsChanged()");
+
+ // redefine policy and persist into service
+ // TODO: kick this onto background thread, since service touches disk
+
+ // TODO: remove this mPolicy null check, since later service will
+ // always define baseline value.
+ final int cycleDay = mPolicy != null ? mPolicy.cycleDay : 1;
+ final long warningBytes = mChart.getWarningBytes();
+ final long limitBytes = mDisableAtLimit.isChecked() ? -1 : mChart.getLimitBytes();
+
+ mPolicy = new NetworkPolicy(cycleDay, warningBytes, limitBytes);
+ if (LOGD) Log.d(TAG, "persisting policy=" + mPolicy);
+
+ try {
+ mPolicyService.setNetworkPolicy(mTemplate, null, mPolicy);
+ } catch (RemoteException e) {
+ Log.w(TAG, "problem persisting policy", e);
+ }
+ }
+ };
+
+
+ /**
+ * List item that reflects a specific data usage cycle.
+ */
+ public static class CycleItem {
+ public CharSequence label;
+ public long start;
+ public long end;
+
+ private static final StringBuilder sBuilder = new StringBuilder(50);
+ private static final java.util.Formatter sFormatter = new java.util.Formatter(
+ sBuilder, Locale.getDefault());
+
+ CycleItem(CharSequence label) {
+ this.label = label;
+ }
+
+ public CycleItem(Context context, long start, long end) {
+ this.label = formatDateRangeUtc(context, start, end);
+ this.start = start;
+ this.end = end;
+ }
+
+ private static String formatDateRangeUtc(Context context, long start, long end) {
+ synchronized (sBuilder) {
+ sBuilder.setLength(0);
+ return DateUtils.formatDateRange(context, sFormatter, start, end,
+ DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_ABBREV_MONTH,
+ Time.TIMEZONE_UTC).toString();
}
- return tickPoints;
}
+ @Override
+ public String toString() {
+ return label.toString();
+ }
+ }
+
+ /**
+ * Special-case data usage cycle that triggers dialog to change
+ * {@link NetworkPolicy#cycleDay}.
+ */
+ public static class CycleChangeItem extends CycleItem {
+ public CycleChangeItem(Context context) {
+ super(context.getString(R.string.data_usage_change_cycle));
+ }
}
- // TODO: make data axis log-scale
+ public static class CycleAdapter extends ArrayAdapter<CycleItem> {
+ public CycleAdapter(Context context) {
+ super(context, android.R.layout.simple_spinner_item);
+ setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
+ }
+ }
- public static class DataAxis implements ChartAxis {
- private long mMin;
- private long mMax;
- private float mSize;
+ /**
+ * Adapter of applications, sorted by total usage descending.
+ */
+ public static class DataUsageAdapter extends BaseAdapter {
+ private ArrayList<AppUsageItem> mItems = Lists.newArrayList();
+
+ private static class AppUsageItem implements Comparable<AppUsageItem> {
+ public int uid;
+ public long total;
- 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 int compareTo(AppUsageItem another) {
+ return Long.compare(another.total, total);
+ }
}
- /** {@inheritDoc} */
- public void setSize(float size) {
- this.mSize = size;
+ public void bindStats(NetworkStats stats) {
+ mItems.clear();
+
+ for (int i = 0; i < stats.length(); i++) {
+ final AppUsageItem item = new AppUsageItem();
+ item.uid = stats.uid[i];
+ item.total = stats.rx[i] + stats.tx[i];
+ mItems.add(item);
+ }
+
+ Collections.sort(mItems);
+ notifyDataSetChanged();
}
- /** {@inheritDoc} */
- public float convertToPoint(long value) {
- return (mSize * (value - mMin)) / (mMax - mMin);
+ @Override
+ public int getCount() {
+ return mItems.size();
}
- /** {@inheritDoc} */
- public long convertToValue(float point) {
- return (long) (mMin + ((point * (mMax - mMin)) / mSize));
+ @Override
+ public Object getItem(int position) {
+ return mItems.get(position);
}
- /** {@inheritDoc} */
- public CharSequence getLabel(long value) {
- // TODO: convert to string
- return Long.toString(value);
+ @Override
+ public long getItemId(int position) {
+ return position;
}
- /** {@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;
+ @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);
}
- return tickPoints;
+
+ 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 AppUsageItem item = mItems.get(position);
+ text1.setText(pm.getNameForUid(item.uid));
+ text2.setText(Formatter.formatFileSize(context, item.total));
+
+ return convertView;
}
+
}
diff --git a/src/com/android/settings/widget/ChartAxis.java b/src/com/android/settings/widget/ChartAxis.java
index 0b77ac6..2b21d28 100644
--- a/src/com/android/settings/widget/ChartAxis.java
+++ b/src/com/android/settings/widget/ChartAxis.java
@@ -22,6 +22,7 @@ package com.android.settings.widget;
*/
public interface ChartAxis {
+ public void setBounds(long min, long max);
public void setSize(float size);
public float convertToPoint(long value);
diff --git a/src/com/android/settings/widget/ChartNetworkSeriesView.java b/src/com/android/settings/widget/ChartNetworkSeriesView.java
index 1008761..d0a2742 100644
--- a/src/com/android/settings/widget/ChartNetworkSeriesView.java
+++ b/src/com/android/settings/widget/ChartNetworkSeriesView.java
@@ -35,7 +35,7 @@ import com.google.common.base.Preconditions;
*/
public class ChartNetworkSeriesView extends View {
private static final String TAG = "ChartNetworkSeriesView";
- private static final boolean LOGD = false;
+ private static final boolean LOGD = true;
private final ChartAxis mHoriz;
private final ChartAxis mVert;
@@ -80,6 +80,9 @@ public class ChartNetworkSeriesView extends View {
public void bindNetworkStats(NetworkStatsHistory stats) {
mStats = stats;
+
+ mPathStroke.reset();
+ mPathFill.reset();
}
public void bindSweepRange(ChartSweepView sweep1, ChartSweepView sweep2) {
@@ -99,7 +102,9 @@ public class ChartNetworkSeriesView extends View {
* Erase any existing {@link Path} and generate series outline based on
* currently bound {@link NetworkStatsHistory} data.
*/
- private void generatePath() {
+ public void generatePath() {
+ if (LOGD) Log.d(TAG, "generatePath()");
+
mPathStroke.reset();
mPathFill.reset();
@@ -114,6 +119,9 @@ public class ChartNetworkSeriesView extends View {
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;
for (int i = 0; i < mStats.bucketCount; i++) {
diff --git a/src/com/android/settings/widget/ChartSweepView.java b/src/com/android/settings/widget/ChartSweepView.java
index e3130ce..788caad 100644
--- a/src/com/android/settings/widget/ChartSweepView.java
+++ b/src/com/android/settings/widget/ChartSweepView.java
@@ -19,6 +19,7 @@ package com.android.settings.widget;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
+import android.graphics.DashPathEffect;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.view.MotionEvent;
@@ -33,6 +34,7 @@ import com.google.common.base.Preconditions;
public class ChartSweepView extends View {
private final Paint mPaintSweep;
+ private final Paint mPaintSweepDisabled;
private final Paint mPaintShadow;
private final ChartAxis mAxis;
@@ -59,6 +61,13 @@ public class ChartSweepView extends View {
mPaintSweep.setStyle(Style.FILL_AND_STROKE);
mPaintSweep.setAntiAlias(true);
+ mPaintSweepDisabled = new Paint();
+ mPaintSweepDisabled.setColor(color);
+ mPaintSweepDisabled.setStrokeWidth(1.5f);
+ mPaintSweepDisabled.setStyle(Style.FILL_AND_STROKE);
+ mPaintSweepDisabled.setPathEffect(new DashPathEffect(new float[] { 5, 5 }, 0));
+ mPaintSweepDisabled.setAntiAlias(true);
+
mPaintShadow = new Paint();
mPaintShadow.setColor(Color.BLACK);
mPaintShadow.setStrokeWidth(6.0f);
@@ -81,6 +90,10 @@ public class ChartSweepView extends View {
return mAxis;
}
+ public void setValue(long value) {
+ mValue = value;
+ }
+
public long getValue() {
return mValue;
}
@@ -91,6 +104,8 @@ public class ChartSweepView extends View {
@Override
public boolean onTouchEvent(MotionEvent event) {
+ if (!isEnabled()) return false;
+
final View parent = (View) getParent();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
@@ -98,6 +113,8 @@ public class ChartSweepView extends View {
return true;
}
case MotionEvent.ACTION_MOVE: {
+ getParent().requestDisallowInterceptTouchEvent(true);
+
if (mHorizontal) {
setTranslationY(event.getRawY() - mTracking.getRawY());
final float point = (getTop() + getTranslationY() + (getHeight() / 2))
@@ -143,12 +160,14 @@ public class ChartSweepView extends View {
mHorizontal = width > height;
+ final Paint linePaint = isEnabled() ? mPaintSweep : mPaintSweepDisabled;
+
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.drawLine(0, centerY, endX, centerY, linePaint);
canvas.drawCircle(endX, centerY, 4.0f, mPaintShadow);
canvas.drawCircle(endX, centerY, 4.0f, mPaintSweep);
} else {
@@ -156,7 +175,7 @@ public class ChartSweepView extends View {
final int endY = height - width;
canvas.drawLine(centerX, 0, centerX, endY, mPaintShadow);
- canvas.drawLine(centerX, 0, centerX, endY, mPaintSweep);
+ canvas.drawLine(centerX, 0, centerX, endY, linePaint);
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
index bcb54f0..3e5fc50 100644
--- a/src/com/android/settings/widget/ChartView.java
+++ b/src/com/android/settings/widget/ChartView.java
@@ -22,6 +22,7 @@ import static com.google.common.base.Preconditions.checkNotNull;
import android.content.Context;
import android.graphics.Rect;
+import android.util.Log;
import android.view.Gravity;
import android.view.View;
import android.widget.FrameLayout;
@@ -37,8 +38,8 @@ public class ChartView extends FrameLayout {
// TODO: extend something that supports two-dimensional scrolling
- private final ChartAxis mHoriz;
- private final ChartAxis mVert;
+ final ChartAxis mHoriz;
+ final ChartAxis mVert;
private Rect mContent = new Rect();
@@ -54,8 +55,8 @@ public class ChartView extends FrameLayout {
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
- mContent.set(l + getPaddingLeft(), t + getPaddingTop(), r - getPaddingRight(),
- b - getPaddingBottom());
+ mContent.set(getPaddingLeft(), getPaddingTop(), r - l - getPaddingRight(),
+ b - t - getPaddingBottom());
final int width = mContent.width();
final int height = mContent.height();
diff --git a/src/com/android/settings/widget/DataUsageChartView.java b/src/com/android/settings/widget/DataUsageChartView.java
new file mode 100644
index 0000000..defa953
--- /dev/null
+++ b/src/com/android/settings/widget/DataUsageChartView.java
@@ -0,0 +1,276 @@
+/*
+ * 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.Color;
+import android.net.NetworkPolicy;
+import android.net.NetworkStatsHistory;
+import android.text.format.DateUtils;
+
+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;
+
+ private ChartNetworkSeriesView mSeries;
+
+ // TODO: limit sweeps at graph boundaries
+ private ChartSweepView mSweepTime1;
+ private ChartSweepView mSweepTime2;
+ private ChartSweepView mSweepDataWarn;
+ private ChartSweepView mSweepDataLimit;
+
+ public interface DataUsageChartListener {
+ public void onInspectRangeChanged();
+ public void onLimitsChanged();
+ }
+
+ private DataUsageChartListener mListener;
+
+ private static ChartAxis buildTimeAxis() {
+ return new TimeAxis();
+ }
+
+ private static ChartAxis buildDataAxis() {
+ return new InvertedChartAxis(new DataAxis());
+ }
+
+ public DataUsageChartView(Context context) {
+ super(context, buildTimeAxis(), buildDataAxis());
+ setPadding(20, 20, 20, 20);
+
+ addView(new ChartGridView(context, mHoriz, mVert), buildChartParams());
+
+ mSeries = new ChartNetworkSeriesView(context, mHoriz, mVert);
+ addView(mSeries, buildChartParams());
+
+ mSweepTime1 = new ChartSweepView(context, mHoriz, 0L, Color.parseColor("#ffffff"));
+ mSweepTime2 = new ChartSweepView(context, mHoriz, 0L, Color.parseColor("#ffffff"));
+ mSweepDataWarn = new ChartSweepView(context, mVert, 0L, Color.parseColor("#f7931d"));
+ mSweepDataLimit = new ChartSweepView(context, mVert, 0L, Color.parseColor("#be1d2c"));
+
+ addView(mSweepTime1, buildSweepParams());
+ addView(mSweepTime2, buildSweepParams());
+ addView(mSweepDataWarn, buildSweepParams());
+ addView(mSweepDataLimit, buildSweepParams());
+
+ mSeries.bindSweepRange(mSweepTime1, mSweepTime2);
+
+ mSweepTime1.addOnSweepListener(mSweepListener);
+ mSweepTime2.addOnSweepListener(mSweepListener);
+
+ }
+
+ public void setListener(DataUsageChartListener listener) {
+ mListener = listener;
+ }
+
+ public void bindNetworkStats(NetworkStatsHistory stats) {
+ mSeries.bindNetworkStats(stats);
+ }
+
+ public void bindNetworkPolicy(NetworkPolicy policy) {
+ if (policy.limitBytes != -1) {
+ mSweepDataLimit.setValue(policy.limitBytes);
+ mSweepDataLimit.setEnabled(true);
+ } else {
+ mSweepDataLimit.setValue(5 * GB_IN_BYTES);
+ mSweepDataLimit.setEnabled(false);
+ }
+
+ mSweepDataWarn.setValue(policy.warningBytes);
+ }
+
+ 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 && mListener != null) {
+ mListener.onInspectRangeChanged();
+ }
+ }
+ };
+
+ /**
+ * Return current inspection range (start and end time) based on internal
+ * {@link ChartSweepView} positions.
+ */
+ public long[] getInspectRange() {
+ final long sweep1 = mSweepTime1.getValue();
+ final long sweep2 = mSweepTime2.getValue();
+ final long start = Math.min(sweep1, sweep2);
+ final long end = Math.max(sweep1, sweep2);
+ return new long[] { start, end };
+ }
+
+ public long getWarningBytes() {
+ return mSweepDataWarn.getValue();
+ }
+
+ public long getLimitBytes() {
+ return mSweepDataLimit.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));
+
+ mSweepTime1.setValue(sweepMin);
+ mSweepTime2.setValue(sweepMax);
+
+ requestLayout();
+ mSeries.generatePath();
+ }
+
+ 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 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;
+ }
+ }
+
+ public static class DataAxis implements ChartAxis {
+ private long mMin;
+ private long mMax;
+ private long mMinLog;
+ private long mMaxLog;
+ private float mSize;
+
+ public DataAxis() {
+ // TODO: adapt ranges to show when history >5GB, and handle 4G
+ // interfaces with higher limits.
+ setBounds(1 * MB_IN_BYTES, 5 * GB_IN_BYTES);
+ }
+
+ /** {@inheritDoc} */
+ public void setBounds(long min, long max) {
+ mMin = min;
+ mMax = max;
+ mMinLog = (long) Math.log(mMin);
+ mMaxLog = (long) Math.log(mMax);
+ }
+
+ /** {@inheritDoc} */
+ public void setSize(float size) {
+ this.mSize = size;
+ }
+
+ /** {@inheritDoc} */
+ public float convertToPoint(long value) {
+ return (mSize * (value - mMin)) / (mMax - mMin);
+
+ // TODO: finish tweaking log scale
+// if (value > mMin) {
+// return (float) ((mSize * (Math.log(value) - mMinLog)) / (mMaxLog - mMinLog));
+// } else {
+// return 0;
+// }
+ }
+
+ /** {@inheritDoc} */
+ public long convertToValue(float point) {
+ return (long) (mMin + ((point * (mMax - mMin)) / mSize));
+
+ // TODO: finish tweaking log scale
+// return (long) Math.pow(Math.E, (mMinLog + ((point * (mMaxLog - mMinLog)) / 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/widget/InvertedChartAxis.java b/src/com/android/settings/widget/InvertedChartAxis.java
index 2bda320..e7e7893 100644
--- a/src/com/android/settings/widget/InvertedChartAxis.java
+++ b/src/com/android/settings/widget/InvertedChartAxis.java
@@ -28,6 +28,11 @@ public class InvertedChartAxis implements ChartAxis {
}
/** {@inheritDoc} */
+ public void setBounds(long min, long max) {
+ mWrapped.setBounds(min, max);
+ }
+
+ /** {@inheritDoc} */
public void setSize(float size) {
mSize = size;
mWrapped.setSize(size);