summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--res/layout/data_usage_header.xml49
-rw-r--r--res/layout/data_usage_summary.xml38
-rw-r--r--res/layout/tab_indicator_thin_holo.xml33
-rw-r--r--res/menu/data_usage.xml33
-rw-r--r--res/values/strings.xml33
-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
12 files changed, 987 insertions, 211 deletions
diff --git a/res/layout/data_usage_header.xml b/res/layout/data_usage_header.xml
new file mode 100644
index 000000000..4d8a5dd06
--- /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 9a356aec8..fc62465d4 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" />
+
+ <!-- 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>
-</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 000000000..e4c4652c7
--- /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 000000000..a95c0748e
--- /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 eaacf4476..0c1abffc8 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -3361,5 +3361,38 @@ found in the list of installed applications.</string>
<!-- 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 b9d192908..e27227f30 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 0b77ac649..2b21d2856 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 100876122..d0a274219 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 e3130cef9..788caad4f 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 bcb54f0a7..3e5fc5051 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 000000000..defa9533c
--- /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 2bda320a5..e7e7893ea 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);