summaryrefslogtreecommitdiffstats
path: root/src/com/android/settings/DataUsageSummary.java
diff options
context:
space:
mode:
authorJeff Sharkey <jsharkey@android.com>2011-06-10 13:31:21 -0700
committerJeff Sharkey <jsharkey@android.com>2011-06-10 16:17:21 -0700
commit8a50364a71e7c261b54840210f8bacff5abecb34 (patch)
tree6ddcd421fd26829af3c6b0a6bcd656c6986dc182 /src/com/android/settings/DataUsageSummary.java
parent86432504d356e3de7e63b48251f653245bcafd32 (diff)
downloadpackages_apps_Settings-8a50364a71e7c261b54840210f8bacff5abecb34.tar.gz
packages_apps_Settings-8a50364a71e7c261b54840210f8bacff5abecb34.tar.bz2
packages_apps_Settings-8a50364a71e7c261b54840210f8bacff5abecb34.zip
Iterating on data usage; tabs, scrolling, cycles.
Added ActionBar items to control complexity of data surfaced; checked state causes tabs to be shown/hidden for "Mobile", "2G-3G", "4G", and "Wi-Fi" network templates. Loading historical stats and policy from system services based on selected tab. Change entire body under tabs to scroll, treating network options and chart as ListView headers. Teach chart sweep to disable intercept to play with ListView, and draw sweep disabled as dashed line. Hijacking Preference views for toggles to offer consistency. No policy updates are persisted yet. Based on available historical network stats and policy cycle reset day, build list of user-selectable cycles. Wired up chart to display cycle data and reset inspection region to last week of available data. Change-Id: Ia561578276fa23908b745fbc06a6ef828d9ccc2e
Diffstat (limited to 'src/com/android/settings/DataUsageSummary.java')
-rw-r--r--src/com/android/settings/DataUsageSummary.java686
1 files changed, 495 insertions, 191 deletions
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;
}
+
}