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
diff --git a/res/layout/data_usage_header.xml b/res/layout/data_usage_header.xml
new file mode 100644
index 0000000..4d8a5dd
--- /dev/null
+++ b/res/layout/data_usage_header.xml
@@ -0,0 +1,49 @@
+<?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="wrap_content"
+ android:orientation="vertical">
+
+ <LinearLayout
+ android:id="@+id/switches"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:paddingLeft="16dip"
+ android:paddingRight="16dip">
+
+ <TextView
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:text="@string/data_usage_cycle" />
+
+ <Spinner
+ android:id="@+id/cycles"
+ android:layout_width="0dip"
+ android:layout_height="wrap_content"
+ android:layout_weight="1" />
+
+ </LinearLayout>
+
+</LinearLayout>
diff --git a/res/layout/data_usage_summary.xml b/res/layout/data_usage_summary.xml
index 9a356ae..fc62465 100644
--- a/res/layout/data_usage_summary.xml
+++ b/res/layout/data_usage_summary.xml
@@ -14,20 +14,34 @@
limitations under the License.
-->
-<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+<TabHost xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@android:id/tabhost"
android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:orientation="vertical">
+ android:layout_height="match_parent">
- <FrameLayout
- android:id="@+id/chart_container"
+ <LinearLayout
android:layout_width="match_parent"
- android:layout_height="200dip" />
+ android:layout_height="match_parent"
+ android:orientation="vertical">
- <ListView
- android:id="@+id/list"
- android:layout_width="match_parent"
- android:layout_height="0dip"
- android:layout_weight="1" />
+ <TabWidget
+ android:id="@android:id/tabs"
+ android:orientation="horizontal"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" />
-</LinearLayout>
+ <!-- give an empty content area to make tabhost happy -->
+ <FrameLayout
+ android:id="@android:id/tabcontent"
+ android:layout_width="0dip"
+ android:layout_height="0dip" />
+
+ <ListView
+ android:id="@android:id/list"
+ android:layout_width="match_parent"
+ android:layout_height="0dip"
+ android:layout_weight="1" />
+
+ </LinearLayout>
+
+</TabHost>
diff --git a/res/layout/tab_indicator_thin_holo.xml b/res/layout/tab_indicator_thin_holo.xml
new file mode 100644
index 0000000..e4c4652
--- /dev/null
+++ b/res/layout/tab_indicator_thin_holo.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.
+-->
+
+<RelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="0dp"
+ android:layout_height="48dp"
+ android:layout_weight="1"
+ android:background="@*android:drawable/tab_indicator_holo">
+
+ <TextView
+ android:id="@android:id/title"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:singleLine="true"
+ android:ellipsize="marquee"
+ android:gravity="center"
+ android:textAppearance="?android:attr/textAppearanceMedium" />
+
+</RelativeLayout>
diff --git a/res/menu/data_usage.xml b/res/menu/data_usage.xml
new file mode 100644
index 0000000..a95c074
--- /dev/null
+++ b/res/menu/data_usage.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.
+-->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+ <item
+ android:id="@+id/action_settings"
+ android:icon="@drawable/ic_sysbar_quicksettings"
+ android:showAsAction="always">
+ <menu>
+ <item
+ android:id="@+id/action_split_4g"
+ android:title="@string/data_usage_menu_split_4g"
+ android:checkable="true" />
+ <item
+ android:id="@+id/action_show_wifi"
+ android:title="@string/data_usage_menu_show_wifi"
+ android:checkable="true" />
+ </menu>
+ </item>
+</menu>
diff --git a/res/values/strings.xml b/res/values/strings.xml
index eaacf44..0c1abff 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -3361,5 +3361,38 @@
<!-- Activity title for network data usage summary. [CHAR LIMIT=25] -->
<string name="data_usage_summary_title">Data usage</string>
+ <!-- Title for option to pick visible time range from a list available usage periods. [CHAR LIMIT=25] -->
+ <string name="data_usage_cycle">Data usage cycle</string>
+ <!-- Title for checkbox menu option to show 4G mobile data usage separate from other mobile data usage. [CHAR LIMIT=32] -->
+ <string name="data_usage_menu_split_4g">Split 4G usage</string>
+ <!-- Title for checkbox menu option to show Wi-Fi data usage. [CHAR LIMIT=32] -->
+ <string name="data_usage_menu_show_wifi">Show Wi-Fi usage</string>
+ <!-- Title for option to change data usage cycle day. [CHAR LIMIT=32] -->
+ <string name="data_usage_change_cycle">Change cycle\u2026</string>
+ <!-- Body of dialog prompting user to change numerical day of month that data usage cycle should reset. [CHAR LIMIT=64] -->
+ <string name="data_usage_pick_cycle_day">Day of month to reset data usage cycle:</string>
+
+ <!-- Checkbox label that will disable mobile network data connection when user-defined limit is reached. [CHAR LIMIT=32] -->
+ <string name="data_usage_disable_mobile_limit">Disable mobile data at limit</string>
+ <!-- Checkbox label that will disable 4G network data connection when user-defined limit is reached. [CHAR LIMIT=32] -->
+ <string name="data_usage_disable_4g_limit">Disable 4G data at limit</string>
+ <!-- Checkbox label that will disable 2G-3G network data connection when user-defined limit is reached. [CHAR LIMIT=32] -->
+ <string name="data_usage_disable_3g_limit">Disable 2G-3G data at limit</string>
+
+ <!-- Tab title for showing Wi-Fi data usage. [CHAR LIMIT=10] -->
+ <string name="data_usage_tab_wifi">Wi-Fi</string>
+ <!-- Tab title for showing combined mobile data usage. [CHAR LIMIT=10] -->
+ <string name="data_usage_tab_mobile">Mobile</string>
+ <!-- Tab title for showing 4G data usage. [CHAR LIMIT=10] -->
+ <string name="data_usage_tab_4g">4G</string>
+ <!-- Tab title for showing 2G and 3G data usage. [CHAR LIMIT=10] -->
+ <string name="data_usage_tab_3g">2G-3G</string>
+
+ <!-- Toggle switch title for enabling all mobile data network connections. [CHAR LIMIT=32] -->
+ <string name="data_usage_enable_mobile">Mobile data</string>
+ <!-- Toggle switch title for enabling 2G and 3G data network connections. [CHAR LIMIT=32] -->
+ <string name="data_usage_enable_3g">2G-3G data</string>
+ <!-- Toggle switch title for enabling 4G data network connection. [CHAR LIMIT=32] -->
+ <string name="data_usage_enable_4g">4G data</string>
</resources>
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 ChartAxis mAxisTime;
- private ChartAxis mAxisData;
-
- private ChartView mChart;
- private ChartNetworkSeriesView mSeries;
-
- private ChartSweepView mSweepTime1;
- private ChartSweepView mSweepTime2;
- private ChartSweepView mSweepDataWarn;
- private ChartSweepView mSweepDataLimit;
-
+ private TabHost mTabHost;
+ private TabWidget mTabWidget;
+ private ListView mListView;
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;
+ private View mHeader;
+ private LinearLayout mSwitches;
+
+ private CheckBoxPreference mDataEnabled;
+ private CheckBoxPreference mDisableAtLimit;
+ private View mDataEnabledView;
+ private View mDisableAtLimitView;
+
+ private DataUsageChartView mChart;
+
+ private Spinner mCycleSpinner;
+ private CycleAdapter mCycleAdapter;
+
+ private boolean mSplit4G = false;
+ private boolean mShowWifi = false;
+
+ private int mTemplate = TEMPLATE_INVALID;
+
+ private NetworkPolicy mPolicy;
+ private NetworkStatsHistory mHistory;
+
+ // TODO: policy service should always provide valid stub policy
@Override
- public View onCreateView(
- LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
-
- final Context context = inflater.getContext();
- final long now = System.currentTimeMillis();
+ 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);
-
- 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 Context context = inflater.getContext();
final View view = inflater.inflate(R.layout.data_usage_summary, container, false);
- mChartContainer = (ViewGroup) view.findViewById(R.id.chart_container);
- mChartContainer.addView(mChart);
+ mTabHost = (TabHost) view.findViewById(android.R.id.tabhost);
+ mTabWidget = (TabWidget) view.findViewById(android.R.id.tabs);
+ mListView = (ListView) view.findViewById(android.R.id.list);
- mList = (ListView) view.findViewById(R.id.list);
- mList.setAdapter(mAdapter);
+ mTabHost.setup();
+ mTabHost.setOnTabChangedListener(mTabListener);
+
+ mHeader = inflater.inflate(R.layout.data_usage_header, mListView, false);
+ mListView.addHeaderView(mHeader, null, false);
+
+ mDataEnabled = new CheckBoxPreference(context);
+ mDisableAtLimit = new CheckBoxPreference(context);
+
+ // kick refresh once to force-create views
+ refreshPreferenceViews();
+
+ // TODO: remove once thin preferences are supported (48dip)
+ mDataEnabledView.setLayoutParams(new LinearLayout.LayoutParams(MATCH_PARENT, 72));
+ mDisableAtLimitView.setLayoutParams(new LinearLayout.LayoutParams(MATCH_PARENT, 72));
+
+ mDataEnabledView.setOnClickListener(mDataEnabledListener);
+ mDisableAtLimitView.setOnClickListener(mDisableAtLimitListener);
+
+ 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);
+
+ mChart = new DataUsageChartView(context);
+ mChart.setListener(mChartListener);
+ mChart.setLayoutParams(new AbsListView.LayoutParams(MATCH_PARENT, 350));
+ mListView.addHeaderView(mChart, null, false);
+
+ mAdapter = new DataUsageAdapter();
+ mListView.setOnItemClickListener(mListListener);
+ mListView.setAdapter(mAdapter);
return view;
}
@@ -140,88 +185,445 @@
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();
}
- 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 void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ inflater.inflate(R.menu.data_usage, menu);
+ }
+
+ @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;
+ }
+
+ /**
+ * 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 boolean tabsVisible = mSplit4G || mShowWifi;
+ mTabWidget.setVisibility(tabsVisible ? View.VISIBLE : View.GONE);
+ mTabHost.clearAllTabs();
+
+ if (mSplit4G) {
+ mTabHost.addTab(buildTabSpec(TAB_3G, R.string.data_usage_tab_3g));
+ mTabHost.addTab(buildTabSpec(TAB_4G, R.string.data_usage_tab_4g));
+ }
+
+ 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();
}
}
- private void updateDetailData() {
- final long sweep1 = mSweepTime1.getValue();
- final long sweep2 = mSweepTime2.getValue();
+ /**
+ * Factory that provide empty {@link View} to make {@link TabHost} happy.
+ */
+ private TabContentFactory mEmptyTabContent = new TabContentFactory() {
+ /** {@inheritDoc} */
+ public View createTabContent(String tag) {
+ return new View(mTabHost.getContext());
+ }
+ };
- final long start = Math.min(sweep1, sweep2);
- final long end = Math.max(sweep1, sweep2);
+ /**
+ * 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);
+ }
+
+ private OnTabChangeListener mTabListener = new OnTabChangeListener() {
+ /** {@inheritDoc} */
+ public void onTabChanged(String tabId) {
+ // user changed tab; update body
+ updateBody();
+ }
+ };
+
+ /**
+ * 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);
+ }
+
+ 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;
+
+ }
+
+ // 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);
+ }
+
+ // TODO: eventually service will always provide stub policy
+ if (mPolicy == null) {
+ mPolicy = new NetworkPolicy(1, 4 * GB_IN_BYTES, -1);
+ }
+
+ // 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];
+ }
+
+ return new long[] { start, end };
+ }
+
+ /**
+ * 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 Context context = mCycleSpinner.getContext();
+
+ final long[] bounds = getHistoryBounds(mHistory);
+ final long historyStart = bounds[0];
+ final long historyEnd = bounds[1];
+
+ // 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);
+ }
+
+ private OnClickListener mDataEnabledListener = new OnClickListener() {
+ /** {@inheritDoc} */
+ public void onClick(View v) {
+ mDataEnabled.setChecked(!mDataEnabled.isChecked());
+ refreshPreferenceViews();
+
+ // TODO: wire up to telephony to enable/disable radios
+ }
+ };
+
+ 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 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 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 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(
- start, end, TrafficStats.TEMPLATE_MOBILE_ALL);
+ range[0], range[1], mTemplate);
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();
+ private DataUsageChartListener mChartListener = new DataUsageChartListener() {
+ /** {@inheritDoc} */
+ public void onInspectRangeChanged() {
+ if (LOGD) Log.d(TAG, "onInspectRangeChanged()");
+ updateDetailData();
+ }
- // update detail list only when done sweeping
- if (sweepDone) {
- updateDetailData();
+ /** {@inheritDoc} */
+ 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();
+ }
+ }
+
+ @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));
+ }
+ }
+
+ 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);
+ }
+ }
+
+ /**
* Adapter of applications, sorted by total usage descending.
*/
public static class DataUsageAdapter extends BaseAdapter {
- private ArrayList<UsageRecord> mData = Lists.newArrayList();
+ private ArrayList<AppUsageItem> mItems = Lists.newArrayList();
- private static class UsageRecord implements Comparable<UsageRecord> {
+ private static class AppUsageItem implements Comparable<AppUsageItem> {
public int uid;
public long total;
/** {@inheritDoc} */
- public int compareTo(UsageRecord another) {
+ public int compareTo(AppUsageItem another) {
return Long.compare(another.total, total);
}
}
public void bindStats(NetworkStats stats) {
- mData.clear();
+ mItems.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);
+ final AppUsageItem item = new AppUsageItem();
+ item.uid = stats.uid[i];
+ item.total = stats.rx[i] + stats.tx[i];
+ mItems.add(item);
}
- Collections.sort(mData);
+ Collections.sort(mItems);
notifyDataSetChanged();
}
@Override
public int getCount() {
- return mData.size();
+ return mItems.size();
}
@Override
public Object getItem(int position) {
- return mData.get(position);
+ return mItems.get(position);
}
@Override
@@ -242,9 +644,9 @@
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));
+ final AppUsageItem item = mItems.get(position);
+ text1.setText(pm.getNameForUid(item.uid));
+ text2.setText(Formatter.formatFileSize(context, item.total));
return convertView;
}
@@ -252,102 +654,4 @@
}
- 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/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 @@
*/
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 @@
*/
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 void bindNetworkStats(NetworkStatsHistory stats) {
mStats = stats;
+
+ mPathStroke.reset();
+ mPathFill.reset();
}
public void bindSweepRange(ChartSweepView sweep1, ChartSweepView sweep2) {
@@ -99,7 +102,9 @@
* 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 @@
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 @@
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 @@
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 @@
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 @@
return mAxis;
}
+ public void setValue(long value) {
+ mValue = value;
+ }
+
public long getValue() {
return mValue;
}
@@ -91,6 +104,8 @@
@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 @@
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 @@
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 @@
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 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 @@
// 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 @@
@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 @@
}
/** {@inheritDoc} */
+ public void setBounds(long min, long max) {
+ mWrapped.setBounds(min, max);
+ }
+
+ /** {@inheritDoc} */
public void setSize(float size) {
mSize = size;
mWrapped.setSize(size);