Iterating on data usage; tabs, scrolling, cycles.

Added ActionBar items to control complexity of data surfaced; checked
state causes tabs to be shown/hidden for "Mobile", "2G-3G", "4G", and
"Wi-Fi" network templates.  Loading historical stats and policy from
system services based on selected tab.

Change entire body under tabs to scroll, treating network options and
chart as ListView headers.  Teach chart sweep to disable intercept to
play with ListView, and draw sweep disabled as dashed line.  Hijacking
Preference views for toggles to offer consistency.  No policy updates
are persisted yet.

Based on available historical network stats and policy cycle reset day,
build list of user-selectable cycles.  Wired up chart to display cycle
data and reset inspection region to last week of available data.

Change-Id: Ia561578276fa23908b745fbc06a6ef828d9ccc2e
This commit is contained in:
Jeff Sharkey
2011-06-10 13:31:21 -07:00
parent 86432504d3
commit 8a50364a71
12 changed files with 1000 additions and 224 deletions

View File

@@ -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>

View File

@@ -14,20 +14,34 @@
limitations under the License. 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">
<LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical"> android:orientation="vertical">
<FrameLayout <TabWidget
android:id="@+id/chart_container" android:id="@android:id/tabs"
android:orientation="horizontal"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="200dip" /> 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 <ListView
android:id="@+id/list" android:id="@android:id/list"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dip" android:layout_height="0dip"
android:layout_weight="1" /> android:layout_weight="1" />
</LinearLayout> </LinearLayout>
</TabHost>

View File

@@ -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>

33
res/menu/data_usage.xml Normal file
View File

@@ -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>

View File

@@ -3361,5 +3361,38 @@ found in the list of installed applications.</string>
<!-- Activity title for network data usage summary. [CHAR LIMIT=25] --> <!-- Activity title for network data usage summary. [CHAR LIMIT=25] -->
<string name="data_usage_summary_title">Data usage</string> <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> </resources>

View File

@@ -16,122 +16,167 @@
package com.android.settings; package com.android.settings;
import static com.android.settings.widget.ChartView.buildChartParams; import static android.net.NetworkPolicyManager.computeLastCycleBoundary;
import static com.android.settings.widget.ChartView.buildSweepParams; 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.app.Fragment;
import android.content.Context; import android.content.Context;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.graphics.Color; import android.net.INetworkPolicyManager;
import android.net.INetworkStatsService; import android.net.INetworkStatsService;
import android.net.NetworkPolicy;
import android.net.NetworkStats; import android.net.NetworkStats;
import android.net.NetworkStatsHistory; import android.net.NetworkStatsHistory;
import android.net.TrafficStats;
import android.os.Bundle; import android.os.Bundle;
import android.os.RemoteException; import android.os.RemoteException;
import android.os.ServiceManager; import android.os.ServiceManager;
import android.preference.CheckBoxPreference;
import android.preference.Preference;
import android.text.format.DateUtils; import android.text.format.DateUtils;
import android.text.format.Formatter; import android.text.format.Formatter;
import android.text.format.Time;
import android.util.Log; import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup; 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.BaseAdapter;
import android.widget.LinearLayout;
import android.widget.ListView; 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 android.widget.TextView;
import com.android.settings.widget.ChartAxis; import com.android.settings.widget.DataUsageChartView;
import com.android.settings.widget.ChartGridView; import com.android.settings.widget.DataUsageChartView.DataUsageChartListener;
import com.android.settings.widget.ChartNetworkSeriesView;
import com.android.settings.widget.ChartSweepView;
import com.android.settings.widget.ChartSweepView.OnSweepListener;
import com.android.settings.widget.ChartView;
import com.android.settings.widget.InvertedChartAxis;
import com.google.android.collect.Lists; import com.google.android.collect.Lists;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.Locale;
public class DataUsageSummary extends Fragment { public class DataUsageSummary extends Fragment {
private static final String TAG = "DataUsage"; 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 KB_IN_BYTES = 1024;
private static final long MB_IN_BYTES = 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 static final long GB_IN_BYTES = MB_IN_BYTES * 1024;
private INetworkStatsService mStatsService; private INetworkStatsService mStatsService;
private INetworkPolicyManager mPolicyService;
private ViewGroup mChartContainer; private TabHost mTabHost;
private ListView mList; private TabWidget mTabWidget;
private ListView mListView;
private ChartAxis mAxisTime;
private ChartAxis mAxisData;
private ChartView mChart;
private ChartNetworkSeriesView mSeries;
private ChartSweepView mSweepTime1;
private ChartSweepView mSweepTime2;
private ChartSweepView mSweepDataWarn;
private ChartSweepView mSweepDataLimit;
private DataUsageAdapter mAdapter; private DataUsageAdapter mAdapter;
// TODO: persist warning/limit into policy service private View mHeader;
private static final long DATA_WARN = (long) 3.2 * GB_IN_BYTES; private LinearLayout mSwitches;
private static final long DATA_LIMIT = (long) 4.8 * GB_IN_BYTES;
private CheckBoxPreference mDataEnabled;
private CheckBoxPreference mDisableAtLimit;
private View mDataEnabledView;
private View mDisableAtLimitView;
private DataUsageChartView mChart;
private Spinner mCycleSpinner;
private CycleAdapter mCycleAdapter;
private boolean mSplit4G = false;
private boolean mShowWifi = false;
private int mTemplate = TEMPLATE_INVALID;
private NetworkPolicy mPolicy;
private NetworkStatsHistory mHistory;
// TODO: policy service should always provide valid stub policy
@Override @Override
public View onCreateView( public void onCreate(Bundle savedInstanceState) {
LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
final Context context = inflater.getContext();
final long now = System.currentTimeMillis();
mStatsService = INetworkStatsService.Stub.asInterface( mStatsService = INetworkStatsService.Stub.asInterface(
ServiceManager.getService(Context.NETWORK_STATS_SERVICE)); ServiceManager.getService(Context.NETWORK_STATS_SERVICE));
mPolicyService = INetworkPolicyManager.Stub.asInterface(
ServiceManager.getService(Context.NETWORK_POLICY_SERVICE));
}
mAxisTime = new TimeAxis(); @Override
mAxisData = new InvertedChartAxis(new DataAxis()); public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
mChart = new ChartView(context, mAxisTime, mAxisData);
mChart.setPadding(20, 20, 20, 20);
mChart.addView(new ChartGridView(context, mAxisTime, mAxisData), buildChartParams());
mSeries = new ChartNetworkSeriesView(context, mAxisTime, mAxisData);
mChart.addView(mSeries, buildChartParams());
mSweepTime1 = new ChartSweepView(context, mAxisTime, now - DateUtils.DAY_IN_MILLIS * 14,
Color.parseColor("#ffffff"));
mSweepTime2 = new ChartSweepView(context, mAxisTime, now - DateUtils.DAY_IN_MILLIS * 7,
Color.parseColor("#ffffff"));
mSweepDataWarn = new ChartSweepView(
context, mAxisData, DATA_WARN, Color.parseColor("#f7931d"));
mSweepDataLimit = new ChartSweepView(
context, mAxisData, DATA_LIMIT, Color.parseColor("#be1d2c"));
mChart.addView(mSweepTime1, buildSweepParams());
mChart.addView(mSweepTime2, buildSweepParams());
mChart.addView(mSweepDataWarn, buildSweepParams());
mChart.addView(mSweepDataLimit, buildSweepParams());
mSeries.bindSweepRange(mSweepTime1, mSweepTime2);
mSweepTime1.addOnSweepListener(mSweepListener);
mSweepTime2.addOnSweepListener(mSweepListener);
mAdapter = new DataUsageAdapter();
final Context context = inflater.getContext();
final View view = inflater.inflate(R.layout.data_usage_summary, container, false); final View view = inflater.inflate(R.layout.data_usage_summary, container, false);
mChartContainer = (ViewGroup) view.findViewById(R.id.chart_container); mTabHost = (TabHost) view.findViewById(android.R.id.tabhost);
mChartContainer.addView(mChart); mTabWidget = (TabWidget) view.findViewById(android.R.id.tabs);
mListView = (ListView) view.findViewById(android.R.id.list);
mList = (ListView) view.findViewById(R.id.list); mTabHost.setup();
mList.setAdapter(mAdapter); mTabHost.setOnTabChangedListener(mTabListener);
mHeader = inflater.inflate(R.layout.data_usage_header, mListView, false);
mListView.addHeaderView(mHeader, null, false);
mDataEnabled = new CheckBoxPreference(context);
mDisableAtLimit = new CheckBoxPreference(context);
// kick refresh once to force-create views
refreshPreferenceViews();
// TODO: remove once thin preferences are supported (48dip)
mDataEnabledView.setLayoutParams(new LinearLayout.LayoutParams(MATCH_PARENT, 72));
mDisableAtLimitView.setLayoutParams(new LinearLayout.LayoutParams(MATCH_PARENT, 72));
mDataEnabledView.setOnClickListener(mDataEnabledListener);
mDisableAtLimitView.setOnClickListener(mDisableAtLimitListener);
mSwitches = (LinearLayout) mHeader.findViewById(R.id.switches);
mSwitches.addView(mDataEnabledView);
mSwitches.addView(mDisableAtLimitView);
mCycleSpinner = (Spinner) mHeader.findViewById(R.id.cycles);
mCycleAdapter = new CycleAdapter(context);
mCycleSpinner.setAdapter(mCycleAdapter);
mCycleSpinner.setOnItemSelectedListener(mCycleListener);
mChart = new DataUsageChartView(context);
mChart.setListener(mChartListener);
mChart.setLayoutParams(new AbsListView.LayoutParams(MATCH_PARENT, 350));
mListView.addHeaderView(mChart, null, false);
mAdapter = new DataUsageAdapter();
mListView.setOnItemClickListener(mListListener);
mListView.setAdapter(mAdapter);
return view; return view;
} }
@@ -140,88 +185,445 @@ public class DataUsageSummary extends Fragment {
public void onResume() { public void onResume() {
super.onResume(); super.onResume();
updateSummaryData(); // this kicks off chain reaction which creates tabs, binds the body to
updateDetailData(); // 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);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// TODO: persist checked-ness of options to restore tabs later
switch (item.getItemId()) {
case R.id.action_split_4g: {
mSplit4G = !item.isChecked();
item.setChecked(mSplit4G);
updateTabs();
return true;
}
case R.id.action_show_wifi: {
mShowWifi = !item.isChecked();
item.setChecked(mShowWifi);
updateTabs();
return true;
}
}
return false;
}
/**
* Rebuild all tabs based on {@link #mSplit4G} and {@link #mShowWifi},
* hiding the tabs entirely when applicable. Selects first tab, and kicks
* off a full rebind of body contents.
*/
private void updateTabs() {
// TODO: persist/restore if user wants mobile split, or wifi visibility
final boolean tabsVisible = mSplit4G || mShowWifi;
mTabWidget.setVisibility(tabsVisible ? View.VISIBLE : View.GONE);
mTabHost.clearAllTabs();
if (mSplit4G) {
mTabHost.addTab(buildTabSpec(TAB_3G, R.string.data_usage_tab_3g));
mTabHost.addTab(buildTabSpec(TAB_4G, R.string.data_usage_tab_4g));
}
if (mShowWifi) {
if (!mSplit4G) {
mTabHost.addTab(buildTabSpec(TAB_MOBILE, R.string.data_usage_tab_mobile));
}
mTabHost.addTab(buildTabSpec(TAB_WIFI, R.string.data_usage_tab_wifi));
}
if (mTabWidget.getTabCount() > 0) {
// select first tab, which will kick off updateBody()
mTabHost.setCurrentTab(0);
} else {
// no tabs shown; update body manually
updateBody();
}
}
/**
* Factory that provide empty {@link View} to make {@link TabHost} happy.
*/
private TabContentFactory mEmptyTabContent = new TabContentFactory() {
/** {@inheritDoc} */
public View createTabContent(String tag) {
return new View(mTabHost.getContext());
}
};
/**
* Build {@link TabSpec} with thin indicator, and empty content.
*/
private TabSpec buildTabSpec(String tag, int titleRes) {
final LayoutInflater inflater = LayoutInflater.from(mTabWidget.getContext());
final View indicator = inflater.inflate(
R.layout.tab_indicator_thin_holo, mTabWidget, false);
final TextView title = (TextView) indicator.findViewById(android.R.id.title);
title.setText(titleRes);
return mTabHost.newTabSpec(tag).setIndicator(indicator).setContent(mEmptyTabContent);
}
private OnTabChangeListener mTabListener = new OnTabChangeListener() {
/** {@inheritDoc} */
public void onTabChanged(String tabId) {
// user changed tab; update body
updateBody();
}
};
/**
* Update body content based on current tab. Loads
* {@link NetworkStatsHistory} and {@link NetworkPolicy} from system, and
* binds them to visible controls.
*/
private void updateBody() {
final String tabTag = mTabHost.getCurrentTabTag();
final String currentTab = tabTag != null ? tabTag : TAB_MOBILE;
if (LOGD) Log.d(TAG, "updateBody() with currentTab=" + currentTab);
if (TAB_WIFI.equals(currentTab)) {
// wifi doesn't have any controls
mDataEnabledView.setVisibility(View.GONE);
mDisableAtLimitView.setVisibility(View.GONE);
mTemplate = TEMPLATE_WIFI;
} else {
// make sure we show for non-wifi
mDataEnabledView.setVisibility(View.VISIBLE);
mDisableAtLimitView.setVisibility(View.VISIBLE);
}
if (TAB_MOBILE.equals(currentTab)) {
mDataEnabled.setTitle(R.string.data_usage_enable_mobile);
mDisableAtLimit.setTitle(R.string.data_usage_disable_mobile_limit);
mTemplate = TEMPLATE_MOBILE_ALL;
} else if (TAB_3G.equals(currentTab)) {
mDataEnabled.setTitle(R.string.data_usage_enable_3g);
mDisableAtLimit.setTitle(R.string.data_usage_disable_3g_limit);
mTemplate = TEMPLATE_MOBILE_3G_LOWER;
} else if (TAB_4G.equals(currentTab)) {
mDataEnabled.setTitle(R.string.data_usage_enable_4g);
mDisableAtLimit.setTitle(R.string.data_usage_disable_4g_limit);
mTemplate = TEMPLATE_MOBILE_4G;
} }
private void updateSummaryData() { // TODO: populate checkbox based on radio preferences
mDataEnabled.setChecked(true);
try { try {
final NetworkStatsHistory history = mStatsService.getHistoryForNetwork( // load policy and stats for current template
TrafficStats.TEMPLATE_MOBILE_ALL); mPolicy = mPolicyService.getNetworkPolicy(mTemplate, null);
mSeries.bindNetworkStats(history); mHistory = mStatsService.getHistoryForNetwork(mTemplate);
} catch (RemoteException e) { } catch (RemoteException e) {
Log.w(TAG, "problem reading stats"); // since we can't do much without policy or history, and we don't
// want to leave with half-baked UI, we bail hard.
throw new RuntimeException("problem reading network policy or stats", e);
}
// TODO: eventually service will always provide stub policy
if (mPolicy == null) {
mPolicy = new NetworkPolicy(1, 4 * GB_IN_BYTES, -1);
}
// bind chart to historical stats
mChart.bindNetworkPolicy(mPolicy);
mChart.bindNetworkStats(mHistory);
// generate cycle list based on policy and available history
updateCycleList();
// reflect policy limit in checkbox
mDisableAtLimit.setChecked(mPolicy.limitBytes != -1);
// force scroll to top of body
mListView.smoothScrollToPosition(0);
// kick preference views so they rebind from changes above
refreshPreferenceViews();
}
/**
* Return full time bounds (earliest and latest time recorded) of the given
* {@link NetworkStatsHistory}.
*/
private static long[] getHistoryBounds(NetworkStatsHistory history) {
final long currentTime = System.currentTimeMillis();
long start = currentTime;
long end = currentTime;
if (history.bucketCount > 0) {
start = history.bucketStart[0];
end = history.bucketStart[history.bucketCount - 1];
}
return new long[] { start, end };
}
/**
* Rebuild {@link #mCycleAdapter} based on {@link NetworkPolicy#cycleDay}
* and available {@link NetworkStatsHistory} data. Always selects the newest
* item, updating the inspection range on {@link #mChart}.
*/
private void updateCycleList() {
mCycleAdapter.clear();
final Context context = mCycleSpinner.getContext();
final long[] bounds = getHistoryBounds(mHistory);
final long historyStart = bounds[0];
final long historyEnd = bounds[1];
// find the next cycle boundary
long cycleEnd = computeNextCycleBoundary(historyEnd, mPolicy);
int guardCount = 0;
// walk backwards, generating all valid cycle ranges
while (cycleEnd > historyStart) {
final long cycleStart = computeLastCycleBoundary(cycleEnd, mPolicy);
Log.d(TAG, "generating cs=" + cycleStart + " to ce=" + cycleEnd + " waiting for hs="
+ historyStart);
mCycleAdapter.add(new CycleItem(context, cycleStart, cycleEnd));
cycleEnd = cycleStart;
// TODO: remove this guard once we have better testing
if (guardCount++ > 50) {
Log.wtf(TAG, "stuck generating ranges for bounds=" + Arrays.toString(bounds)
+ " and policy=" + mPolicy);
} }
} }
// one last cycle entry to change date
mCycleAdapter.add(new CycleChangeItem(context));
// force pick the current cycle (first item)
mCycleSpinner.setSelection(0);
mCycleListener.onItemSelected(mCycleSpinner, null, 0, 0);
}
/**
* Force rebind of hijacked {@link Preference} views.
*/
private void refreshPreferenceViews() {
mDataEnabledView = mDataEnabled.getView(mDataEnabledView, mListView);
mDisableAtLimitView = mDisableAtLimit.getView(mDisableAtLimitView, mListView);
}
private OnClickListener mDataEnabledListener = new OnClickListener() {
/** {@inheritDoc} */
public void onClick(View v) {
mDataEnabled.setChecked(!mDataEnabled.isChecked());
refreshPreferenceViews();
// TODO: wire up to telephony to enable/disable radios
}
};
private OnClickListener mDisableAtLimitListener = new OnClickListener() {
/** {@inheritDoc} */
public void onClick(View v) {
final boolean disableAtLimit = !mDisableAtLimit.isChecked();
mDisableAtLimit.setChecked(disableAtLimit);
refreshPreferenceViews();
// TODO: push updated policy to service
// TODO: show interstitial warning dialog to user
final long limitBytes = disableAtLimit ? 5 * GB_IN_BYTES : -1;
mPolicy = new NetworkPolicy(mPolicy.cycleDay, mPolicy.warningBytes, limitBytes);
mChart.bindNetworkPolicy(mPolicy);
}
};
private OnItemClickListener mListListener = new OnItemClickListener() {
/** {@inheritDoc} */
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
final Object object = parent.getItemAtPosition(position);
// TODO: show app details
Log.d(TAG, "showing app details for " + object);
}
};
private OnItemSelectedListener mCycleListener = new OnItemSelectedListener() {
/** {@inheritDoc} */
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
final CycleItem cycle = (CycleItem) parent.getItemAtPosition(position);
if (cycle instanceof CycleChangeItem) {
// TODO: show "define cycle" dialog
// also reset back to first cycle
Log.d(TAG, "CHANGE CYCLE DIALOG!!");
} else {
if (LOGD) Log.d(TAG, "shoiwng cycle " + cycle);
// update chart to show selected cycle, and update detail data
// to match updated sweep bounds.
final long[] bounds = getHistoryBounds(mHistory);
mChart.setVisibleRange(cycle.start, cycle.end, bounds[1]);
updateDetailData();
}
}
/** {@inheritDoc} */
public void onNothingSelected(AdapterView<?> parent) {
// ignored
}
};
/**
* Update {@link #mAdapter} with sorted list of applications data usage,
* based on current inspection from {@link #mChart}.
*/
private void updateDetailData() { private void updateDetailData() {
final long sweep1 = mSweepTime1.getValue(); if (LOGD) Log.d(TAG, "updateDetailData()");
final long sweep2 = mSweepTime2.getValue();
final long start = Math.min(sweep1, sweep2);
final long end = Math.max(sweep1, sweep2);
try { try {
final long[] range = mChart.getInspectRange();
final NetworkStats stats = mStatsService.getSummaryForAllUid( final NetworkStats stats = mStatsService.getSummaryForAllUid(
start, end, TrafficStats.TEMPLATE_MOBILE_ALL); range[0], range[1], mTemplate);
mAdapter.bindStats(stats); mAdapter.bindStats(stats);
} catch (RemoteException e) { } catch (RemoteException e) {
Log.w(TAG, "problem reading stats"); Log.w(TAG, "problem reading stats");
} }
} }
private OnSweepListener mSweepListener = new OnSweepListener() { private DataUsageChartListener mChartListener = new DataUsageChartListener() {
public void onSweep(ChartSweepView sweep, boolean sweepDone) { /** {@inheritDoc} */
// always update graph clip region public void onInspectRangeChanged() {
mSeries.invalidate(); if (LOGD) Log.d(TAG, "onInspectRangeChanged()");
// update detail list only when done sweeping
if (sweepDone) {
updateDetailData(); updateDetailData();
} }
/** {@inheritDoc} */
public void onLimitsChanged() {
if (LOGD) Log.d(TAG, "onLimitsChanged()");
// redefine policy and persist into service
// TODO: kick this onto background thread, since service touches disk
// TODO: remove this mPolicy null check, since later service will
// always define baseline value.
final int cycleDay = mPolicy != null ? mPolicy.cycleDay : 1;
final long warningBytes = mChart.getWarningBytes();
final long limitBytes = mDisableAtLimit.isChecked() ? -1 : mChart.getLimitBytes();
mPolicy = new NetworkPolicy(cycleDay, warningBytes, limitBytes);
if (LOGD) Log.d(TAG, "persisting policy=" + mPolicy);
try {
mPolicyService.setNetworkPolicy(mTemplate, null, mPolicy);
} catch (RemoteException e) {
Log.w(TAG, "problem persisting policy", e);
}
} }
}; };
/**
* List item that reflects a specific data usage cycle.
*/
public static class CycleItem {
public CharSequence label;
public long start;
public long end;
private static final StringBuilder sBuilder = new StringBuilder(50);
private static final java.util.Formatter sFormatter = new java.util.Formatter(
sBuilder, Locale.getDefault());
CycleItem(CharSequence label) {
this.label = label;
}
public CycleItem(Context context, long start, long end) {
this.label = formatDateRangeUtc(context, start, end);
this.start = start;
this.end = end;
}
private static String formatDateRangeUtc(Context context, long start, long end) {
synchronized (sBuilder) {
sBuilder.setLength(0);
return DateUtils.formatDateRange(context, sFormatter, start, end,
DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_ABBREV_MONTH,
Time.TIMEZONE_UTC).toString();
}
}
@Override
public String toString() {
return label.toString();
}
}
/**
* Special-case data usage cycle that triggers dialog to change
* {@link NetworkPolicy#cycleDay}.
*/
public static class CycleChangeItem extends CycleItem {
public CycleChangeItem(Context context) {
super(context.getString(R.string.data_usage_change_cycle));
}
}
public static class CycleAdapter extends ArrayAdapter<CycleItem> {
public CycleAdapter(Context context) {
super(context, android.R.layout.simple_spinner_item);
setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
}
}
/** /**
* Adapter of applications, sorted by total usage descending. * Adapter of applications, sorted by total usage descending.
*/ */
public static class DataUsageAdapter extends BaseAdapter { public static class DataUsageAdapter extends BaseAdapter {
private ArrayList<UsageRecord> mData = Lists.newArrayList(); private ArrayList<AppUsageItem> mItems = Lists.newArrayList();
private static class UsageRecord implements Comparable<UsageRecord> { private static class AppUsageItem implements Comparable<AppUsageItem> {
public int uid; public int uid;
public long total; public long total;
/** {@inheritDoc} */ /** {@inheritDoc} */
public int compareTo(UsageRecord another) { public int compareTo(AppUsageItem another) {
return Long.compare(another.total, total); return Long.compare(another.total, total);
} }
} }
public void bindStats(NetworkStats stats) { public void bindStats(NetworkStats stats) {
mData.clear(); mItems.clear();
for (int i = 0; i < stats.length(); i++) { for (int i = 0; i < stats.length(); i++) {
final UsageRecord record = new UsageRecord(); final AppUsageItem item = new AppUsageItem();
record.uid = stats.uid[i]; item.uid = stats.uid[i];
record.total = stats.rx[i] + stats.tx[i]; item.total = stats.rx[i] + stats.tx[i];
mData.add(record); mItems.add(item);
} }
Collections.sort(mData); Collections.sort(mItems);
notifyDataSetChanged(); notifyDataSetChanged();
} }
@Override @Override
public int getCount() { public int getCount() {
return mData.size(); return mItems.size();
} }
@Override @Override
public Object getItem(int position) { public Object getItem(int position) {
return mData.get(position); return mItems.get(position);
} }
@Override @Override
@@ -242,9 +644,9 @@ public class DataUsageSummary extends Fragment {
final TextView text1 = (TextView) convertView.findViewById(android.R.id.text1); final TextView text1 = (TextView) convertView.findViewById(android.R.id.text1);
final TextView text2 = (TextView) convertView.findViewById(android.R.id.text2); final TextView text2 = (TextView) convertView.findViewById(android.R.id.text2);
final UsageRecord record = mData.get(position); final AppUsageItem item = mItems.get(position);
text1.setText(pm.getNameForUid(record.uid)); text1.setText(pm.getNameForUid(item.uid));
text2.setText(Formatter.formatFileSize(context, record.total)); text2.setText(Formatter.formatFileSize(context, item.total));
return convertView; return convertView;
} }
@@ -252,102 +654,4 @@ public class DataUsageSummary extends Fragment {
} }
public static class TimeAxis implements ChartAxis {
private static final long TICK_INTERVAL = DateUtils.DAY_IN_MILLIS * 7;
private long mMin;
private long mMax;
private float mSize;
public TimeAxis() {
// TODO: hook up these ranges to policy service
mMax = System.currentTimeMillis();
mMin = mMax - DateUtils.DAY_IN_MILLIS * 30;
}
/** {@inheritDoc} */
public void setSize(float size) {
this.mSize = size;
}
/** {@inheritDoc} */
public float convertToPoint(long value) {
return (mSize * (value - mMin)) / (mMax - mMin);
}
/** {@inheritDoc} */
public long convertToValue(float point) {
return (long) (mMin + ((point * (mMax - mMin)) / mSize));
}
/** {@inheritDoc} */
public CharSequence getLabel(long value) {
// TODO: convert to string
return Long.toString(value);
}
/** {@inheritDoc} */
public float[] getTickPoints() {
// tick mark for every week
final int tickCount = (int) ((mMax - mMin) / TICK_INTERVAL);
final float[] tickPoints = new float[tickCount];
for (int i = 0; i < tickCount; i++) {
tickPoints[i] = convertToPoint(mMax - (TICK_INTERVAL * i));
}
return tickPoints;
}
}
// TODO: make data axis log-scale
public static class DataAxis implements ChartAxis {
private long mMin;
private long mMax;
private float mSize;
public DataAxis() {
// TODO: adapt ranges to show when history >5GB, and handle 4G
// interfaces with higher limits.
mMin = 0;
mMax = 5 * GB_IN_BYTES;
}
/** {@inheritDoc} */
public void setSize(float size) {
this.mSize = size;
}
/** {@inheritDoc} */
public float convertToPoint(long value) {
return (mSize * (value - mMin)) / (mMax - mMin);
}
/** {@inheritDoc} */
public long convertToValue(float point) {
return (long) (mMin + ((point * (mMax - mMin)) / mSize));
}
/** {@inheritDoc} */
public CharSequence getLabel(long value) {
// TODO: convert to string
return Long.toString(value);
}
/** {@inheritDoc} */
public float[] getTickPoints() {
final float[] tickPoints = new float[16];
long value = mMax;
float mult = 0.8f;
for (int i = 0; i < tickPoints.length; i++) {
tickPoints[i] = convertToPoint(value);
value = (long) (value * mult);
mult *= 0.9;
}
return tickPoints;
}
}
} }

View File

@@ -22,6 +22,7 @@ package com.android.settings.widget;
*/ */
public interface ChartAxis { public interface ChartAxis {
public void setBounds(long min, long max);
public void setSize(float size); public void setSize(float size);
public float convertToPoint(long value); public float convertToPoint(long value);

View File

@@ -35,7 +35,7 @@ import com.google.common.base.Preconditions;
*/ */
public class ChartNetworkSeriesView extends View { public class ChartNetworkSeriesView extends View {
private static final String TAG = "ChartNetworkSeriesView"; 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 mHoriz;
private final ChartAxis mVert; private final ChartAxis mVert;
@@ -80,6 +80,9 @@ public class ChartNetworkSeriesView extends View {
public void bindNetworkStats(NetworkStatsHistory stats) { public void bindNetworkStats(NetworkStatsHistory stats) {
mStats = stats; mStats = stats;
mPathStroke.reset();
mPathFill.reset();
} }
public void bindSweepRange(ChartSweepView sweep1, ChartSweepView sweep2) { 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 * Erase any existing {@link Path} and generate series outline based on
* currently bound {@link NetworkStatsHistory} data. * currently bound {@link NetworkStatsHistory} data.
*/ */
private void generatePath() { public void generatePath() {
if (LOGD) Log.d(TAG, "generatePath()");
mPathStroke.reset(); mPathStroke.reset();
mPathFill.reset(); mPathFill.reset();
@@ -114,6 +119,9 @@ public class ChartNetworkSeriesView extends View {
float lastX = 0; float lastX = 0;
float lastY = 0; float lastY = 0;
// TODO: count fractional data from first bucket crossing start;
// currently it only accepts first full bucket.
long totalData = 0; long totalData = 0;
for (int i = 0; i < mStats.bucketCount; i++) { for (int i = 0; i < mStats.bucketCount; i++) {

View File

@@ -19,6 +19,7 @@ package com.android.settings.widget;
import android.content.Context; import android.content.Context;
import android.graphics.Canvas; import android.graphics.Canvas;
import android.graphics.Color; import android.graphics.Color;
import android.graphics.DashPathEffect;
import android.graphics.Paint; import android.graphics.Paint;
import android.graphics.Paint.Style; import android.graphics.Paint.Style;
import android.view.MotionEvent; import android.view.MotionEvent;
@@ -33,6 +34,7 @@ import com.google.common.base.Preconditions;
public class ChartSweepView extends View { public class ChartSweepView extends View {
private final Paint mPaintSweep; private final Paint mPaintSweep;
private final Paint mPaintSweepDisabled;
private final Paint mPaintShadow; private final Paint mPaintShadow;
private final ChartAxis mAxis; private final ChartAxis mAxis;
@@ -59,6 +61,13 @@ public class ChartSweepView extends View {
mPaintSweep.setStyle(Style.FILL_AND_STROKE); mPaintSweep.setStyle(Style.FILL_AND_STROKE);
mPaintSweep.setAntiAlias(true); 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 = new Paint();
mPaintShadow.setColor(Color.BLACK); mPaintShadow.setColor(Color.BLACK);
mPaintShadow.setStrokeWidth(6.0f); mPaintShadow.setStrokeWidth(6.0f);
@@ -81,6 +90,10 @@ public class ChartSweepView extends View {
return mAxis; return mAxis;
} }
public void setValue(long value) {
mValue = value;
}
public long getValue() { public long getValue() {
return mValue; return mValue;
} }
@@ -91,6 +104,8 @@ public class ChartSweepView extends View {
@Override @Override
public boolean onTouchEvent(MotionEvent event) { public boolean onTouchEvent(MotionEvent event) {
if (!isEnabled()) return false;
final View parent = (View) getParent(); final View parent = (View) getParent();
switch (event.getAction()) { switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: { case MotionEvent.ACTION_DOWN: {
@@ -98,6 +113,8 @@ public class ChartSweepView extends View {
return true; return true;
} }
case MotionEvent.ACTION_MOVE: { case MotionEvent.ACTION_MOVE: {
getParent().requestDisallowInterceptTouchEvent(true);
if (mHorizontal) { if (mHorizontal) {
setTranslationY(event.getRawY() - mTracking.getRawY()); setTranslationY(event.getRawY() - mTracking.getRawY());
final float point = (getTop() + getTranslationY() + (getHeight() / 2)) final float point = (getTop() + getTranslationY() + (getHeight() / 2))
@@ -143,12 +160,14 @@ public class ChartSweepView extends View {
mHorizontal = width > height; mHorizontal = width > height;
final Paint linePaint = isEnabled() ? mPaintSweep : mPaintSweepDisabled;
if (mHorizontal) { if (mHorizontal) {
final int centerY = height / 2; final int centerY = height / 2;
final int endX = width - height; final int endX = width - height;
canvas.drawLine(0, centerY, endX, centerY, mPaintShadow); 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, mPaintShadow);
canvas.drawCircle(endX, centerY, 4.0f, mPaintSweep); canvas.drawCircle(endX, centerY, 4.0f, mPaintSweep);
} else { } else {
@@ -156,7 +175,7 @@ public class ChartSweepView extends View {
final int endY = height - width; final int endY = height - width;
canvas.drawLine(centerX, 0, centerX, endY, mPaintShadow); 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, mPaintShadow);
canvas.drawCircle(centerX, endY, 4.0f, mPaintSweep); canvas.drawCircle(centerX, endY, 4.0f, mPaintSweep);
} }

View File

@@ -22,6 +22,7 @@ import static com.google.common.base.Preconditions.checkNotNull;
import android.content.Context; import android.content.Context;
import android.graphics.Rect; import android.graphics.Rect;
import android.util.Log;
import android.view.Gravity; import android.view.Gravity;
import android.view.View; import android.view.View;
import android.widget.FrameLayout; import android.widget.FrameLayout;
@@ -37,8 +38,8 @@ public class ChartView extends FrameLayout {
// TODO: extend something that supports two-dimensional scrolling // TODO: extend something that supports two-dimensional scrolling
private final ChartAxis mHoriz; final ChartAxis mHoriz;
private final ChartAxis mVert; final ChartAxis mVert;
private Rect mContent = new Rect(); private Rect mContent = new Rect();
@@ -54,8 +55,8 @@ public class ChartView extends FrameLayout {
@Override @Override
protected void onLayout(boolean changed, int l, int t, int r, int b) { protected void onLayout(boolean changed, int l, int t, int r, int b) {
mContent.set(l + getPaddingLeft(), t + getPaddingTop(), r - getPaddingRight(), mContent.set(getPaddingLeft(), getPaddingTop(), r - l - getPaddingRight(),
b - getPaddingBottom()); b - t - getPaddingBottom());
final int width = mContent.width(); final int width = mContent.width();
final int height = mContent.height(); final int height = mContent.height();

View File

@@ -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;
}
}
}

View File

@@ -27,6 +27,11 @@ public class InvertedChartAxis implements ChartAxis {
mWrapped = wrapped; mWrapped = wrapped;
} }
/** {@inheritDoc} */
public void setBounds(long min, long max) {
mWrapped.setBounds(min, max);
}
/** {@inheritDoc} */ /** {@inheritDoc} */
public void setSize(float size) { public void setSize(float size) {
mSize = size; mSize = size;