diff options
-rw-r--r-- | res/layout/data_usage_header.xml | 49 | ||||
-rw-r--r-- | res/layout/data_usage_summary.xml | 38 | ||||
-rw-r--r-- | res/layout/tab_indicator_thin_holo.xml | 33 | ||||
-rw-r--r-- | res/menu/data_usage.xml | 33 | ||||
-rw-r--r-- | res/values/strings.xml | 33 | ||||
-rw-r--r-- | src/com/android/settings/DataUsageSummary.java | 686 | ||||
-rw-r--r-- | src/com/android/settings/widget/ChartAxis.java | 1 | ||||
-rw-r--r-- | src/com/android/settings/widget/ChartNetworkSeriesView.java | 12 | ||||
-rw-r--r-- | src/com/android/settings/widget/ChartSweepView.java | 23 | ||||
-rw-r--r-- | src/com/android/settings/widget/ChartView.java | 9 | ||||
-rw-r--r-- | src/com/android/settings/widget/DataUsageChartView.java | 276 | ||||
-rw-r--r-- | src/com/android/settings/widget/InvertedChartAxis.java | 5 |
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); |